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 文件中文英文翻译工具
使用 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