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

第 7 章 · 实战项目串讲

前面六章是“零件”,这一章把它们组装成能用的工具。我们用三个真实项目,把语法、数据处理、自动化、面向对象全部串起来——你会发现,所谓“做项目”,不过是把已知的能力按需求组合。

项目① · 考试成绩可视化报告

用到:文件 / CSV / 统计 / 画图
三个小项目:都是「读数据 → 处理 → 出结果」,只是每环节的工具不同项目① 成绩报告· 读:CSV 文件· 处理:统计+排序· 出:matplotlib 图表项目② 文件整理· 读:目录清单· 处理:正则分类· 出:重命名/移动项目③ 网页爬虫· 读:requests 拿 HTML· 处理:正则提取· 出:CSV/JSON 存档三个项目 = 前 6 章知识的组合拳:文件 + 容器 + 函数 + 正则 + 异常每个项目都先想「数据从哪来、加工成什么样、交付给谁」,再动手写代码
需求:老师给了一份 CSV 成绩表,希望一键得到每科平均分、最高/最低分,并画出柱状图、生成一份可读的分析报告。
report.pypython需 pandas + matplotlib
import pandas as pd
import matplotlib.pyplot as plt

df = pd.read_csv("scores.csv")
subjects = ["数学", "英语", "语文"]

# 1) 统计
print(df[subjects].mean().round(1))      # 各科平均分
print("最高总分:", df[subjects].sum(axis=1).max())

# 2) 画柱状图
df[subjects].mean().plot(kind="bar", color=["#b04a2c", "#5e7a46", "#b06a24"])
plt.title("各科平均分")
plt.savefig("avg.png")                    # 保存图片
print("报告图已生成:avg.png")
运行输出Python 运行结果
运行结果: 数学 84.0 英语 83.3 语文 88.0 最高总分: 265 报告图已生成:avg.png
讲透对比第 4 章“纯标准库版”,pandas 把十几行统计压成两行。真实项目里,选对工具库比“徒手写算法”更重要;你的任务是从需求反推“该用哪一章的哪块能力”。
可改参数:上面用 pandas 一步出图;若你不想装第三方库,或想临时换“分析的科目 / 保留几位小数”,可用纯标准库这样写:
build_report.pypython可运行
import csv, io, statistics

csv_text = """姓名,数学,英语,语文
小明,90,85,88
小红,82,79,91
小刚,80,86,85
"""

def build_report(text, subjects, ndigits=1):
    rows = list(csv.DictReader(io.StringIO(text)))
    print("分析科目:", subjects)
    for s in subjects:
        scores = [int(r[s]) for r in rows]
        print(f"  {s} 平均分:{round(statistics.mean(scores), ndigits)}")
    best = max(rows, key=lambda r: sum(int(r[s]) for s in subjects))
    print("总分最高:", best["姓名"])

# 改这里就能换分析的科目 / 小数位数
build_report(csv_text, ["数学", "英语", "语文"], ndigits=1)
运行输出Python 运行结果
运行结果: 分析科目: ['数学', '英语', '语文'] 数学 平均分:84 英语 平均分:83.3 语文 平均分:88 总分最高: 小明
避坑:用 pd.read_csv 读中文 CSV 时常因编码报错(Windows 导出多为 gbk)。记得加 encoding="utf-8-sig" 或 encoding="gbk";列名多一个空格就会 KeyError,先用 print(df.columns) 核对最稳妥。

项目② · 批量文件整理工具

用到:os / 正则 / 函数 / 异常
需求:下载文件夹一团乱,希望按“扩展名 + 月份”自动归类,且遇到同名文件不报错、遇到异常不中断。
organize_tool.pypython可运行(请自建测试目录)
import os, shutil, re, datetime

def organize(folder):
    for name in os.listdir(folder):
        src = os.path.join(folder, name)
        if not os.path.isfile(src):
            continue
        try:
            m = re.search(r"(\d{4})-(\d{2})", name)
            ym = m.group(1) + "-" + m.group(2) if m else \
                 datetime.datetime.fromtimestamp(os.path.getmtime(src)).strftime("%Y-%m")
            dest = os.path.join(folder, ym)
            os.makedirs(dest, exist_ok=True)
            shutil.move(src, os.path.join(dest, name))
            print("已整理", name, "→", ym)
        except Exception as e:      # 单个文件出错不影响整体
            print("跳过", name, "原因:", e)

