春江暮客

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

用 Python 把 CSV 转成 HTML 报告:文本转义、多行字段与覆盖保护

2026-10-24 技术
用 Python 把 CSV 转成 HTML 报告:文本转义、多行字段与覆盖保护

CSV 汇总已经生成,但每次查看都要打开表格软件,字段里的逗号和换行又容易被误读。用 Python 标准库生成一个可离线打开的 HTML 表格,就能把小型任务结果交给浏览器查看。

本文面向初级到中级读者,完成 CSV 解析、表头和列数检查、HTML 文本转义,以及独立报告导出。报表不依赖 JavaScript、外部样式或网络资源。

1. 方法与输入约定

  1. 输入是逗号分隔的 UTF-8 CSV,允许文件开头带 BOM。第一条记录是表头;列名不能空白或重复,比较时不去除空格。
  2. 每条数据记录必须与表头列数一致。空白物理行也会被当作列数不符;只有表头的文件合法,报告记录数为 0。
  3. 所有字段保持字符串,001 不变成 1。字段、表头和报告标题全部作为 HTML 文本转义,CSV 中的标签只显示,不执行。
  4. 默认最多接受 1000 条数据记录,超过就失败,不生成截断报告。此限制不等于文件大小或内存上限;工具把生成的 HTML 留在内存中,适合小型报表。

如果需要固定业务列名或校验数值,先接上 CSV 结构校验。这里的报告器只检查通用表格结构。

2. 准备环境与示例数据

使用已有的 Python 3.10 或更新版本,不需要第三方依赖。本文示例在 Linux、Python 3.11.4 下验证。选择一个尚不存在的实验目录:

python3 --version
mkdir csv-html-demo
cd csv-html-demo
cat > results.csv <<'CSV'
sample_id,note
001,中文结果
002,"alpha, beta"
003,"first line
second line"
004,<script>alert(1)</script>
CSV

把下一节保存为 csv_to_html.py,也可以下载完整脚本。第四条数据是故意加入的标签文本,用来验证转义。

3. 完整实现

"""Build a small standalone HTML table from UTF-8 CSV. Python 3.10+."""
import argparse
import csv
from html import escape
from pathlib import Path
import sys


def positive(value):
    number = int(value)
    if number < 1:
        raise argparse.ArgumentTypeError("must be at least 1")
    return number


def render(source, title, max_rows):
    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("missing or empty column name")
        if len(set(header)) != len(header):
            raise ValueError("duplicate column name")
        parts = []
        for number, row in enumerate(reader, 1):
            if number > max_rows:
                raise ValueError(f"more than {max_rows} data records")
            if len(row) != len(header):
                raise ValueError(f"record {number}: expected {len(header)} fields, got {len(row)}")
            parts.append("<tr>" + "".join("<td>" + escape(cell) + "</td>" for cell in row) + "</tr>")
    headings = "".join('<th scope="col">' + escape(name) + "</th>" for name in header)
    safe_title = escape(title)
    document = f'''<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{safe_title}</title>
<style>
body {{ font: 16px system-ui, sans-serif; margin: 2rem; color: #172033; }}
.scroll {{ overflow-x: auto; }}
table {{ border-collapse: collapse; }}
th, td {{ border: 1px solid #b8c2d1; padding: .5rem; text-align: left;
           white-space: pre-wrap; overflow-wrap: anywhere; max-width: 36rem; }}
th {{ background: #edf2f8; }}
</style>
</head>
<body>
<h1>{safe_title}</h1>
<p>Records: {len(parts)}</p>
<div class="scroll"><table>
<thead><tr>{headings}</tr></thead>
<tbody>{"".join(parts)}</tbody>
</table></div>
</body>
</html>
'''
    return document, len(parts)


def main():
    parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
    parser.add_argument("source", type=Path)
    parser.add_argument("output", type=Path)
    parser.add_argument("--title", default="CSV report")
    parser.add_argument("--max-rows", type=positive, default=1000)
    args = parser.parse_args()
    try:
        document, count = render(args.source, args.title, args.max_rows)
        payload = document.encode("utf-8")
        with args.output.open("xb") as file:
            file.write(payload)
    except (OSError, UnicodeError, csv.Error, ValueError) as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 1
    print(f"OK: {count} records -> {args.output}")
    return 0


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

Python CSV 官方文档要求文件用 newline="" 打开;解析器负责识别引号中的逗号和换行。strict=True 能报告部分坏引号等解析错误,列数和表头仍需代码单独检查。

