统计电脑中的文本文件由哪些编码构成?

电脑上最常见的文本编码格式有UTF-8和GBK,但是我最近遇到一个问题,我尝试用这两种编码对文本文件进行解析,却发现有的文件不能读取。

经过探测之后,发现有部分文件是UTF-16格式的。

于是我就想知道,电脑中的所有纯文本文件中,各种编码文件的占比是什么样的?

我可以用 chardet 库的 detect 函数可以对文件编码进行探测,但是得到的结果不够精确。

所以我设计了一个 Encoding 对象,用于统计指定文件是否符合某种编码:

  1. 对象属性中记录需要检测的编码格式、BOM匹配规则、和保存日志路径。

  2. 对象内的 check 方法传入待检测文件路径 path,尝试探测文件是否符合设定编码。

  3. 尝试以二进制模式打开路径,可能会遇到若干异常情况(文件不存在、文件权限不足、文件为网络路径无法打开等),如遇打开失败则认为检测失败。

  4. 如果没有捕获异常,则读取文件的前16个字节(为了提高速度),并判断是否和设定BOM保持一致。当BOM设定为空字节时,应总可以通过。

  5. 尝试以指定编码模式解码读取文本,如果解码错误则返回失败。如果编码类型为UNKNOWN,则跳过这一步骤。

  6. 如果成功读取文件内容,说明文件符合目标编码,将文件路径记录入日志,并更新计数值,返回结果为成功。否则返回结果为失败。

  7. 由于日志文件可能会被频繁读写,频繁打开文件可能导致写入失败,所以在创建Encoding对象时保存文件句柄,一次打开多次写入,最后统一关闭句柄资源。

Encoding类代码设计如下:

class Encoding:
    def __init__(self, encoding, suffix='', bom=b''):
        self.encoding = encoding
        self.bom = bom
        self.name = encoding + suffix
        self.log = open(f'{self.name}.txt', 'w', encoding='u8')
        self.count = 0

    def __repr__(self):
        return f"'{self.name}': {self.count}"

    def check(self, path):
        try:
            with open(path, 'rb') as f:
                assert f.read(16).startswith(self.bom)
            if self.encoding != 'unknown':
                with open(path, encoding=self.encoding) as f:
                    f.read()
            self.count += 1
            self.log.write(path + '\n')
            return True
        except Exception:
            return False

Python中遍历路径速度较慢,可用文件检索软件 Everything 更快速地获取电脑中的文件列表。

输入搜索语法:

ext:c;cpp;csv;cxx;h;hpp;htm;html;hxx;ini;java;lua;mht;mhtml;txt;xml;log size:<1MB

将文件列表保存为 filelist.txt,需要注意保存为TXT格式。

在完整代码中,按顺序设定9种编码检测规则,如果前面的检测已经成功则不再进行后面的检测。

将检测要求高的(比如对BOM有要求的)编码放在前面,编码规律不明显的GBK和BIG5放在Unicode编码的后面。

UNKNOWN放在最后面,这样前面8种规则都未能匹配的,就会落入UNKNOWN的统计范围。

但是如果在UNKNOWN编码的探测过程中,文件在一开始的打开时就已经失败,则不会落入任何一个统计项——因为未知原因读取不了文件内容,我不记录也罢。

然后程序打开 filelist.txt 文件,逐行读取文件路径,并用 Encoding 类中的 check 方法对文件编码进行探测。

完整代码如下:

import codecs

class Encoding:
    def __init__(self, encoding, suffix='', bom=b''):
        self.encoding = encoding
        self.bom = bom
        self.name = encoding + suffix
        self.log = open(f'{self.name}.txt', 'w', encoding='u8')
        self.count = 0

    def __repr__(self):
        return f"'{self.name}': {self.count}"

    def check(self, path):
        try:
            with open(path, 'rb') as f:
                assert f.read(16).startswith(self.bom)
            if self.encoding != 'unknown':
                with open(path, encoding=self.encoding) as f:
                    f.read()
            self.count += 1
            self.log.write(path + '\n')
            return True
        except Exception:
            return False

    def close(self):
        self.log.close()

encodings = [
    Encoding('ascii'),
    Encoding('utf-8', '-bom', codecs.BOM_UTF8),
    Encoding('utf-8'),
    Encoding('gbk'),
    Encoding('big5'),
    Encoding('utf-16', '-bom-le', codecs.BOM_UTF16_LE),
    Encoding('utf-16', '-bom-be', codecs.BOM_UTF16_BE),
    Encoding('utf-16'),
    Encoding('unknown'),
]

with open('filelist.txt', encoding='u8') as f:
    paths = f.read().splitlines()

print('total:', len(paths))

for cnt, path in enumerate(paths):
    if cnt % 1000 == 0:
        print(cnt, encodings)
    any(en.check(path) for en in encodings)

for en in encodings:
    en.close()

print(cnt + 1, encodings)

我分别在两台电脑上运行测试,得到统计结果如下:

编码 计数 占比
ascii 117229 60.02%
utf-8-bom 3913 2.00%
utf-8 47270 24.20%
gbk 21993 11.26%
big5 151 0.08%
utf-16-bom-le 1697 0.87%
utf-16-bom-be 37 0.02%
unknown 3015 1.54%
总数 195305 100.00%

另一台电脑:

编码 计数 占比
ascii 594589 88.58%
utf-8-bom 5021 0.75%
utf-8 38579 5.75%
gbk 16218 2.42%
big5 160 0.02%
utf-16-bom-le 1909 0.28%
utf-16-bom-be 0 0.00%
unknown 14789 2.20%
总数 671265 100.00%

两台电脑上测试结果基本一致,基本结论如下:

  1. 除去只包含纯英文字符的ASCII文件,占比最高的是UTF-8编码。但是其中约有10%含有3字节的BOM文件头。

  2. UTF-8编码和GBK(本地ANSI)的编码占比约为2:1,并且为电脑中占比最高的2种编码,这是和预期一致的。

  3. 意料之外的干扰项是UTF-16编码,其中以Big-Endian居多,和当前电脑的字节序一致。这一部分占GBK编码的约1/10。

  4. 然后还有极少量的UTF-16-BE编码和繁体字的BIG5编码。

  5. 一些无法探测的编码文件手动确认后发现,有部分存在俄文和日文,还有的存在局部乱码。由于文本并未全部符合某一种编码类型,所以也会认为检测失败。

另外说到UTF-16编码,我曾考虑过是否存在某些文件不包含BOM头,但是也符合UTF-16编码。但是实际情况是,在不含有BOM的1200多个能被UTF-16解码的文件中,经手动确认,100%全部都是误检测(但是含有一个UTF-32文件)。

这是由于无BOM的UTF-16编码格式只有非常弱的编码规律。生成一段随机的ASCII文本,都有可能可以按照UTF-16编码进行解码,当然解码出来的文字都是乱码。

你可能感兴趣的:(Python,python)