Skip to content

Commit 8151cc7

Browse files
k-ibarakiclaude
andcommitted
feat: add include_cell_styles parameter for Excel cell formatting
Issue #43対応: Excelセル書式情報(背景色・列幅・行高さ)を取得する include_cell_stylesパラメータを追加 設計方針: - デフォルトはFalse(YAGNI原則、トークン消費最小化) - 色分けされたデータ抽出時のみ使用を想定 - 実測でトークン消費+約20% 変更内容: - src/sharepoint_excel.py: _parse_cell()にスタイル情報取得ロジック追加 - src/server.py: sharepoint_excel()にinclude_cell_stylesパラメータ追加 - tests/: 新規テスト4件追加、既存テスト3件修正 - README: パラメータドキュメント追加 LLMフィードバックに基づき、説明文を簡潔化: - "default: false"を明記 - 用途を限定("色分けデータ抽出時のみ") - 冗長な例示を削除 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 762a50e commit 8151cc7

6 files changed

Lines changed: 182 additions & 5 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ Two authentication methods are supported:
3737
- Read mode: get data from specific sheets/ranges with `sheet` and `cell_range` parameters
3838
- **Automatic header inclusion**: when `cell_range` is specified, frozen rows (headers) are automatically included by default
3939
- Set `include_frozen_rows=False` to get only the specified range
40+
- **Cell style information** (optional): set `include_cell_styles=True` to get background colors, column widths, and row heights
41+
- Default is `False` to minimize token usage
42+
- Useful for identifying highlighted cells, colored headers, or visually emphasized content
4043
- Response includes cell data in `rows` (value and coordinate) and structural information when available
4144
- Structural info: sheet name, dimensions, frozen_rows, frozen_cols, freeze_panes (when present), merged_ranges (when merged cells exist)
4245
- No Excel Services dependency - uses direct file download + openpyxl parsing

README_ja.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ stdioとHTTPの両方のトランスポートに対応しています。
3737
- 読み取りモード: `sheet``cell_range`パラメータで特定シート/範囲を取得
3838
- **ヘッダー自動追加**: `cell_range`指定時、デフォルトで固定行(ヘッダー)を自動的に含める
3939
- `include_frozen_rows=False`を指定すると、指定範囲のみを取得
40+
- **セルスタイル情報**(オプション): `include_cell_styles=True`を指定すると、背景色・列幅・行高さを取得
41+
- デフォルトは`False`でトークン消費を最小化
42+
- 強調表示されたセル、色付きヘッダー、視覚的に強調されたコンテンツの識別に便利
4043
- レスポンスには`rows`内のセルデータ(値と座標)と構造情報(利用可能な場合)を含む
4144
- 構造情報: シート名、dimensions、frozen_rows、frozen_cols、freeze_panes(存在する場合)、merged_ranges(結合セルが存在する場合)
4245
- Excel Services不要 - 直接ファイルダウンロード+openpyxl解析方式

