python 实现文本文件的编码检测和转换

记得在前公司做嵌入式开发时,总是会遇到因代码文件编码不对导致的注释乱码问题。因为部门里很多人喜欢使用 GB2312,而我比较倾向于用 utf-8 编码,所以每次合并他们代码的时候,通常我会使用一个 findenca 的组合命令,对工程检索将 GB2312 的 .c.h 文件转码为 utf-8,那么我们是不是可以自己编写一个 python 工具实现文件编码的检测和转换呢?当然可以,借助第三方库 chardet 就可以做到!

编码操作

要对文件编码转换前,必须也要知道源文件是什么编码,所以也得进行编码检测。

编码检测

这里是用 chardet 库的 UniversalDetector 类实现文件编码的检测,在使用前需要安装:

$ pip3 install chardet

比如针对编码为 utf-8 的文件 README.md,可以如下进行代码检测,检测的同时也会得到一个置信度表征检测正确的可信度:

from chardet.universaldetector import UniversalDetector

detector = UniversalDetector()
detector.reset()
for each in open(filepath, 'rb'):
    detector.feed(each)
    if detector.done:
        break
detector.close()
fileencoding = detector.result['encoding']
confidence = detector.result['confidence']

其中注意的是:

  • 要以二进制读取模式打开文件
  • fileencoding 是编码,confidence 是置信度,取之范围 0~1

编码转换

文件编码的转换其实使用文件读写操作就可以完成,打开文件的时候可以指定编码,所以可以使用源文件编码形式打开读取内容,再以新的编码打开要生成的文件,将读取去的内容写入新的文件,就实现编码转换了。

f = open(sourcefile, 'r', encoding='gb2312', errors='replace')
text = f.read()
f.close()
f = open(targetfile, 'w', encoding='utf-8', errors='replace')
f.write(text)
f.close()

其中:

  • 调用 open 的时候指定 error 参数为 replace,说明会将标记字符插入到错误位置
  • 如果使用 with 语法的话可能会在这里出现一点小问题,如果输出文件路径与原文件路径一致会出现保存编码异常,所以没有使用 with

工具实现

这个 python 脚本工具的实现会用到内建库 argparse 库(点我了解基本用法)、os 库和第三方库 chardet 库。

准备脚本

  1. 新建文件 encoding: touch encoding

  2. 添加运行权限: chmod +x encoding

  3. 指明脚本解释器,文件中添加内容:

    #!/usr/bin/env python3
    
  4. 导入必要库,文件中添加:

    import os
    import argparse
    from chardet.universaldetector import UniversalDetector
    

添加命令参数解析函数

封装命令行参数解析操作为函数:

def getargs():
    parser = argparse.ArgumentParser(description='这是一个检测或转换文件编码的工具')
    parser.add_argument('files', nargs='+', help='指定一个或多个文件的路径')
    parser.add_argument('-e', '--encoding', nargs='?', const='UTF-8',
                        help='''指定目标编码,可选择的编码:
ASCII, (Default) UTF-8 (with or without a BOM), UTF-16 (with a BOM),
UTF-32 (with a BOM), Big5, GB2312/GB18030, EUC-TW, HZ-GB-2312, 
ISO-2022-CN, EUC-JP, SHIFT_JIS, ISO-2022-JP, ISO-2022-KR, KOI8-R, 
MacCyrillic, IBM855, IBM866, ISO-8859-5, windows-1251, ISO-8859-2, 
windows-1250, EUC-KR, ISO-8859-5, windows-1251, ISO-8859-1, windows-1252, 
ISO-8859-7, windows-1253, ISO-8859-8, windows-1255, TIS-620''')
    parser.add_argument('-o', '--outdir', help='指定转换文件的输出目录')
    return parser.parse_args()
参数 描述
files 文件路径 指定一个或多个文件的路径
-e, –encoding 编码名称, 默认为 UTF-8 指定目标编码
-o, –outdir 目录路径 指定转换文件的输出目录

封装编码检测函数

为了方便处理,根据 编码检测 小节,封装编码检测函数如下:

def detcect_encoding(filepath):
    """检测文件编码
    Args:
        detector: UniversalDetector 对象
        filepath: 文件路径
    Return:
        fileencoding: 文件编码
        confidence: 检测结果的置信度,百分比
    """
    detector = UniversalDetector()
    detector.reset()
    for each in open(filepath, 'rb'):
        detector.feed(each)
        if detector.done:
            break
    detector.close()
    fileencoding = detector.result['encoding']
    confidence = detector.result['confidence']
    if fileencoding is None:
        fileencoding = 'unknown'
        confidence = 0.99
    return fileencoding, confidence * 100

