码上问学问代码之理,学编程之道

第 5 章 · 自动化

学 Python 最大的“爽点”就在这章:把重复、枯燥、易错的人工活,交给几十行脚本。我们讲批量文件处理、正则表达式、网络请求、Excel/PDF 与邮件自动化,最后用一个“文件整理工具”把知识落地。

批量文件处理:让电脑替你整理

批量文件处理流水线:找到 → 过滤 → 改名/移动 → 记录结果① os.listdir / glob列出目录下所有文件② 条件过滤只要 .jpg?文件名含某词?③ 处理重命名 / 移动 /批量压缩④ 记录打印 or 写 CSV方便核对回滚安全铁律:先在「干跑模式」只打印不操作,确认清单没问题,再真正动手os.rename / shutil.move 是不可逆的——处理前把原目录复制一份路径拼接用 os.path.join(...),别手动拼 '/'
先讲为什么“把 100 张照片按月份分文件夹”“给 50 个文档批量改名”这种活,人工做既慢又容易漏。用 os 遍历 + 移动,几秒完成。
batch.pypython可运行(请自建测试目录)
import os, shutil

src = "inbox"                 # 待整理目录
for name in os.listdir(src):
    path = os.path.join(src, name)
    if os.path.isfile(path):
        ext = name.split(".")[-1].lower()     # 取扩展名
        target = os.path.join(src, ext)        # 按类型建文件夹
        os.makedirs(target, exist_ok=True)
        shutil.move(path, os.path.join(target, name))
        print("已移动", name, "→", ext + "/")
运行输出Python 运行结果
运行结果: 已移动 photo.jpg → jpg/ 已移动 note.pdf → pdf/
讲透os.path.join 用来拼路径,跨 Windows/Mac 都不会错;exist_ok=True 让“文件夹已存在”不再是错误。这种“遍历—判断—移动”三段式,是几乎所有批量脚本的骨架。
避坑三个高频坑:① shutil.move 遇到目标目录已有同名文件会直接覆盖(或在某些系统上抛错),移动前最好先判断目标是否存在;② os.listdir 返回顺序不固定,批量重命名前务必先 sorted();③ 不要在遍历目录的循环里同时增删该目录的文件,容易漏处理——正确做法是先 list() 收集文件名,再逐个操作。

下面两个小例子把“遍历—判断—移动”落地:先给一批文件批量加序号前缀(生活场景),再统计目录里各类型文件的数量(贴近整理工具)。

rename_demo.pypython可运行
import os, tempfile, shutil

d = tempfile.mkdtemp()
for n in ["a.txt", "b.txt", "c.txt"]:
    open(os.path.join(d, n), "w").close()

i = 1
for name in sorted(os.listdir(d)):
    if name.endswith(".txt"):
        new = f"笔记_{i}.txt"
        os.rename(os.path.join(d, name), os.path.join(d, new))
        i += 1

print(sorted(os.listdir(d)))
shutil.rmtree(d)
运行输出Python 运行结果
运行结果: ['笔记_1.txt', '笔记_2.txt', '笔记_3.txt']
count_type.pypython可运行
import os, tempfile, shutil
from collections import Counter

d = tempfile.mkdtemp()
for n in ["报告.txt", "图片.png", "数据.csv", "照片.jpg", "笔记.txt"]:
    open(os.path.join(d, n), "w").close()

exts = Counter(n.split(".")[-1].lower() for n in os.listdir(d))
print("按类型统计:", dict(exts))
shutil.rmtree(d)
运行输出Python 运行结果
运行结果: 按类型统计: {'txt': 2, 'png': 1, 'jpg': 1, 'csv': 1}

正则表达式:用“模式”抓取文字

正则:用「模式」描述你要的文字长什么样,re 模块负责抓取re.findall(r'1[3-9]\d{9}', text) —— 从长文本里抓所有手机号模式逐段读:1 开头 → 第二位 3-9 → 后面正好 9 个数字\d一个数字{9}重复 9 次[3-9]范围内一个字符+至少一次*零次或多次^ $开头 / 结尾抓数据 findall · 替换 sub · 切分 split · 校验 match(配合 ^ $ 全串匹配)先在 regex101.com 之类的工具里把模式调通,再写进代码
regex.pypython可运行
import re

text = "联系:alice@mail.com,bob@test.org,电话 138-0000-1234"
# 提取所有邮箱
emails = re.findall(r"[\w.-]+@[\w.-]+", text)
print(emails)

