春江暮客

春江暮客的个人学习分享网站

Python 大型 CSV 排序:用 SQLite 按整数列稳定排序

2026-11-05 技术
Python 大型 CSV 排序:用 SQLite 按整数列稳定排序
文章目录

文件越来越大,sorted(list(csv.reader(...))) 开始占用大量内存;直接按文本排序,又可能把 10 放到 2 前面,甚至拆坏带引号换行的字段。

这篇教程面向初学者到中级读者:逐条读取一个未排序 CSV,存进临时 SQLite 数据库,按整数列排序后生成新文件。已有的大型 CSV 流式处理教程适合计数等逐条任务;这里补上必须看到全部数据才能完成的全局排序。

1. 方法:整数键排序,相同键保留原顺序

  1. 用 csv.reader 解析记录,不按物理行拆分 CSV。
  2. 将排序键转换为整数,另存输入序号 seq 与完整字段列表。
  3. 执行 ORDER BY sort_key, seq,逐条写出结果,不调用 fetchall()。
  4. 先完成临时输出,再发布为一个不存在的新路径;任何已有输出都保留。

SQLite 的 ORDER BY 文档说明,多项排序表达式会依次打破平局。第二个键 seq 明确保证相同整数键按输入顺序排列,而不是依赖数据库碰巧返回的顺序。

输入约定:UTF-8、逗号分隔、双引号引用,允许文件开头带 BOM。表头必须非空、列名非空且不重复;每条记录字段数必须与表头一致。排序列按列名精确匹配,只接受带可选正负号的 ASCII 整数,范围为 -2**63 到 2**63-1,不接受空白、小数或缺失值。02 与 2 的排序键相同,原始文本仍分别保留。

2. 准备环境与测试数据

使用 Linux、本地可写文件系统、Python 3.9 或更新版本,并确认能导入 sqlite3。无需 pip 包或独立数据库服务;本文在 Python 3.11.4、SQLite 3.41.2 上测试。下面命令适用于 bash 或 zsh,全部在同一个新目录运行;重复实验时换一个目录名。

python3 --version
python3 -c 'import sqlite3; print(sqlite3.sqlite_version)'
mkdir csv-sort-demo
cd csv-sort-demo
mkdir scratch
cat > jobs.csv <<'CSV'
id,priority,note
A,10,"needs, review"
B,2,"line one
line two"
C,2,ready
D,-1,first
CSV

将下一节保存为 sort_csv.py,也可下载完整排序脚本。使用已经写完、运行期间不会变化的普通输入文件;输出父目录和 --work-dir 指定的目录必须已存在。

3. 完整实现:逐条入库与逐条导出

"""Sort UTF-8 CSV by one signed 64-bit integer column. Python 3.9+."""
import argparse
from contextlib import closing
import csv
import json
import os
from pathlib import Path
import re
import sqlite3
import sys
import tempfile


def records(reader, key_index, width):
    for seq, row in enumerate(reader, 1):
        if len(row) != width:
            raise ValueError(f"record {seq}: expected {width} fields, got {len(row)}")
        text = row[key_index]
        if not re.fullmatch(r"[+-]?[0-9]+", text):
            raise ValueError(f"record {seq}: key must be an integer, got {text!r}")
        number = int(text)
        if not -(2**63) <= number < 2**63:
            raise ValueError(f"record {seq}: key outside signed 64-bit range")
        yield seq, number, json.dumps(row, ensure_ascii=True)