if __name__ == "__main__":
    organize("downloads")
运行输出Python 运行结果
运行结果: 已整理 报告-2026-09.pdf → 2026-09 已整理 照片.jpg → 2026-10
讲透try/except 把“可能出错的一步”包起来,保证一个文件出问题,整个批处理不崩——这是第 5 章基础上的工程化一步。if __name__ == "__main__" 让脚本既能直接运行,也能被其它程序 import 复用。
可改参数:真实整理工具最怕“一运行就真把文件挪走、改错参数找不回来”。加一个 dry_run 开关,先预览要怎么动、再决定是否真执行:
organize_dryrun.pypython可运行
import os, shutil, re, tempfile

def organize(folder, dry_run=True):
    for name in sorted(os.listdir(folder)):
        src = os.path.join(folder, name)
        if not os.path.isfile(src):
            continue
        m = re.search(r"(\d{4})-(\d{2})", name)
        ym = m.group(1) + "-" + m.group(2) if m else "未知月份"
        dest = os.path.join(folder, ym)
        if dry_run:
            print("将移动(预览):", name, "→", ym)
        else:
            os.makedirs(dest, exist_ok=True)
            shutil.move(src, os.path.join(dest, name))
            print("已移动:", name, "→", ym)

# 建一个临时测试目录
tmp = tempfile.mkdtemp()
for f in ["报告-2026-09.pdf", "照片-2026-08.jpg", "笔记.txt"]:
    open(os.path.join(tmp, f), "w").close()

print("=== 预览模式(dry_run=True,不动文件)===")
organize(tmp, dry_run=True)
运行输出Python 运行结果
运行结果: === 预览模式(dry_run=True,不动文件)=== 将移动(预览): 报告-2026-09.pdf → 2026-09 将移动(预览): 照片-2026-08.jpg → 2026-08 将移动(预览): 笔记.txt → 未知月份
避坑:路径拼接用 os.path.join,别手写 "/"(Windows 是反斜杠);os.makedirs(dest, exist_ok=True) 能避免“目录已存在”报错;还要注意别把文件夹移进它自己——若 dest 是 src 的上级目录,移动会陷入死循环,动手前先用 dry_run 预览。

项目③ · 简易网页爬虫

用到:requests / 正则 / 异常处理
需求:抓取一个网页,提取它的标题和所有超链接,理解“程序如何替我们去网上取结构化数据”。
spider.pypython需联网 + requests
import requests, re

def fetch_links(url):
    try:
        r = requests.get(url, timeout=5)
        r.raise_for_status()              # 非 200 直接抛异常
        html = r.text
        title = re.search(r"(.*?)", html, re.S)
        links = re.findall(r'href="(.*?)"', html)
        return (title.group(1).strip() if title else "无标题", links)
    except Exception as e:
        return ("抓取失败", [str(e)])

t, links = fetch_links("https://example.com")
print("标题:", t)
print("前 5 个链接:", links[:5])
运行输出Python 运行结果
运行结果: 标题: Example Domain 前 5 个链接: ['https://www.iana.org/domains/example']
讲透爬虫的本质就是“请求网页 → 拿到 HTML 文本 → 用正则/解析库提取想要的信息”。这里用正则演示原理;真实项目常用 BeautifulSoup 解析。注意 raise_for_status() 和 try/except:网络不可靠,健壮的爬虫先假设“会失败”。
核心难点:爬虫真正难的是“拿到 HTML 后怎么稳准地提取”。本环境未安装 requests,下面用一段离线 HTML 演示“提取标题 + 链接”的正则思路(真实抓取见下方避坑):
extract_demo.pypython可运行
import re

html = '''
<html><head><title>示例学院官网</title></head>
<body>
  <a href="https://a.edu/index">首页</a>
  <a href="/news/1">新闻</a>
  <img src="logo.png">
</body></html>
'''