# 提取手机号(1 开头,11 位)
phone = re.search(r"1\d{2}-\d{4}-\d{4}", text)
print(phone.group() if phone else "无")
运行输出Python 运行结果
运行结果: ['alice@mail.com', 'bob@test.org'] 138-0000-1234
讲透正则像“文字的搜索语法”:\d 是数字、{4} 表示重复 4 次、\w 是字母数字下划线。它不“理解”语义,只“匹配形状”——所以最适合从日志、网页、文本里批量抠出结构化信息。
避坑三个高频坑:① .* 默认“贪婪”,会一直匹配到最后一个才停,用 .*? 才是“懒惰”到第一个;② re.findall 当正则里带分组 () 时,只返回分组内容(多个分组返回元组列表),不是整段匹配;③ 写正则务必加 r"..." 原始字符串,否则 \b、\d 会被 Python 当成转义字符。

下面两个小例子:先从一段订单文字里抠出所有日期(生活场景),再批量统计日志里的报错时间(贴近监控项目)。

find_dates.pypython可运行
import re
text = "订单时间:2026-09-15 和 2026-10-01,发货 2026/11/20"
dates = re.findall(r"\d{4}[-/]\d{2}[-/]\d{2}", text)
print("找到日期:", dates)
运行输出Python 运行结果
运行结果: 找到日期: ['2026-09-15', '2026-10-01', '2026/11/20']
scan_log.pypython可运行
import re
log = "ERROR 10:22:01 磁盘满 | INFO 10:25:03 已恢复 | ERROR 10:30:11 网络断开"
errors = re.findall(r"ERROR (\d{2}:\d{2}:\d{2})", log)
print("ERROR 次数:", len(errors))
print("发生时间:", errors)
运行输出Python 运行结果
运行结果: ERROR 次数: 2 发生时间: ['10:22:01', '10:30:11']

网络请求:让程序去网上取数据

动手做第三方库 requests 让“访问网页”变成一行。安装:pip install requests。
net.pypython需联网 + requests
import requests

r = requests.get("https://example.com", timeout=5)
print("状态码:", r.status_code)
print("网页长度:", len(r.text))     # r.text 是网页 HTML 文本

# 取 JSON 接口更简单
# data = requests.get("https://api.xxx.com/weather").json()
运行输出Python 运行结果
运行结果: 状态码: 200 网页长度: 1256
讲透r.status_code == 200 表示“成功拿到”。网络请求是“异步的、会失败的”,所以务必加 timeout,并在真实项目里做异常处理(第 7 章项目③会演示)。
避坑三个高频坑:① 真实网络会超时或失败,务必给 urlopen/get 传 timeout= 并用 try/except 包裹;② 拿到响应后先判断状态码(如 200)再解析,别假设一定成功;③ 中文网页记得 decode("utf-8"),否则可能乱码。

下面两个小例子都用标准库 urllib(无需安装第三方库):先学会正确地“拼出请求地址”,再演示“取回网页文本并解析标题”。

build_url.pypython可运行
from urllib.parse import urlencode, quote

base = "https://api.example.com/search"
params = {"q": "Python 自动化", "page": 1}
url = base + "?" + urlencode(params)
print("请求地址:", url)
print("编码后关键词:", quote("Python 自动化"))
运行输出Python 运行结果
运行结果: 请求地址: https://api.example.com/search?q=Python+%E8%87%AA%E5%8A%A8%E5%8C%96&page=1 编码后关键词: Python%20%E8%87%AA%E5%8A%A8%E5%8C%96
fetch_title.pypython可运行(离线演示,用 data: 协议)
import urllib.request, urllib.parse, re

html = "<html><head><title>码上问学</title></head><body>欢迎学习</body></html>"
url = "data:text/html," + urllib.parse.quote(html)
with urllib.request.urlopen(url) as resp:
    page = resp.read().decode("utf-8")

title = re.search(r"<title>(.*?)</title>", page)
print("网页标题:", title.group(1) if title else "无")
运行输出Python 运行结果
运行结果: 网页标题: 码上问学

Excel / PDF / 邮件:打通办公场景

excel.pypython需 openpyxl
from openpyxl import Workbook

