电脑上最常见的文本编码格式有UTF-8和GBK,但是我最近遇到一个问题,我尝试用这两种编码对文本文件进行解析,却发现有的文件不能读取。
经过探测之后,发现有部分文件是UTF-16格式的。
于是我就想知道,电脑中的所有纯文本文件中,各种编码文件的占比是什么样的?
我可以用 chardet
库的 detect
函数可以对文件编码进行探测,但是得到的结果不够精确。
所以我设计了一个 Encoding
对象,用于统计指定文件是否符合某种编码:
对象属性中记录需要检测的编码格式、BOM匹配规则、和保存日志路径。
对象内的 check
方法传入待检测文件路径 path
,尝试探测文件是否符合设定编码。
尝试以二进制模式打开路径,可能会遇到若干异常情况(文件不存在、文件权限不足、文件为网络路径无法打开等),如遇打开失败则认为检测失败。
如果没有捕获异常,则读取文件的前16个字节(为了提高速度),并判断是否和设定BOM保持一致。当BOM设定为空字节时,应总可以通过。
尝试以指定编码模式解码读取文本,如果解码错误则返回失败。如果编码类型为UNKNOWN,则跳过这一步骤。
如果成功读取文件内容,说明文件符合目标编码,将文件路径记录入日志,并更新计数值,返回结果为成功。否则返回结果为失败。
由于日志文件可能会被频繁读写,频繁打开文件可能导致写入失败,所以在创建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% |
两台电脑上测试结果基本一致,基本结论如下:
除去只包含纯英文字符的ASCII文件,占比最高的是UTF-8编码。但是其中约有10%含有3字节的BOM文件头。
UTF-8编码和GBK(本地ANSI)的编码占比约为2:1,并且为电脑中占比最高的2种编码,这是和预期一致的。
意料之外的干扰项是UTF-16编码,其中以Big-Endian居多,和当前电脑的字节序一致。这一部分占GBK编码的约1/10。
然后还有极少量的UTF-16-BE编码和繁体字的BIG5编码。
一些无法探测的编码文件手动确认后发现,有部分存在俄文和日文,还有的存在局部乱码。由于文本并未全部符合某一种编码类型,所以也会认为检测失败。
另外说到UTF-16编码,我曾考虑过是否存在某些文件不包含BOM头,但是也符合UTF-16编码。但是实际情况是,在不含有BOM的1200多个能被UTF-16解码的文件中,经手动确认,100%全部都是误检测(但是含有一个UTF-32文件)。
这是由于无BOM的UTF-16编码格式只有非常弱的编码规律。生成一段随机的ASCII文本,都有可能可以按照UTF-16编码进行解码,当然解码出来的文字都是乱码。