编程语言研究书库

文件与时间 — 四种数据格式怎么选、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

为什么有效、边界在哪: 简单是它的全部优点,也是全部缺点——书里列了两条硬伤:

  1. 没有真正的标准:首行是注释还是数据?分隔符是逗号还是分号?「看上去是小问题,我保证你会被各种 CSV 文件折磨得头疼」;
  2. 值里含逗号就解析错——所以实务上常用分号 ; 当分隔符。

边界: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.932706

6.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

  1. 四种格式各就各位:CSV 扁平、JSON 结构化默认、HDF5 大、SQLite 要查询。
  2. CSV 没有标准——分隔符、注释行都要现场确认;别在 CSV 里造结构。
  3. JSON 读入即结构,还是跨语言接口;大多数存储需求的好选择。
  4. HDF5 = 二进制:省空间、快,TB 级数据的科研标配。
  5. SQLite 整个数据库是一个文件;SQL 语句是字符串,真实代码必须参数化防注入。
  6. with open 自动关文件——「能让人忘,就是坏设计」的正面教材;老式的手动 close 别写。
  7. time.time() 是秒数(1970 起),不是日期;人读日期找 datetime;日期差是 timedelta 不是日期。
  8. 时区没有银弹:存 UTC、显时换算是常见解(补充)。
  9. 计时就掐 time.time() 两头;装饰器版更体面(第 10 章 args/kwargs 的应用)。
  10. 在 Python 里追性能是本末倒置:NumPy 快、其余慢;可读性优先。

11. 原文地图

主题原书章原文位置
本章定位9 Utiltext/25-ch09-9-util.txt:3(搜「how to deal with files」)
CSV 格式与无标准之痛9 Utiltext/25-ch09-9-util.txt:15(搜「Comma-Separated」) · text/25-ch09-9-util.txt:25(搜「headaches」)
CSV 读写代码9 Utiltext/25-ch09-9-util.txt:32(搜「startswith」) · text/25-ch09-9-util.txt:42(搜「“w” means」)
with open 与上下文管理器9 Utiltext/25-ch09-9-util.txt:47(搜「With Open」) · text/25-ch09-9-util.txt:63(搜「support the context」)
JSON 与读入示例9 Utiltext/25-ch09-9-util.txt:66(搜「JSON and Co」) · text/25-ch09-9-util.txt:96(搜「json.load」)
HDF5 的理由与 h5py9 Utiltext/25-ch09-9-util.txt:103(搜「bytes of memory」) · text/25-ch09-9-util.txt:108(搜「h5py」)
SQL、SQLite 与口音9 Utiltext/25-ch09-9-util.txt:132(搜「Structured Query Language」) · text/25-ch09-9-util.txt:139(搜「SQLite」)
SQLite 读写骨架9 Utiltext/25-ch09-9-util.txt:147(搜「sqlite3.connect」) · text/25-ch09-9-util.txt:161(搜「fetchall」)
SQL 语法批评(Robert C. Martin)9 Utiltext/25-ch09-9-util.txt:165(搜「criticized」)
四种格式总结9 Utiltext/25-ch09-9-util.txt:171(搜「Summary」)
time.time() 是秒数9 Utiltext/25-ch09-9-util.txt:209(搜「January 1, 1970」)
datetime、timedelta、时区9 Utiltext/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 Utiltext/25-ch09-9-util.txt:223(搜「Timing Functions」) · text/25-ch09-9-util.txt:245(搜「timing_decorator」)
「用 Python 别看性能」9 Utiltext/25-ch09-9-util.txt:260(搜「Performance」)

Footnotes

  1. 出处:「9 Util」第 3–5 段(text/25-ch09-9-util.txt:3,搜「how to deal with files」)。 ↩

  2. 出处:「9 Util」第 12–46 段(text/25-ch09-9-util.txt:16,搜「Each line is a row」;「没有真正的标准」见第 22 段,搜「no real standard」)。 ↩ ↩2 ↩3

  3. 出处:「9 Util」第 68–98 段(text/25-ch09-9-util.txt:70,搜「several advantages」;代码见第 93–98 段,搜「json.load」)。 ↩ ↩2 ↩3

  4. 出处:「9 Util」第 100–130 段(text/25-ch09-9-util.txt:103,搜「bytes of memory」;「recommended to use HDF5 for large files」见第 105 段)。 ↩ ↩2

  5. 出处:「9 Util」第 131–164 段(text/25-ch09-9-util.txt:133,搜「petabyte-sized」;「SQLite 是一个文件」见第 142–143 段,搜「single file」;代码见第 144–163 段)。 ↩ ↩2

  6. 出处:「9 Util」第 165–169 段(text/25-ch09-9-util.txt:165,搜「criticized」)。脚注引 Robert C. Martin 的 X 帖子,链接见 text/25-ch09-9-util.txt:186。 ↩

  7. 出处:「9 Util」第 48–64 段(「With Open」框,text/25-ch09-9-util.txt:54,搜「bad things could happen」;「context manager protocol」见第 64 段)。 ↩ ↩2 ↩3

  8. 出处:「9 Util」第 198–213 段(text/25-ch09-9-util.txt:209,搜「January 1, 1970」;「you have to use the datetime module」见第 210 段)。 ↩ ↩2

  9. 出处:「9 Util」第 215–221 段(text/25-ch09-9-util.txt:216,搜「timedelta object」;时区见第 220–221 段,搜「failed」)。 ↩ ↩2 ↩3 ↩4

  10. 出处:「9 Util」第 223–258 段(text/25-ch09-9-util.txt:245,搜「timing_decorator」;「onion layers」之诫见第 242 段,搜「onion」)。 ↩ ↩2 ↩3

  11. 出处:「9 Util」第 260–265 段(「Performance」框,text/25-ch09-9-util.txt:261,搜「hard to read」)。 ↩

On this page