wb = Workbook()
ws = wb.active
ws.append(["姓名", "分数"])
ws.append(["张三", 90])
ws.append(["李四", 85])
wb.save("成绩表.xlsx")     # 一键生成 Excel
运行输出Python 运行结果
运行结果: 生成文件:成绩表.xlsx
讲透这些库把“办公软件里的点击操作”变成了“代码里的几行调用”。自动化的本质,就是找到那些“规则明确、重复发生”的环节,用代码替代手工。
避坑三个高频坑:① 用 csv 写 Excel 能打开的表格时,中文要加 encoding="utf-8-sig",否则 Excel 打开乱码;② openpyxl 的 save 路径若已存在同名文件会直接覆盖;③ 发邮件常用 25/465/587 端口,且多数邮箱需要“授权码”而非登录密码,密码不要写进代码里。

下面用标准库实现两个小例子(无需安装):先用 csv 读成绩表算平均分(生活场景),再用 smtplib/email 构造一封通知邮件(贴近项目)。最后补充一个 PDF 抽取示例(需先安装第三方库)。

grade_csv.pypython可运行
import csv, io

data = "姓名,分数\n张三,90\n李四,85\n王五,95\n"
rows = list(csv.DictReader(io.StringIO(data)))
total = sum(int(r["分数"]) for r in rows)
print("人数:", len(rows))
print("平均分:", total / len(rows))
运行输出Python 运行结果
运行结果: 人数: 3 平均分: 90.0
make_mail.pypython可运行(仅构造,不发送)
from email.message import EmailMessage

msg = EmailMessage()
msg["From"] = "bot@school.com"
msg["To"] = "teacher@school.com"
msg["Subject"] = "每日成绩报告"
msg.set_content("今日已完成 3 份成绩汇总,平均分 90。")

print("收件人:", msg["To"])
print("主题:", msg["Subject"])
print("正文:", msg.get_content().strip())
运行输出Python 运行结果
运行结果: 收件人: teacher@school.com 主题: 每日成绩报告 正文: 今日已完成 3 份成绩汇总,平均分 90。
pdf_read.pypython需 pip install pdfplumber
# 需先安装:pip install pdfplumber
import pdfplumber

with pdfplumber.open("报告.pdf") as pdf:
    text = pdf.pages[0].extract_text()
    print(text)
运行输出Python 运行结果
运行结果: 报告.pdf 第 1 页提取的文字示例: 季度营收:120 万 同比增长:8.5%

实战:批量文件整理工具

综合 os / 正则 / 函数

把“按类型归类”升级成“按日期归类”:扫描目录,把每个文件按“年-月”放进对应文件夹,并用正则从文件名里提取日期(没有则用文件修改时间)。

organize.pypython可运行(请自建测试目录)
import os, shutil, re, datetime

def organize(folder):
    for name in os.listdir(folder):
        path = os.path.join(folder, name)
        if not os.path.isfile(path):
            continue
        m = re.search(r"(\d{4})-(\d{2})", name)   # 先从文件名找 年-月
        if m:
            ym = f"{m.group(1)}-{m.group(2)}"
        else:
            t = os.path.getmtime(path)            # 否则用修改时间
            ym = datetime.datetime.fromtimestamp(t).strftime("%Y-%m")
        dest = os.path.join(folder, ym)
        os.makedirs(dest, exist_ok=True)
        shutil.move(path, os.path.join(dest, name))
        print(name, "→", ym)

organize("downloads")
运行输出Python 运行结果
运行结果: 报告-2026-09.pdf → 2026-09 照片.jpg → 2026-10
讲透注意这里的“兜底逻辑”:先尝试从文件名解析日期,解析不到再用文件时间。真实项目里 80% 的功夫都在处理“意外情况”——这正是一个脚本从“玩具”变“工具”的分水岭。

第 5 章小结

动手练习

1用 pathlib 遍历一个目录,找出所有大于 0 字节的 .py 文件,打印它们的文件名和字节大小(从小到大排序)。

提示:用 Path.glob("*.py") 匹配文件,用 p.stat().st_size 取大小,再判断 > 0;最后 sorted() 保证顺序固定。

practice_1.pypython可运行
from pathlib import Path
import tempfile, shutil

d = Path(tempfile.mkdtemp())
for n, size in [("a.py", 10), ("b.py", 20), ("c.txt", 5)]:
    (d / n).write_bytes(b"x" * size)

for p in sorted(d.glob("*.py")):
    if p.stat().st_size > 0:
        print(p.name, p.stat().st_size, "字节")
shutil.rmtree(d)
运行输出Python 运行结果
运行结果: a.py 10 字节 b.py 20 字节