Skip to content

文件与文本处理

写脚本几乎绕不开文件——读配置、写报告、解析数据、备份内容。Python 标准库在这块准备得很全:pathlib 管路径、json 管 JSON、csv 管表格、re 管正则、datetime 管时间。

一、pathlib——路径和文件操作

pathlib.Path 用对象表示路径,比手工拼字符串稳得多——/ 当分隔符、自动处理跨平台差异、链式调用一行搞定好几件事。

python
from pathlib import Path

# 路径拼接:用 / 运算符,不用拼字符串
notes_dir = Path("notes")
article_path = notes_dir / "python" / "intro.md"

# 读整个文件
content = article_path.read_text(encoding="utf-8")

# 写整个文件
article_path.write_text("正文内容", encoding="utf-8")

read_textwrite_text 一行搞定读写,内部自动开关文件,不用手动 open close

判断文件和目录:

python
path = Path("notes/article.txt")

path.exists()      # 是否存在
path.is_file()     # 是不是文件
path.is_dir()      # 是不是目录

创建目录:

python
output = Path("output/reports")
output.mkdir(parents=True, exist_ok=True)
# parents=True:父目录不存在时一起建
# exist_ok=True:目录已存在不报错

mkdir 不加 exist_ok=True 时,目录已经存在会抛异常——这是写脚本时常见的一个坑,先建目录再写文件,跑第二次就报"目录已存在"。

1 按行读大文件

read_text 会把整个文件读进内存。文件几个 G 时用 open 逐行读:

python
with Path("notes/big_log.txt").open(encoding="utf-8") as f:
    for line in f:
        if "错误" in line:
            print(line.strip())

with 保证文件用完自动关闭。逐行读时内存里始终只有一行,不会因为文件大就爆内存。

2 追加写入

write_text 是覆盖写。要往文件末尾追加,用 open"a" 模式:

python
log = Path("notes/changelog.txt")

with log.open("a", encoding="utf-8") as f:
    f.write("2024-06-01 新增文件处理章节\n")

"a" 是 append,每次写在文件末尾;"w" 是 write,每次覆盖整个文件。

二、JSON

配置和数据交换经常用 JSON。标准库 json 处理:

python
import json
from pathlib import Path

# JSON 字符串 → Python 对象
raw = '{"title": "Python 入门", "author": "张三"}'
article = json.loads(raw)
print(article["title"])  # Python 入门

# Python 对象 → JSON 字符串
text = json.dumps(article, ensure_ascii=False, indent=2)
print(text)

json.loads 把 JSON 字符串解析成 Python 的字典/列表;json.dumps 反过来。ensure_ascii=False 让中文保持原样,不加这个参数,中文会被转成 张三 这种,人没法读。indent=2 让输出带缩进,好看。

读写 JSON 文件:

python
path = Path("notes/articles.json")

# 写
data = [
    {"title": "Python 入门", "status": "已发布"},
    {"title": "Go 并发", "status": "草稿"},
]
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")

# 读
loaded = json.loads(path.read_text(encoding="utf-8"))
print(loaded[0]["title"])  # Python 入门

json.loadsJSONDecodeError 说明格式不对——少了引号、多了逗号、括号没闭合。排查时把 JSON 内容贴到校验工具里看一眼,比盯着代码猜快。

三、CSV

表格数据导出成 CSV 很常见。标准库 csvDictReaderDictWriter 按表头读写,不用记列下标:

python
import csv
from pathlib import Path

# 读:DictReader 把每行变成字典,键是表头
with Path("contacts.csv").open(encoding="utf-8", newline="") as f:
    reader = csv.DictReader(f)
    for row in reader:
        print(row["姓名"], row["电话"])  # 直接用表头名取值
python
# 写:DictWriter 按字段名写
rows = [
    {"姓名": "张三", "电话": "13800138000"},
    {"姓名": "李四", "电话": "13900139000"},
]

with Path("contacts_out.csv").open("w", encoding="utf-8", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=["姓名", "电话"])
    writer.writeheader()
    writer.writerows(rows)

newline="" 是 CSV 的固定写法,不加的话 Windows 上行间会多出空行。

四、正则表达式

正则从非结构化文本里抽字段——日志行、命令输出、自由格式的文本。有结构化格式(JSON、CSV)时优先用对应解析器,正则留给没有固定结构的文本

python
import re

# 从日志行提取时间、级别、内容
line = "2024-06-01 10:30:15 INFO 服务启动成功"
pattern = r"(\S+ \S+) (\w+) (.*)"

match = re.match(pattern, line)
if match:
    time_str, level, message = match.groups()
    print(time_str)  # 2024-06-01 10:30:15
    print(level)     # INFO
    print(message)   # 服务启动成功

re.match 从开头匹配,() 分组,match.groups() 取出各组。用 (?P<名字>...) 给分组起名,可读性更好:

python
pattern = r"(?P<time>\S+ \S+) (?P<level>\w+) (?P<message>.*)"
match = re.match(pattern, line)
print(match.group("level"))  # INFO

找出所有匹配(findall)和替换(sub):

python
# 找出文本里所有邮箱
emails = re.findall(r"[\w.]+@[\w.]+", text)

# 把密码、token 替换成星号
masked = re.sub(r"(password|token)=\S+", r"\1=***", "password=abc123 token=xyz")
print(masked)  # password=*** token=***

正则能跑通不等于准确——IP、邮箱、URL 这些格式要求严格时,表达式要按实际数据收紧,否则会漏匹配或误匹配。

五、datetime——时间和日期

时间处理在脚本里很常见——给文件名加日期、算两个时间点差多少天、格式化输出。标准库 datetime 管这些:

python
from datetime import datetime

# 当前时间
now = datetime.now()
print(now)  # 2024-06-01 10:30:15.123456

# 格式化成字符串
print(now.strftime("%Y-%m-%d %H:%M"))  # 2024-06-01 10:30

# 字符串解析成 datetime
dt = datetime.strptime("2024-06-01", "%Y-%m-%d")
print(dt)  # 2024-06-01 00:00:00

strftime(string format time)把时间对象格式化成字符串,strptime(string parse time)反过来。常用的格式符:%Y 年、%m 月、%d 日、%H 时、%M 分、%S 秒。

1 给文件名加日期

python
from datetime import datetime
from pathlib import Path

today = datetime.now().strftime("%Y-%m-%d")
backup_path = Path(f"backup/articles-{today}.json")
print(backup_path)  # backup/articles-2024-06-01.json

2 时间差

python
from datetime import datetime

created = datetime.strptime("2024-05-01", "%Y-%m-%d")
now = datetime.now()

delta = now - created
print(delta.days)        # 相差多少天
print(delta.total_seconds())  # 相差多少秒

两个 datetime 相减得到 timedelta 对象,.days 是天数,.total_seconds() 是总秒数。

3 时区

python
from datetime import datetime, timezone, timedelta

# 东八区
tz_beijing = timezone(timedelta(hours=8))
now_bj = datetime.now(tz_beijing)
print(now_bj)  # 2024-06-01 10:30:15+08:00

不指定时区时,datetime.now() 返回的是本地时间(跟系统时区一致)。跨时区场景(服务器在国外、日志要统一时间)要显式带时区,不然时间对不上。容器里这个坑特别常见——容器时区是 UTC,应用按本地时间记日志,跟宿主机差 8 小时。