89539


画像のような表があり、それを下記のようなjson型に変形したいと考えています。  
（途中にリストはないものとしました。）  
再帰関数を使って組もうとしているのですが、途中のキーが飛んでしまったりとうまく組めずにいます。  
アルゴリズムに強い方、お力添えいただけないでしょうか？

使用モジュール  
openpyxl

試した実装  
キーで同じレベルのものをリスト化（①）し、下位のレベルのものも同様にリスト化（②）。  
forで①のリストを回わし、対応する②のリストに含まれる数が2以上の場合、再帰させ、数が1の場合returnする

[![画像の説明をここに入力](https://i.stack.imgur.com/1IpL0.png)](https://i.stack.imgur.com/1IpL0.png)

[code]

    [{
        "k1": {
            "k2": {
                "k3": "v3"
            },
            "k4": "v4",
            "k5": {
                "k6": "v6"
            }
        },
        "k7": "v7",
        "k8": {
            "k9": "v9",
            "k10": {
                "k11": {
                    "k12": "v12",
                    "k13": "v13"
                },
                "k14": "v14"
            },
            "k15": {
                "k16": "v16"
            }
    
        },
        "k17": "v17",
        "k18": {
            "k19": "v19"
        }
    }]
    
[/code]


CC BY-SA 4.0
2022-06-22T15:07:00.707
89529
2022-06-23T05:21:13.297
43566
question
python アルゴリズム
表を辞書型に直す再帰関数について


色々と方法はあるでしょうが、データ構造の入れ子的な部分を探し、それを引数にすると解り易い再帰関数になるかなと思います。

質問のケースですと、キーの値となる部分が表の全体の構造と同じで、再帰的な構造になっています。下の画像では、その内の一部、`k8` の値と `k10`
の値をハイライトしています。それぞれ全体と同じアルゴリズムで処理できるはずです。

[![再帰部分](https://i.stack.imgur.com/zmdoq.png)](https://i.stack.imgur.com/zmdoq.png)

ですから、表の一部を引数で受けとる関数により、再帰的に処理できます。

私は `openpyxl`
は良く知らず、表の部分を表現するスマートな方法が解りませんので、下のコードでは、開始行、最終行、開始列、最終列、を数値で受けとっています。また、テストなど行なっていませんので、参考までに留めて下さい。

[code]

    import json
    from openpyxl import Workbook
    
    wb = Workbook()
    ws = wb.active
    
    data = [
        #  1      2      3      4      5      6
        ["k1",  None,  None,  None,  None,  None],  #  1
        [None,  "k2",  None,  None,  None,  None],  #  2
        [None,  None,  "k3",  None,  None,  "v3"],  #  3
        [None,  "k4",  None,  None,  None,  "v4"],  #  4
        [None,  "k5",  None,  None,  None,  None],  #  5
        [None,  None,  "k6",  None,  None,  "v9"],  #  6
        ["k7",  None,  None,  None,  None,  "v7"],  #  7
        ["k8",  None,  None,  None,  None,  None],  #  8
        [None,  "k9",  None,  None,  None,  "v9"],  #  9
        [None,  "k10", None,  None,  None,  None],  # 10
        [None,  None,  "k11", None,  None,  None],  # 11
        [None,  None,  None,  "k12", None,  "v12"], # 12
        [None,  None,  None,  "k13", None,  "v13"], # 13
        [None,  None,  "k14", None,  None,  "k14"], # 14
        [None,  "k15", None,  None,  None,  None],  # 15
        [None,  None,  "k16", None,  None,  "v16"], # 16
        ["k17", None,  None,  None,  None,  "v17"], # 17
        ["k18", None,  None,  None,  None,  None],  # 18
        [None,  "k19", None,  None,  None,  "v19"], # 19
    ]
    
    for row in data:
        ws.append(row)
    
    
    def cell_range_to_dic(ws, row_first, row_last, column_first, column_last, depth=0):
        indent = " " * depth * 4
        print(f"{indent}-- cell_range_to_dic --")
        print(f"{indent}{row_first=} {row_last=} {column_first=} {column_last=} {depth=}")
    
        result = {}
        row = row_first
    
        # 左端に存在するキー毎にループする
        while row <= row_last:
    
            # キーは左端のセル
            key = ws.cell(row, column_first).value
            if key == None:
                raise ValueError(f"キー用のセルが空です: {row=}, {column_first=}")
            print(f"{indent}{key=}")
    
            # 右端のセルに値が有るかも知れない
            value_or_none = ws.cell(row, column_last).value
    
            # 次のループでは次のキーを扱う
            row += 1
    
            if value_or_none != None:
                # 右端のセルが値だった
                value = value_or_none
            else:
                # 後続行に含まれる再帰的構造が値だった
                # 次のキーまで読み飛ばす
                child_row_first = row
                while ws.cell(row, column_first).value == None and row <= row_last:
                    row += 1
    
                # 読み飛ばした部分を再帰呼び出しで計算し、値とする
                value = cell_range_to_dic(
                    ws, child_row_first, row - 1, column_first + 1, column_last, depth + 1)
    
            print(f"{indent}{value=}")
            result[key] = value
    
        return result
    
    
    out_data = cell_range_to_dic(ws, 1, 19, 1, 6)
    
    print("\n-- 結果 --\n")
    print(json.dumps(out_data, sort_keys=False, indent=4))
    
[/code]


CC BY-SA 4.0
2022-06-23T03:43:10.313
89539
2022-06-23T03:55:52.973
2022-06-23T03:55:52.973
3054
3054
89529
answer


冒頭のエクセルシートを"openpyxl.xlsx"というファイル名でSheet1に作成してあるものとします。

再帰関数を使用しなくてもこの場合は実現可能ですね。  
・同一行にキーが複数あった場合、最初のキーのみ有効となります。  
-> breakを削除すれば、同一行に複数キーを許容します。  
・値がなければ、空の辞書型が値に代入されます。  
・同一深さに重複キーがあっても、それより下層のキー内容も追加されます。  
-> 例えば、k19->k1にしてみればわかります。
[code]

    import json
    import numpy as np
    import openpyxl
    
    wb = openpyxl.load_workbook("openpyxl.xlsx")
    sh1 = wb['Sheet1']
    wb.close()
    
    
    xls_list = list()
    for row_index in range(sh1.min_row, sh1.max_row+1):
        clm_list = list()
        for col_index in range(sh1.min_column, sh1.max_column+1):
            cell = sh1.cell(row_index, col_index)
            clm_list.append(cell.value)
        xls_list.append(clm_list)
    
    # 項目が一番上にある前提で削除
    xls_list.pop(0)
    
    dict_array = np.asarray(xls_list)
    row_num, clm_num = dict_array.shape
    
    value_index = clm_num-1
    
    disp_dict = dict()
    _temp_dict_ = disp_dict
    key_list = list()
    
    for row_index in range(row_num):
        row_array = dict_array[row_index]
        val = row_array[value_index]
        for i, k in enumerate(row_array[:value_index]):
            if k is not None:
                if len(key_list) > i:
                    key_list = key_list[:i]
                    _temp_dict_ = disp_dict
                    for k1 in key_list:
                        _temp_dict_ = _temp_dict_[k1]
    
                key_list.append(k)
    
                if val is not None:
                    _temp_dict_.setdefault(k, val)
                else:
                    _temp_dict_.setdefault(k, dict())
                    _temp_dict_ = _temp_dict_[k]
                break
    
    
    print(json.dumps(disp_dict, indent=2))
    
    
[/code]


CC BY-SA 4.0
2022-06-23T05:21:13.297
89541
2022-06-23T05:21:13.297
32891
89529
answer