專案遷移、舊系統整合時常需將大量原始碼、設定檔、文檔從 GBK/Big5 等舊編碼統一轉為 UTF-8。手動逐檔轉換不現實,本文提供三個版本的 Python 腳本,分別針對 Linux(呼叫 iconv)、Python 2、Python 3 純標準庫實現,支援遞迴目錄、跳過二進位檔、錯誤處理與乾淨輸出。
| 環境 | 推薦版本 | 特點 |
|---|---|---|
| Linux/macOS 且裝有 iconv | 版本 A(呼叫 iconv) | 速度最快、支援最多編碼、自動處理 BOM |
| 純 Python 2 環境 | 版本 B | 無外部依賴、相容舊系統 |
| Python 3.6+ | 版本 C(推薦) | 標準庫 codecs/open、錯誤處理完善、跨平台 |
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
import os, sys, subprocess
TARGET_EXTS = ('.py', '.txt', '.md', '.html', '.css', '.js', '.json', '.xml', '.yml', '.yaml', '.ini', '.cfg', '.conf', '.sh', '.sql')
SKIP_DIRS = ('.git', '.svn', '__pycache__', 'node_modules', 'venv', 'env', 'dist', 'build')
def is_text_file(path):
"""簡易判斷:嘗試以 utf-8 讀取前 1024 位元組,失敗再嘗試 gbk"""
try:
with open(path, 'rb') as f:
chunk = f.read(1024)
if b'\x00' in chunk: # 含 null byte 視為二進位
return False
chunk.decode('utf-8')
return True
except UnicodeDecodeError:
try:
chunk.decode('gbk')
return True
except UnicodeDecodeError:
return False
def convert_file(path):
# 使用 iconv -f gbk -t utf-8 -c(忽略無效字元) -o 輸出
# 先備份再覆寫,避免 iconv 失敗損壞原檔
backup = path + '.bak'
os.rename(path, backup)
cmd = ['iconv', '-f', 'gbk', '-t', 'utf-8', '-c', '-o', path, backup]
ret = subprocess.call(cmd)
if ret == 0:
os.remove(backup)
print(f"✅ {path}")
else:
os.rename(backup, path)
print(f"❌ {path} 轉換失敗,已還原")
def main(root_dir):
for dirpath, dirnames, filenames in os.walk(root_dir):
# 就地修改 dirnames 以跳過目錄
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
for fname in filenames:
if fname.lower().endswith(TARGET_EXTS):
full = os.path.join(dirpath, fname)
if is_text_file(full):
convert_file(full)
if __name__ == "__main__":
target = sys.argv[1] if len(sys.argv) > 1 else '.'
main(os.path.abspath(target))
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
import os, sys, codecs
def convert(path, in_enc='GBK', out_enc='UTF-8'):
try:
with open(path, 'rb') as f:
content = f.read()
# 解碼 -> 編碼
new_content = content.decode(in_enc).encode(out_enc)
with open(path, 'wb') as f:
f.write(new_content)
print(f"✅ {path}")
except Exception as e:
print(f"❌ {path}: {e}")
def walk(root):
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if not d.startswith('.')]
for fname in filenames:
if fname.lower().endswith(('.py', '.txt', '.md', '.html', '.js', '.css', '.json', '.xml')):
convert(os.path.join(dirpath, fname))
if __name__ == "__main__":
for p in sys.argv[1:] or ['.']:
if os.path.isfile(p):
convert(p)
elif os.path.isdir(p):
walk(p)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os, sys, codecs, argparse
from pathlib import Path
DEFAULT_EXTS = {'.py', '.txt', '.md', '.html', '.htm', '.css', '.js', '.json', '.xml', '.yml', '.yaml', '.ini', '.cfg', '.conf', '.sh', '.sql', '.csv', '.log'}
SKIP_DIRS = {'.git', '.svn', '__pycache__', 'node_modules', 'venv', 'env', 'dist', 'build', '.idea', '.vscode'}
def detect_encoding(path, sample_size=4096):
"""嘗試常見編碼,回傳 (encoding, confidence)"""
with open(path, 'rb') as f:
raw = f.read(sample_size)
if b'\x00' in raw:
return None # 二進位
for enc in ('utf-8', 'utf-8-sig', 'gbk', 'gb18030', 'big5', 'latin-1'):
try:
raw.decode(enc)
return enc
except UnicodeDecodeError:
continue
return None
def convert_file(path, target_enc='utf-8', dry_run=False):
src_enc = detect_encoding(path)
if not src_enc:
print(f"⏭ 跳過(二進位/無法識別): {path}")
return False
if src_enc.lower().replace('-', '') == target_enc.lower().replace('-', ''):
print(f"⏭ 已是 {target_enc}: {path}")
return True
try:
# 讀取完整內容(若檔案極大可改分塊,但文字檔通常較小)
with open(path, 'r', encoding=src_enc) as f:
content = f.read()
if not dry_run:
# 寫入 UTF-8(不加 BOM)
with open(path, 'w', encoding=target_enc, newline='') as f:
f.write(content)
print(f"✅ {path} ({src_enc} → {target_enc})")
return True
except Exception as e:
print(f"❌ {path}: {e}")
return False
def main():
parser = argparse.ArgumentParser(description="批量將文字檔轉為 UTF-8 編碼")
parser.add_argument('paths', nargs='*', default=['.'], help='檔案或目錄路徑')
parser.add_argument('-e', '--ext', nargs='*', help='額外副檔名(如 .vue .tsx)')
parser.add_argument('--dry-run', action='store_true', help='僅模擬,不寫入')
parser.add_argument('--target', default='utf-8', help='目標編碼(預設 utf-8)')
args = parser.parse_args()
exts = DEFAULT_EXTS | set(args.ext) if args.ext else DEFAULT_EXTS
total = converted = 0
for p in args.paths:
path = Path(p).resolve()
if path.is_file():
if path.suffix.lower() in exts:
total += 1
if convert_file(path, args.target, args.dry_run):
converted += 1
elif path.is_dir():
for file in path.rglob('*'):
if file.is_file() and file.suffix.lower() in exts:
# 跳過 SKIP_DIRS
if any(part in SKIP_DIRS for part in file.parts):
continue
total += 1
if convert_file(file, args.target, args.dry_run):
converted += 1
print(f"\n完成:共 {total} 個候選檔案,成功轉換 {converted} 個。")
if __name__ == "__main__":
main()
# Python 3 版:預設遞迴當前目錄,支援乾跑模式
python3 convert_encoding.py -e .vue .tsx --dry-run
python3 convert_encoding.py /path/to/project
# Python 2 版
python2 convert_py2.py /path/to/project
選對版本、做好備份、善用版控,批量編碼轉換就能安全、高效完成。