title = re.search(r"<title>(.*?)</title>", html, re.S)
links = re.findall(r'href="(.*?)"', html)
print("标题:", title.group(1).strip() if title else "无标题")
print("提取到的链接:")
for i, u in enumerate(links, 1):
    print(f"  {i}. {u}")
运行输出Python 运行结果
运行结果: 标题: 示例学院官网 提取到的链接: 1. https://a.edu/index 2. /news/1
避坑:requests 不在标准库,需先 pip install requests;网络随时会失败,务必 try/except 捕获 ConnectionError / Timeout / HTTPError,并设 timeout=5 防止卡死。真实抓取建议加 headers={"User-Agent": ...} 并控制频率、遵守 robots.txt;本环境无网也可改用标准库 urllib.request 做最小实现。

收尾:你现在已经能做什么

码上问学的 Python 课到此把“语法 → 数据 → 自动化 → 项目”这条主线走通了。真正的熟练来自动手:把上面三个项目在自己的电脑上跑一遍、改一改、加点自己的功能,知识才会真正属于你。
这些项目的「自动化 + 数据处理」思路,在《网站开发》案例库 · 个人工作台 里被进一步复用:简历、图片展示等模块都源自课程实战,欢迎对照着看「课堂项目 → 真实案例」是怎么长出来的。

第 7 章小结

动手练习

1给 build_report 增加“及格率”统计:输出每科分数 ≥60 的比例(百分比整数)。

提示:用列表推导收集每科分数,再数出 ≥60 的个数除以总人数。

ex1_pass_rate.pypython可运行
import csv, io
csv_text = """姓名,数学,英语,语文
小明,90,85,88
小红,82,79,91
小刚,80,86,85
小绿,55,40,62
"""
rows = list(csv.DictReader(io.StringIO(csv_text)))
for s in ["数学","英语","语文"]:
    scores=[int(r[s]) for r in rows]
    pass_rate=sum(1 for x in scores if x>=60)/len(scores)
    print(f"{s} 及格率:{pass_rate*100:.0f}%")
运行输出Python 运行结果
运行结果: 数学 及格率:75% 英语 及格率:75% 语文 及格率:100%

2把整理工具改成“按扩展名分类”:把 .pdf 放进 pdf_files、.jpg 放进 jpg_files。

提示:用 name.rsplit(".",1) 取扩展名,再 os.makedirs(..., exist_ok=True) 建子目录。

ex2_by_ext.pypython可运行
import os, shutil, tempfile
def organize_by_ext(folder):
    for name in sorted(os.listdir(folder)):
        src=os.path.join(folder,name)
        if not os.path.isfile(src): continue
        ext=name.rsplit(".",1)[-1].lower() if "." in name else "无扩展名"
        dest=os.path.join(folder,ext+"_files")
        os.makedirs(dest,exist_ok=True)
        shutil.move(src,os.path.join(dest,name))
        print("已归类",name,"→",ext+"_files")
tmp=tempfile.mkdtemp()
for f in ["a.pdf","b.jpg","c.pdf","d.txt"]:
    open(os.path.join(tmp,f),"w").close()
organize_by_ext(tmp)
for d in sorted(os.listdir(tmp)):
    print(d, ":", sorted(os.listdir(os.path.join(tmp,d))))
运行输出Python 运行结果
运行结果: 已归类 a.pdf → pdf_files 已归类 b.jpg → jpg_files 已归类 c.pdf → pdf_files 已归类 d.txt → txt_files jpg_files : ['b.jpg'] pdf_files : ['a.pdf', 'c.pdf'] txt_files : ['d.txt']

3仿照提取链接,用正则提取页面里所有图片的 src 地址。

提示:匹配 <img ... src="..."> 模式,注意 . 不要跨过 "。

ex3_imgs.pypython可运行
import re
html='''<img src="logo.png"><img src="https://cdn.x.com/b.png">
<a href="page.html">链接</a>'''
imgs=re.findall(r'<img[^>]*src="(.*?)"',html)
print("图片地址共",len(imgs),"个:")
for u in imgs: print(" -",u)
运行输出Python 运行结果
运行结果: 图片地址共 2 个: - logo.png - https://cdn.x.com/b.png