我平时处理 txt、csv、日志、网页片段时,经常会遇到一个很烦的问题:文件能打开,但读出来就是乱码。很多新手第一反应是反复试 FileEncoding,试到能读为止。这个办法能救急,但一旦文件来源多起来,就很难维护。
ChardetLiteAHK 是我整理出来的一个 AHK v1 轻量编码检测库。我借鉴了 Python 的 chardet,但它不追求识别世界上所有编码。它的目标更实际:帮 AHK 脚本在读取文件前,先大概判断这个文件更像 UTF-8、GBK、Big5、Shift_JIS、ASCII,或者是否带 BOM。
它适合解决什么问题
- 批量读取 txt、csv、log 前,先判断文件编码。
- 下载网页片段、字幕、配置文件后,先做一个编码预判。
- 脚本需要兼容别人发来的 ANSI、UTF-8 BOM、UTF-16 文档。
- 排查中文乱码时,想知道文件本身更像哪种编码。
我做这个库时没有把它写成“万能识别器”。AHK 自动化里真正常见的,通常就是 UTF-8、带 BOM 的 Unicode、GBK、Big5、Shift_JIS 这几类。把这几类处理稳,比堆一大堆用不上的模型更实用。
基本用法
#Requires AutoHotkey v1.1
#Include <ChardetLiteAHK>
result := ChardetLite_DetectFile("data.csv")
MsgBox, % "encoding: " . result.encoding . "`n"
. "confidence: " . result.confidence . "`n"
. "bom: " . result.bom . "`n"
. "reason: " . result.reason
返回结果不是只有一个编码名,还会带上置信度、BOM 信息和判断原因。调试乱码问题时,reason 这个字段很有用,它能告诉你为什么库会这样判断。
检测顺序
- 先看 BOM,命中 UTF-8 BOM、UTF-16、UTF-32 时直接返回。
- 再判断是不是纯 ASCII。
- 然后校验 UTF-8 字节是否合法。
- UTF-8 不成立时,再用规则给 GBK、Big5、Shift_JIS 打分。
- 规则也不可靠时返回 Unknown,不硬猜。
读取大文件时别全量读
库默认只读取前 64KB 做采样。对于日志、导出数据、大 CSV 来说,这个习惯很重要。编码判断没必要把几百 MB 文件整个读进内存。
result := ChardetLite_DetectFile("large.log", 262144) ; 读取前 256 KB
if (result.encoding = "GBK")
FileEncoding, CP936
else if (result.encoding = "UTF-8")
FileEncoding, UTF-8
我建议怎么用
如果你的脚本只是读取自己生成的文件,最稳的办法仍然是统一写成 UTF-8。这个库更适合“文件来源不受你控制”的场景,比如用户导入、别人发来的表格导出、第三方日志、旧系统文本。
它的定位是轻量预判断,不是司法鉴定。特别短的中文文本、混合编码、二进制伪装文本,都可能误判。实际项目里,我会把检测结果、置信度和失败日志一起保存,方便后面复查。
打包库和示例的下载地址:
demo代码片段展示:
#NoEnv
#SingleInstance Force
#Include <ChardetLiteAHK>
; 直接运行 demo.ahk,会弹出 MsgBox 显示检测结果。
tmpDir := CL_CreateTempDir()
if (tmpDir = "")
CL_Fail("无法创建临时目录。")
; 为了演示不同编码的检测效果,先临时生成一组样例文件。
samples := []
if (!CL_CreateSamples(tmpDir, samples)) {
FileRemoveDir, %tmpDir%, 1
CL_Fail("无法创建示例文件。")
}
summary := ""
CL_Log("ChardetLiteAHK 编码检测演示")
CL_Log("")
; 逐个检测样例文件,并输出编码、置信度、BOM 和判断原因。
for _, sample in samples {
result := ChardetLite_DetectFile(sample.path)
line := sample.label . " -> 编码=" . result.encoding . ",置信度=" . result.confidence
if (result.bom != "")
line .= ",BOM=" . result.bom
line .= ",原因=" . result.reason
CL_Log(line)
summary .= line . "`n"
}
; 额外演示:检测项目目录里的真实测试文档。
realFile := A_ScriptDir . "\测试编码文档.txt"
CL_Log("")
if (FileExist(realFile)) {
result := ChardetLite_DetectFile(realFile)
line := "真实文件:测试编码文档.txt -> 编码=" . result.encoding . ",置信度=" . result.confidence
if (result.bom != "")
line .= ",BOM=" . result.bom
line .= ",原因=" . result.reason
CL_Log(line)
summary .= "`n" . line . "`n"
} else {
line := "真实文件:测试编码文档.txt -> 未找到文件:" . realFile
CL_Log(line)
summary .= "`n" . line . "`n"
}
; 演示文件只用于本次检测,结束后立即清理。
FileRemoveDir, %tmpDir%, 1
MsgBox, 64, ChardetLiteAHK 示例完成, % "编码检测演示完成。`n`n" . summary
ExitApp, 0
CL_CreateSamples(tmpDir, ByRef samples) {
; 文件名只放在临时目录里,不污染项目目录。
asciiPath := tmpDir . "\ascii.txt"
utf8BomPath := tmpDir . "\utf8_bom.txt"
utf8Path := tmpDir . "\utf8.txt"
utf16LePath := tmpDir . "\utf16le.txt"
utf16BePath := tmpDir . "\utf16be.txt"
utf32LePath := tmpDir . "\utf32le.txt"
utf32BePath := tmpDir . "\utf32be.txt"
gbkPath := tmpDir . "\gbk.txt"
big5Path := tmpDir . "\big5.txt"
sjisPath := tmpDir . "\shift_jis.txt"
if (!CL_WriteEncodedFile(asciiPath, "Plain ASCII log line 123", 20127))
return 0
samples.Push({label: "纯 ASCII 文本", path: asciiPath})
utf8Text := CL_SimplifiedChineseText()
if (!CL_WriteEncodedFile(utf8BomPath, utf8Text, 65001, [0xEF, 0xBB, 0xBF]))
return 0
samples.Push({label: "UTF-8 带 BOM", path: utf8BomPath})
if (!CL_WriteEncodedFile(utf8Path, utf8Text, 65001))
return 0
samples.Push({label: "UTF-8 无 BOM", path: utf8Path})
if (!CL_WriteByteArray(utf16LePath, [0xFF, 0xFE, 0x41, 0x00, 0x42, 0x00]))
return 0
samples.Push({label: "UTF-16LE 带 BOM", path: utf16LePath})
if (!CL_WriteByteArray(utf16BePath, [0xFE, 0xFF, 0x00, 0x41, 0x00, 0x42]))
return 0
samples.Push({label: "UTF-16BE 带 BOM", path: utf16BePath})
if (!CL_WriteByteArray(utf32LePath, [0xFF, 0xFE, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00]))
return 0
samples.Push({label: "UTF-32LE 带 BOM", path: utf32LePath})
if (!CL_WriteByteArray(utf32BePath, [0x00, 0x00, 0xFE, 0xFF, 0x00, 0x00, 0x00, 0x41]))
return 0
samples.Push({label: "UTF-32BE 带 BOM", path: utf32BePath})
if (!CL_WriteEncodedFile(gbkPath, CL_SimplifiedChineseText(), 936))
return 0
samples.Push({label: "简体中文 GBK", path: gbkPath})
if (!CL_WriteEncodedFile(big5Path, CL_TraditionalChineseText(), 950))
return 0
samples.Push({label: "繁体中文 Big5", path: big5Path})
if (!CL_WriteEncodedFile(sjisPath, CL_JapaneseText(), 932))
return 0
samples.Push({label: "日文 Shift_JIS", path: sjisPath})
return 1
}
CL_SimplifiedChineseText() {
return Chr(0x4E2D) . Chr(0x6587) . Chr(0x7F16) . Chr(0x7801) . Chr(0x6D4B) . Chr(0x8BD5) . " ABC 123"
}
CL_TraditionalChineseText() {
return Chr(0x7E41) . Chr(0x9AD4) . Chr(0x4E2D) . Chr(0x6587) . Chr(0x6E2C) . Chr(0x8A66) . " ABC 123"
}
CL_JapaneseText() {
return Chr(0x65E5) . Chr(0x672C) . Chr(0x8A9E) . Chr(0x30C6) . Chr(0x30B9) . Chr(0x30C8) . " ABC 123"
}
CL_WriteEncodedFile(path, text, codePage, bomValues := "") {
f := FileOpen(path, "w")
if (!IsObject(f))
return 0
if (IsObject(bomValues))
CL_WriteValuesToOpenFile(f, bomValues)
; AHK v1 内部是 Unicode 字符串,这里调用 Windows API 转成指定代码页的原始字节。
charCount := StrLen(text)
if (charCount > 0) {
byteCount := DllCall("WideCharToMultiByte", "UInt", codePage, "UInt", 0, "WStr", text, "Int", charCount, "Ptr", 0, "Int", 0, "Ptr", 0, "Ptr", 0, "Int")
if (byteCount <= 0) {
f.Close()
return 0
}
VarSetCapacity(buf, byteCount, 0)
writtenBytes := DllCall("WideCharToMultiByte", "UInt", codePage, "UInt", 0, "WStr", text, "Int", charCount, "Ptr", &buf, "Int", byteCount, "Ptr", 0, "Ptr", 0, "Int")
if (writtenBytes <= 0) {
f.Close()
return 0
}
f.RawWrite(buf, byteCount)
}
f.Close()
return 1
}
CL_WriteByteArray(path, values) {
f := FileOpen(path, "w")
if (!IsObject(f))
return 0
ok := CL_WriteValuesToOpenFile(f, values)
f.Close()
return ok
}
CL_WriteValuesToOpenFile(file, values) {
count := 0
for _, value in values
count++
if (count <= 0)
return 1
VarSetCapacity(buf, count, 0)
i := 0
; RawWrite 用于写入 BOM、UTF-16/UTF-32 这类包含 NUL 的二进制字节。
for _, value in values {
NumPut(value & 0xFF, buf, i, "UChar")
i++
}
file.RawWrite(buf, count)
return 1
}
CL_CreateTempDir() {
Random, suffix, 1000, 9999
path := A_Temp . "\ChardetLiteAHK_" . A_Now . "_" . A_TickCount . "_" . suffix
FileCreateDir, %path%
return FileExist(path) ? path : ""
}
CL_Log(text) {
line := text . "`n"
FileAppend, %line%, *
}
CL_Fail(message) {
CL_Log("[失败] " . message)
MsgBox, 16, ChardetLiteAHK 示例失败, %message%
ExitApp, 1
}
声明:站内资源为整理优化好的代码上传分享与学习研究,如果是开源代码基本都会标明出处,方便大家扩展学习路径。请不要恶意搬运,破坏站长辛苦整理维护的劳动成果。本站为爱好者分享站点,所有内容不作为商业行为。如若本站上传内容侵犯了原著者的合法权益,请联系我们进行删除下架。

评论(0)