def sort_csv(source, output, key, work_dir):
    if source.resolve() == output.resolve():
        raise ValueError("input and output must differ")
    if os.path.lexists(output):
        raise FileExistsError(f"output already exists: {output}")
    staged = None
    try:
        with tempfile.TemporaryDirectory(prefix="csv-sort-", dir=work_dir) as tmp:
            with closing(sqlite3.connect(str(Path(tmp) / "rows.sqlite"))) as con:
                con.execute("PRAGMA temp_store=FILE")
                con.execute("PRAGMA cache_size=-8192")
                con.execute("CREATE TABLE items (seq INTEGER PRIMARY KEY, "
                            "sort_key INTEGER NOT NULL, payload TEXT NOT NULL)")
                with source.open(encoding="utf-8-sig", newline="") as file:
                    reader = csv.reader(file, strict=True)
                    header = next(reader, None)
                    if not header or any(not name.strip() for name in header):
                        raise ValueError("a nonempty header is required")
                    if len(set(header)) != len(header):
                        raise ValueError("duplicate header names")
                    if key not in header:
                        raise ValueError(f"missing key column: {key!r}")
                    with con:
                        con.executemany(
                            "INSERT INTO items VALUES (?, ?, ?)",
                            records(reader, header.index(key), len(header)),
                        )
                count = con.execute("SELECT count(*) FROM items").fetchone()[0]
                with tempfile.NamedTemporaryFile(
                    mode="w", encoding="utf-8", newline="", delete=False,
                    prefix=".csv-sort-", suffix=".tmp", dir=output.parent,
                ) as file:
                    staged = Path(file.name)
                    writer = csv.writer(file, lineterminator="\n")
                    writer.writerow(header)
                    for (payload,) in con.execute(
                        "SELECT payload FROM items ORDER BY sort_key, seq"
                    ):
                        writer.writerow(json.loads(payload))
        # Same-directory hard link: publish a complete file without overwriting.
        os.link(staged, output)
        return count
    finally:
        if staged is not None:
            staged.unlink(missing_ok=True)


