文件与时间 — 四种数据格式怎么选、with open 与计时
文件与时间 — 四种数据格式怎么选、with open 与计时
这一章是「实用工具箱」:数据该存成什么文件,时间该怎么处理,代码跑了多久怎么量。 原书的真正价值在一张选择表:CSV 存扁平数据、JSON 存结构化数据、HDF5 存大型二进制数据、 SQLite 存要查询的数据——四种格式各就各位,项目就不再为「存哪儿」纠结。
1. 这一章讲什么
作者定位:讲一些「有用的功能」——文件和时间。1 文件部分表面是格式清单,实质是信息形状决定存储格式的又一次应用(第 05 章容器选择的文件版);时间部分则是「看似简单、处处是坑」的典型。
数据存哪儿 CSV(扁平) → JSON(结构化) → HDF5(大、二进制) → SQLite(要查询)
时间怎么处 time(秒数) → datetime(可读日期) → timedelta(差不是日期!) → 时区(没有通用解)
怎么量快慢 time.time() 掐表;装饰器封装;「用 Python 就别看性能」
图说:三条线各有一个「反直觉的第一课」——CSV 没标准、JSON 比 CSV 还好读、
time.time() 给你的不是时间。2. 核心原理一:CSV——简单到没有标准
解决什么问题: 表格状的扁平数据,要能人用 Excel 打开、机器好读。
怎么做(主线例子): 一行一条记录,逗号分列:
#name,age,city
Donald Duck,42,Duckburg
Mickey Mouse,40,Disneyland读它,用第 05 章的工具就够(open + 逐行 + strip().split(",")),跳过 # 开头的注释行;
写它就是反过来的字符串拼接。2
为什么有效、边界在哪: 简单是它的全部优点,也是全部缺点——书里列了两条硬伤:
- 没有真正的标准:首行是注释还是数据?分隔符是逗号还是分号?「看上去是小问题,我保证你会被各种 CSV 文件折磨得头疼」;
- 值里含逗号就解析错——所以实务上常用分号
;当分隔符。
边界:CSV 只该装扁平数据(一张表);「别试图在 CSV 里造结构化数据,只会头痛。」(第 05 章「字符串反模式」的文件版:CSV 本质就是字符串约定。)
3. 核心原理二:JSON——结构化数据的默认答案
解决什么问题: CSV 装不下的层级结构(人 → 多个字段 → 其中还能套列表)。
怎么做: 人读的 JSON 文件长这样:
{
"people": [
{"name": "Donald Duck", "age": 42, "city": "Duckburg"},
{"name": "Mickey Mouse", "age": 40, "city": "Disneyland"}
]
}读它比读 CSV 还简单——json.load() 一次到位,直接拿到嵌套的 dict/list,
用 [] 一层层取:3
import json
with open("people.json") as f:
d = json.load(f)
print(d["people"][1]["age"]) # 40 —— 第二个人的年龄为什么有效: JSON 把「结构」写进了格式本身——读到什么形状,文件里就是什么形状,没有 CSV 的猜测环节; 它还天然是库与库、语言与语言之间的交换格式。代价是啰嗦(同样的数据比 CSV 占更多字符), 但「用 Python 读文件时这不是问题」。3
作者的选择表原话:「JSON 是大多数数据存储需求的好选择。」——除非你很大(见 HDF5)或要查询(见 SQLite)。
4. 核心原理三:HDF5——大数据的二进制仓库
解决什么问题: 数据量大到文本格式存不下、读得慢(科学计算的日常:一个实验几个 GB)。
怎么做: HDF5 是二进制格式,两条硬优势:
- 省空间:存一个浮点数只要 8 字节(字节是存储容量的基本单位,一个英文字母约占 1 字节);转成文本可能要好几十个字符;
- 读写快:大文件的瓶颈是你的 SSD,二进制让它一次搬更多。4
结构上也类似 JSON(组 group、数据集 dataset),但存进去的东西人眼读不了,要用专门的查看器(HDFView)或 Python 的 h5py 库:
import h5py
with h5py.File("people.h5", "w") as f:
people = f.create_group("people")
person1 = people.create_group("person1")
person1.create_dataset("name", data="Donald Duck")
person1.create_dataset("age", data=42)
# 读:f["people"]["person1"]["age"][()] → 42为什么有效、边界在哪: 「文件非常大就用 HDF5」——大型研究设施靠它轻松存 TB 级数据。 边界:二进制不可肉眼检查(调试成本转移到了工具上),小数据用它纯属 overkill。
5. 核心原理四:SQLite——要「查询」的数据
解决什么问题: 数据不只是存,还要按条件查(「所有 30 岁以上的人」「某月的订单总额」)。
怎么做: SQL(Structured Query Language,结构化查询语言)是最主流的数据库技术, 「按名字就知道,它擅长对 PB 级(1 PB = 10^15 字节,大型公司数据仓库的量级)数据集做查询」。 SQL 有「几种口音」:MySQL、PostgreSQL 用于大项目;SQLite 轻到整个数据库就是一个文件, Python 自带,适合小项目:5
import sqlite3
conn = sqlite3.connect("people.db")
cursor = conn.cursor()
cursor.execute("CREATE TABLE people (name TEXT, age INTEGER)")
cursor.execute("INSERT INTO people (name, age) VALUES ('Alice', 30)")
conn.commit()
cursor.execute("SELECT * FROM people")
print(cursor.fetchall()) # [('Alice', 30)]
conn.close()三步骨架:connect 连库 → cursor 执行 SQL 语句 → commit 落盘 / fetchall 取结果。
注意 SQL 语句是字符串——参数要拼进去,拼不好就是 SQL 注入(第 05 章的警告在这里兑现;
正确的参数化写法 execute("... VALUES (?, ?)", (name, age)) 书里没讲,务必自查)。
作者对 SQL 的批评(带出处): 把命令当字符串传,「1970 年代也许是个好主意,现在不是」—— 语法错了只给你一条没什么帮助的报错;「更好的设计是一个有正经 API、能检查语法并给出有用报错的接口。 当然有人会说 AI 时代这不算事了——那是另一个故事。」(他引的是 Robert C. Martin 的公开批评。)6
为什么有效、边界在哪: 科学计算里要「查询」的场景其实不多(除非大数据),所以本章只演示 SQLite—— 「存大量结构化数据且要查询」是它在选择表里的唯一位置。
四种格式的选择表(本章正菜)
| 格式 | 存什么 | 一句话 |
|---|---|---|
| CSV | 扁平数据;要 Excel 能打开 | 简单但没标准,别造结构 |
| JSON | 结构化数据的默认 | 啰嗦但清晰,还是跨语言接口 |
| HDF5 | 非常大的二进制数据 | 省空间、读写快,科研机构标配 |
| SQLite | 要查询的结构化数据 | 一个文件就是整个数据库 |
with open:一次设计改良
读文件的老写法要手动 close(),忘了关「会发生糟糕的事,具体多糟取决于很多因素——
能让人犯这种错,本身就是糟糕设计的标志」。解法:with open——块结束自动关文件,
「好得多的设计」。且 with 不限于文件,任何支持「上下文管理器协议」的对象都行。7
5.1 CSV 读写的一次完整往返
把书里的读写代码连成一次「写进去 → 读出来」的往返:2
# 写:with open + "w"(write,新建/覆盖)
with open("people.csv", "w") as file:
file.write("#name,age,city
") # 注释行,读的时候跳过
file.write("Donald Duck,42,Duckburg
") # 一条记录一行
file.write("Mickey Mouse,40,Disneyland
")
# 读:逐行 + strip + split
with open("people.csv") as file: # 默认 "r"(read)
for row in file:
if row.startswith("#"):
continue # 跳过注释行
print(row.strip().split(",")) # ['Donald Duck', '42', 'Duckburg'] …三个细节:"w" 模式会覆盖同名文件(不存在则新建);strip() 去掉行尾的换行符
(不然最后一列会变成 "Duckburg ");split(",") 得到的是字符串列表——
年龄 "42" 还是字符串,要算数得先 int()(第 03 章的转换纪律在此兑现)。2
5.2 with open 之前的世界
书里给了老式写法的「罪案现场」:7
file = open("people.csv")
for row in file:
print(row)
file.close() # 忘了这行 → 「会发生糟糕的事」糟糕的事指什么?文件句柄(操作系统登记的这个打开文件)一直没还——
程序跑久了,句柄耗尽,新的文件再也打不开。「能让人犯这种错,本身就是糟糕设计的标志」:
语言把「记得收尾」的责任压给人,人必然忘。with open 的设计就是把这个责任收归语言:
块结束,文件必关,连异常抛出时也关。这就是它被叫做「上下文管理器」的原因——
它管理的是一段资源的生命周期(进入时打开、退出时关闭),不止文件能用,网络和锁也吃这一套。7
6. 核心原理五:时间——看似简单,处处是坑
6.1 第一课:time.time() 给你的不是时间
import time
print(time.time()) # 1737400945.7503603「第一个意外:你拿到的不是日期,而是从 1970 年 1 月 1 日至今的秒数」
(这个数叫 Unix 时间戳;想要人读的日期,用 datetime 模块:8
import datetime
print(datetime.datetime.now()) # 2025-01-20 20:26:24.9327066.2 两个坑:timedelta 与时区
坑一,日期之差不是日期。 「两个日期的差是一个 timedelta 对象,不是日期——它有自己的一套复杂度,用它要当心。」
(2025-01-20 减 2025-01-10 得到的是「10 天」这个时长,你不能给它再加一个日期。)
坑二,时区没有通用解。 「很多公司想用通用方案解决时区问题,都失败了——你得找到适合自己需求的方案。」 (常识补充:存 UTC、显示时换算,是多数团队的选择;不在书里,但比「找自己的方案」可操作。)9
7. 核心原理六:计时与那个性能态度
怎么做: 掐表只需 time.time() 前后各取一次、相减;包装成装饰器更体面:
import time
def timing_decorator(func):
def wrapper(*args, **kwargs): # 第 10 章的 *args/**kwargs 主场!
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"Execution time: {end - start}")
return result
return wrapper
@timing_decorator # 等价于 slow_function = timing_decorator(slow_function)
def slow_function():
time.sleep(1)
slow_function() # Execution time: 1.0129为什么有效: 装饰器把「计时」横切在任意函数上,业务函数本身保持干净;
@ 符号只是「把函数名重新绑定到包装后版本」的语法糖。10
作者的性能态度(加框原话): 「为性能优化的代码通常难读;需要性能的场景极少,而你用 Python, 说明你的场景不在其中。 NumPy 相对快,Python 其余部分相当慢。写可读、可维护的代码, 比写优化的代码重要得多——不然你大概率会在开发阶段就被不可理解的代码拖死。」11
这是第 01 章「大项目别用 Python」的温和版:不是不能快,是别在 Python 里追快。
7.1 计时装饰器的完整走查
@timing_decorator
def slow_function():
time.sleep(1)
slow_function() # 打印 Execution time: 1.0129调用发生了什么,拆开看:10
① @timing_decorator 这一行,等价于:slow_function = timing_decorator(slow_function)
—— 函数名被重新绑定到「包装后的新函数」,原函数还在,只是名字指向了包装
② 调用 slow_function() → 实际进的是 wrapper(*args, **kwargs)
③ wrapper 里:start = time.time(); 调原函数(睡 1 秒);end = time.time()
④ 打印差值 1.0129(多出的 0.0129 是调度开销),返回原函数的返回值@ 只是「先定义、再把名字重新绑定」的语法糖——理解了这步,
第 11 章 @dataclass、第 12 章 @abstractmethod、第 19 章 @staticmethod 就都是同一个动作。
装饰器的警告书里也给了:「装饰器看着酷,但少用——太多装饰器就像洋葱,剥一层还有一层。」10
7.2 时间的完整对账
import time
time.time() # 1737400945.7503603 → 1970-01-01 以来的秒数(带小数)
import datetime
datetime.datetime.now() # 2025-01-20 20:26:24.932706 → 人读的「现在」两个数对账:1737400945 秒 ÷ 31556952(年均秒数)≈ 55.0——1970 + 55 = 2025,和 datetime 的年份吻合。 时间戳算时长,datetime 给人看——分工明确,别拿时间戳去解析年月日(那是自己给自己找时区麻烦)。9
书里对 timedelta 的那句警告,展开一点:两个日期相减得到的是「时长」(10 天),
时长没有年月日的位置——「10 天后的第 3 个月」这种问题,时长对象答不了,得回到日期上重算。
而时区那句「很多公司试过通用方案都失败了」,2026 年的落地版(书外补充):
系统内部一律存 UTC,显示层再换算;跨夏令时的历史时长计算,交给 zoneinfo 模块,别自己手算。9
8. 作者的判断与证据
书里给了证据的: 四种格式的读写代码都可直接运行;time.time() 的输出示例、timedelta 的提醒是机制事实。89
作者的判断(立场):
- 「CSV 没有标准,会折磨你」——经验之谈,可信;
- 「SQL 字符串是遗产」——转述 Robert C. Martin 的批评,作者站台;
- 「Python 里别谈性能」——与他的大项目警告同构;数据管道从业者会部分反对(NumPy 向量化、Polars 很快),但对「手写优化」的劝阻是对的。
判断(我们的,不是书里的): 四种格式的选择表是本章最该带走的东西, 但 2026 年的实践里还缺一行:Parquet/Arrow——表格型大数据在数据科学界的实际默认, 介于「JSON 太啰嗦」和「HDF5 太科研」之间。书里没提(它偏传统科研栈)。 如果错,会错在: 如果你的数据严格在 HDF5 生态(某些大型设施的采集格式)里, Parquet 这行就是多余的;选择表永远该按生态走,不按潮流走。
9. 边界与局限
- JSON 的
json.dump()(写出)没演示,只演示了读; - SQLite 的参数化查询没讲——这是安全红线,本章的代码示例是教学简化,真实代码必须参数化;
- HDF5 只讲了 h5py 的组/数据集骨架,压缩选项、分块没提;
- 时间部分没讲
zoneinfo模块(Python 3.9 起的官方时区方案),「自己找方案」显得比实际更绝望。
10. 可带走的
本章主线例子,一行写完: 读 JSON json.load(f) → d["people"][1]["age"] 得 40;
存大数据用 HDF5;要查询用 SQLite 三件套(connect/cursor/commit)。345
- 四种格式各就各位:CSV 扁平、JSON 结构化默认、HDF5 大、SQLite 要查询。
- CSV 没有标准——分隔符、注释行都要现场确认;别在 CSV 里造结构。
- JSON 读入即结构,还是跨语言接口;大多数存储需求的好选择。
- HDF5 = 二进制:省空间、快,TB 级数据的科研标配。
- SQLite 整个数据库是一个文件;SQL 语句是字符串,真实代码必须参数化防注入。
- with open 自动关文件——「能让人忘,就是坏设计」的正面教材;老式的手动 close 别写。
- time.time() 是秒数(1970 起),不是日期;人读日期找 datetime;日期差是 timedelta 不是日期。
- 时区没有银弹:存 UTC、显时换算是常见解(补充)。
- 计时就掐 time.time() 两头;装饰器版更体面(第 10 章 args/kwargs 的应用)。
- 在 Python 里追性能是本末倒置:NumPy 快、其余慢;可读性优先。
11. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 本章定位 | 9 Util | text/25-ch09-9-util.txt:3(搜「how to deal with files」) |
| CSV 格式与无标准之痛 | 9 Util | text/25-ch09-9-util.txt:15(搜「Comma-Separated」) · text/25-ch09-9-util.txt:25(搜「headaches」) |
| CSV 读写代码 | 9 Util | text/25-ch09-9-util.txt:32(搜「startswith」) · text/25-ch09-9-util.txt:42(搜「“w” means」) |
| with open 与上下文管理器 | 9 Util | text/25-ch09-9-util.txt:47(搜「With Open」) · text/25-ch09-9-util.txt:63(搜「support the context」) |
| JSON 与读入示例 | 9 Util | text/25-ch09-9-util.txt:66(搜「JSON and Co」) · text/25-ch09-9-util.txt:96(搜「json.load」) |
| HDF5 的理由与 h5py | 9 Util | text/25-ch09-9-util.txt:103(搜「bytes of memory」) · text/25-ch09-9-util.txt:108(搜「h5py」) |
| SQL、SQLite 与口音 | 9 Util | text/25-ch09-9-util.txt:132(搜「Structured Query Language」) · text/25-ch09-9-util.txt:139(搜「SQLite」) |
| SQLite 读写骨架 | 9 Util | text/25-ch09-9-util.txt:147(搜「sqlite3.connect」) · text/25-ch09-9-util.txt:161(搜「fetchall」) |
| SQL 语法批评(Robert C. Martin) | 9 Util | text/25-ch09-9-util.txt:165(搜「criticized」) |
| 四种格式总结 | 9 Util | text/25-ch09-9-util.txt:171(搜「Summary」) |
| time.time() 是秒数 | 9 Util | text/25-ch09-9-util.txt:209(搜「January 1, 1970」) |
| datetime、timedelta、时区 | 9 Util | text/25-ch09-9-util.txt:213(搜「datetime.datetime.now」) · text/25-ch09-9-util.txt:216(搜「timedelta」) · text/25-ch09-9-util.txt:218(搜「time zones」) |
| 计时与装饰器版 | 9 Util | text/25-ch09-9-util.txt:223(搜「Timing Functions」) · text/25-ch09-9-util.txt:245(搜「timing_decorator」) |
| 「用 Python 别看性能」 | 9 Util | text/25-ch09-9-util.txt:260(搜「Performance」) |
Footnotes
-
出处:「9 Util」第 3–5 段(
text/25-ch09-9-util.txt:3,搜「how to deal with files」)。 ↩ -
出处:「9 Util」第 12–46 段(
text/25-ch09-9-util.txt:16,搜「Each line is a row」;「没有真正的标准」见第 22 段,搜「no real standard」)。 ↩ ↩2 ↩3 -
出处:「9 Util」第 68–98 段(
text/25-ch09-9-util.txt:70,搜「several advantages」;代码见第 93–98 段,搜「json.load」)。 ↩ ↩2 ↩3 -
出处:「9 Util」第 100–130 段(
text/25-ch09-9-util.txt:103,搜「bytes of memory」;「recommended to use HDF5 for large files」见第 105 段)。 ↩ ↩2 -
出处:「9 Util」第 131–164 段(
text/25-ch09-9-util.txt:133,搜「petabyte-sized」;「SQLite 是一个文件」见第 142–143 段,搜「single file」;代码见第 144–163 段)。 ↩ ↩2 -
出处:「9 Util」第 165–169 段(
text/25-ch09-9-util.txt:165,搜「criticized」)。脚注引 Robert C. Martin 的 X 帖子,链接见text/25-ch09-9-util.txt:186。 ↩ -
出处:「9 Util」第 48–64 段(「With Open」框,
text/25-ch09-9-util.txt:54,搜「bad things could happen」;「context manager protocol」见第 64 段)。 ↩ ↩2 ↩3 -
出处:「9 Util」第 198–213 段(
text/25-ch09-9-util.txt:209,搜「January 1, 1970」;「you have to use the datetime module」见第 210 段)。 ↩ ↩2 -
出处:「9 Util」第 215–221 段(
text/25-ch09-9-util.txt:216,搜「timedelta object」;时区见第 220–221 段,搜「failed」)。 ↩ ↩2 ↩3 ↩4 -
出处:「9 Util」第 223–258 段(
text/25-ch09-9-util.txt:245,搜「timing_decorator」;「onion layers」之诫见第 242 段,搜「onion」)。 ↩ ↩2 ↩3 -
出处:「9 Util」第 260–265 段(「Performance」框,
text/25-ch09-9-util.txt:261,搜「hard to read」)。 ↩