打开一个文件需要两件事同时对:格式解析器知道数据的结构(这是 JSON 还是 YAML?键值在哪,数组在哪),字符编码告诉解析器字节如何翻译成字符(这是 UTF-8 还是 GBK?E4 BD A0 是”你”还是乱码?)。两个层次各自独立,又紧密嵌套——格式选错会让程序崩溃,编码选错会让内容变乱码。
1. 数据交换格式
格式是人、程序、CI、Agent 和远程服务之间的契约。选择格式不是审美偏好,而是在可读性、类型能力、注释支持、性能和生态兼容性之间做取舍。
JSON
JSON 是 Web API 和服务间通信的事实标准,几乎所有语言都内置解析库。
{
"users": [
{ "name": "Alice", "age": 30, "active": true },
{ "name": "Bob", "age": 25, "active": false }
],
"version": "1.0"
}特点:类型明确(字符串/数字/布尔/null/数组/对象),无注释支持(这是有意为之——JSON 是数据格式,不是配置格式),结构清晰,机器解析快。
变体:VS Code 的配置文件用 JSONC(支持 // 和 /* */ 注释),JSON5 支持尾逗号和无引号键名。这两种变体不是标准 JSON,不能用于 API 数据交换,只适合工具配置。
何时用:API 响应、服务间数据传输、配置不需要注释的轻量场景。
YAML
YAML 用缩进代替括号,对人更友好,是 DevOps 生态的主流配置格式。
users:
- name: Alice
age: 30
active: true
- name: Bob
age: 25
active: false
settings:
theme: dark
timeout: 30特点:支持注释(#),缩进即层级,类型推断自动(true 是布尔,30 是整数,"30" 是字符串),表达力强。
陷阱:缩进敏感——一个 Tab 混进 Space 会让解析报错,而且错误信息往往指向错误的行。YAML 类型推断有出名的歧义:no 在某些实现里会被解析为布尔 false,1.0 可能成为浮点数。还有一个安全风险:某些语言的 YAML 库(如 Python PyYAML 的 load())支持反序列化对象,可以执行任意代码。不可信来源的 YAML 必须用 safe_load()。
何时用:Kubernetes 配置、GitHub Actions / GitLab CI、Ansible Playbook、Helm Chart。人写机读的复杂配置场景。
TOML
TOML 的设计目标是”显而易见,无歧义”,是现代项目配置的新标准。
[database]
server = "192.168.1.1"
port = 5432
enabled = true
[owner]
name = "Alice"
joined = 2024-01-15 # 原生日期类型特点:支持注释,类型严格(没有 YAML 那样的隐式推断),[section] 结构清晰,原生支持日期时间类型,不允许任何歧义写法。
生态:Cargo.toml(Rust)、pyproject.toml(Python)已把 TOML 推成主流语言生态的项目配置默认格式。
何时用:工具配置文件、项目元数据(语言版本、依赖声明)、需要有注释且结构不太复杂的场景。
CSV
CSV 是最古老也最通用的表格数据格式,几乎所有数据工具都能读写。
name,age,city
Alice,30,Beijing
Bob,25,Shanghai
"Smith, Jr.",28,"San Francisco"包含逗号或换行的字段用双引号包裹,双引号本身用 "" 转义。TSV(Tab 分隔)是变体,避免了数据中含逗号时的转义问题。
陷阱:CSV 没有类型信息,所有值都是字符串,数字和布尔由读取方自行判断。分隔符(逗号/Tab/分号)、引号规则和换行符(\n vs \r\n)在跨系统传输时要提前约定。
何时用:数据导入导出(数据库、电子表格)、数据分析和机器学习数据集、Excel 兼容交换。
XML 与 Protobuf
XML:标记语言,支持属性和嵌套元素,表达力强但冗长。仍然活跃于企业系统(SOAP)、Android 布局、Maven 配置等场景,新项目通常不选它。
<person>
<name>Alice</name>
<age>30</age>
<address city="Beijing" />
</person>Protobuf:Google 开发的二进制序列化格式。用 .proto 文件定义 schema,编译后生成强类型的序列化/反序列化代码。体积比 JSON 小 3-10 倍,解析速度快几倍,但不可直接阅读。
syntax = "proto3";
message Person {
string name = 1;
int32 age = 2;
}何时用 Protobuf:gRPC 服务间通信、高频微服务调用、移动端流量敏感场景——需要管理 schema,团队接受人类不可读的代价。
格式选型
| 场景 | 推荐格式 | 原因 |
|---|---|---|
| Web API / 服务间通信 | JSON | 生态最广,所有语言原生支持 |
| DevOps 配置(K8s、CI/CD) | YAML | 生态既成,无法绕开 |
| 项目配置(依赖、工具链) | TOML | 无歧义,注释友好,现代语言默认 |
| 表格数据交换 | CSV | 最广兼容,Excel/数据库通吃 |
| 需要注释的 JSON 工具配置 | JSONC | VS Code、TypeScript 配置场景 |
| 高性能服务间通信 | Protobuf | 速度和体积优先于可读性 |
| 企业遗留系统对接 | XML | 无法选择,按已有格式适配 |
格式一旦进入项目就是承诺,后续自动化脚本、日志解析、API 客户端都依赖它。选型时想清楚:谁来写(人还是程序)、谁来读(人还是程序)、需不需要注释、类型是否关键、和什么工具集成。
2. 字符编码
格式告诉你数据结构,编码告诉你字节如何变成字符。两者任一出错,结果都是乱码或程序崩溃。
编码的本质
编码是一个映射约定:
字符 → 码点(唯一编号) → 字节序列 → 存储 / 传输乱码的根源:发送方用编码 A 把字符变成字节,接收方用编码 B 解读——A ≠ B 就是乱码。同一个字节序列在不同编码规则下代表完全不同的字符。
ASCII 与编码分裂
ASCII 用 7 位表示 128 个字符(英文字母、数字、标点、控制符),足以覆盖英文,却装不下中文、日文、阿拉伯文。
这催生了各地区的独立编码:GBK(中文简体)、Big5(中文繁体)、Shift-JIS(日文)、Latin-1(西欧)……同一份文件在不同系统上打开内容不同,文件交换是噩梦。这种混乱催生了 Unicode。
ASCII 的遗产:所有现代编码的前 128 个码位与 ASCII 完全兼容。纯英文文本在任何编码下看起来都一样——这是 Unicode 过渡成本低的原因之一。
Unicode 与 UTF-8
Unicode 是字符集(给每个字符分配唯一编号),UTF-8 是编码方式(把编号转成字节)。两者不是同一件事。
Unicode 给每个字符一个码点(Code Point):
A → U+0041 中 → U+4E2D 🎉 → U+1F389Unicode 只是一张对照表,不规定如何存储为字节。UTF-8、UTF-16、UTF-32 才是实现。
UTF-8 是互联网的事实标准,核心是变长编码(1-4 字节):ASCII 字符占 1 字节,中文字符占 3 字节,Emoji 和生僻字占 4 字节。
UTF-8 赢了有几个原因:向后兼容 ASCII(迁移成本为零)、自同步(可以从任意位置判断是否是字符开头)、无字节序问题、英文场景空间效率高。HTML、JSON、HTTP 默认字符集都是 UTF-8。
UTF-16 是 Windows 内部文本(wchar_t)和 Java/JavaScript 字符串内部表示的格式——BMP 内字符 2 字节,辅助平面字符需要代理对(4 字节)。这就是为什么 JavaScript 里 "𠮷".length === 2——该字符落在辅助平面,用了代理对,被 JS 引擎算成 2 个代码单元。
BOM
BOM(Byte Order Mark)是文件开头的特殊字节,用于标识编码和字节序:
| 编码 | BOM 字节 |
|---|---|
| UTF-8 | EF BB BF(可选) |
| UTF-16 BE | FE FF |
| UTF-16 LE | FF FE |
UTF-8 文件不应加 BOM。 UTF-8 没有字节序问题,BOM 只有副作用:带 BOM 的 shell 脚本会执行失败(EF BB BF 被当成 shebang 的一部分);HTML 文件开头可能渲染出不可见字符;CSV 文件第一列第一个单元格前面会多一个看不见的 ,让 if header[0] == "name" 的判断失败。
Windows 记事本默认保存 UTF-8-with-BOM,这是很多跨平台问题的来源。
Base64
Base64 把任意二进制数据转为可打印 ASCII 字符,原理是每 3 字节变成 4 个字符(6 位一组,映射到 64 个字符 A-Z、a-z、0-9、+、/)。不足 3 字节用 = 填充,体积增加约 33%。
echo -n "Hello" | base64 # SGVsbG8=
echo "SGVsbG8=" | base64 -d # Hello常见场景:邮件附件(MIME)、Data URL(data:image/png;base64,...)、JWT Token(Base64URL 变体,+ → -,/ → _,不需要 = 填充)。
Base64 不是加密。 任何人都能解码,安全性来自签名或加密层,不来自 Base64 本身。看到 JWT 被截取时,先 Base64 解码看 header 和 payload 是标准调试操作。
乱码诊断
乱码的诊断流程:确认当前编码 → 推测原始编码 → 用正确编码重新解读。
file -I hello.txt # 检测文件编码
hexdump -C hello.txt # 看原始字节
iconv -f GBK -t UTF-8 input.txt > out.txt # 编码转换常见乱码模式:
| 看到的 | 原因 | 修复 |
|---|---|---|
䏿–‡ | UTF-8 字节被当 Latin-1 解读 | 用 UTF-8 重新打开 |
涓枃 | UTF-8 字节被当 GBK 解读 | 用 UTF-8 重新打开 |
锟斤拷 | 双重错误转码 | 原始数据已丢失,无法修复 |
| UTF-8 BOM 被当 Latin-1 解读 | 去掉文件 BOM |
Python 里遇到 UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4,几乎都是用了错误的 encoding 参数——指定 encoding="utf-8" 即可。
Emoji 与字形簇
Emoji 看起来是一个字符,实际可能是多个码点的序列。家庭 Emoji 👨👩👧 是 5 个码点通过 ZWJ(Zero Width Joiner,U+200D)拼合的:
👨(U+1F468) + ZWJ + 👩(U+1F469) + ZWJ + 👧(U+1F467)所以 len("👨👩👧") 在 Python 里返回 5,在 JavaScript 里返回 11(UTF-16 代码单元数)。
处理用户输入中的字符数(用户名长度、短信长度、UI 截断)时,不能用简单的 len(),需要按**字形簇(Grapheme Cluster)**计算——用户眼中看到的”一个字符”。
3. 工程默认值
数据格式:给机器和 API 用 JSON,给人维护的工程配置用 TOML 或 YAML,给表格交换用 CSV,给强类型高性能服务用 Protobuf。不要在 API 里用 YAML,不要把 JSONC 当成标准 JSON 传给第三方。
字符编码:新文件、新接口、新脚本统一 UTF-8,不加 BOM。在文件、HTTP 响应头、数据库连接、脚本运行环境里保持一致——编码问题几乎都发生在跨边界的地方,而不是单一系统内部。
跨平台换行:Unix 用 LF(\n),Windows 用 CRLF(\r\n)。Git 通过 .gitattributes 统一管理,团队协作时要明确设置,否则 diff 会全红。
乱码不靠猜:看到乱码时先 file -I 或 hexdump -C 确认原始字节,再判断用了什么编码、应该用什么编码,最后用 iconv 或编辑器的”以指定编码重新打开”功能修复。