上一篇说了把文本打包成 ZIM 来省空间,顺手就想把"文本"这件事展开说说。
这些年折腾数据,从地球化学的仪器导出,到爬虫抓回来的网页,再到给大模型准备的训练语料,几乎每天都跟各种纯文本格式打交道。什么 .txt、.dat、.csv、.json、.jsonl、.md,后缀五花八门,很多人是"知道有这个东西,但说不清它到底是啥、为什么存在、跟旁边那个有什么区别"。
这篇文章就把常见的纯文本格式一个个过一遍:它是干嘛的,典型用在什么场景,优势劣势是什么,以及实际用的时候会踩的坑。
先说清楚,什么是"纯文本"
纯文本(plain text)就是一个字节序列,每个字节都对应着字符,没有任何关于字体、颜色、加粗这类排版信息。你用记事本能打开、用 Vim 能打开、用 VS Code 能打开,看到的内容是一样的,这就是纯文本。
跟它相对的是二进制格式:Word 的 .docx、Excel 的 .xlsx、照片的 .jpg。这些用记事本打开就是一堆乱码,因为里面存的是压缩后的结构,不是字符。
纯文本有两个特别容易被忽略的基础问题,几乎所有"打开是乱码"的事故都出在这。
第一是编码。 计算机里只有数字,字符要用数字来表示,这个对应关系就是编码。老祖宗是 ASCII,一个字节搞定 128 个英文字符;中文进来之后,国家搞了 GB2312、GBK,一个汉字两个字节;后来全世界统一到 UTF-8,英文一个字节、中文三个字节,兼容 ASCII,成为事实标准。
乱码的根源基本都是一个:文件是一种编码写的,你用另一种编码去读。最典型的就是 Windows 上老版本记事本,默认用本地编码(中文系统就是 GBK)保存,文件拿给 Mac 上默认 UTF-8 的编辑器一打开,就是"鏂囨湰鏍煎紡"这种天书。反过来,UTF-8 的文件用 GBK 去读,也是一锅粥。
第二是换行。 Windows 用回车加换行(\r\n,写作 CRLF),Linux 和 macOS 用换行(\n,写作 LF),老款 Mac 用单独的回车(\r,基本绝迹了)。同一个文件在两边倒腾,有时会出现"每行末尾多了个 ^M"或者"整个文件挤成一行"的怪象,就是这俩在打架。Git 配置里那个 core.autocrlf,就是为了处理这个。
后面讲每个格式的时候,这两个坑会反复出现,先记住。
txt:万能的口袋
.txt 是最原始的格式,它的定义几乎等于"纯文本本身":没有任何结构约定,就是一段字符。
它的用途也就因此特别宽:写笔记、存日志、装语料、放配置、当程序之间交换数据的最简陋协议,都行。我手里那个相声文本库,最早就是 234 个 .txt 文件,后来才打包成 ZIM 的。
txt 的优势是极致的简单和通用。任何设备、任何年代的系统都能打开它,五十年前的程序能读,五十年后的也能读。作为"给人看、给人存"的载体,它几乎不会出错。
劣势是没有任何结构。没有行列的概念,没有字段的区分,程序想从里面提取"第二列的分数",就得自己写正则去抠。数据一有结构,txt 就不够用了。所以 txt 更像一张白纸,而后面这些格式,都是在这张纸上画了不同的格子。
dat:一个约定俗成的黑盒
.dat 挺有意思,它是这堆格式里唯一没有标准的一个。
data 的缩写,谁都可以用,所以它代表什么,完全取决于生成它的那个软件。可能是制表符分隔的表格,可能是逗号分隔,可能是自己发明的格式,甚至可能是二进制数据(比如很多仪器的原始数据、Windows 的某些缓存文件)混着叫 .dat。
我早些年处理地球化学数据的时候,经常从仪器软件里导出 .dat 文件,每次拿到一个新仪器的导出,第一件事就是用文本编辑器打开看一眼:是文本就好办,用 pandas read_csv 配上正确的分隔符就能读;打不开或者满屏乱码,那就得找说明书,或者找厂家要格式说明。
所以对 .dat 的态度就是:别看后缀,先打开看内容。它是文本还是二进制、用什么分隔符、有没有表头,都得靠眼睛确认。它的存在更多是一种历史习惯,而不是一种"格式"。
csv 和 tsv:表格的最低配
.csv,Comma-Separated Values,逗号分隔值,是表格数据的最低配实现:第一行表头,往后每行一条记录,字段之间用逗号隔开。
name,age,city
张三,30,北京
李四,25,上海
.tsv 是它的兄弟,Tab-Separated Values,用制表符分隔。制表符在正常文本里出现得少,所以 tsv 省去了转义的麻烦,但肉眼看起来没那么直观。
csv 的优势很明显:表格数据的通用交换语言。Excel、数据库、pandas、R、各种 BI 工具,全都认它。文本格式意味着版本控制能看 diff、任何语言都能解析、几十年的工具链都支持。
但 csv 的坑,可能比它的优势还多,我一个个说。
第一个坑,分隔符本身会出现在数据里。 比如地址是"北京市,海淀区",逗号一进去,程序就把一行读成两列了。标准解法是用引号把字段包起来,"北京市,海淀区";引号本身要转义成两个引号。这套规则写在 RFC 4180 里,但很多软件并不严格遵守,于是"能生成 csv 的软件"和"能正确读 csv 的软件"之间就出现了大量错位。
第二个坑,Excel。 用 Excel 打开 UTF-8 编码的 csv,中文经常是乱码,因为它默认不认 UTF-8,除非你带上 BOM(一个隐藏在文件开头的 EF BB BF 三字节标记)。所以用 Python 写 csv 给 Excel 用户时,要特意用 utf-8-sig 编码:
import pandas as pd
df.to_csv("out.csv", index=False, encoding="utf-8-sig")
Excel 还有更出名的毛病:打开 csv 时自作聪明做类型转换。基因名 SEPT1 被它理解成 9 月 1 日,MARCH1 变成 3 月 1 日;长数字(比如银行卡号、经纬度)被显示成科学计数法,再保存回去,末尾几位就永久变成 0 了。这不是段子,2016 年《Genome Biology》上有一篇论文就因为这个把五分之一的基因名写错了,后来人类基因组织(HGNC)干脆在 2020 年把二十多个带这类前缀的基因符号改了名,就为了躲开 Excel。
第三个坑,它没有类型。 csv 里的一切都是字符串,1.0、01、1e3 之间毫无区别,全靠读取方去猜。日期格式更是五花八门,2026-09-16、2026/9/16、16-09-2026 都可能是"日期",解析的时候得自己指定。
所以我对 csv 的使用心得是:它是交换格式,不是存储格式。中间文件、临时导出用它没问题;正经存数据,要么进数据库,要么用 Parquet 这类列式格式。
json:结构化数据的事实标准
.json,JavaScript Object Notation,现在已经是跨语言的结构化数据交换标准,网页 API 返回数据、各种配置文件、NoSQL 数据库,几乎都用它。
长这样:
{
"name": "CycleUser",
"age": 30,
"tags": ["Python", "Geology"],
"address": {
"city": "Beijing",
"zip": "100083"
},
"active": true
}
它只有六种数据类型:字符串、数字、布尔、null、数组([])、对象({},键值对)。靠数组和对象的嵌套,能表达任意复杂的结构。
json 的优势是结构明确、机器友好。相比 csv,它天然支持嵌套(一个对象里套对象、套数组)、字段名就是键、类型信息由语法直接表达(带引号的是字符串,不带的可以当数字)。Python 里 json.loads() 一下就变成字典和列表,配合使用毫无障碍。
劣势主要有三个,都挺扎心。
不支持注释。 给配置文件写 json,想加一行"这个参数是干嘛的",没门。这是它作为配置格式被后来者(yaml、toml)赶超的核心原因。
不允许尾逗号。 {"a": 1, "b": 2,} 这种写法,在 Python 的字典字面量里合法,在 json 里是语法错误。手写 json 的时候特别容易中招。
大数精度问题。 json 的数字按浮点数处理,JavaScript 里超过 2^53 - 1(900719919254740991 之后)的整数就会丢精度。订单号、雪花 ID 这种长数字,经过一次 JS 处理可能就变了。所以很多 API 干脆把大整数序列化成字符串来传。
import json
# 标准库足够日常使用
data = json.loads('{"name": "张三", "scores": [89, 73]}')
print(data["scores"][0]) # 89
print(json.dumps(data, ensure_ascii=False, indent=2))
中文用户还要注意 ensure_ascii=False 这个参数,不加的话中文会被转成 \u5f20\u4e09 这种转义,文件大小翻好几倍,虽然合法但没法看。
jsonl:一行一条,为流式而生
.jsonl,JSON Lines,有时也叫 .ndjson,就是把很多个 json 对象一行一个地排进同一个文件:
{"name": "张三", "score": 89}
{"name": "李四", "score": 73}
{"name": "王五", "score": 91}
它和"一个巨大的 json 数组"的区别,初看不大,用起来天差地别。
一个装着一百万条记录的 json 数组,程序必须整个读进内存、从头解析到尾,中间任何一行坏了,整个文件都废了。而 jsonl 是一行一条独立记录,可以逐行读、逐行解析:读一行处理一行,内存占用几乎恒定;第 800 万行坏了,跳过它接着读,前面 799 万行完好无损。
典型场景:日志系统、大模型训练数据的存放。你去看 Hugging Face 上很多数据集、看各家微调框架的输入格式,基本就是 jsonl:一行一条对话或一条样本。逐行处理意味着可以流式喂给训练管线,内存里永远只有当前这一条。
import json
with open("data.jsonl", encoding="utf-8") as f:
for i, line in enumerate(f):
line = line.strip()
if not line:
continue
try:
record = json.loads(line)
except json.JSONDecodeError:
print(f"第 {i + 1} 行解析失败,跳过")
continue
# 处理 record
劣势就是它放弃了"一整个结构"的表达能力:顶层只能是一串平级的对象,想表达"这批数据整体有个元信息头"这种结构,就得自己定约定(比如第一行放一个 type: meta 的行)。另外它也是纯文本,体积比 Parquet 这类二进制列式格式大不少,大数据量下读写速度吃亏。
md:给"人"看才舒服的标记
.md,Markdown,是我写博客、写笔记、写公众号初稿用的格式,也是这两年几乎所有技术文档的事实标准。
它的思路跟上面那些"数据格式"完全不同:csv、json 是给机器读的,md 是给人读的,只是顺便让机器也能解析出结构。星号包起来是斜体,井号开头是标题,大于号是引用,反引号是代码——这些符号都是从纯文本时代电子邮件的约定里继承来的,一眼能看懂,不需要渲染。
Markdown 由 John Gruber 在 2004 年设计,最初的语法就一小页纸。后来因为太流行,各家开始加私货,于是有了两个重要的标准化努力:CommonMark 把核心语法钉死了,GFM(GitHub Flavored Markdown)在它之上加了表格、任务列表、删除线、代码块语言标注这些特别实用的扩展。
我写博客用的是 Pelican,markdown 文件头部有 front matter 存标题、日期、分类、标签,正文里数学公式靠 pymdownx 扩展转给 KaTeX 渲染。这些都是 md 生态的一部分:核心语法极简,按需加扩展。
md 的优势很明显:写作体验好、版本控制友好(纯文本,diff 清晰)、可移植(任何编辑器都能写)、渲染后好看。劣势是方言太多:同一个语法点,GitHub 支持、公众号不支持;表格、脚注、数学公式这些都不在核心语法里,换个渲染器可能就原样显示出符号来。
yaml:配置文件的主力
.yaml(.yml 是同一个东西的缩写后缀),重点用在配置文件上。GitHub Actions 的工作流、Docker Compose、conda 的 .condarc、Kubernetes 的资源清单,全是 yaml。
channels:
- conda-forge
show_channel_urls: true
custom_channels:
bioconda: https://mirrors.ustc.edu.cn/anaconda/cloud
它支持注释,支持多行文本,支持嵌套结构,写起来比 json 舒服太多。配置文件要经常被人手工编辑,注释和可读性是刚需,所以 yaml 成了主流。
它的劣势也很独特:缩进就是语法。空格数错了、用了 Tab,配置的含义就变了,而且这类错误报错信息往往不友好。还有一个著名的 "Norway problem":
country: no
yaml 会把 no 解析成布尔值 false 而不是字符串 "no"。类似的还有 on、off、yes。写挪威的代码,得加引号。这种"帮你猜类型"的体贴,偶尔就是灾难。
toml:新一代配置标准
.toml(Tom's Obvious, Minimal Language)是近几年崛起的配置格式,Python 官方把它定为项目元数据的标准,pyproject.toml 就是它:
[project]
name = "ds-holiday"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["numpy", "pandas"]
它跟 yaml 解决同一个问题,但思路相反:用显式的类型语法代替缩进。字符串带引号、日期是原生类型、布尔就是 true/false,没有 Norway problem,缩进错了顶多难看,不会改变语义。
劣势是深层嵌套的写法比较啰嗦(靠 [a.b.c] 这种分段标题逐层进入),表达复杂结构不如 yaml 灵活。但配置文件一般也没多深,所以 toml 正在快速取代 ini 和一部分 yaml 的地盘。
ini:老派但还活着
.ini 是 Windows 时代传下来的配置格式,等号赋值,方括号分节:
[global]
index-url = https://pypi.mirrors.ustc.edu.cn/simple
trusted-host = pypi.mirrors.ustc.edu.cn
pip 的配置文件就是 ini。它简单到几乎没什么可学的,但正因为简单,嵌套能力几乎没有、类型全靠读的人自己理解。新项目一般直接上 toml,不过维护老系统、读别人的配置,ini 还是绕不开。
xml 和 html:标签家族
.xml 和 .html 是亲戚,都用尖括号标签来标记内容。
XML 是"通用标记语言",特点是严格:必须有唯一的根节点、标签必须闭合、大小写敏感。RSS 订阅源、SVG 矢量图、Maven 的 pom.xml、Android 的布局文件,都是 XML。它的优势是规范极其严格,解析器写起来有标准可依,还带命名空间机制;劣势是啰嗦,同样的数据,XML 的体积通常比 json 大不少,嵌套深了之后可读性也不行。现在除了老系统和特定领域(RSS、SVG、Office 文档底层),新项目很少选它。
顺带一提,Word 的 .docx、Excel 的 .xlsx 本质是个 zip 压缩包,里面装的是一堆 XML 和媒体文件。把后缀改成 zip 解开就能看到,挺涨知识的。
HTML 是给浏览器看的,天生宽容:标签没闭合它也尽力渲染。它不算"数据格式",是文档格式,这里提它是因为做数据抓取的时候,你写的解析器多半就是在跟 HTML 打交道(Beautiful Soup、XPath 那一套)。
还有一批常见的:log、sql、srt、tex
.log 是日志文件,通常一行一条记录,开头是时间戳。它没有标准 schema,但"一行一条、按时间追加"这个习惯是通用的。查问题的时候 grep 日志,是每个开发者的日常。
.sql 是数据库脚本文件,里面就是 SQL 语句。数据库导出、迁移、备份常用它。文本格式的好处是能进版本控制,能 code review。
.srt 是字幕文件,序号 + 时间轴 + 文本,三段一组:
1
00:00:01,000 --> 00:00:04,000
大家好,今天说说纯文本格式。
.tex 是 LaTeX 排版源码,.bib 是它的参考文献库。写论文的人躲不开。这两个语法门槛比前面都高,但排版质量确实是所见即所得编辑器很难达到的。
另外还有一大批"没有后缀的文本文件":Dockerfile、Makefile、.gitignore、requirements.txt、.env。它们也是纯文本,只是约定了固定的名字和语法。这恰恰说明:纯文本的本质是"内容和结构靠约定,不靠后缀"。
怎么选,一张表
| 格式 | 定位 | 人读 | 机器读 | 典型场景 |
|---|---|---|---|---|
| txt | 纯文本口袋 | 很好 | 要自己解析 | 笔记、日志、语料 |
| dat | 无标准黑盒 | 看约定 | 看约定 | 仪器导出、各软件自定义 |
| csv / tsv | 表格交换 | 一般 | 很好 | 数据导出导入、Excel 交换 |
| json | 结构化对象 | 一般 | 很好 | API 返回、通用数据交换 |
| jsonl | 一行一条 json | 差 | 很好 | 日志、大模型训练数据 |
| md | 轻量标记写作 | 很好 | 好(需解析器) | 博客、README、文档 |
| yaml | 配置 | 很好 | 好 | Docker Compose、CI、conda |
| toml | 配置 | 很好 | 好 | pyproject.toml、Cargo |
| ini | 极简配置 | 好 | 好 | pip、Git 老式配置 |
| xml | 严格标记 | 一般 | 好 | RSS、SVG、Office 底层 |
| html | 网页文档 | 差 | 好(宽容) | 网页 |
| sql | 语句脚本 | 好 | 好 | 数据库导出、迁移 |
我的选择习惯大致是:
- 给人看的记录,用 md;
- 跟别的软件交换表格数据,csv;
- 程序之间传结构化数据、调 API,json;
- 大量记录要逐行流式处理,jsonl;
- 写配置文件,新项目 toml,跟随既有生态(Docker、CI)就 yaml;
- 数据要长期存、频繁按列查询,别用以上任何一个,上数据库或者 Parquet。
说一下编码和细节
第一,编码统一用 UTF-8,没有例外。所有历史遗留的 GBK、Latin-1 文件,碰到就转成 UTF-8 存一份。Python 里读写文件养成显式写 encoding="utf-8" 的习惯,能躲开九成乱码问题。要照顾 Excel,就用 utf-8-sig。
第二,csv 是交换格式,不是存储格式。它没有类型、没有约束、动不动被 Excel 篡改,正经数据请交给数据库或 Parquet,csv 只负责在系统之间搬运。
第三,配置文件选 yaml 还是 toml,看生态跟随。容器和 CI 的世界已经被 yaml 占了,Python 的新世界是 toml 的,跟大流走,别逆着来。
第四,jsonl 是被低估的格式。很多人一提批量数据就想到 json 数组或者 csv,实际上日志、训练样本、爬虫输出这些"一行一条、逐行处理"的场景,jsonl 都是最舒服的选择:坏一行跳一行,内存占用恒定,还能直接按行 append。
纯文本格式看着朴素,背后全是几十年演化出来的取舍。理解每个格式"为什么长这样",比记住"怎么用"更重要——因为格式会过时,但是取舍的逻辑其实还是类似的。