纯文本格式盘点:txt、dat、csv、json、jsonl、md 都是干嘛的

上一篇说了把文本打包成 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.0011e3 之间毫无区别,全靠读取方去猜。日期格式更是五花八门,2026-09-162026/9/1616-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"。类似的还有 onoffyes。写挪威的代码,得加引号。这种"帮你猜类型"的体贴,偶尔就是灾难。

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 是它的参考文献库。写论文的人躲不开。这两个语法门槛比前面都高,但排版质量确实是所见即所得编辑器很难达到的。

另外还有一大批"没有后缀的文本文件":DockerfileMakefile.gitignorerequirements.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。

纯文本格式看着朴素,背后全是几十年演化出来的取舍。理解每个格式"为什么长这样",比记住"怎么用"更重要——因为格式会过时,但是取舍的逻辑其实还是类似的。