我平时处理 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 这个字段很有用,它能告诉你为什么库会这样判断。

检测顺序

  1. 先看 BOM,命中 UTF-8 BOM、UTF-16、UTF-32 时直接返回。
  2. 再判断是不是纯 ASCII。
  3. 然后校验 UTF-8 字节是否合法。
  4. UTF-8 不成立时,再用规则给 GBK、Big5、Shift_JIS 打分。
  5. 规则也不可靠时返回 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
}
声明:站内资源为整理优化好的代码上传分享与学习研究,如果是开源代码基本都会标明出处,方便大家扩展学习路径。请不要恶意搬运,破坏站长辛苦整理维护的劳动成果。本站为爱好者分享站点,所有内容不作为商业行为。如若本站上传内容侵犯了原著者的合法权益,请联系我们进行删除下架。