|
1 | 1 | from collections.abc import Generator |
2 | 2 | from typing import Any |
3 | | -import pandas as pd |
4 | | -from io import BytesIO |
5 | | -import json |
6 | 3 | import requests |
7 | | -from urllib.parse import urlparse |
| 4 | +import openpyxl |
| 5 | +from openpyxl.utils import get_column_letter |
| 6 | +from io import BytesIO |
8 | 7 |
|
9 | 8 | from dify_plugin import Tool |
10 | 9 | from dify_plugin.entities.tool import ToolInvokeMessage |
11 | 10 |
|
| 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 | + |
12 | 124 | class DifyPluginExcelToXMLTool(Tool): |
13 | 125 | 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: |
17 | 147 | yield ToolInvokeMessage( |
18 | 148 | type="text", |
19 | | - message={"text": "ファイルURLが提供されていません。"} |
| 149 | + message={"text": "ファイルが提供されていません。"} |
20 | 150 | ) |
21 | 151 | return |
22 | 152 |
|
23 | 153 | 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 |
28 | 164 |
|
29 | 165 | # ファイルをダウンロード |
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) |
36 | 180 |
|
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) |
39 | 183 |
|
40 | 184 | # XML形式のテキストを返す |
41 | 185 | yield ToolInvokeMessage( |
42 | 186 | type="text", |
43 | 187 | message={"text": xml_data} |
44 | 188 | ) |
45 | 189 |
|
46 | | - except requests.exceptions.RequestException as e: |
47 | | - yield ToolInvokeMessage( |
48 | | - type="text", |
49 | | - message={"text": f"ファイルのダウンロードに失敗しました: {str(e)}"} |
50 | | - ) |
51 | 190 | except Exception as e: |
52 | 191 | yield ToolInvokeMessage( |
53 | 192 | type="text", |
|
0 commit comments