def main():
    parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
    parser.add_argument("input", type=Path)
    parser.add_argument("output", type=Path, help="new file in an existing directory")
    parser.add_argument("--key", required=True, help="exact integer column name")
    parser.add_argument("--work-dir", type=Path, help="existing SQLite workspace parent")
    args = parser.parse_args()
    csv.field_size_limit(1024 * 1024)
    try:
        count = sort_csv(args.input, args.output, args.key, args.work_dir)
    except (OSError, ValueError, csv.Error, sqlite3.Error) as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 1
    print(f"OK: rows={count} key={args.key} output={args.output}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Python sqlite3 文档介绍了磁盘数据库、参数占位符和游标迭代。代码使用固定 SQL 与参数,不把用户提供的列名拼接进 SQL;列名仅用于找到 CSV 中的字段位置。

Python CSV 文档建议打开 CSV 文件时使用 newline=""。脚本通过解析器保留字段内的逗号、引号和换行,但会重新生成 CSV 引用和记录分隔符;保留的是字段值,不是原文件字节。每个字段上限设为 1 MiB 字符,表头也受这个限制。

临时数据库由 TemporaryDirectory 管理,连接显式关闭后才清理目录。临时输出放在目标同目录,写完关闭后使用 os.link()创建最终文件名;目标已存在时失败,不替换旧文件。此方式需要支持硬链接的本地文件系统,不用于保证断电后的持久性。正常返回和普通异常会清理临时文件;强制杀进程或断电后可能需要人工检查残留。

4. 执行与验证

  1. 查看帮助,再运行排序:
python3 sort_csv.py --help
python3 sort_csv.py jobs.csv sorted.csv --key priority --work-dir scratch

预期输出:

OK: rows=4 key=priority output=sorted.csv
  1. 用 CSV 解析器检查顺序和字段,不用 wc -l 当作记录数:
python3 - <<'PY_CHECK'
import csv
from pathlib import Path
with open("sorted.csv", encoding="utf-8", newline="") as file:
    rows = list(csv.DictReader(file))
assert [row["id"] for row in rows] == ["D", "B", "C", "A"]
assert rows[1]["note"] == "line one\nline two"
assert rows[-1]["note"] == "needs, review"
assert not list(Path("scratch").iterdir())
assert not list(Path(".").glob(".csv-sort-*.tmp"))
print("OK: numeric order, stable ties, intact CSV, cleaned workspace")
PY_CHECK

应打印 OK: numeric order, stable ties, intact CSV, cleaned workspace。顺序是 D、B、C、A,不是按字符串得到的 D、A、B、C;B 和 C 的键同为 2,因此保持原顺序。这里的小测试可以用列表,实际排序脚本不保存全部记录到 Python 列表。

5. 验证失败分支

在上一节的同一目录运行,保留已经生成的 sorted.csv:

printf 'id,priority\nX,not-a-number\n' > bad.csv
if python3 sort_csv.py bad.csv rejected.csv --key priority --work-dir scratch; then
  printf 'UNEXPECTED SUCCESS\n'
else
  printf 'STOP: invalid key\n'
fi
test ! -e rejected.csv && printf 'OK: no output published\n'
if python3 sort_csv.py jobs.csv sorted.csv --key priority --work-dir scratch; then
  printf 'UNEXPECTED OVERWRITE\n'
else
  printf 'OK: existing output kept\n'
fi

坏键检查在 stderr 提示 record 1: key must be an integer,脚本退出码为 1,且不会创建 rejected.csv。第二次排序提示 output already exists,旧的 sorted.csv 保持不变。成功返回 0;命令行参数错误返回 2。

自动化任务应仅在脚本返回 0 时消费输出。运行验证覆盖了负数、正号、前导零、64 位边界、相同键、中文、BOM、CRLF、带换行字段、空文件、只有表头、重复列名、缺失列、字段数错误、非法 UTF-8、损坏引号、已有输出与工作目录清理。另用 100,000 条逆序记录检查了完整排序与记录数。

6. 磁盘与常见问题

现象 具体处理
key must be an integer 根据提示的记录序号修复原始键值;不要把空值静默转换成 0。记录序号从表头后的第一条数据开始,不是物理行号。
key outside signed 64-bit range 当前实现只支持 64 位整数;超大整数或精确小数需要另外定义存储与排序策略,不要直接改成浮点数。
missing key column 检查 --key 的大小写与表头空白;脚本不自动修剪列名。
output already exists 换用新的输出名;先检查旧结果,不要为重跑而盲目删除。
database or disk is full 检查数据库工作目录、输出目录和系统临时目录所在磁盘,换到空间充足的目录。
field larger than field limit 检查输入分隔符与引号;确认字段确实很大后,再按内存预算调整代码中的 csv.field_size_limit()。
Operation not supported 或链接权限错误 检查输出所在文件系统是否支持创建硬链接;本示例选择支持硬链接的本地目录。

磁盘数据库不等于固定总内存。 temp_store=FILE 请求使用文件存储 SQLite 临时数据,但 SQLite 仍使用缓存;cache_size=-8192 是页缓存建议值,不是进程 8 MiB 内存上限。SQLite 临时文件文档说明排序可能使用额外临时存储,PRAGMA 文档还列出了编译选项的影响。内存同时受最大记录大小、解析器和排序器开销影响。

--work-dir 仅选择主数据库所在临时目录,不保证改变 SQLite 内部所有临时文件的位置。需要为输入、数据库、排序临时文件和新输出一起预算空间,没有适用于所有 CSV 的固定倍率;运行前可参考Python 磁盘空间预检。需要检测重复 ID 而不是排序时,使用CSV 重复 ID 检查教程。

总结

逐条解析 CSV,把全局排序交给 SQLite,再用整数键与输入序号获得稳定结果。先用小样本验证字段和失败退出,再处理真实文件;给临时数据留足磁盘空间,并保留原始输入。这个方法减少 Python 端保存全部记录的需要,但不承诺恒定内存或磁盘用量。

标签

1024 12306 2fa ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn chatgpt checkpoint cli cloudflare codex cofoldarena concurrency configparser configuration copy cprofile cpu监控 cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging decimal deployment desktop-app devtools difflib disk-space disown dns docker dovecot download electron encoding esm esm2 esm3 esmc esmfold2 etag evaluation faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io file-management filecmp filenames files flashppi flask folium frontend game generator git github-actions google google-research gpt-6.1-sol grep gzip harness heapq hls html http hugo imports indexnow ipaddress jailbreak javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp memory mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics multiprocessing mutation mysql nanobert nanobodies networking networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 performance permissions phaser pillow pip postfix preprocessing print prompt-engineering protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security selenium seo sequence-identity sha256 shell signals sitemap sklearn socket solana somaticsignatures sorting spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tempfile tensorflow tkinter tls toml totp tracemalloc tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode upsert urllib usdt utf-8 uv venv vhhbert vite web-tools webp wordcloud wordpress workflow xml yaml zipfile zoneinfo 后台 寓言 概率 经济 贸易 迅雷解析 钱包

友情链接

其它