src/server.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,7 @@ def sharepoint_excel(
454454
sheet: str | None = None,
455455
cell_range: str | None = None,
456456
include_frozen_rows: bool = True,
457+
include_cell_styles: bool = False,
457458
ctx: Context | None = None,
458459
) -> str:
459460
"""
@@ -471,6 +472,10 @@ def sharepoint_excel(
471472
include_frozen_rows: cell_range指定時に固定行を自動追加(デフォルト: True)
472473
True: frozen_rowsで指定された行(通常はヘッダー)を自動的に取得
473474
False: 指定されたcell_rangeのみを取得
475+
include_cell_styles: セルの色・サイズ情報(default: false)
476+
色分けされたデータを抽出する場合のみTrueを指定
477+
背景色(fill)、列幅(width)、行高さ(height)を取得
478+
※トークン消費が約20%増加
474479
ctx: FastMCP context (injected automatically)
475480
476481
Returns:
@@ -498,6 +503,7 @@ def sharepoint_excel(
498503
sheet_name=sheet,
499504
cell_range=cell_range,
500505
include_frozen_rows=include_frozen_rows,
506+
include_cell_styles=include_cell_styles,
501507
)
502508

503509
except Exception as e:
@@ -542,6 +548,7 @@ def register_tools():
542548
"frozen at the top of the sheet (typically column headers). "
543549
"Response includes cell data in 'rows' (value and coordinate) and structural information "
544550
"(sheet name, dimensions, frozen_rows, frozen_cols, freeze_panes when present, merged_ranges when merged cells exist). "
551+
"Cell styles (include_cell_styles, default: false): background colors and sizes. Use only for color-coded data extraction. "
545552
"Header detection: Cannot be auto-detected from frozen_rows. "
546553
"ALWAYS read exactly 5 rows for header check: 'A1:Z5' (NOT 'A1:Z50' or more). "
547554
"Prefer 'query' search when possible to locate data first. "

src/sharepoint_excel.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def parse_to_json(
107107
sheet_name: str | None = None,
108108
cell_range: str | None = None,
109109
include_frozen_rows: bool = True,
110+
include_cell_styles: bool = False,
110111
) -> str:
111112
"""
112113
Excelファイルを解析してJSON形式で返す
@@ -118,13 +119,16 @@ def parse_to_json(
118119
include_frozen_rows: cell_range指定時に固定行(ヘッダー)を自動追加
119120
True(デフォルト): frozen_rowsで指定された行を自動的に取得
120121
False: 指定されたcell_rangeのみを取得
122+
include_cell_styles: セルの色・サイズ情報(default: false)
123+
色分けデータ抽出時のみ使用。トークン消費+約20%
121124
122125
Returns:
123126
JSON文字列
124127
- 各セルのデータ: value(値)、coordinate(座標)
125128
- 構造情報: シート名、dimensions(シート全体のセル範囲、例: "A1:D10")
126129
- 構造情報: frozen_rows(固定行数)、frozen_cols(固定列数)
127130
- 条件付き構造情報: freeze_panes(存在する場合)、merged_ranges(結合セルが存在する場合)
131+
- スタイル情報(include_cell_styles=Trueの場合): fill(背景色)、width(列幅)、height(行高さ)
128132
"""
129133
logger.info(
130134
f"Parsing Excel file: {file_path} (sheet={sheet_name}, range={cell_range})"
@@ -205,6 +209,7 @@ def parse_to_json(
205209
sheet,
206210
cell_range,
207211
include_frozen_rows,
212+
include_cell_styles,
208213
)
209214
result["sheets"].append(sheet_data)
210215

@@ -289,9 +294,7 @@ def _scan_sheet(
289294
}
290295
)
291296

292-
def _calculate_header_range(
293-
self, cell_range: str, frozen_rows: int
294-
) -> str | None:
297+
def _calculate_header_range(self, cell_range: str, frozen_rows: int) -> str | None:
295298
"""
296299
セル範囲に対してfrozen_rowsに基づくヘッダー範囲を計算
297300
@@ -393,6 +396,7 @@ def _parse_sheet(
393396
sheet,
394397
cell_range: str | None = None,
395398
include_frozen_rows: bool = True,
399+
include_cell_styles: bool = False,
396400
) -> dict[str, Any]:
397401
"""
398402
シートを解析してdict形式で返す
@@ -401,6 +405,7 @@ def _parse_sheet(
401405
sheet: openpyxl Worksheet
402406
cell_range: セル範囲指定(例: "A1:D10")
403407
include_frozen_rows: cell_range指定時に固定行(ヘッダー)を自動追加
408+
include_cell_styles: セルのスタイル情報を含めるか
404409
405410
Returns:
406411
シートデータのdict
@@ -467,7 +472,9 @@ def _parse_sheet(
467472

468473
# ヘッダー自動追加の場合、マージセルキャッシュにもヘッダー範囲を含める
469474
if include_frozen_rows and frozen_rows > 0:
470-
header_range = self._calculate_header_range(effective_range, frozen_rows)
475+
header_range = self._calculate_header_range(
476+
effective_range, frozen_rows
477+
)
471478
if header_range:
472479
# ヘッダー範囲とデータ範囲を結合した範囲を計算
473480
effective_range_for_merge = self._merge_ranges(
@@ -530,6 +537,7 @@ def _parse_sheet(
530537
all_rows.extend(
531538
self._parse_rows(
532539
header_rows,
540+
include_cell_styles,
533541
merged_cell_map,
534542
merged_anchor_value_map,
535543
)
@@ -541,6 +549,7 @@ def _parse_sheet(
541549
all_rows.extend(
542550
self._parse_rows(
543551
rows_to_process,
552+
include_cell_styles,
544553
merged_cell_map,
545554
merged_anchor_value_map,
546555
)
@@ -554,6 +563,7 @@ def _parse_sheet(
554563
all_rows.extend(
555564
self._parse_rows(
556565
rows_to_process,
566+
include_cell_styles,
557567
merged_cell_map,
558568
merged_anchor_value_map,
559569
)
@@ -741,6 +751,7 @@ def _expand_axis_range(self, range_str: str) -> str:
741751
def _parse_cell(
742752
self,
743753
cell,
754+
include_cell_styles: bool = False,
744755
merged_cell_map: dict[str, str] | None = None,
745756
merged_anchor_value_map: dict[str, Any] | None = None,
746757
) -> dict[str, Any]:
@@ -749,6 +760,7 @@ def _parse_cell(
749760
750761
Args:
751762
cell: openpyxl Cell
763+
include_cell_styles: セルのスタイル情報を含めるか(デフォルト: False)
752764
merged_cell_map: マージセル座標からマージ範囲へのマップ(パフォーマンス最適化用)
753765
merged_anchor_value_map: マージ範囲 -> アンカー値 のマップ(結合セルの値埋め用)
754766
@@ -776,12 +788,43 @@ def _parse_cell(
776788
if anchor_value is not None:
777789
cell_data["value"] = anchor_value
778790

779-
# 書式情報(fill/width/height/data_type)は現在サポートされていません
791+
# スタイル情報(include_cell_styles=Trueの場合のみ)
792+
if include_cell_styles:
793+
# 背景色情報
794+
if cell.fill and cell.fill.patternType:
795+
fill_info = {
796+
"pattern_type": cell.fill.patternType,
797+
}
798+
fg_color = self._color_to_hex(cell.fill.fgColor)
799+
if fg_color:
800+
fill_info["fg_color"] = fg_color
801+
bg_color = self._color_to_hex(cell.fill.bgColor)
802+
if bg_color:
803+
fill_info["bg_color"] = bg_color
804+
cell_data["fill"] = fill_info
805+
806+
# セルサイズ(列幅・行高さ)
807+
# MergedCellの場合は属性が存在しないため、hasattrでチェック
808+
if hasattr(cell, "column_letter") and hasattr(cell, "row"):
809+
if cell.column_letter and cell.row:
810+
sheet = cell.parent
811+
# 列幅
812+
if cell.column_letter in sheet.column_dimensions:
813+
col_dim = sheet.column_dimensions[cell.column_letter]
814+
if col_dim.width:
815+
cell_data["width"] = col_dim.width
816+
# 行高さ
817+
if cell.row in sheet.row_dimensions:
818+
row_dim = sheet.row_dimensions[cell.row]
819+
if row_dim.height:
820+
cell_data["height"] = row_dim.height
821+
780822
return cell_data
781823

782824
def _parse_rows(
783825
self,
784826
rows: tuple[tuple[Cell, ...], ...],
827+
include_cell_styles: bool = False,
785828
merged_cell_map: dict[str, str] | None = None,
786829
merged_anchor_value_map: dict[str, Any] | None = None,
787830
) -> list[list[dict[str, Any]]]:
@@ -790,6 +833,7 @@ def _parse_rows(
790833
791834
Args:
792835
rows: 行データのタプル
836+
include_cell_styles: セルのスタイル情報を含めるか
793837
merged_cell_map: マージセル情報
794838
merged_anchor_value_map: マージ範囲 -> アンカー値
795839
@@ -801,6 +845,7 @@ def _parse_rows(
801845
row_data = [
802846
self._parse_cell(
803847
cell,
848+
include_cell_styles,
804849
merged_cell_map,
805850
merged_anchor_value_map,
806851
)

tests/test_server.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ def test_excel_read_default(
225225
sheet_name=None,
226226
cell_range=None,
227227
include_frozen_rows=True,
228+
include_cell_styles=False,
228229
)
229230

230231
@pytest.mark.unit
@@ -265,6 +266,7 @@ def test_excel_with_sheet_parameter(
265266
sheet_name="Sheet2",
266267
cell_range=None,
267268
include_frozen_rows=True,
269+
include_cell_styles=False,
268270
)
269271

270272
@pytest.mark.unit
@@ -287,6 +289,7 @@ def test_excel_with_cell_range_parameter(
287289
sheet_name="Sheet1",
288290
cell_range="A1:D10",
289291
include_frozen_rows=True,
292+
include_cell_styles=False,
290293
)
291294

292295
@pytest.mark.unit

tests/test_sharepoint_excel.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,3 +1389,119 @@ def test_frozen_rows_dos_mitigation_custom_limit(self):
13891389

13901390
sheet_data = result["sheets"][0]
13911391
assert sheet_data["frozen_rows"] == 120
1392+
1393+
def test_parse_with_cell_styles_disabled(self):
1394+
"""include_cell_styles=False(デフォルト)でスタイル情報が含まれないこと"""
1395+
excel_bytes = self._create_formatted_excel()
1396+
self.mock_download_client.download_file.return_value = excel_bytes
1397+
1398+
parser = SharePointExcelParser(self.mock_download_client)
1399+
# デフォルト(include_cell_styles=False)で解析
1400+
result_json = parser.parse_to_json("/test/formatted.xlsx")
1401+
1402+
result = json.loads(result_json)
1403+
cell = result["sheets"][0]["rows"][0][0]
1404+
1405+
# value と coordinate は含まれる
1406+
assert "value" in cell
1407+
assert "coordinate" in cell
1408+
assert cell["value"] == "Name"
1409+
1410+
# スタイル情報は含まれない
1411+
assert "fill" not in cell
1412+
assert "width" not in cell
1413+
assert "height" not in cell
1414+
1415+
def test_parse_with_cell_styles_enabled(self):
1416+
"""include_cell_styles=Trueでスタイル情報が含まれること"""
1417+
excel_bytes = self._create_formatted_excel()
1418+
self.mock_download_client.download_file.return_value = excel_bytes
1419+
1420+
parser = SharePointExcelParser(self.mock_download_client)
1421+
# include_cell_styles=Trueで解析
1422+
result_json = parser.parse_to_json(
1423+
"/test/formatted.xlsx", include_cell_styles=True
1424+
)
1425+
1426+
result = json.loads(result_json)
1427+
cell = result["sheets"][0]["rows"][0][0]
1428+
1429+
# 基本情報は含まれる
1430+
assert cell["value"] == "Name"
1431+
assert cell["coordinate"] == "A1"
1432+
1433+
# 背景色情報が含まれる
1434+
assert "fill" in cell
1435+
assert cell["fill"]["pattern_type"] == "solid"
1436+
assert "fg_color" in cell["fill"]
1437+
# 16進数形式であることを確認
1438+
if cell["fill"]["fg_color"]:
1439+
assert cell["fill"]["fg_color"].startswith("#")
1440+
1441+
def test_cell_styles_with_width_height(self):
1442+
"""列幅・行高さが設定されたセルでwidth/heightが正しく返されること"""
1443+
wb = Workbook()
1444+
ws = wb.active
1445+
ws.title = "SizedSheet"
1446+
1447+
# セルにデータを設定
1448+
ws["A1"] = "Wide Column"
1449+
ws["A2"] = "Tall Row"
1450+
1451+
# 列幅と行高さを設定
1452+
ws.column_dimensions["A"].width = 30
1453+
ws.row_dimensions[2].height = 50
1454+
1455+
excel_bytes = BytesIO()
1456+
wb.save(excel_bytes)
1457+
excel_bytes.seek(0)
1458+
1459+
self.mock_download_client.download_file.return_value = excel_bytes.getvalue()
1460+
1461+
parser = SharePointExcelParser(self.mock_download_client)
1462+
result_json = parser.parse_to_json("/test/sized.xlsx", include_cell_styles=True)
1463+
1464+
result = json.loads(result_json)
1465+
rows = result["sheets"][0]["rows"]
1466+
1467+
# A1セル: 列幅が設定されている
1468+
cell_a1 = rows[0][0]
1469+
assert cell_a1["coordinate"] == "A1"
1470+
assert "width" in cell_a1
1471+
assert cell_a1["width"] == 30
1472+
1473+
# A2セル: 列幅と行高さが設定されている
1474+
cell_a2 = rows[1][0]
1475+
assert cell_a2["coordinate"] == "A2"
1476+
assert "width" in cell_a2
1477+
assert cell_a2["width"] == 30
1478+
assert "height" in cell_a2
1479+
assert cell_a2["height"] == 50
1480+
1481+
def test_cell_styles_none_when_not_set(self):
1482+
"""スタイルが設定されていないセルではfillなどが含まれないこと"""
1483+
# シンプルなExcelファイルを作成(スタイルなし)
1484+
excel_bytes = self._create_test_excel()
1485+
self.mock_download_client.download_file.return_value = excel_bytes
1486+
1487+
parser = SharePointExcelParser(self.mock_download_client)
1488+
# include_cell_styles=Trueで解析
1489+
result_json = parser.parse_to_json(
1490+
"/test/file.xlsx", include_cell_styles=True
1491+
)
1492+
1493+
result = json.loads(result_json)
1494+
cell = result["sheets"][0]["rows"][0][0]
1495+
1496+
# 基本情報は含まれる
1497+
assert cell["value"] == "Name"
1498+
assert cell["coordinate"] == "A1"
1499+
1500+
# スタイルが設定されていないので、fillやwidth/heightは含まれない
1501+
# (openpyxlがデフォルト値を返す場合もあるが、明示的に設定されていない)
1502+
# fill情報がない、またはpattern_typeがNoneの場合は含まれない
1503+
if "fill" in cell:
1504+
# fillが含まれる場合でも、pattern_typeが設定されていないはず
1505+
assert cell["fill"].get("pattern_type") is None or cell["fill"].get(
1506+
"pattern_type"
1507+
) == "none"

0 commit comments

Comments
 (0)