409 lines
14 KiB
Python
409 lines
14 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
中文文件/目录名重命名为英文
|
||
|
||
功能:
|
||
1. 扫描所有中文文件和目录
|
||
2. 生成英文重命名映射表
|
||
3. 执行重命名
|
||
4. 更新所有引用链接
|
||
"""
|
||
|
||
import os
|
||
import re
|
||
import csv
|
||
import argparse
|
||
from pathlib import Path
|
||
from typing import List, Dict, Tuple
|
||
|
||
# 中文到英文的翻译字典
|
||
TRANSLATION_DICT = {
|
||
# 项目相关
|
||
"广汽": "GAC",
|
||
"物联网AIOT": "AIoT",
|
||
"物联网": "IoT",
|
||
"AIOT": "AIoT",
|
||
"自动驾驶": "Autonomous-Driving",
|
||
"从化应急": "Conghua-Emergency",
|
||
"公文交换": "Document-Exchange",
|
||
"商务局": "Commerce-Bureau",
|
||
"市发改委": "Municipal-Development-Reform",
|
||
"虚拟数据中心": "Virtual-Data-Center",
|
||
"增城区": "Zengcheng-District",
|
||
"番禺区": "Panyu-District",
|
||
"工信": "Industry-Info",
|
||
"道路秩序": "Road-Order",
|
||
"南电": "CSG",
|
||
"南方电网": "China-Southern-Grid",
|
||
|
||
# 文件夹名称
|
||
"城中村": "Urban-Village",
|
||
"番禺城中村": "Panyu-Urban-Village",
|
||
"番禺公文": "Panyu-Document",
|
||
"番禺信用": "Panyu-Credit",
|
||
"番禺住建": "Panyu-Housing",
|
||
"公文": "Document",
|
||
"信用": "Credit",
|
||
"住建": "Housing",
|
||
"增城区信息化项目管理系统": "Zengcheng-IT-Project-Mgmt",
|
||
|
||
# 文件名关键词
|
||
"不明确的问题": "Unclear-Issues",
|
||
"培训记录整理": "Training-Records",
|
||
"设备添加": "Device-Addition",
|
||
"测试环境": "Test-Environment",
|
||
"维护": "Maintenance",
|
||
"数据流向": "Data-Flow",
|
||
"系统架构": "System-Architecture",
|
||
"登陆": "Login",
|
||
"配置": "Configuration",
|
||
"问题处理": "Issue-Handling",
|
||
"处理": "Handling",
|
||
"升级": "Upgrade",
|
||
"漏洞处理": "Vulnerability-Fix",
|
||
"达梦": "DM-Database",
|
||
"等保问题描述": "Security-Issues",
|
||
"等保修复": "Security-Fixes",
|
||
"基线": "Baseline",
|
||
"重建主节点": "Rebuild-Master-Node",
|
||
"设备电源": "Device-Power",
|
||
"智谱清言": "ChatGLM",
|
||
"热点升级": "Hotspot-Upgrade",
|
||
"自行车": "Bicycle",
|
||
"酸黄瓜制作": "Pickled-Cucumber-Recipe",
|
||
"酸黄瓜": "Pickled-Cucumber",
|
||
"文明": "Civilization",
|
||
"摩托罗拉": "Motorola",
|
||
"产出": "Deliverables",
|
||
"架构目标": "Architecture-Goals",
|
||
"决策方法": "Decision-Methods",
|
||
"系统架构分析员知识体系": "SA-Knowledge-System",
|
||
"芹菜炒牛肉": "Celery-Beef-Stir-Fry",
|
||
"图片生成提示词模板": "Image-Prompt-Template",
|
||
"在非原生ESIM设备上申请Giffgaff ESIM": "Apply-Giffgaff-ESIM-on-Non-Native-Device",
|
||
|
||
# 网页剪藏常用词
|
||
"联想移动互联及数字家庭产品服务支持": "Lenovo-Support",
|
||
"节点访问局域网服务器的配置方法": "LAN-Server-Access-Config",
|
||
"边界路由器配置": "Border-Router-Config",
|
||
"树莓派": "Raspberry-Pi",
|
||
"机场推荐": "VPN-Recommendations",
|
||
"机场评测": "VPN-Reviews",
|
||
"软件供应商手册": "Software-Vendor-Handbook",
|
||
"解读": "Interpretation",
|
||
"关于": "About",
|
||
"软件物料清单": "SBOM",
|
||
"如何": "How-to",
|
||
"教程": "Tutorial",
|
||
"博客": "Blog",
|
||
"技术文档": "Technical-Docs",
|
||
"学习笔记": "Learning-Notes",
|
||
"使用攻略": "Guide",
|
||
"安装": "Installation",
|
||
"配置": "Config",
|
||
"实现": "Implementation",
|
||
"什麼是": "What-is",
|
||
"軟體物料清單": "SBOM",
|
||
"網路安全解決方案": "Network-Security",
|
||
"艾索科技": "AISEC",
|
||
"作为旁路网关": "Bypass-Gateway",
|
||
"不是旁路由": "",
|
||
"单臂路由": "",
|
||
"的终极设置方法": "Ultimate-Setup",
|
||
"破解迷思": "Debunking-Myths",
|
||
"少数派": "sspai",
|
||
"麒麟": "Kylin",
|
||
"离线安装": "Offline-Installation",
|
||
"报文简介": "Message-Intro",
|
||
"公开镜像仓库未授权访问": "Public-Registry-Unauth",
|
||
"数据库从": "DB-Migration-from",
|
||
"迁移到": "to",
|
||
"综合讨论区": "Discussion",
|
||
"瀚思彼岸": "HASSBIAN",
|
||
"智能家居技术论坛": "Smart-Home-Forum",
|
||
"雾凇拼音": "Rime-Ice",
|
||
"长期维护的简体词库": "Simplified-Dict",
|
||
"中国申请": "China-Application",
|
||
"中国区免费": "China-Free",
|
||
"年使用攻略": "Year-Guide",
|
||
"云": "Cloud",
|
||
"详细": "Detailed",
|
||
"注册申请图文教程": "Registration-Tutorial",
|
||
"整合": "Integration",
|
||
"软件开发实施": "Software-Dev",
|
||
"广州尚鹏": "Guangzhou-SP",
|
||
"服装生鲜家具外贸供应链开源": "Supply-Chain",
|
||
"专业实施": "Pro-Impl",
|
||
"学习经验": "Learning-Experience",
|
||
"安装和打包": "Installation-Packaging",
|
||
"权限校验": "Permission-Validation",
|
||
"官网": "Official",
|
||
"学习笔记": "Learning-Notes",
|
||
"应用": "Application",
|
||
"部署": "Deployment",
|
||
"单实例数据库": "Single-Instance-DB",
|
||
"的工作学习笔记": "Work-Notes",
|
||
"集成": "Integration",
|
||
"国内镜像源列表": "China-Mirror-List",
|
||
"亲测可用": "Tested",
|
||
"技术文档": "Tech-Docs",
|
||
"安装前准备": "Pre-Installation",
|
||
"配置实例": "Config-Instance",
|
||
"数据库安装": "DB-Installation",
|
||
"技术我的一些": "My",
|
||
}
|
||
|
||
|
||
def has_chinese(text: str) -> bool:
|
||
"""检查文本是否包含中文"""
|
||
return bool(re.search(r'[\u4e00-\u9fa5]', text))
|
||
|
||
|
||
def translate_chinese(text: str, max_length: int = 50) -> str:
|
||
"""将中文翻译为英文"""
|
||
result = text
|
||
|
||
# 按长度排序,优先匹配长词组
|
||
sorted_dict = sorted(TRANSLATION_DICT.items(), key=lambda x: len(x[0]), reverse=True)
|
||
|
||
for chinese, english in sorted_dict:
|
||
result = result.replace(chinese, english)
|
||
|
||
# 移除剩余的中文(如果翻译不完整,保留拼音或删除)
|
||
result = re.sub(r'[\u4e00-\u9fa5]+', '-', result)
|
||
|
||
# 清理特殊字符
|
||
result = re.sub(r'[\u3000-\u303f\uff00-\uffef\s]+', '-', result)
|
||
result = re.sub(r'[<>:"/\\|?*]+', '-', result)
|
||
result = re.sub(r'-+', '-', result)
|
||
result = result.strip('-')
|
||
|
||
# 控制长度
|
||
if len(result) > max_length:
|
||
result = result[:max_length].rstrip('-')
|
||
|
||
return result
|
||
|
||
|
||
def scan_chinese_files_and_dirs(root_dir: Path) -> Tuple[List[Path], List[Path]]:
|
||
"""扫描所有包含中文的文件和目录"""
|
||
chinese_files = []
|
||
chinese_dirs = []
|
||
|
||
# 排除的目录
|
||
exclude_dirs = {'.git', '.obsidian', '.smart-env', 'node_modules'}
|
||
|
||
for item in root_dir.rglob('*'):
|
||
# 跳过排除的目录
|
||
if any(excl in item.parts for excl in exclude_dirs):
|
||
continue
|
||
|
||
if has_chinese(item.name):
|
||
if item.is_file():
|
||
chinese_files.append(item)
|
||
elif item.is_dir():
|
||
chinese_dirs.append(item)
|
||
|
||
# 目录按深度排序(深的在前,避免父目录先改名)
|
||
chinese_dirs.sort(key=lambda p: len(p.parts), reverse=True)
|
||
|
||
return chinese_files, chinese_dirs
|
||
|
||
|
||
def generate_mappings(files: List[Path], dirs: List[Path], root_dir: Path, max_length: int = 50) -> List[Dict]:
|
||
"""生成重命名映射"""
|
||
mappings = []
|
||
|
||
# 处理目录
|
||
for dir_path in dirs:
|
||
old_name = dir_path.name
|
||
new_name = translate_chinese(old_name, max_length)
|
||
|
||
rel_path = dir_path.relative_to(root_dir)
|
||
new_path = dir_path.parent / new_name
|
||
new_rel_path = new_path.relative_to(root_dir)
|
||
|
||
mappings.append({
|
||
'type': 'Directory',
|
||
'old_path': str(rel_path),
|
||
'new_path': str(new_rel_path),
|
||
'old_name': old_name,
|
||
'new_name': new_name,
|
||
'abs_old_path': str(dir_path),
|
||
'abs_new_path': str(new_path)
|
||
})
|
||
|
||
# 处理文件
|
||
for file_path in files:
|
||
old_name = file_path.name
|
||
stem = file_path.stem
|
||
suffix = file_path.suffix
|
||
|
||
new_stem = translate_chinese(stem, max_length - len(suffix))
|
||
new_name = new_stem + suffix
|
||
|
||
rel_path = file_path.relative_to(root_dir)
|
||
new_path = file_path.parent / new_name
|
||
new_rel_path = new_path.relative_to(root_dir)
|
||
|
||
mappings.append({
|
||
'type': 'File',
|
||
'old_path': str(rel_path),
|
||
'new_path': str(new_rel_path),
|
||
'old_name': old_name,
|
||
'new_name': new_name,
|
||
'abs_old_path': str(file_path),
|
||
'abs_new_path': str(new_path)
|
||
})
|
||
|
||
return mappings
|
||
|
||
|
||
def save_mappings(mappings: List[Dict], output_file: Path):
|
||
"""保存映射表到CSV"""
|
||
with open(output_file, 'w', encoding='utf-8', newline='') as f:
|
||
writer = csv.DictWriter(f, fieldnames=['type', 'old_path', 'new_path', 'old_name', 'new_name'])
|
||
writer.writeheader()
|
||
for mapping in mappings:
|
||
writer.writerow({
|
||
'type': mapping['type'],
|
||
'old_path': mapping['old_path'],
|
||
'new_path': mapping['new_path'],
|
||
'old_name': mapping['old_name'],
|
||
'new_name': mapping['new_name']
|
||
})
|
||
|
||
|
||
def execute_rename(mappings: List[Dict]) -> Tuple[int, int]:
|
||
"""执行重命名"""
|
||
renamed = 0
|
||
failed = 0
|
||
|
||
for mapping in mappings:
|
||
try:
|
||
old_path = Path(mapping['abs_old_path'])
|
||
new_path = Path(mapping['abs_new_path'])
|
||
|
||
if old_path.exists():
|
||
old_path.rename(new_path)
|
||
print(f"[+] {mapping['type']}: {mapping['old_name']} -> {mapping['new_name']}")
|
||
renamed += 1
|
||
else:
|
||
print(f"[!] Skip (not exist): {mapping['old_path']}")
|
||
except Exception as e:
|
||
print(f"[-] Failed: {mapping['old_path']} - {e}")
|
||
failed += 1
|
||
|
||
return renamed, failed
|
||
|
||
|
||
def update_markdown_links(root_dir: Path, mappings: List[Dict]) -> int:
|
||
"""更新 Markdown 文件中的链接"""
|
||
updated_count = 0
|
||
md_files = list(root_dir.rglob('*.md'))
|
||
|
||
# 排除某些目录
|
||
exclude_dirs = {'.git', '.obsidian', '.smart-env'}
|
||
md_files = [f for f in md_files if not any(excl in f.parts for excl in exclude_dirs)]
|
||
|
||
for md_file in md_files:
|
||
try:
|
||
content = md_file.read_text(encoding='utf-8')
|
||
original_content = content
|
||
|
||
# 更新 Wiki 链接 [[中文名]]
|
||
for mapping in mappings:
|
||
if mapping['type'] == 'File':
|
||
old_stem = Path(mapping['old_name']).stem
|
||
new_stem = Path(mapping['new_name']).stem
|
||
|
||
# Wiki 链接
|
||
content = re.sub(
|
||
rf'\[\[{re.escape(old_stem)}\]\]',
|
||
f'[[{new_stem}]]',
|
||
content
|
||
)
|
||
|
||
# 相对路径链接
|
||
old_path_escaped = re.escape(mapping['old_path'].replace('\\', '/'))
|
||
new_path_fixed = mapping['new_path'].replace('\\', '/')
|
||
content = re.sub(old_path_escaped, new_path_fixed, content)
|
||
|
||
if content != original_content:
|
||
md_file.write_text(content, encoding='utf-8')
|
||
rel_path = md_file.relative_to(root_dir)
|
||
print(f"[+] Updated: {rel_path}")
|
||
updated_count += 1
|
||
|
||
except Exception as e:
|
||
print(f"[-] Update failed: {md_file} - {e}")
|
||
|
||
return updated_count
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description='中文文件名重命名为英文')
|
||
parser.add_argument('--execute', action='store_true', help='执行重命名(默认为预览模式)')
|
||
parser.add_argument('--max-length', type=int, default=50, help='文件名最大长度(默认50)')
|
||
args = parser.parse_args()
|
||
|
||
root_dir = Path.cwd()
|
||
mapping_file = root_dir / 'chinese-to-english-mapping.csv'
|
||
|
||
print("=== Chinese to English Rename Tool ===\n")
|
||
|
||
if args.execute:
|
||
print("[!] EXECUTE MODE: Will rename files and update links")
|
||
else:
|
||
print("[i] PREVIEW MODE: Generate mapping only, no actual rename")
|
||
print(" Use --execute to perform actual rename")
|
||
print()
|
||
|
||
# Scan
|
||
print("[*] Scanning Chinese files and directories...")
|
||
chinese_files, chinese_dirs = scan_chinese_files_and_dirs(root_dir)
|
||
print(f" Found {len(chinese_files)} Chinese files")
|
||
print(f" Found {len(chinese_dirs)} Chinese directories\n")
|
||
|
||
# Generate mappings
|
||
print("[*] Generating rename mappings...")
|
||
mappings = generate_mappings(chinese_files, chinese_dirs, root_dir, args.max_length)
|
||
save_mappings(mappings, mapping_file)
|
||
print(f" [+] Mapping saved to: {mapping_file}")
|
||
print(f" [+] Total {len(mappings)} rename tasks\n")
|
||
|
||
# Preview
|
||
print("[*] Rename Preview (first 20):")
|
||
print(f"{'Type':<10} {'Old Name':<40} {'New Name':<40}")
|
||
print("-" * 90)
|
||
for mapping in mappings[:20]:
|
||
print(f"{mapping['type']:<10} {mapping['old_name']:<40} {mapping['new_name']:<40}")
|
||
|
||
if len(mappings) > 20:
|
||
print(f"\n ... and {len(mappings) - 20} more (see full list: {mapping_file})")
|
||
print()
|
||
|
||
# Execute rename
|
||
if args.execute:
|
||
print("[*] Starting rename...")
|
||
renamed, failed = execute_rename(mappings)
|
||
print(f"\n[*] Rename complete!")
|
||
print(f" Success: {renamed}")
|
||
print(f" Failed: {failed}\n")
|
||
|
||
# Update links
|
||
print("[*] Updating Markdown links...")
|
||
updated_count = update_markdown_links(root_dir, mappings)
|
||
print(f"\n[*] Link update complete! Updated {updated_count} files\n")
|
||
else:
|
||
print("[i] Next steps:")
|
||
print(f" 1. Review mapping: {mapping_file}")
|
||
print(f" 2. If OK, run: python .scripts/rename-chinese-to-english.py --execute\n")
|
||
|
||
print("[+] Done!")
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|