html.escape() 官方说明列出了它转换的 HTML 特殊字符。这里仅把动态值放进标题、表头和单元格的文本位置;不要把同一套写法直接改成动态脚本、CSS 或 URL。表格使用 white-space: pre-wrap 显示字段中的换行。

只有整个输入检查成功后才打开输出。open() 的 x 模式拒绝覆盖已有路径,包括输入文件本身。它不提供原子提交或断电恢复;写入阶段若磁盘已满,仍可能留下不完整的新文件,需处理失败后换一个新文件名重跑。

4. 运行与查看

在实验目录执行:

python3 csv_to_html.py --help
python3 csv_to_html.py results.csv report.html --title 'Batch <A> & results'
python3 -m http.server 8000 --bind 127.0.0.1

第二条命令输出 OK: 4 records -> report.html。在浏览器打开 http://127.0.0.1:8000/report.html;同一机器运行浏览器时可访问这个地址,也可以直接打开本地 HTML 文件。查看完在服务终端按 Ctrl+C 停止;它只监听本机。

应看到四条记录,中文正常,001 保留,逗号留在一个单元格,多行备注分两行显示。最后一格显示字面 <script>alert(1)</script>,页面不会弹窗。报告会包含原始数据,分享前应自行确认内容适合分享。

5. 验证输出与失败行为

先在另一个终端进入相同实验目录,检查实际文件内容:

python3 - <<'PY_CHECK'
from pathlib import Path
text = Path("report.html").read_text(encoding="utf-8")
assert "Records: 4" in text
assert "<td>001</td>" in text
assert "中文结果" in text
assert "alpha, beta" in text
assert "first line\nsecond line" in text
assert "&lt;script&gt;alert(1)&lt;/script&gt;" in text
assert "<script>" not in text
assert "Batch &lt;A&gt; &amp; results" in text
print("OK: records, text, and escaping")
PY_CHECK

预期输出 OK: records, text, and escaping。再分别检查覆盖保护、记录上限与坏列数;下面命令在普通交互终端运行,便于检查每个退出码:

python3 csv_to_html.py results.csv report.html
printf 'exit=%s\n' "$?"
python3 csv_to_html.py results.csv too-many.html --max-rows 3
printf 'exit=%s\n' "$?"
printf 'a,b\n1,2,3\n' > bad.csv
python3 csv_to_html.py bad.csv bad.html
printf 'exit=%s\n' "$?"

三次均应退出 1;已有 report.html 保持不变,too-many.html 和 bad.html 不会创建。参数错误,例如 --max-rows 0,由 argparse 返回 2。成功生成返回 0。

本次还验证了 UTF-8 BOM、CRLF、只有表头、空文件、重复表头、无效 UTF-8、坏引号以及表头和标题中的 HTML 字符。若要处理海量数据,先按流式处理大型 CSV聚合成小表,再生成报告。

6. 常见错误

现象 处理方法
File exists 使用新的输出文件名;不要直接删掉仍需保留的旧报告。
missing or empty column name / duplicate column name 修正 CSV 第一条记录,确保列名非空且唯一。
expected ... fields 检查对应记录的引号、分隔符和多余空白行;不要直接按逗号切分。
more than ... data records 先筛选或聚合输入;确认规模可接受后才提高 --max-rows。
UnicodeDecodeError 类解码错误 先按 CSV 编码排障教程确认并转换编码,避免忽略坏字节。
field larger than field limit 缩小不适合在浏览器表格展示的超长字段,或明确评估后调整解析器上限。
No such file or directory / Permission denied 核对输入路径,并为输出选择已经存在且可写的目录。

总结

用 CSV 解析器保持记录完整,用 HTML 文本转义保留原始内容,再生成不依赖外部资源的小型报告。通过退出码、覆盖保护和内容断言检查成功与失败,之后就可以把这个步骤接到日常批处理的结果汇总后面。

标签

1024 12306 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 configuration copy cpu监控 cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging decimal deployment desktop-app devtools disk-space disown dns docker dovecot download electron encoding esm esm2 esm3 esmc esmfold2 faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io flashppi flask folium frontend game generator git github-actions google google-research grep gzip harness hls html http hugo imports indexnow 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 mutation mysql nanobert nanobodies networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 phaser pillow pip postfix preprocessing print 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 sklearn socket solana somaticsignatures spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tensorflow tkinter tls toml tracemalloc tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode urllib usdt utf-8 uv vhhbert vite webp wordcloud wordpress workflow yaml zipfile zoneinfo 后台 寓言 概率 经济 贸易 迅雷解析 钱包

友情链接

其它