浏览知识库目录

Python

文件系统、序列化、正则与时间

通过 pathlib、JSON、正则和带时区时间安全处理常见本地数据边界。

文件系统、序列化、正则与时间

文件、JSON、路径和时间都位于系统边界。边界代码必须明确编码、格式、时区、错误策略和资源生命周期,不能依赖当前机器的偶然默认值。


一、学习目标

  • 使用 pathlib 处理跨平台路径
  • 以 UTF-8 安全读写文本
  • 在 JSON 与领域对象之间显式转换
  • 适度使用正则表达式
  • 使用带时区的日期时间
  • 为任务实现导入与导出

二、pathlib

from pathlib import Path

data_dir = Path.home() / ".study_tasks"
data_dir.mkdir(parents=True, exist_ok=True)
tasks_file = data_dir / "tasks.json"

不要手工用 /\ 拼接路径。Path 能表达存在检查、目录遍历、后缀替换和文件操作。

for path in sorted(data_dir.glob("*.json")):
    print(path.name, path.stat().st_size)

用户输入的路径可能指向符号链接、目录或不存在的对象,操作前要根据业务需要检查。


三、文本编码

content = tasks_file.read_text(encoding="utf-8")
tasks_file.write_text(content, encoding="utf-8")

始终为持久格式指定编码。平台默认编码会随系统语言和配置变化。

处理大文件时逐行读取:

with tasks_file.open(encoding="utf-8") as file:
    for line in file:
        process(line)

四、JSON 序列化

import json
from dataclasses import asdict

payload = [asdict(task) for task in tasks]
text = json.dumps(payload, ensure_ascii=False, indent=2, default=str)
tasks_file.write_text(text, encoding="utf-8")

default=str 适合演示,但正式格式应显式决定日期时间和枚举表示:

def task_to_dict(task: Task) -> dict[str, object]:
    return {
        "id": task.id,
        "title": task.title,
        "status": task.status.value,
        "created_at": task.created_at.isoformat(),
        "updated_at": task.updated_at.isoformat(),
    }

反序列化:

from datetime import datetime


def task_from_dict(value: dict[str, object]) -> Task:
    return Task(
        id=int(value["id"]),
        title=str(value["title"]),
        status=TaskStatus(str(value["status"])),
        created_at=datetime.fromisoformat(str(value["created_at"])),
        updated_at=datetime.fromisoformat(str(value["updated_at"])),
    )

外部输入还需要更严格的键、类型和范围检查。


五、JSON Lines

大量记录适合每行一个 JSON 对象:

with Path("tasks.jsonl").open("w", encoding="utf-8") as file:
    for task in tasks:
        json.dump(task_to_dict(task), file, ensure_ascii=False)
        file.write("\n")

优点是可以流式处理,某一行损坏时也能报告准确行号。缺点是文件整体不再是一个标准 JSON 数组。


六、正则表达式的边界

提取 #123 形式的任务引用:

import re

TASK_REFERENCE = re.compile(r"(?<!\w)#(?P<id>[1-9]\d*)\b")

for match in TASK_REFERENCE.finditer("完成 #12,阻塞 #30"):
    print(int(match.group("id")))

使用原始字符串 r"..." 减少反斜杠转义。

简单前缀、分隔符和成员判断优先使用字符串方法:

if title.startswith("[urgent]"):
    ...

不要用正则解析完整 JSON、HTML 或 Python 语法;应使用相应解析器。


七、日期时间和时区

from datetime import UTC, datetime, timedelta

now = datetime.now(UTC)
tomorrow = now + timedelta(days=1)
serialized = now.isoformat()
restored = datetime.fromisoformat(serialized)

持久化和跨系统交换通常使用 UTC 的带时区时间;展示时再转换为用户时区。

from zoneinfo import ZoneInfo

shanghai = restored.astimezone(ZoneInfo("Asia/Shanghai"))
print(shanghai.isoformat())

不要混合比较带时区和不带时区的 datetime


八、安全导入流程

导入任务时分三个阶段:

  1. 读取并解析外部格式。
  2. 验证全部记录,转换为领域对象。
  3. 验证成功后一次性写入。

不要边解析边覆盖正式数据,否则中间一条错误会留下部分导入结果。


九、常见错误

  • 未指定编码,导致换机器后乱码。
  • 用字符串拼路径,产生平台分隔符问题。
  • pickle 用于不可信输入;它可能执行任意代码。
  • 使用本地无时区时间跨系统传输。
  • 读取完整大文件后才逐行处理。

十、练习与自测

  1. 实现 JSON Lines 导出和带行号的导入错误。
  2. 将 UTC 时间显示为 Asia/Shanghai。
  3. 使用临时文件与替换实现原子 JSON 导出。

自测:

  • 为什么外部文本应显式指定编码?
  • JSON 解析成功是否代表业务数据有效?
  • 什么时候 JSON Lines 比 JSON 数组更合适?

十一、官方资料

上一篇:类型注解、dataclass 与结构化数据 | 下一篇:命令行、配置与日志