打开一个文件需要两件事同时对:格式解析器知道数据的结构(这是 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 在某些实现里会被解析为布尔 false1.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 工具配置JSONCVS 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+1F389

Unicode 只是一张对照表,不规定如何存储为字节。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-8EF BB BF(可选)
UTF-16 BEFE FF
UTF-16 LEFF 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 -Ihexdump -C 确认原始字节,再判断用了什么编码、应该用什么编码,最后用 iconv 或编辑器的”以指定编码重新打开”功能修复。