数据落地:CSV、JSON 与 Excel
采集程序的输出格式决定了后面数据能怎么用:CSV 方便丢给 Excel 与 BI 工具,JSONL 适合流式处理与入库,Excel 适合交付给不做技术的同事。选错格式的代价往往在数据量涨到几十万条时才显现——内存爆掉、文件打不开、中文乱码。
三种格式的取舍
| 格式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| CSV | 通用、体积极小、可流式追加 | 无嵌套结构、类型靠约定、编码易踩坑 | 字段固定、要给 Excel/BI 看 |
| JSONL(每行一个 JSON) | 保留嵌套与类型、可逐行解析 | 不适合人眼直读、体积较大 | 入库前的落地层、字段会变 |
| Excel(xlsx) | 多 sheet、可加样式、交付友好 | 单表上限 1048576 行、内存占用高 | 一次性报表、给业务方交付 |
经验规则:管道内部用 JSONL,交付用 Excel,长期存档用 CSV 或数据库。不要用 CSV 存嵌套对象列表,那只会得到一列 [object Object] 式的字符串。
CSV:csv.DictWriter 与中文编码
# writer_csv.py —— Python 3.10+
import csv
from pathlib import Path
def write_csv(rows: list[dict], path: Path, fields: list[str]) -> int:
"""utf-8-sig 让 Excel 双击打开不出现中文乱码"""
path.parent.mkdir(parents=True, exist_ok=True)
# newline="" 是官方要求:否则 Windows 上每行之间会多出一个空行
with path.open("w", encoding="utf-8-sig", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")
writer.writeheader()
for row in rows:
# 去掉换行,避免一条数据被撑成多行
clean = {k: str(row.get(k, "")).replace("\n", " ") for k in fields}
writer.writerow(clean)
return len(rows)
if __name__ == "__main__":
data = [{"title": "示例文章一", "url": "https://example.com/1", "pub_time": "2024-05-01 09:00"}]
print(write_csv(data, Path("output/2024-05-01/news.csv"), ["title", "url", "pub_time"]))
| 编码写法 | 是否带 BOM | 谁认得 | 建议 |
|---|---|---|---|
utf-8 | 否 | Python、Linux 工具链、现代 Excel | 给程序读 |
utf-8-sig | 是(\ufeff) | Excel 双击也能正确显示中文 | 给人用 Excel 打开 |
gbk | 否 | 老版 Windows Excel | 仅在必须兼容老环境时用 |
若下游程序把 utf-8-sig 的第一列名读成 \ufefftitle,改成 utf-8 即可——这是「格式没错但字段名对不上」最常见的原因。
JSONL:追加写与 ensure_ascii=False
# writer_jsonl.py
import json
from pathlib import Path
def append_jsonl(rows: list[dict], path: Path) -> None:
"""每行一个 JSON,可追加;ensure_ascii=False 保证中文按原样存储"""
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("a", encoding="utf-8") as f:
for row in rows:
# separators 去掉多余空格;default=str 兜住 datetime 等类型
line = json.dumps(row, ensure_ascii=False, separators=(",", ":"), default=str)
f.write(line + "\n")
def read_jsonl(path: Path):
"""逐行读取,内存占用与文件大小无关,适合几十万行以上"""
with path.open("r", encoding="utf-8") as f:
for line in f:
if line.strip():
yield json.loads(line)
Excel:pandas + openpyxl 多 sheet 与样式
# writer_excel.py
import pandas as pd
from openpyxl.styles import Alignment, Font, PatternFill
from openpyxl.utils import get_column_letter
def write_excel(frames: dict[str, pd.DataFrame], path: str) -> None:
"""一个 dict 一个 sheet,写完再统一设置列宽与表头样式"""
with pd.ExcelWriter(path, engine="openpyxl") as writer:
for sheet_name, df in frames.items():
df.to_excel(writer, sheet_name=sheet_name, index=False) # 关掉索引列
for sheet_name in frames:
ws = writer.sheets[sheet_name]
ws.freeze_panes = "A2" # 冻结首行
for idx, column in enumerate(ws.iter_cols(min_row=1, max_row=1), start=1):
cell = column[0]
cell.font = Font(bold=True, color="FFFFFF")
cell.fill = PatternFill("solid", start_color="4472C4")
cell.alignment = Alignment(horizontal="center")
letter = get_column_letter(idx)
# 中文按两个字符宽度估算列宽
width = max((len(str(c.value or "")) * 2 for c in ws[letter]), default=10)
ws.column_dimensions[letter].width = min(max(width + 4, 10), 60)
if __name__ == "__main__":
write_excel({"明细": pd.DataFrame({"标题": ["示例文章一"]})}, "output/日报.xlsx")
超过 5 万行不要用 to_excel 一次写:先在 pandas 里聚合或分片,再逐片写不同 sheet;xlsx 单表 1048576 行是硬上限,to_excel 超限时不会给出友好错误。
目录规范与常见坑
| 层级 | 建议写法 | 原因 |
|---|---|---|
| 站点 | output/<site>/ | 多站点互不覆盖 |
| 日期 | output/<site>/2024-05-01/ | 按天分区,便于补跑与清理 |
| 文件名 | <站点>-<类型>-<时间戳>.jsonl | 可排序、不重名覆盖 |
| 临时文件 | 先写 .tmp 再 os.replace 改名 | 避免下游读到半个文件 |
| 现象 | 原因 | 处理 |
|---|---|---|
| Excel 打开中文乱码 | CSV 用了 utf-8 无 BOM | 改 utf-8-sig,或用「数据 → 从文本」导入 |
| 每行之间多一个空行 | 未设置 newline="" | open(..., newline="") |
| 一条数据变成好几行 | 字段里含 \n | 写之前统一替换换行符 |
JSON 里中文变 \u4e2d | ensure_ascii 默认 True | 传 ensure_ascii=False |
| 大文件读入内存 OOM | json.load 读整个数组 | 改 JSONL 并逐行 json.loads |
合规提示:落地的数据只应来自公开数据源,遵守站点 robots.txt 与服务条款,控制采集频率与并发;不采集手机号、身份证号、简历个人信息等隐私数据,不绕过登录、验证码与付费墙。对外发布或再分发前,需确认《个人信息保护法》《数据安全法》《著作权法》相关要求并取得授权,优先使用官方 API 与开放数据集。
小结:把格式选择当成架构决策——管道内部用 JSONL 逐行追加,交付用 Excel 并关闭索引、设置列宽与表头样式,长期存档用 CSV 并注意 utf-8-sig 与 newline="" 这两个必写参数;文件按站点与日期分目录,先写临时文件再改名。