一次跨越字符编码的探险
那天,我随手给 AI 发了一句话:"帮我找找这个汉字乱码问题出现的原因。"本以为最多几分钟就能得到答案,没想到 AI 竟然足足折腾了半个多小时。
它没有直接给我答案,而是像一位执着的侦探,不断翻阅资料、反复做实验、一步步排除可能性。最终,AI 结合微软文档和 PowerShell 源码,硬是把这个藏在管道深处的编码陷阱挖了出来。
以下是我和 AI 这次合作的完整记录。这个故事告诉我们:有时候,一个好问题比答案更有价值;而 AI 的潜力,往往在我们给它足够空间去探索时才会真正爆发。
🤔 问题初现
事情是这样的。我的朋友(以下简称"用户")在使用 opencode 时,有一个会话备份脚本,功能是把 AI 对话历史导出为 JSON 文件保存。某天用户发现,脚本运行时明明显示"Export: success",但用 Python 打开文件却报错了:
这是一个 JSON 解析错误,意味着文件内容在某个地方被破坏了。
🕵️ 初步调查
我首先想到的是 BOM(Byte Order Mark)问题。BOM 是 UTF-8 文件开头的 3 个特殊字节,告诉程序"这是一个 UTF-8 文件"。某些情况下,有 BOM 的 JSON 会导致解析失败。
我检查了备份文件的开头:
Get-Content "backup.json" -Encoding Byte -TotalCount 3
# 结果:239, 187, 191 —— 这就是 UTF-8 BOM!
239, 187, 191 正好是 0xEF 0xBB 0xBF,标准的 UTF-8 BOM。
🔧 第一轮修复:向 BOM 宣战
我开始尝试去掉 BOM,尝试了多种方法:
$Utf8NoBom = New-Object System.Text.UTF8Encoding($False)
[System.IO.File]::WriteAllText($outputFile, $result, $Utf8NoBom)
# 方法2:手动去掉 FEFF 字符
$result = $result.TrimStart([char]0xFEFF)
文件头确实干净了(不再是 239,187,191),但 JSON 解析仍然失败。
🔍 深挖:十六进制下的真相
我决定直接看文件内容,特别是报错提到的"第 6 行第 28 列"。我把文件拆开,一行一行地检查:
$raw = opencode export ses_xxx 2>&1 | Select-Object -Skip 1
$bytes = [System.Text.Encoding]::UTF8.GetBytes(($raw -join "`n"))
然后用 Python 检查原始字节:
lines = content.split(b'\n')
print('Line 6:', lines[5][:80])
结果让我愣住了:
"directory": "D:\\资料库\\文档\\THU\\活动\\ALS Program"
# 实际保存的是(编码被打乱了):
"directory": "D:\\\\\\xe7\\x92\\xa7\\xe5\\x8b\\xac\\xe6\\x9e\\xa1..."
路径中的中文字符完全变样了!这些 \xe7\x92\xa7 看起来像是被错误解读后的 UTF-8 字节序列。
🎯 关键突破:opencode 本身是清白的
我开始怀疑是不是 opencode 本身输出就有问题。于是我换了一种方式捕获输出——用 Start-Process 的重定向功能,直接把标准输出写到文件,不经过 PowerShell 管道:
Start-Process -FilePath "opencode" -ArgumentList "export","ses_xxx" `
-NoNewWindow -Wait -PassThru `
-RedirectStandardOutput "$env:TEMP\opencode_out.txt"
然后验证这个文件:
这证明了 opencode 输出的 JSON 数据本身是完整正确的,问题出在 PowerShell 处理输出的过程中!
🕳️ 罪魁祸首:PowerShell 管道的编码陷阱
🔬 还原作案现场
我终于理解了真正发生了什么:
opencode export 这个命令实际上会往两个地方写东西:
- stdout(标准输出):纯净的 UTF-8 JSON 数据
- stderr(标准错误):一条状态信息 "Exporting session: ses_xxx"
原脚本用 2>&1 把两者合并,然后通过 PowerShell 管道传递:
$jsonLines = $rawOutput | Select-Object -Skip 1 # 管道处理
[System.IO.File]::WriteAllLines($outputFile, $jsonLines) # 写入文件
问题就出在管道的每一步:PowerShell 在处理这个外部命令的输出时,会把字节流当成"字符串"来处理。而中文、日文等非 ASCII 字符在 UTF-8 中是多个字节(通常 3-4 字节),PowerShell 的字符串系统在处理过程中会错误地拆分/合并这些字节,导致编码彻底破坏。
✅ 修复方案
既然管道会破坏编码,那就不用管道。改用进程重定向,让 stdout 的字节流直接落盘:
$rawOutput = opencode export $sessionId 2>&1
$jsonLines = $rawOutput | Select-Object -Skip 1
[System.IO.File]::WriteAllLines($outputFile, $jsonLines)
# 之后(完美保留编码)
$proc = Start-Process -FilePath "opencode" -ArgumentList "export", $sessionId `
-NoNewWindow -Wait -PassThru `
-RedirectStandardOutput $tempFile
if ($proc.ExitCode -eq 0) {
Copy-Item $tempFile $outputFile -Force
}
关键在于:Start-Process -RedirectStandardOutput 直接把进程的 stdout 字节流写入文件,完全不经过 PowerShell 的字符串系统,所以 UTF-8 编码得以完整保留。
📊 问题总结
| 做法 | 结果 |
|---|---|
| PowerShell 管道捕获外部命令输出 | ❌ 编码会被破坏 |
Start-Process -RedirectStandardOutput |
✅ 字节流直接落盘 |
管道 + -join "`n" + WriteAllLines |
❌ 仍会损坏 UTF-8 |
| 直接重定向后 Copy-Item | ✅ 保持原始编码 |
🌟 经验教训
Step 1: 不要假设
一开始我以为"BOM 导致的",花了大量时间研究如何去 BOM,结果走了弯路。正确的做法是先验证"原始数据是否正确"。
Step 2: 用正确的工具
十六进制查看是排查编码问题的利器。Python 的 bytes 类型可以精确看到每个字节,而不会被字符串解释干扰。
Step 3: 隔离变量
当怀疑某个环节有问题时,想办法绕过它。我通过 Start-Process 绕过了整个管道系统,一下就定位到了问题。
Step 4: 理解底层
PowerShell 的管道是基于 .NET 的字符串对象工作的,而不是字节流。这在处理二进制数据或非 ASCII 文本时容易出问题。
📚 附录:什么是 BOM?
UTF-8 的 BOM 是文件开头的 3 个字节 0xEF 0xBB 0xBF,作用是告诉阅读者"这个文件是 UTF-8 编码的"。但很多 JSON 解析器不认可 BOM,认为它不是合法的 JSON 内容。
EF BB BF
📚 附录:stdout 和 stderr 是什么?
每个程序运行时都可以向两个"通道"输出信息:
- stdout(标准输出):正常输出,如程序的计算结果、数据等
- stderr(标准错误):错误信息、状态提示等
默认情况下,两者都显示在终端上,但它们是独立的。2>&1 表示"把 stderr 合并到 stdout"。
Comments NOTHING