refactor: 重构翻译流程为三阶段(提取→翻译→应用)

新流程:
1. 提取:收集所有中文内容及其位置映射(CellPosition)
2. 翻译:批量翻译所有中文内容(一次 API 调用)
3. 应用:将翻译结果写入新 Excel 文件

优势:
- 清晰的职责分离
- 完整的映射关系(Sheet、行、列)
- 批量翻译减少 API 调用次数
- 更容易调试和重试
- 支持 dry-run 预览模式
This commit is contained in:
ivanberry 2026-03-11 15:40:54 +08:00
parent 2ffda7c788
commit e8885401cf
1 changed files with 270 additions and 151 deletions

View File

@ -3,15 +3,19 @@
""" """
Excel 文件中文英文翻译工具 Excel 文件中文英文翻译工具
使用 Google Gemini Flash Lite API 进行翻译 使用 Google Gemini Flash Lite API 进行翻译
流程
1. 提取收集所有中文内容及其位置映射
2. 翻译批量翻译所有中文内容
3. 应用将翻译结果写入新 Excel 文件
""" """
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import json import json
import math
import re
import sys import sys
from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -38,6 +42,47 @@ except ImportError as exc:
) from 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: def detect_chinese(text: str) -> bool:
"""检测文本中是否包含中文字符""" """检测文本中是否包含中文字符"""
if not text or not isinstance(text, str): 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)) 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: def get_api_key() -> str:
"""获取 Gemini API 密钥""" """获取 Gemini API 密钥"""
import os import os
@ -67,123 +103,25 @@ def get_api_key() -> str:
) )
def translate_batch( def extract_chinese_content(
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(
input_path: Path, input_path: Path,
output_path: Path | None = None,
columns: list[str] | None = None, columns: list[str] | None = None,
sheet_name: str | None = None, sheet_name: str | None = None,
model_name: str = "gemini-2.0-flash-lite", ) -> tuple[list[TranslationEntry], dict[str, list[str]]]:
api_key: str | None = None,
dry_run: bool = False,
) -> Path:
""" """
翻译 Excel 文件中的中文内容 步骤 1: 提取所有中文内容及其位置
Args: Args:
input_path: 输入文件路径 input_path: 输入文件路径
output_path: 输出文件路径默认生成 {原文件名}_en.xlsx
columns: 指定要翻译的列名列表 columns: 指定要翻译的列名列表
sheet_name: 指定要翻译的 Sheet 名称 sheet_name: 指定要翻译的 Sheet 名称
model_name: Gemini 模型名称
api_key: API 密钥
dry_run: 预览模式不实际生成文件
Returns: 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) wb = load_workbook(input_path)
entries: list[TranslationEntry] = []
sheet_headers: dict[str, list[str]] = {}
# 确定要处理的 Sheet 列表 # 确定要处理的 Sheet 列表
if sheet_name: if sheet_name:
@ -196,19 +134,17 @@ def translate_excel_file(
print(f"📄 文件:{input_path}") print(f"📄 文件:{input_path}")
print(f"📊 Sheet 列表:{sheet_names}") print(f"📊 Sheet 列表:{sheet_names}")
total_cells = 0
translated_cells = 0
for sn in sheet_names: for sn in sheet_names:
ws = wb[sn] ws = wb[sn]
print(f"\n处理 Sheet: {sn}") print(f"\n处理 Sheet: {sn}")
# 获取表头 # 获取表头(第 1 行)
headers = [] headers = []
for col in range(1, ws.max_column + 1): for col in range(1, ws.max_column + 1):
cell_value = ws.cell(row=1, column=col).value cell_value = ws.cell(row=1, column=col).value
headers.append(str(cell_value) if cell_value else f"{col}") headers.append(str(cell_value) if cell_value else f"{col}")
sheet_headers[sn] = headers
print(f"表头:{headers}") print(f"表头:{headers}")
# 确定要翻译的列索引 # 确定要翻译的列索引
@ -226,54 +162,235 @@ def translate_excel_file(
print(f"要翻译的列索引:{col_indices}") print(f"要翻译的列索引:{col_indices}")
# 收集所有需要翻译的单元格内容 # 提取中文内容
texts_to_translate = [] count = 0
cell_positions = [] # (row, col)
for row in range(2, ws.max_row + 1): # 跳过表头 for row in range(2, ws.max_row + 1): # 跳过表头
for col in col_indices: for col in col_indices:
cell = ws.cell(row=row, column=col) cell = ws.cell(row=row, column=col)
value = cell.value value = cell.value
if value and isinstance(value, str) and detect_chinese(value): if value and isinstance(value, str) and detect_chinese(value):
texts_to_translate.append(value) pos = CellPosition(sheet=sn, row=row, col=col)
cell_positions.append((row, col)) entry = TranslationEntry(position=pos, original=value)
total_cells += 1 entries.append(entry)
count += 1
if not texts_to_translate: print(f" ✓ 发现 {count} 个中文单元格")
print(" ✓ 没有需要翻译的中文内容")
continue
print(f" 发现 {len(texts_to_translate)} 个中文单元格") wb.close()
return entries, sheet_headers
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
# 批量翻译 def translate_entries(
print(f" 正在翻译...") entries: list[TranslationEntry],
translations = translate_batch(texts_to_translate, model_name, api_key) 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)
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()
# 清理 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): for i, entry in enumerate(entries):
original = texts_to_translate[i] entry.translated = translations[i] if i < len(translations) else entry.original
translated = translations.get(original, original)
ws.cell(row=row, column=col).value = translated
translated_cells += 1
print(f" ✓ 完成翻译 {translated_cells} 个单元格") print(f"✅ 翻译完成!")
if dry_run: # 显示统计
print(f"\n[预览模式] 共发现 {total_cells} 个中文单元格需要翻译") translated_count = sum(1 for e in entries if e.translated != e.original)
return input_path print(f" 成功翻译:{translated_count}/{len(entries)}")
except Exception as e:
print(f"❌ 翻译失败:{e}")
print(f" 保留原文")
# 翻译失败时保留原文
for entry in entries:
entry.translated = entry.original
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) wb.save(output_path)
print(f"\n✅ 翻译完成!输出文件:{output_path}") wb.close()
print(f"📊 统计:共处理 {total_cells} 个单元格,翻译 {translated_cells} 个中文内容")
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 return output_path
@ -352,6 +469,8 @@ def main() -> int:
except Exception as e: except Exception as e:
print(f"❌ 错误:{e}", file=sys.stderr) print(f"❌ 错误:{e}", file=sys.stderr)
import traceback
traceback.print_exc()
return 1 return 1