UniversalDetector 实例对象完成检测后,如果不能识别编码,detector.result['encoding'] 就会是 None ,所以这里做了一定处理,并且最后返回的置信度这个值取的是百分比的值。

最终实现流程

__main__ 中实现整个功能如下:

if __name__ == '__main__':
    args = getargs()
    if args.outdir:
        if not os.path.exists(args.outdir):
            answer = input(f'[-] 无效的导出路径: {args.outdir} [-]\n要用转码后的文件直接替换源文件吗? y or n\n')
            if 'y' in answer:
                args.outdir = None
            else:
                exit(1)
    for file in args.files:
        if not os.path.isfile(file):
            print(f'[-] 无效的文件路径: {file} [-]')
            continue
        encoding, confidence = detcect_encoding(file)
        print(f'[+] {file}: 编码 -> {encoding} (置信度 {confidence}%) [+]')
        if args.encoding and (encoding is not 'unknown') and (confidence > 0.75):
            if args.encoding == encoding:
                print(f'[*] {file} 已经是 {encoding} 编码了,无需转换![*]')
                continue
            f = open(file, 'r', encoding=encoding, errors='replace')
            text = f.read()
            f.close()
            outpath = os.path.join(args.outdir, file) if args.outdir else file
            f = open(outpath, 'w', encoding=args.encoding, errors='replace')
            f.write(text)
            f.close()
            print(f'[+] 转码成功: {file}({encoding}) -> {outpath}({args.encoding}) [+]')
  1. 获取命令行参数
  2. 如果命令行中指定了转换文件输出目录,检查目录是否有效
  3. 枚举指定的文件列表
  4. 针对文件,首先检查路径的有效性,然后通过 detect_encoding 方法获取编码检测结果
  5. 如果指定了目标编码并且检测的文件编码不是未知和置信度高于 75% 那就进行编码转换
  6. 在编码转换中,检测转换的必要性
  7. 如果需要转换那就按照小节编码转换进行编码的转换

最终实现完整的工具代码,可参考:tools-with-script/encoding/encoding

测试

实现了工具,必须测试一下,这里用到了文件 gb2312.txt 和文件 big5.txt

$ ./encoding -h
usage: encoding [-h] [-e [ENCODING]] [-o OUTDIR] files [files ...]

这是一个检测或转换文件编码的工具

positional arguments:
  files                 指定一个或多个文件的路径

optional arguments:
  -h, --help            show this help message and exit
  -e [ENCODING], --encoding [ENCODING]
                        指定目标编码,可选择的编码: ASCII, (Default) UTF-8 (with or without
                        a BOM), UTF-16 (with a BOM), UTF-32 (with a BOM),
                        Big5, GB2312/GB18030, EUC-TW, HZ-GB-2312, ISO-2022-CN,
                        EUC-JP, SHIFT_JIS, ISO-2022-JP, ISO-2022-KR, KOI8-R,
                        MacCyrillic, IBM855, IBM866, ISO-8859-5, windows-1251,
                        ISO-8859-2, windows-1250, EUC-KR, ISO-8859-5,
                        windows-1251, ISO-8859-1, windows-1252, ISO-8859-7,
                        windows-1253, ISO-8859-8, windows-1255, TIS-620
  -o OUTDIR, --outdir OUTDIR
                        指定转换文件的输出目录
$ ./encoding big5.txt gb2312.txt
[+] big5.txt: 编码 -> Big5 (置信度 99.0%) [+]
[+] gb2312.txt: 编码 -> GB2312 (置信度 99.0%) [+]
$ ./encoding big5.txt gb2312.txt -e utf-8
[+] big5.txt: 编码 -> Big5 (置信度 99.0%) [+]
[+] 转码成功: big5.txt(Big5) -> big5.txt(utf-8) [+]
[+] gb2312.txt: 编码 -> GB2312 (置信度 99.0%) [+]
[+] 转码成功: gb2312.txt(GB2312) -> gb2312.txt(utf-8) [+]
$ ✗ ./encoding big5.txt gb2312.txt
[+] big5.txt: 编码 -> utf-8 (置信度 99.0%) [+]
[+] gb2312.txt: 编码 -> utf-8 (置信度 99.0%) [+]

python

1880 字

2018-10-25 14:05 +0000