TheZTHEZ / UNDERSTANDING FIRST
Python 项目进阶28 分钟阅读

Python 数据可视化:把学习记录画成图

读取 CSV 学习记录,用标准库生成可在浏览器打开的 SVG。

项目介绍

把一周每天学习的分钟数变成横向柱状图。为兼顾手机环境,只用 Python 标准库,不要求安装 Matplotlib。输出 SVG 是可打开的真实图像,不是打印的伪图表。

学习目标

  • 识别 CSV 表头、行与数值字段。
  • 验证空值、范围与非有限数。
  • 把分钟映射为条形长度。
  • 生成文字标签,并转义输入避免把文本当标记。

前置知识与运行环境

需要本地 Python 3、CSV 读取与输出目录写权限。可用浏览器或支持 SVG 的查看器打开图像。安卓 Python 应用仍需文件授权。网页运行器只写虚拟文件,写 study.svg 不会自动保存到设备;本网站没有提供或假装提供这种接口。

前置:Python 函数、循环与列表、CSV 基础。项目涉及的标准库会在下方解释,不要求首次就记住全部接口。

实现步骤

  1. 创建 study.csv,表头使用 day,minutes,每行一个记录。
  2. csv.DictReader 按字段读取;utf-8-sig 可兼容 UTF-8 BOM。
  3. minutes 转 float,验证有限且在 0–1440,这是日时长数据约定。
  4. 最大值对应 480 像素,全零时以 1 为分母。
  5. 循环生成矩形和文本,html.escape 转义标签。
  6. 保存新 SVG,核对标签、相对长度与总时长。

样例 study.csv:

day,minutes
周一,30
周二,45
周三,20
周四,60
周五,35
周六,50
周日,40

保存 UTF-8 文本,不要将 Markdown 围栏复制进去。

完整代码

源码 ZIP 的 examples/projects 目录含同一文件。保存完整代码,不要只复制 main。

"""Generate SVG from local learning CSV using only the standard library."""
import argparse
import csv
from html import escape
from math import isfinite
from pathlib import Path

def read_records(path):
    with path.open(encoding="utf-8-sig", newline="") as file:
        reader = csv.DictReader(file)
        if reader.fieldnames != ["day", "minutes"]:
            raise ValueError("CSV 表头必须为 day,minutes")
        records = []
        for row in reader:
            label = row["day"].strip()
            value = float(row["minutes"])
            if not label or not isfinite(value) or not 0 <= value <= 1440:
                raise ValueError("每天时长必须为 0–1440 的有限数值,日期不能为空")
            records.append((label, value))
            if len(records) > 30:
                raise ValueError("入门示例最多绘制 30 条记录")
    if not records:
        raise ValueError("CSV 不能为空")
    return records

def make_svg(records):
    width = 720
    height = 90 + 45 * len(records)
    maximum = max(max(value for _, value in records), 1)
    parts = [
        f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}">',
        '<rect width="100%" height="100%" fill="#f7f8fb"/>',
        '<text x="24" y="34" font-size="22" font-family="sans-serif" fill="#172235">学习时长 / 分钟</text>',
    ]
    for index, (label, value) in enumerate(records):
        y = 60 + 45 * index
        length = value / maximum * 480
        label = escape(label[:20], quote=True)
        parts.append(f'<text x="24" y="{y + 20}" font-size="14" font-family="sans-serif" fill="#536176">{label}</text>')
        parts.append(f'<rect x="145" y="{y}" width="{length:.2f}" height="28" rx="4" fill="#345bf4"/>')
        parts.append(f'<text x="{155 + length:.2f}" y="{y + 20}" font-size="13" font-family="sans-serif" fill="#172235">{value:g}</text>')
    parts.append('</svg>')
    return '\n'.join(parts)

def main():
    parser = argparse.ArgumentParser(description="将学习时长 CSV 生成为 SVG")
    parser.add_argument("csv_file", type=Path)
    parser.add_argument("--output", type=Path, default=Path("study.svg"))
    args = parser.parse_args()
    try:
        records = read_records(args.csv_file)
        if args.output.exists():
            raise ValueError("输出文件已存在,请选择新文件名以免覆盖")
        args.output.write_text(make_svg(records), encoding="utf-8")
        total = sum(value for _, value in records)
        print(f"已生成 {args.output.name};{len(records)} 天,共 {total:g} 分钟")
    except (OSError, ValueError, TypeError) as error:
        parser.exit(1, f"错误:{error}\n")

if __name__ == "__main__":
    main()

代码解释

  • DictReader 每行返回字段字典;表头约定明确,避免误读其他表格。
  • isfinite 拒绝 nan 与无穷值,因为它们不是合理坐标。
  • 高度随条目数变化,首版最多 30 行。
  • value / maximum * 480 把数据转换为像素,零值对应零长度。
  • viewBox 表示 SVG 坐标范围,矢量图可以清晰缩放。
  • 输入文字先转义,输出存在时拒绝覆盖,避免无意丢失文件。

运行方法与验证

在独立练习目录运行:

python study-chart.py study.csv
python study-chart.py study.csv --output study-week.svg

样例数据的查看或预览结果为:

已生成 study.svg;7 天,共 280 分钟

完整代码已在临时目录核对,文件整理的预览、真实移动与冲突分别检查。实际操作时仍需验证文件,不只看成功提示。

常见错误

  • 表头空格或顺序错误:改为 day,minutes。
  • 空值、负数或 nan:修正原始数据,程序不会偷偷当成零。
  • 输出已存在:选择新的 —output 名称,程序不会删除旧图。

拓展方向

增加目标参考线与均值标签,再学习 Matplotlib/Pandas 比较库的能力。讨论图表是否误导:条形长度从零开始,不用截断坐标夸大差异。

这一节,你理解到哪里了?

这是你的自我反馈,与练习估计、课程进度分别记录。

本文为TheZ原创教学内容。正文采用 CC BY-NC-SA 4.0,代码采用 MIT 许可。请尊重署名与授权范围。