From e8885401cfe9edb0dcd8f4f9cb5cadfbd47ef168 Mon Sep 17 00:00:00 2001 From: ivanberry Date: Wed, 11 Mar 2026 15:40:54 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E7=BF=BB?= =?UTF-8?q?=E8=AF=91=E6=B5=81=E7=A8=8B=E4=B8=BA=E4=B8=89=E9=98=B6=E6=AE=B5?= =?UTF-8?q?=EF=BC=88=E6=8F=90=E5=8F=96=E2=86=92=E7=BF=BB=E8=AF=91=E2=86=92?= =?UTF-8?q?=E5=BA=94=E7=94=A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新流程: 1. 提取:收集所有中文内容及其位置映射(CellPosition) 2. 翻译:批量翻译所有中文内容(一次 API 调用) 3. 应用:将翻译结果写入新 Excel 文件 优势: - 清晰的职责分离 - 完整的映射关系(Sheet、行、列) - 批量翻译减少 API 调用次数 - 更容易调试和重试 - 支持 dry-run 预览模式 --- scripts/translate_excel.py | 421 ++++++++++++++++++++++++------------- 1 file changed, 270 insertions(+), 151 deletions(-) diff --git a/scripts/translate_excel.py b/scripts/translate_excel.py index e085f58..dd23eec 100755 --- a/scripts/translate_excel.py +++ b/scripts/translate_excel.py @@ -3,15 +3,19 @@ """ Excel 文件中文→英文翻译工具 使用 Google Gemini Flash Lite API 进行翻译 + +流程: +1. 提取:收集所有中文内容及其位置映射 +2. 翻译:批量翻译所有中文内容 +3. 应用:将翻译结果写入新 Excel 文件 """ from __future__ import annotations import argparse import json -import math -import re import sys +from dataclasses import dataclass, field from pathlib import Path from typing import Any @@ -38,6 +42,47 @@ except ImportError as exc: ) from exc +@dataclass +class CellPosition: + """单元格位置信息""" + sheet: str + row: int + col: int + col_letter: str = field(init=False) + + def __post_init__(self): + # 将列索引转换为字母(1→A, 2→B, 27→AA) + self.col_letter = self._index_to_letter(self.col) + + @staticmethod + def _index_to_letter(col: int) -> str: + """将列索引转换为 Excel 列字母""" + result = "" + while col > 0: + col -= 1 + result = chr(65 + (col % 26)) + result + col //= 26 + return result + + @property + def cell_ref(self) -> str: + """返回 Excel 单元格引用(如:A1, B2)""" + return f"{self.col_letter}{self.row}" + + @property + def full_ref(self) -> str: + """返回完整引用(如:Sheet1!A1)""" + return f"{self.sheet}!{self.cell_ref}" + + +@dataclass +class TranslationEntry: + """翻译条目""" + position: CellPosition + original: str + translated: str = "" + + def detect_chinese(text: str) -> bool: """检测文本中是否包含中文字符""" if not text or not isinstance(text, str): @@ -45,15 +90,6 @@ def detect_chinese(text: str) -> bool: return bool(re.search(r"[\u4e00-\u9fff]", text)) -def format_cell_value(value: Any) -> Any: - """格式化单元格值,保持原始类型""" - if value is None: - return None - if isinstance(value, float) and math.isnan(value): - return None - return value - - def get_api_key() -> str: """获取 Gemini API 密钥""" import os @@ -67,123 +103,25 @@ def get_api_key() -> str: ) -def translate_batch( - texts: list[str], - model_name: str = "gemini-2.0-flash-lite", - api_key: str | None = None, -) -> dict[str, str]: - """ - 批量翻译文本(使用 Gemini Deep Research) - - Args: - texts: 待翻译的文本列表 - model_name: 使用的模型名称 - api_key: API 密钥(可选) - - Returns: - 原文到译文的映射字典 - """ - if not texts: - return {} - - # 过滤掉空文本和不含中文的文本 - chinese_texts = [(i, t) for i, t in enumerate(texts) if t and t.strip() and detect_chinese(t)] - if not chinese_texts: - return {} - - # 配置 API - if not api_key: - api_key = get_api_key() - - client = genai.Client(api_key=api_key) - - # 构建批量翻译请求 - translation_pairs = [] - for _, text in chinese_texts: - translation_pairs.append(f'"{text}"') - - # 使用 Deep Research 进行翻译 - prompt = f"""你是一个专业的翻译助手。请将以下中文文本翻译成英文。 - -要求: -1. 保持专业术语准确 -2. 人名使用拼音(如:张三 → Zhang San) -3. 公司名、产品名保持原名或标准英文名 -4. 邮箱、数字等非中文内容保持不变 -5. 只返回翻译结果,不要额外解释 - -输入文本(JSON 数组格式): -{json.dumps([t for _, t in chinese_texts], ensure_ascii=False)} - -请以 JSON 数组格式返回翻译结果,保持相同顺序。""" - - try: - response = client.models.generate_content( - model=model_name, - contents=prompt, - config=types.GenerateContentConfig( - temperature=0.3, - top_p=0.8, - ) - ) - - # 解析响应 - result_text = response.text.strip() - - # 尝试解析 JSON - if result_text.startswith("```json"): - result_text = result_text[7:] - if result_text.endswith("```"): - result_text = result_text[:-3] - result_text = result_text.strip() - - translations = json.loads(result_text) - - # 构建映射字典 - result = {} - for idx, (_, original) in enumerate(chinese_texts): - if idx < len(translations): - result[original] = translations[idx] - else: - result[original] = original # 翻译失败时保留原文 - - return result - - except Exception as e: - print(f"⚠️ 翻译失败:{e}", file=sys.stderr) - # 翻译失败时返回原文 - return {text: text for _, text in chinese_texts} - - -def translate_excel_file( +def extract_chinese_content( input_path: Path, - output_path: Path | None = None, columns: list[str] | None = None, sheet_name: str | None = None, - model_name: str = "gemini-2.0-flash-lite", - api_key: str | None = None, - dry_run: bool = False, -) -> Path: +) -> tuple[list[TranslationEntry], dict[str, list[str]]]: """ - 翻译 Excel 文件中的中文内容 + 步骤 1: 提取所有中文内容及其位置 Args: input_path: 输入文件路径 - output_path: 输出文件路径(默认生成 {原文件名}_en.xlsx) columns: 指定要翻译的列名列表 sheet_name: 指定要翻译的 Sheet 名称 - model_name: Gemini 模型名称 - api_key: API 密钥 - dry_run: 预览模式,不实际生成文件 Returns: - 输出文件路径 + (entries, sheet_headers) - 翻译条目列表和各 Sheet 的表头 """ - if not output_path: - output_path = input_path.parent / f"{input_path.stem}_en{input_path.suffix}" - - # 加载工作簿 wb = load_workbook(input_path) + entries: list[TranslationEntry] = [] + sheet_headers: dict[str, list[str]] = {} # 确定要处理的 Sheet 列表 if sheet_name: @@ -196,19 +134,17 @@ def translate_excel_file( print(f"📄 文件:{input_path}") print(f"📊 Sheet 列表:{sheet_names}") - total_cells = 0 - translated_cells = 0 - for sn in sheet_names: ws = wb[sn] print(f"\n处理 Sheet: {sn}") - # 获取表头 + # 获取表头(第 1 行) headers = [] for col in range(1, ws.max_column + 1): cell_value = ws.cell(row=1, column=col).value headers.append(str(cell_value) if cell_value else f"列{col}") + sheet_headers[sn] = headers print(f"表头:{headers}") # 确定要翻译的列索引 @@ -226,54 +162,235 @@ def translate_excel_file( print(f"要翻译的列索引:{col_indices}") - # 收集所有需要翻译的单元格内容 - texts_to_translate = [] - cell_positions = [] # (row, col) - + # 提取中文内容 + count = 0 for row in range(2, ws.max_row + 1): # 跳过表头 for col in col_indices: cell = ws.cell(row=row, column=col) value = cell.value if value and isinstance(value, str) and detect_chinese(value): - texts_to_translate.append(value) - cell_positions.append((row, col)) - total_cells += 1 + pos = CellPosition(sheet=sn, row=row, col=col) + entry = TranslationEntry(position=pos, original=value) + entries.append(entry) + count += 1 - if not texts_to_translate: - print(" ✓ 没有需要翻译的中文内容") - continue + print(f" ✓ 发现 {count} 个中文单元格") + + wb.close() + return entries, sheet_headers + + +def translate_entries( + entries: list[TranslationEntry], + model_name: str = "gemini-2.0-flash-lite", + api_key: str | None = None, +) -> list[TranslationEntry]: + """ + 步骤 2: 批量翻译所有条目 + + Args: + entries: 翻译条目列表(会被修改) + model_name: Gemini 模型名称 + api_key: API 密钥 + + Returns: + 翻译后的条目列表 + """ + if not entries: + print("✓ 没有需要翻译的内容") + return entries + + # 获取 API Key + if not api_key: + api_key = get_api_key() + + # 提取所有原文 + originals = [entry.original for entry in entries] + + print(f"\n🌐 正在翻译 {len(originals)} 个中文内容...") + print(f" 模型:{model_name}") + + # 构建翻译请求 + prompt = f"""你是一个专业的翻译助手。请将以下中文文本翻译成英文。 + +要求: +1. 保持专业术语准确 +2. 人名使用拼音(如:张三 → Zhang San) +3. 公司名、产品名保持原名或标准英文名 +4. 邮箱、数字、URL 等非中文内容保持不变 +5. 只返回翻译结果,不要额外解释 + +输入文本(JSON 数组格式): +{json.dumps(originals, ensure_ascii=False)} + +请以 JSON 数组格式返回翻译结果,保持相同顺序。""" + + try: + client = genai.Client(api_key=api_key) - print(f" 发现 {len(texts_to_translate)} 个中文单元格") + response = client.models.generate_content( + model=model_name, + contents=prompt, + config=types.GenerateContentConfig( + temperature=0.3, + top_p=0.8, + ) + ) - if dry_run: - print(f" [预览模式] 将翻译以下内容:") - for i, text in enumerate(texts_to_translate[:10]): # 只显示前 10 个 - print(f" {cell_positions[i]}: {text}") - if len(texts_to_translate) > 10: - print(f" ... 还有 {len(texts_to_translate) - 10} 个") - continue + # 解析响应 + result_text = response.text.strip() - # 批量翻译 - print(f" 正在翻译...") - translations = translate_batch(texts_to_translate, model_name, api_key) + # 清理 Markdown 代码块标记 + if result_text.startswith("```json"): + result_text = result_text[7:] + if result_text.endswith("```"): + result_text = result_text[:-3] + result_text = result_text.strip() + + translations = json.loads(result_text) + + # 验证返回数量 + if len(translations) != len(originals): + print(f"⚠️ 警告:翻译返回 {len(translations)} 条,期望 {len(originals)} 条") + # 填充缺失的翻译 + while len(translations) < len(originals): + translations.append(originals[len(translations)]) # 应用翻译结果 - for i, (row, col) in enumerate(cell_positions): - original = texts_to_translate[i] - translated = translations.get(original, original) - ws.cell(row=row, column=col).value = translated - translated_cells += 1 + for i, entry in enumerate(entries): + entry.translated = translations[i] if i < len(translations) else entry.original - print(f" ✓ 完成翻译 {translated_cells} 个单元格") + print(f"✅ 翻译完成!") + + # 显示统计 + translated_count = sum(1 for e in entries if e.translated != e.original) + print(f" 成功翻译:{translated_count}/{len(entries)}") + + except Exception as e: + print(f"❌ 翻译失败:{e}") + print(f" 保留原文") + # 翻译失败时保留原文 + for entry in entries: + entry.translated = entry.original - if dry_run: - print(f"\n[预览模式] 共发现 {total_cells} 个中文单元格需要翻译") - return input_path + return entries + + +def apply_translations( + input_path: Path, + output_path: Path, + entries: list[TranslationEntry], + sheet_headers: dict[str, list[str]], +) -> Path: + """ + 步骤 3: 将翻译结果应用到新文件 + + Args: + input_path: 输入文件路径 + output_path: 输出文件路径 + entries: 翻译后的条目列表 + sheet_headers: 各 Sheet 的表头 + + Returns: + 输出文件路径 + """ + print(f"\n💾 保存翻译结果到:{output_path}") + + # 加载工作簿(复制模式) + wb = load_workbook(input_path) + + # 应用翻译 + applied_count = 0 + for entry in entries: + if entry.translated and entry.translated != entry.original: + ws = wb[entry.position.sheet] + ws.cell(row=entry.position.row, column=entry.position.col).value = entry.translated + applied_count += 1 # 保存新文件 wb.save(output_path) - print(f"\n✅ 翻译完成!输出文件:{output_path}") - print(f"📊 统计:共处理 {total_cells} 个单元格,翻译 {translated_cells} 个中文内容") + wb.close() + + print(f"✅ 完成!共更新 {applied_count} 个单元格") + + # 显示翻译预览 + print(f"\n📋 翻译预览(前 10 条):") + for i, entry in enumerate(entries[:10]): + if entry.translated != entry.original: + print(f" {entry.position.full_ref}: {entry.original} → {entry.translated}") + + if len(entries) > 10: + print(f" ... 还有 {len(entries) - 10} 条") + + return output_path + + +def translate_excel_file( + input_path: Path, + output_path: Path | None = None, + columns: list[str] | None = None, + sheet_name: str | None = None, + model_name: str = "gemini-2.0-flash-lite", + api_key: str | None = None, + dry_run: bool = False, +) -> Path: + """ + 翻译 Excel 文件中的中文内容(三阶段流程) + + 流程: + 1. 提取:收集所有中文内容及其位置映射 + 2. 翻译:批量翻译所有中文内容 + 3. 应用:将翻译结果写入新 Excel 文件 + """ + if not output_path: + output_path = input_path.parent / f"{input_path.stem}_en{input_path.suffix}" + + print("=" * 60) + print("Excel 中文→英文翻译工具") + print("=" * 60) + + # 阶段 1: 提取 + print("\n【阶段 1/3】提取中文内容...") + entries, sheet_headers = extract_chinese_content( + input_path=input_path, + columns=columns, + sheet_name=sheet_name, + ) + + if not entries: + print("\n✓ 没有发现中文内容,无需翻译") + return input_path + + print(f"\n共发现 {len(entries)} 个中文单元格") + + if dry_run: + print(f"\n[预览模式] 将翻译以下内容:") + for i, entry in enumerate(entries[:20]): + print(f" {entry.position.full_ref}: {entry.original}") + if len(entries) > 20: + print(f" ... 还有 {len(entries) - 20} 个") + return input_path + + # 阶段 2: 翻译 + print("\n【阶段 2/3】批量翻译...") + entries = translate_entries( + entries=entries, + model_name=model_name, + api_key=api_key, + ) + + # 阶段 3: 应用 + print("\n【阶段 3/3】应用翻译结果...") + output_path = apply_translations( + input_path=input_path, + output_path=output_path, + entries=entries, + sheet_headers=sheet_headers, + ) + + print("\n" + "=" * 60) + print("✅ 翻译完成!") + print("=" * 60) return output_path @@ -352,6 +469,8 @@ def main() -> int: except Exception as e: print(f"❌ 错误:{e}", file=sys.stderr) + import traceback + traceback.print_exc() return 1