@@ -37,6 +37,7 @@ def search_cells(
3737 file_path : str ,
3838 query : str ,
3939 sheet_name : str | None = None ,
40+ include_row_data : bool = False ,
4041 ) -> str :
4142 """
4243 セル内容を検索して該当位置を返す
@@ -70,25 +71,35 @@ def search_cells(
7071 # sheet_name 指定がある場合はそのシートを優先して検索
7172 if sheet_name :
7273 if sheet_name in workbook .sheetnames :
73- self ._scan_sheet (workbook [sheet_name ], sheet_name , query , matches )
74+ self ._scan_sheet (
75+ workbook [sheet_name ],
76+ sheet_name ,
77+ query ,
78+ matches ,
79+ include_row_data ,
80+ )
7481
7582 # マッチが無ければ全シート走査にフォールバック
7683 if len (matches ) == 0 :
7784 for sn in workbook .sheetnames :
7885 if sn == sheet_name :
7986 continue
80- self ._scan_sheet (workbook [sn ], sn , query , matches )
87+ self ._scan_sheet (
88+ workbook [sn ], sn , query , matches , include_row_data
89+ )
8190 else :
8291 # sheet_name が存在しない場合は「指定なし」と同じ扱いで全シート検索
8392 warnings .append (
8493 f"Sheet '{ sheet_name } ' not found. Searching all sheets instead."
8594 )
8695 for sn in workbook .sheetnames :
87- self ._scan_sheet (workbook [sn ], sn , query , matches )
96+ self ._scan_sheet (
97+ workbook [sn ], sn , query , matches , include_row_data
98+ )
8899 else :
89100 # 全シート検索
90101 for sn in workbook .sheetnames :
91- self ._scan_sheet (workbook [sn ], sn , query , matches )
102+ self ._scan_sheet (workbook [sn ], sn , query , matches , include_row_data )
92103
93104 logger .info (f"Found { len (matches )} matches for query '{ query } '" )
94105
@@ -273,6 +284,7 @@ def _scan_sheet(
273284 sheet_name_for_result : str ,
274285 query : str ,
275286 matches : list [dict [str , Any ]],
287+ include_row_data : bool = False ,
276288 ) -> None :
277289 """
278290 シート内のセルを走査してqueryに一致するセルをmatchesに追加する
@@ -284,31 +296,72 @@ def _scan_sheet(
284296 # その場合はiter_rows()を使用するフォールバックロジックが動作します。
285297 if hasattr (sheet , "_cells" ):
286298 # 実在セルのみを走査(高速)
299+ # まずマッチを収集(_cellsのイテレーション中にsheetアクセスすると辞書が変わるため)
300+ new_matches : list [dict [str , Any ]] = []
287301 for cell in sheet ._cells .values ():
288302 if cell .value is not None :
289303 cell_value_str = str (cell .value )
290304 if query in cell_value_str :
291- matches .append (
305+ new_matches .append (
292306 {
293307 "sheet" : sheet_name_for_result ,
294308 "coordinate" : cell .coordinate ,
295309 "value" : self ._serialize_value (cell .value ),
310+ "_row" : cell .row ,
296311 }
297312 )
313+ # イテレーション完了後に行データを取得
314+ for match in new_matches :
315+ row_num = match .pop ("_row" )
316+ if include_row_data :
317+ match ["row_data" ] = self ._get_row_data (sheet , row_num )
318+ matches .append (match )
298319 else :
299320 # openpyxl公開APIを使用(互換性確保)
300321 for row in sheet .iter_rows (values_only = False ):
301322 for cell in row :
302323 if cell .value is not None :
303324 cell_value_str = str (cell .value )
304325 if query in cell_value_str :
305- matches .append (
306- {
307- "sheet" : sheet_name_for_result ,
308- "coordinate" : cell .coordinate ,
309- "value" : self ._serialize_value (cell .value ),
310- }
311- )
326+ match = {
327+ "sheet" : sheet_name_for_result ,
328+ "coordinate" : cell .coordinate ,
329+ "value" : self ._serialize_value (cell .value ),
330+ }
331+ if include_row_data :
332+ match ["row_data" ] = [
333+ {
334+ "coordinate" : c .coordinate ,
335+ "value" : self ._serialize_value (c .value ),
336+ }
337+ for c in row
338+ if c .value is not None
339+ ]
340+ matches .append (match )
341+
342+ def _get_row_data (self , sheet , row_num : int ) -> list [dict [str , Any ]]:
343+ """
344+ 指定行の非nullセルデータをリストとして返す
345+
346+ Args:
347+ sheet: openpyxl Worksheet
348+ row_num: 行番号
349+
350+ Returns:
351+ 非nullセルの [{coordinate, value}, ...] リスト
352+ """
353+ row_cells = sheet [row_num ]
354+ # 単一列シートではCellオブジェクト単体が返される場合がある
355+ if isinstance (row_cells , Cell ):
356+ row_cells = (row_cells ,)
357+ return [
358+ {
359+ "coordinate" : c .coordinate ,
360+ "value" : self ._serialize_value (c .value ),
361+ }
362+ for c in row_cells
363+ if c .value is not None
364+ ]
312365
313366 def _parse_sheet (
314367 self ,
0 commit comments