@@ -106,6 +106,7 @@ def parse_to_json(
106106 file_path : str ,
107107 sheet_name : str | None = None ,
108108 cell_range : str | None = None ,
109+ include_frozen_rows : bool = True ,
109110 ) -> str :
110111 """
111112 Excelファイルを解析してJSON形式で返す
@@ -114,11 +115,15 @@ def parse_to_json(
114115 file_path: Excelファイルのパス
115116 sheet_name: 特定シートのみ取得(Noneで全シート)
116117 cell_range: セル範囲指定(例: "A1:D10")
118+ include_frozen_rows: cell_range指定時に固定行(ヘッダー)を自動追加
119+ True(デフォルト): frozen_rowsで指定された行を自動的に取得
120+ False: 指定されたcell_rangeのみを取得
117121
118122 Returns:
119123 JSON文字列
120124 - 各セルのデータ: value(値)、coordinate(座標)
121- - 構造情報: シート名、dimensions(範囲)
125+ - 構造情報: シート名、dimensions(シート全体のセル範囲、例: "A1:D10")
126+ - 構造情報: frozen_rows(固定行数)、frozen_cols(固定列数)
122127 - 条件付き構造情報: freeze_panes(存在する場合)、merged_ranges(結合セルが存在する場合)
123128 """
124129 logger .info (
@@ -199,6 +204,7 @@ def parse_to_json(
199204 sheet_data = self ._parse_sheet (
200205 sheet ,
201206 cell_range ,
207+ include_frozen_rows ,
202208 )
203209 result ["sheets" ].append (sheet_data )
204210
@@ -283,17 +289,118 @@ def _scan_sheet(
283289 }
284290 )
285291
292+ def _calculate_header_range (
293+ self , cell_range : str , frozen_rows : int
294+ ) -> str | None :
295+ """
296+ セル範囲に対してfrozen_rowsに基づくヘッダー範囲を計算
297+
298+ Args:
299+ cell_range: セル範囲(例: "A5:D10")
300+ 拡張後のeffective_rangeを渡すこと(軸拡張済み)
301+ frozen_rows: 固定行数
302+
303+ Returns:
304+ ヘッダー範囲(例: "A1:D2")またはNone
305+
306+ 早期リターン条件:
307+ - frozen_rows=0: ヘッダーなし
308+ - start_row == 1: 既に1行目から開始(ヘッダー全体を含む)
309+
310+ 部分的な重なり処理:
311+ - frozen_rows=2, cell_range="A2:B6" の場合
312+ → 不足分 "A1:B1" を返して、最終的に "A1:B6" になる
313+ """
314+ # frozen_rowsが0の場合はヘッダーなし
315+ if frozen_rows == 0 :
316+ return None
317+
318+ # セル範囲を解析
319+ # "A5:D10" -> start="A5", end="D10"
320+ if ":" in cell_range :
321+ start , end = cell_range .split (":" )
322+ else :
323+ # 単一セル(例: "B5")
324+ start = end = cell_range
325+
326+ # 開始セルの座標を解析
327+ start_col_letter , start_row = coordinate_from_string (start )
328+ end_col_letter , _ = coordinate_from_string (end )
329+
330+ # 既に1行目から開始している場合は追加不要(ヘッダー全体を含む)
331+ if start_row == 1 :
332+ return None
333+
334+ # 部分的な重なりがある場合は、不足している上部のヘッダー行を追加
335+ if start_row <= frozen_rows :
336+ # 1行目から(start_row-1)行目までを追加
337+ header_range = f"{ start_col_letter } 1:{ end_col_letter } { start_row - 1 } "
338+ return header_range
339+
340+ # ヘッダー範囲を計算: {start_col}1:{end_col}{frozen_rows}
341+ header_range = f"{ start_col_letter } 1:{ end_col_letter } { frozen_rows } "
342+ return header_range
343+
344+ def _merge_ranges (self , range1 : str , range2 : str ) -> str :
345+ """
346+ 2つのセル範囲を結合して、最小の包含範囲を返す
347+
348+ Args:
349+ range1: 範囲1(例: "A1:B2")
350+ range2: 範囲2(例: "A4:B6")
351+
352+ Returns:
353+ 結合された範囲(例: "A1:B6")
354+ """
355+ # 範囲1を解析
356+ if ":" in range1 :
357+ start1 , end1 = range1 .split (":" )
358+ else :
359+ start1 = end1 = range1
360+
361+ # 範囲2を解析
362+ if ":" in range2 :
363+ start2 , end2 = range2 .split (":" )
364+ else :
365+ start2 = end2 = range2
366+
367+ # 座標を取得
368+ col1_start , row1_start = coordinate_from_string (start1 )
369+ col1_end , row1_end = coordinate_from_string (end1 )
370+ col2_start , row2_start = coordinate_from_string (start2 )
371+ col2_end , row2_end = coordinate_from_string (end2 )
372+
373+ # 最小/最大の列を決定
374+ col_start_idx = min (
375+ column_index_from_string (col1_start ), column_index_from_string (col2_start )
376+ )
377+ col_end_idx = max (
378+ column_index_from_string (col1_end ), column_index_from_string (col2_end )
379+ )
380+
381+ # 最小/最大の行を決定
382+ row_start = min (row1_start , row2_start )
383+ row_end = max (row1_end , row2_end )
384+
385+ # 列インデックスを文字に変換
386+ col_start = get_column_letter (col_start_idx )
387+ col_end = get_column_letter (col_end_idx )
388+
389+ return f"{ col_start } { row_start } :{ col_end } { row_end } "
390+
286391 def _parse_sheet (
287392 self ,
288393 sheet ,
289394 cell_range : str | None = None ,
395+ include_frozen_rows : bool = True ,
290396 ) -> dict [str , Any ]:
291397 """
292398 シートを解析してdict形式で返す
293399
294400 Args:
295401 sheet: openpyxl Worksheet
296402 cell_range: セル範囲指定(例: "A1:D10")
403+ include_frozen_rows: cell_range指定時に固定行(ヘッダー)を自動追加
297404
298405 Returns:
299406 シートデータのdict
@@ -332,6 +439,7 @@ def _parse_sheet(
332439 # セル範囲の正規化・拡張(cell_rangeがある場合)
333440 # マージセル情報のキャッシュに使用するため、先に計算する
334441 effective_range_for_merge = None
442+ header_range = None # ヘッダー範囲(再利用のため事前に初期化)
335443 if cell_range :
336444 sheet_data ["requested_range" ] = cell_range
337445 effective_range = self ._normalize_column_range (cell_range , sheet )
@@ -355,18 +463,17 @@ def _parse_sheet(
355463 sheet_data ["effective_range" ] = effective_range
356464 effective_range_for_merge = effective_range
357465
358- # マージセル情報をキャッシュ(パフォーマンス最適化)
359- # 計算済みのeffective_range(effective_range_for_merge)を渡してキャッシュを構築し、
360- # 戻り値としてmerged_ranges(結合セル範囲の一覧)を取得することで重複計算を回避
361- merged_cell_map , merged_anchor_value_map , merged_ranges = (
362- self ._build_merged_cell_cache (sheet , effective_range_for_merge )
363- )
364-
365- # ここは「結合セルがある時だけ」返す
366- if merged_ranges :
367- sheet_data ["merged_ranges" ] = merged_ranges
466+ # ヘッダー自動追加の場合、マージセルキャッシュにもヘッダー範囲を含める
467+ if include_frozen_rows and frozen_rows > 0 :
468+ header_range = self ._calculate_header_range (effective_range , frozen_rows )
469+ if header_range :
470+ # ヘッダー範囲とデータ範囲を結合した範囲を計算
471+ effective_range_for_merge = self ._merge_ranges (
472+ header_range , effective_range
473+ )
368474
369- # セル範囲のデータサイズ検証とデータ取得
475+ # データサイズ検証(DoS対策)
476+ # マージセルキャッシュ構築前に検証することで、巨大な範囲によるメモリ枯渇を防ぐ
370477 all_rows = []
371478
372479 if cell_range :
@@ -384,18 +491,6 @@ def _parse_sheet(
384491 f"例: cell_range='A1:Z1000'"
385492 )
386493
387- else :
388- # 通常のセル範囲取得
389- range_data = sheet [effective_range ]
390- rows_to_process = self ._normalize_range_data (range_data )
391- all_rows .extend (
392- self ._parse_rows (
393- rows_to_process ,
394- merged_cell_map ,
395- merged_anchor_value_map ,
396- )
397- )
398-
399494 elif sheet .dimensions :
400495 # シート全体を取得
401496 # データサイズ検証(DoS対策)
@@ -411,6 +506,45 @@ def _parse_sheet(
411506 f"例: cell_range='A1:Z1000'"
412507 )
413508
509+ # データサイズ検証後にマージセル情報をキャッシュ(パフォーマンス最適化 + DoS対策)
510+ # 計算済みのeffective_range(effective_range_for_merge)を渡してキャッシュを構築し、
511+ # 戻り値としてmerged_ranges(結合セル範囲の一覧)を取得することで重複計算を回避
512+ merged_cell_map , merged_anchor_value_map , merged_ranges = (
513+ self ._build_merged_cell_cache (sheet , effective_range_for_merge )
514+ )
515+
516+ # ここは「結合セルがある時だけ」返す
517+ if merged_ranges :
518+ sheet_data ["merged_ranges" ] = merged_ranges
519+
520+ # データ取得
521+ if cell_range :
522+ # ヘッダー自動追加(include_frozen_rows=Trueの場合)
523+ # header_rangeは既に計算済みなので再利用
524+ if header_range :
525+ # ヘッダー範囲を取得
526+ header_data = sheet [header_range ]
527+ header_rows = self ._normalize_range_data (header_data )
528+ all_rows .extend (
529+ self ._parse_rows (
530+ header_rows ,
531+ merged_cell_map ,
532+ merged_anchor_value_map ,
533+ )
534+ )
535+
536+ # 通常のセル範囲取得(データ範囲)
537+ range_data = sheet [effective_range ]
538+ rows_to_process = self ._normalize_range_data (range_data )
539+ all_rows .extend (
540+ self ._parse_rows (
541+ rows_to_process ,
542+ merged_cell_map ,
543+ merged_anchor_value_map ,
544+ )
545+ )
546+
547+ elif sheet .dimensions :
414548 # 全データを取得
415549 rows_to_process = tuple (sheet .iter_rows ())
416550
0 commit comments