用 Python 直接读取 tar.gz 中的 CSV:免解包统计与错误检查
备份或每日导出把 CSV 放在 daily.tar.gz 里,你只想统计几列,却先要解压整包、腾出磁盘、清理临时文件。本文面向初级到中级读者,用 Python 标准库直接打开归档中的指定 CSV,逐条统计,并在输入有问题时返回非零退出码。
1. 方法:把归档成员当作文件读取
流程是:打开 tar.gz → 检查成员名唯一且为普通文件 → 获取二进制文件对象 → 转成文本 → 交给 CSV 解析器。extractfile() 返回可读对象;这里不会调用向磁盘写成员的 extract() 或 extractall()。接口区别见 Python tarfile 官方文档。
本例处理来源可信、已经写完且读取期间不再变化的本地归档。只接受表头严格为 id,value 的 UTF-8 CSV,可带 BOM;id 不能为空或全为空白,value 由 int() 转为整数。只含表头的文件返回零条记录;重复 ID 按多条记录累计。
与普通 CSV 流式处理相比,新增的是归档成员选择和二进制到文本的衔接。本文先用 getmembers() 扫描成员,因此会保存成员元数据,gzip 扫描也可能耗时;只对 CSV 行逐条处理,不把整张表装进列表。它适合成员数量适中的导出包,不承诺整个归档处理过程恒定内存。
2. 准备环境和演示归档
使用 Python 3.9 或更新版本及可用的 zlib,无需安装第三方包。以下命令和脚本在 Linux、Python 3.11.4 上验证,适用于 bash 和 zsh。
- 在同一个终端执行。重复实验时换一个新目录名:
python3 --version
mkdir tar-csv-demo
cd tar-csv-demo
python3 - <<'PY_DATA'
import io
import tarfile
payload = b'\xef\xbb\xbfid,value\n001,10\n"sample,002",12\n003,20\n'
with tarfile.open("daily.tar.gz", "x:gz") as archive:
info = tarfile.TarInfo("reports/daily.csv")
info.size = len(payload)
archive.addfile(info, io.BytesIO(payload))
PY_DATA
python3 -m tarfile -l daily.tar.gz
输出清单应包含 reports/daily.csv。示例在内存中构造 CSV 并打包,没有创建 reports/ 目录;x:gz 会拒绝覆盖已有归档。
- 将下一节保存为
read_tar_csv.py,也可以下载完整脚本。成员参数必须与清单中的名字完全一致,./reports/daily.csv和reports/daily.csv在这里是不同的字符串。
3. 完整实现
"""Summarize one CSV member in a completed, trusted tar.gz. Python 3.9+."""
import argparse
import csv
import io
import json
import sys
import tarfile
import zlib
def summarize(path, member, max_bytes):
with tarfile.open(path, "r:gz") as archive:
matches = [item for item in archive.getmembers() if item.name == member]
if not matches:
raise ValueError(f"member not found: {member!r}")
if len(matches) != 1:
raise ValueError(f"duplicate member: {member!r}")
info = matches[0]
if not info.isfile():
raise ValueError(f"member is not a regular file: {member!r}")
if info.size > max_bytes:
raise ValueError(f"member size {info.size} exceeds limit {max_bytes}")
records = total = 0
with archive.extractfile(info) as raw:
with io.TextIOWrapper(raw, encoding="utf-8-sig", newline="") as text:
reader = csv.reader(text, strict=True)
if next(reader, None) != ["id", "value"]:
raise ValueError("expected header: id,value")
for row in reader:
where = f"physical line {reader.line_num}"
if len(row) != 2 or not row[0].strip():
raise ValueError(f"{where}: expected nonempty id and value")
try:
value = int(row[1])
except ValueError as exc:
raise ValueError(f"{where}: value must be an integer") from exc
records += 1
total += value
return {"member": member, "records": records, "total": total}
def main():
parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
parser.add_argument("archive", help="completed local tar.gz")
parser.add_argument("member", help="exact member name, as listed by tarfile")
parser.add_argument("--max-bytes", type=int, default=64 * 1024 * 1024,
help="maximum declared CSV size; default: 67108864")
args = parser.parse_args()
if args.max_bytes <= 0:
parser.error("--max-bytes must be positive")
try:
result = summarize(args.archive, args.member, args.max_bytes)
except (OSError, EOFError, tarfile.TarError, zlib.error,
UnicodeError, csv.Error, ValueError) as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 1
print(json.dumps(result, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())
utf-8-sig 接受带或不带 BOM 的 UTF-8,其他编码会严格报错。TextIOWrapper 官方文档说明了二进制流到文本流的包装方式;按 CSV 官方文档的要求设置 newline="",让解析器处理引号内的换行。
默认只接受声明大小不超过 64 MiB 的目标成员,可用 --max-bytes 调整。这个限制不约束整个归档的解压量、成员数或执行时间,也不是处理恶意归档的资源隔离机制。CSV 解析仍受标准库字段大小限制;异常大字段会报错。
4. 运行和验证
- 运行帮助和正常示例:
python3 read_tar_csv.py --help
python3 read_tar_csv.py daily.tar.gz reports/daily.csv
{"member": "reports/daily.csv", "records": 3, "total": 42}
"sample,002" 是一个完整 ID,不能用 split(",") 拆行。成功时标准输出只有 JSON,退出码为 0;读取或数据检查失败时只向标准错误输出 ERROR,退出码为 1。参数错误由 argparse 返回 2。
- 验证缺失成员和大小限制:
python3 read_tar_csv.py daily.tar.gz missing.csv
printf 'exit=%s\n' "$?"
python3 read_tar_csv.py daily.tar.gz reports/daily.csv --max-bytes 10
printf 'exit=%s\n' "$?"
两次都应显示 ERROR 和 exit=1,不能产生成功 JSON。
- 验证同名成员不会被悄悄选中:
python3 - <<'PY_DUP'
import io
import tarfile
payload = b"id,value\n001,1\n"
with tarfile.open("duplicate.tar.gz", "x:gz") as archive:
for _ in range(2):
info = tarfile.TarInfo("reports/daily.csv")
info.size = len(payload)
archive.addfile(info, io.BytesIO(payload))
PY_DUP
python3 read_tar_csv.py duplicate.tar.gz reports/daily.csv
printf 'exit=%s\n' "$?"
预期错误包含 duplicate member,退出码为 1。先扫描成员再读取,可在统计前发现重复名字。
- 在全新的演示目录里确认 CSV 没有落地:
python3 - <<'PY_CHECK'
from pathlib import Path
assert not Path("reports").exists()
assert not Path("daily.csv").exists()
print("OK: no extracted CSV")
PY_CHECK
这个脚本验证的是指定 CSV 的读取和格式,并非整包完整性证明;tar 的结束位置不等于 gzip 字节流结束位置。若要验收备份,请另做文件 SHA-256 清单核对。
5. 常见错误和修复
源 CSV 的表头约定可参照 CSV 结构校验教程,修正后重新打包。
| 错误 | 处理方法 |
|---|---|
member not found |
运行 python3 -m tarfile -l daily.tar.gz,复制完整成员名,包括可能存在的 ./ 前缀。 |
duplicate member |
从原始导出重新生成归档,确保目标名字只出现一次,不要改成默认取最后一个。 |
member is not a regular file |
目标是目录、符号链接或硬链接;重新打包实际 CSV 普通文件。 |
expected header 或整数错误 |
修正源 CSV 为 id,value 两列和整数值,再重新打包。 |
解码错误或 csv.Error |
确认源文件为 UTF-8 且引号闭合;不要用 errors="ignore" 隐藏损坏。 |
ReadError、EOFError 或 gzip 错误 |
等待写入完成,重新复制或生成归档后再读;不要把半成品当成报表。 |
6. 小结
用 extractfile() 接上文本包装和 CSV 解析器,可以直接统计 tar.gz 内的 CSV,省去解包和清理步骤。固定成员名、拒绝重复项、检查表头并保留失败退出码,能把它接进日常自动化。先用小型归档验证数据规则,再按实际规模调整目标成员大小上限。
- 原文作者:春江暮客
- 原文链接:https://www.bobobk.com/python-tar-gz-csv-without-extract.html
- 版权声明:本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。