Skip to content

Commit ecd433a

Browse files
feat:xml変換
1 parent ecfca9e commit ecd433a

File tree

3 files changed

+172
-33
lines changed

3 files changed

+172
-33
lines changed

tools/dify-plugin-excel-to-xml.py

Lines changed: 164 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,192 @@
11
from collections.abc import Generator
22
from typing import Any
3-
import pandas as pd
4-
from io import BytesIO
5-
import json
63
import requests
7-
from urllib.parse import urlparse
4+
import openpyxl
5+
from openpyxl.utils import get_column_letter
6+
from io import BytesIO
87

98
from dify_plugin import Tool
109
from dify_plugin.entities.tool import ToolInvokeMessage
1110

11+
def get_important_cell_styles(cell) -> dict:
12+
"""デフォルトから変更のある重要なスタイル情報のみを取得"""
13+
styles = {}
14+
15+
# セル結合情報の取得
16+
if cell.parent.merged_cells: # merged_cellsはワークシートの属性
17+
for merged_range in cell.parent.merged_cells.ranges:
18+
if cell.coordinate in merged_range:
19+
styles['merge'] = {
20+
'start': merged_range.coord.split(':')[0], # 結合開始セル
21+
'end': merged_range.coord.split(':')[1] # 結合終了セル
22+
}
23+
break
24+
25+
# 罫線情報(デフォルトのNoneまたはthinは除外)
26+
borders = {}
27+
for side in ['top', 'bottom', 'left', 'right']:
28+
border = getattr(cell.border, side)
29+
if border.style and border.style not in ['thin', None]:
30+
borders[side] = border.style
31+
if borders:
32+
styles['borders'] = borders
33+
34+
# 背景色(デフォルトの白や透明以外で、かつ有効な値の場合のみ)
35+
if (cell.fill and cell.fill.start_color and
36+
cell.fill.start_color.rgb and
37+
cell.fill.start_color.rgb not in ['FFFFFFFF', '00000000'] and # 白と透明を除外
38+
isinstance(cell.fill.start_color.rgb, str)):
39+
styles['background'] = cell.fill.start_color.rgb
40+
41+
# 文字色(デフォルトの黒以外で、かつ有効な値の場合のみ)
42+
if (cell.font and cell.font.color and
43+
cell.font.color.rgb and
44+
cell.font.color.rgb != 'FF000000' and
45+
isinstance(cell.font.color.rgb, str)):
46+
styles['color'] = cell.font.color.rgb
47+
48+
return styles if styles else None
49+
50+
def create_xml_with_styles(wb) -> str:
51+
"""ワークシートの内容と変更のあるスタイル情報のみをXMLに変換"""
52+
ws = wb.active
53+
xml_parts = ['<?xml version="1.0" encoding="UTF-8"?>\n<workbook>']
54+
55+
# データ範囲を取得
56+
data_rows = list(ws.rows)
57+
if not data_rows:
58+
return "<workbook></workbook>"
59+
60+
xml_parts.append(' <worksheet>')
61+
62+
for row_idx, row in enumerate(data_rows, 1):
63+
has_content = False
64+
row_parts = []
65+
66+
for col_idx, cell in enumerate(row, 1):
67+
col_letter = get_column_letter(col_idx)
68+
value = cell.value if cell.value is not None else ""
69+
styles = get_important_cell_styles(cell)
70+
71+
# 値かスタイルがある場合のみ出力
72+
if value or styles:
73+
has_content = True
74+
cell_parts = [f' <cell ref="{col_letter}{row_idx}">']
75+
76+
if value:
77+
cell_parts.append(f' <value>{value}</value>')
78+
79+
if styles:
80+
for style_type, style_value in styles.items():
81+
if isinstance(style_value, dict):
82+
# 罫線情報の場合
83+
if style_value: # 空の辞書は出力しない
84+
cell_parts.append(f' <{style_type}>')
85+
for side, style in style_value.items():
86+
cell_parts.append(f' <border side="{side}" style="{style}"/>')
87+
cell_parts.append(f' </{style_type}>')
88+
else:
89+
# 背景色や文字色の場合(有効な値の場合のみ)
90+
cell_parts.append(f' <{style_type}>{style_value}</{style_type}>')
91+
92+
cell_parts.append(' </cell>')
93+
row_parts.extend(cell_parts)
94+
95+
# 行に内容がある場合のみ出力
96+
if has_content:
97+
xml_parts.append(f' <row index="{row_idx}">')
98+
xml_parts.extend(row_parts)
99+
xml_parts.append(' </row>')
100+
101+
xml_parts.append(' </worksheet>')
102+
xml_parts.append('</workbook>')
103+
104+
return '\n'.join(xml_parts)
105+
106+
def get_url_from_file_data(file_data: Any) -> str:
107+
"""ファイルデータからURLを抽出する"""
108+
if isinstance(file_data, str):
109+
# 直接URLが渡された場合
110+
return file_data
111+
elif isinstance(file_data, list) and len(file_data) > 0:
112+
# 配列形式で渡された場合
113+
first_item = file_data[0]
114+
if isinstance(first_item, dict) and 'url' in first_item:
115+
return first_item['url']
116+
elif isinstance(file_data, dict) and 'url' in file_data:
117+
# 辞書形式で渡された場合
118+
return file_data['url']
119+
elif hasattr(file_data, 'url'):
120+
# Fileオブジェクトの場合
121+
return file_data.url
122+
return None
123+
12124
class DifyPluginExcelToXMLTool(Tool):
13125
def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage, None, None]:
14-
# ファイルURLを取得
15-
file_url = tool_parameters.get("file_url", "")
16-
if not file_url:
126+
# フバッグ用にパラメータの内容を出力(ファイルオブジェクトを除外)
127+
try:
128+
print("Debug - tool_parameters keys:", tool_parameters.keys())
129+
except:
130+
print("Debug - tool_parameters: <not serializable>")
131+
132+
# ファイルオブジェクトを取得
133+
file_data = tool_parameters.get("file_url")
134+
try:
135+
if isinstance(file_data, dict):
136+
print("Debug - file_data keys:", file_data.keys())
137+
elif isinstance(file_data, list):
138+
print("Debug - file_data is list of length:", len(file_data))
139+
if len(file_data) > 0:
140+
print("Debug - first item keys:", file_data[0].keys())
141+
else:
142+
print("Debug - file_data type:", type(file_data))
143+
except:
144+
print("Debug - file_data: <not serializable>")
145+
146+
if not file_data:
17147
yield ToolInvokeMessage(
18148
type="text",
19-
message={"text": "ファイルURLが提供されていません。"}
149+
message={"text": "ファイルが提供されていません。"}
20150
)
21151
return
22152

23153
try:
24-
# URLの検証
25-
parsed_url = urlparse(file_url)
26-
if not parsed_url.scheme or not parsed_url.netloc:
27-
raise ValueError("無効なURLです")
154+
# ファイルのURLを取得
155+
file_url = get_url_from_file_data(file_data)
156+
print("Debug - file_url:", file_url) # 取得したURLを確認
157+
158+
if not file_url:
159+
yield ToolInvokeMessage(
160+
type="text",
161+
message={"text": "ファイルのURLが見つかりません。"}
162+
)
163+
return
28164

29165
# ファイルをダウンロード
30-
response = requests.get(file_url)
31-
response.raise_for_status()
32-
33-
# ExcelファイルをPandasで読み込む
34-
excel_data = BytesIO(response.content)
35-
df = pd.read_excel(excel_data)
166+
try:
167+
response = requests.get(file_url)
168+
response.raise_for_status()
169+
file_content = response.content
170+
except Exception as e:
171+
yield ToolInvokeMessage(
172+
type="text",
173+
message={"text": f"ファイルのダウンロードに失敗しました: {str(e)}"}
174+
)
175+
return
176+
177+
# ExcelファイルをOpenpyxlで読み込む
178+
excel_data = BytesIO(file_content)
179+
wb = openpyxl.load_workbook(excel_data)
36180

37-
# XMLに変換
38-
xml_data = df.to_xml(index=False, root_name="data", row_name="row")
181+
# XMLに変換(罫線情報を含む)
182+
xml_data = create_xml_with_styles(wb)
39183

40184
# XML形式のテキストを返す
41185
yield ToolInvokeMessage(
42186
type="text",
43187
message={"text": xml_data}
44188
)
45189

46-
except requests.exceptions.RequestException as e:
47-
yield ToolInvokeMessage(
48-
type="text",
49-
message={"text": f"ファイルのダウンロードに失敗しました: {str(e)}"}
50-
)
51190
except Exception as e:
52191
yield ToolInvokeMessage(
53192
type="text",

tools/dify-plugin-excel-to-xml.yaml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ parameters:
1818
type: file
1919
required: true
2020
label:
21-
en_US: Excel File URL
22-
ja_JP: ExcelファイルのURL
23-
zh_Hans: Excel文件URL
21+
en_US: Excel File
22+
ja_JP: Excelファイル
23+
zh_Hans: Excel文件
2424
human_description:
25-
en_US: The URL of the Excel file to convert
26-
ja_JP: 変換するExcelファイルのURL
27-
zh_Hans: 需要转换的Excel文件的URL
28-
llm_description: The URL of the Excel file that needs to be converted to XML format
25+
en_US: The Excel file to convert to XML
26+
ja_JP: XMLに変換するExcelファイル
27+
zh_Hans: 需要转换为XML的Excel文件
28+
llm_description: The Excel file that needs to be converted to XML format
2929
form: llm
3030

3131
extra:

tools/dify-plugin-markdown-to-excel.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ identity:
44
label:
55
en_US: Markdown to Excel
66
ja_JP: マークダウンからExcel
7-
zh_Hans: Markdown转Excel
7+
zh_Hans: Markdown to Excel
88

99
description:
1010
human:

0 commit comments

Comments
 (0)