10 · 进阶应用:爬虫 + 数据分析

📅 预计 120 分钟 | 🎯 爬虫与数据分析实战
✍️ 本讲配套练习:本地 python grader.py 10,或网页练习
🖥 你已在本项目 spider-demo/ 里跑通过两个真实爬虫(豆瓣 Top250 + B站热门),这里把它们讲透。


💡 一句话懂本讲:爬虫 = 派人去取情报,数据分析 = 把情报挑拣、归类、算出观点。 前半部分让机器替你上网抓数据,后半部分让抓回来的数据替你说话——这俩加起来,就是期末大作业的两根柱子。

本讲练习速览

练习 ex01 ~ ex20 从易到难,全部是纯数据处理题:数据都已经放进内存,你在浏览器里写完直接跑,不需要联网。题目用的数据形态和你亲手写的 spider-demo/ 完全一致,练完就等于把爬虫后处理的功夫练熟了。

题号 考点 对应讲义
ex01~ex03 嵌套取字段 / 排序 TOP / 平均播放 10.3 解析
ex04~ex06 CSV 拆行 / 正则提取 / 清洗数字 10.3 / 10.5
ex07~ex09 多字段提取 / 多条件排序 / 分区统计 10.5 变换与分组
ex10~ex11 带引号 CSV / 抓全部标题 10.3 解析进阶
ex12~ex14 去空去重 / 单位换算 / 剔异常值 10.5 清洗
ex15~ex16 筛选聚合 / 中位数众数 10.5 统计
ex17~ex18 格式化输出 / 文本提取数字 10.5 输出与正则
ex19~ex20 综合:CSV 流水线 / 报告生成 10.5 综合实战

10.1 网络爬虫是什么

把网站想象成一座藏有情报的仓库,爬虫就是你派出去的情报员,流程永远三步:

  1. 发请求:情报员走到仓库门口,递上证件(User-Agent),问"开门让我看看"。
  2. 解析:门开了,情报员在一堆文件里翻出你要的那几条,抄到本子上。
  3. 存储:把本子带回驻地,整理成 CSV / JSON 归档,方便后续分析。

你已亲手跑过的两个案例,恰好覆盖爬虫的两条主流路线:

案例 路线 核心 文件
豆瓣 Top250 解析 HTML requests + BeautifulSoup + CSS 选择器 spider-demo/douban_top250.py
B站热门 调 JSON 接口 requests 直接拿 JSON spider-demo/bilibili_hot.py

判断走哪条路的诀窍:打开浏览器 F12 → Network(网络) 看请求,如果有返回 JSON 的接口就走 B,没有就走 A。JSON 接口永远更省事——因为它本来就是数据结构,不用像 HTML 那样拿选择器去"抠"。

10.2 第一步:发请求

import requests

HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..."}

resp = requests.get(url, headers=HEADERS, timeout=10)
print(resp.status_code)   # 输入 url → 输出 200(= 成功)
print(resp.text)          # 输入 url → 输出 网页/JSON 原文

⭐ 四要点:

  1. User-Agent:假装自己是浏览器。网站看到"我是程序"会直接赶人(反爬)。
  2. status_code:先检查是不是 200,防爬失败却无感知。
  3. timeout:必带,防网络卡死让你的程序挂住不动。
  4. Windows 中文输出:加 sys.stdout.reconfigure(encoding='utf-8'),否则终端 GBK 会乱码——这是你踩过的坑。

⚠️ 常见错误(发请求)

  • 不写 User-Agent:被 403/418 拒绝,还以为自己代码写错了。
  • 不查 status_code:拿到的是 404 错误页,还硬着头皮往下解析,解析出一堆空。
  • 不写 timeout:某次请求卡住,整个程序无限等待。
  • 中文乱码:忘了强制 UTF-8,标题全变 ????

10.3 第二步:解析

路线 A:HTML 解析(BeautifulSoup)

HTML 像一本目录很混乱的书,CSS 选择器就是书签,指哪翻哪:

from bs4 import BeautifulSoup

soup = BeautifulSoup(resp.text, "html.parser")
items = soup.select(".grid_view .item")     # 选所有 class 为 item 的块
for it in items:
    title = it.select_one(".title").text    # 块内再找 .title,取文字
    print(title)

常用选择器速记:标签(如 p)、.类名(如 .title)、#id(如 #main)、外层 内层(后代)、.a .b(嵌套取)。

路线 B:JSON 接口(更常用、更省事)

JSON 长得就像快递单:最外层是包裹,里面一层套一层,每个格子里都贴着标签(键)。取数据不用翻找,直接按标签取:

data = resp.json()["data"]["list"]   # B站热门视频都在这里
for v in data:
    print(v["title"], v["stat"]["view"])   # 标题、播放量

B站热门接口返回的一个视频 v,长这样(本讲练习都用这个结构):

{
    "title": "我们拍到了水下风暴",        # 标题
    "owner": {"name": "影视飓风"},        # UP主(嵌套字典)
    "tname": "数码",                      # 分区
    "stat": {"view": 3135749, "like": 225790, "danmaku": 14126},  # 播放/点赞/弹幕
}

取嵌套字段的规律:有多少层,就写多少个中括号。v["owner"]["name"] 是"UP主的名字",v["stat"]["view"] 是"播放量"。漏一层、错一层都会 KeyError

🔗 本讲练习 ex01(取字段)、ex07(一次取多个字段)就是练这个。

解析进阶:字符串处理三件套

爬下来的数据不全是干净的结构,经常是"夹在文本里"的,这时用字符串和正则:

import re

line = "播放: 3135749,点赞: 225790"
nums = re.findall(r"\d+", line)          # 抓所有连续数字
print(nums)                              # ['3135749', '225790']

html = "<title>A</title><title>B</title>"
print(re.findall(r"<title>(.*?)</title>", html))   # ['A', 'B']

CSV 行也常带引号,引号里可能还藏逗号(简单 split(",") 会拆错):

row = '"小明,软件工程",85'
fields = []
cur, in_q = "", False
for ch in row:
    if ch == '"':
        in_q = not in_q          # 引号内逗号不算分隔符
    elif ch == "," and not in_q:
        fields.append(cur); cur = ""
    else:
        cur += ch
fields.append(cur)
print(fields)                    # ['小明,软件工程', '85']

🔗 ex04(简单 CSV)、ex10(带引号 CSV)、ex11(抓全部标题)、ex18(文本提取数字)。

⚠️ 常见错误(解析)

  • 嵌套层数数错v["stat"]["view"] 写成 v["stat.view"]v["stat"],直接 KeyError
  • split(",") 拆带引号的 CSV:字段里有逗号就被拆成两半。
  • 正则贪婪匹配<title>.*</title> 会把两个标题中间的字符一起吞掉;用 .*? 非贪婪。
  • json 解析失败:接口返回的不是 JSON(被反爬返回了 HTML 错误页),resp.json() 直接抛异常。

10.4 第三步:存储

爬完别丢内存里,落盘才叫"存住了"。两种主流格式:

import csv, json

# CSV(Excel 可开;utf-8-sig 防中文乱码)
with open("data.csv", "w", newline="", encoding="utf-8-sig") as f:
    w = csv.writer(f)
    w.writerow(["标题", "播放量"])
    w.writerow([v["title"], v["stat"]["view"]])

# JSON(保真:字段、嵌套、类型都不丢)
with open("data.json", "w", encoding="utf-8") as f:
    json.dump(all_data, f, ensure_ascii=False)   # ensure_ascii=False 保留中文

⚠️ 常见错误(存储)

  • CSV 用 "w" 写中文:没加 utf-8-sig,Excel 打开乱码。
  • JSON 不写 ensure_ascii=False:中文全变成 \uXXXX 转义。
  • newline="" 忘写:Windows 上写 CSV 每行之间多空一行。

10.5 数据分析:先用手,再上库 ⭐

练习环节里数据都已经在内存里了,这一节教你纯 Python 的整套后处理流程(浏览器里就能跑),是本讲练习的重点。核心就四件事:清洗 → 变换 → 统计 → 输出

10.5.1 清洗:挑菜摘菜

爬来的数据像刚从菜场买回的菜:有黄叶(空值)、有重复、有带泥的(脏格式)、有烂的(异常值)。清洗就是把好菜挑出来。

# ① 去空去重(保持顺序)—— ex12
titles = ["A", "  ", "B", "", "A"]
seen, out = set(), []
for t in titles:
    if not t.strip():          # 空串 / 全空白 → 跳过
        continue
    if t not in seen:
        seen.add(t); out.append(t)
print(out)                     # ['A', 'B']

# ② 类型转换:B站播放量带"万/亿"单位 —— ex13
def to_view(s):
    s = s.strip()
    if not s:
        return 0
    if s.endswith("万"):
        return round(float(s[:-1]) * 10000)
    if s.endswith("亿"):
        return round(float(s[:-1]) * 100000000)
    return int(float(s))

print(to_view("1.5万"))        # 15000
print(to_view("2.3亿"))        # 230000000
print(to_view("123"))          # 123

# ③ 剔异常值:播放量为负是脏数据,超上限像刷量 —— ex14
def clean_view(v):
    return 0 <= v["stat"]["view"] <= 10_000_000

10.5.2 变换:映射 / 筛选 / 聚合

数据加工的三板斧,用列表推导式最顺手:

videos = [
    {"title": "A", "stat": {"view": 100, "like": 500}},
    {"title": "B", "stat": {"view": 300, "like": 100}},
    {"title": "C", "stat": {"view": 200, "like": 500}},
]

views = [v["stat"]["view"] for v in videos]                 # 映射:每个都取出来
hot   = [v["title"] for v in videos if v["stat"]["like"] >= 200]  # 筛选:点赞达标的
avg   = round(sum(views) / len(views), 1)                    # 聚合:求和再平均
print(views, hot, avg)      # [100, 300, 200] ['A', 'C'] 200.0

🔗 ex15(筛选 + 聚合的组合)、ex03(均值)、ex19/ex20(整条流水线)。

10.5.3 排序与分组

排序sortedkey 指定"按什么排",reverse=True 从高到低。多条件就把多个键打包成元组:

# 按播放量从高到低 —— ex02
top = sorted(videos, key=lambda v: v["stat"]["view"], reverse=True)
print([v["title"] for v in top])      # ['B', 'C', 'A']

# 多条件:先播放降序,播放相同再按点赞降序 —— ex08
top2 = sorted(videos,
              key=lambda v: (v["stat"]["view"], v["stat"]["like"]),
              reverse=True)

分组统计:遍历一遍,用 dict.get(key, 0) + 1 计数,几行搞定:

cats = ["科技", "美食", "科技"]
count = {}
for c in cats:
    count[c] = count.get(c, 0) + 1
print(count)                    # {'科技': 2, '美食': 1}

🔗 ex08(多条件排序)、ex09(分区统计)、ex19(分组求均值)。

10.5.4 统计:中位数与众数

除了求和、均值、最大最小,分析里常用的还有中位数(排中间的值,抗异常值)和众数(出现最多的值):

def median_and_mode(nums):
    a = sorted(nums)
    n = len(a)
    mid = n // 2
    med = a[mid] if n % 2 == 1 else (a[mid - 1] + a[mid]) / 2
    counts = {}
    for x in nums:
        counts[x] = counts.get(x, 0) + 1
    mode = nums[0]                       # 次数相同保留先出现的
    for x in nums:
        if counts[x] > counts[mode]:
            mode = x
    return med, mode

print(median_and_mode([3, 1, 2, 2]))     # (2.0, 2)
print(median_and_mode([5, 5, 1, 1, 3]))  # (3, 5)

🔗 ex16 就练这个。注意:中位数要先排序再取中间。

10.5.5 格式化输出

分析结果要"排得整齐"才好看,f-string 的格式化语法是利器:

def format_row(rank, title, view):
    return f"{rank:>3}  {view:>8,}  {title}"   # 右对齐3位、播放量千分位右对齐8位

print(format_row(1, "凡人修仙传", 54517))       #   1    54,517  凡人修仙传
print(format_row(2, "小时候看不懂", 49744))     #   2    49,744  小时候看不懂

f"{x:>8,}"> 右对齐、8 占 8 个宽度、, 自动加千分位。排行榜一下就有了"榜单感"。

🔗 ex17 就练这个;ex20 把整条流水线做成最终报告。

⚠️ 常见错误(数据分析)

  • 空列表直接求均值len(views) 为 0 会 ZeroDivisionError,先判空。
  • 直接 int() 转换脏数据int("1.5万") 直接 ValueError,要先去掉单位、用 try/except 兜底。
  • innot in 混淆:去重判断写反,全被当重复项丢掉。
  • 中位数忘了排序:直接取中间下标,结果完全错。
  • dict.get 不写默认值count[c] += 1 遇到新键直接 KeyError

10.6 数据分析:pandas + matplotlib(进阶可选)

数据多了以后,手写容易累,交给专业库。核心三件事:读数据 → 筛选/分组/排序 → 统计/出图

import pandas as pd
import matplotlib.pyplot as plt

df = pd.read_csv("bili_hot.csv")          # DataFrame 数据帧 ⭐
df.head()                    # 前几行
df["播放"].max()             # 最大播放
df["分区"].value_counts()    # 各分区数量 ⭐
top = df.sort_values("播放", ascending=False).head(5)

df["分区"].value_counts().plot.bar()      # 条形图
plt.title("B站热门视频分区分布")
plt.savefig("analysis.png")               # 存图,交作业用

pandas 和手写的对应关系df["列"] ≈ 列表推导取字段;value_counts()dict.get 计数;sort_valuessorted(key=...)。先把 10.5 的手艺练熟,pandas 只是更快的手套。

🔗 练习阶段用纯 Python 版(浏览器跑不了 pandas),但代码套路和这节一模一样。

⚠️ 常见错误(pandas)

  • plt.show() 在无界面上卡住:作业里用 plt.savefig() 存图更稳。
  • 列名用中文df["播放"] 与 CSV 表头必须一字不差,多了空格就 KeyError
  • value_counts 想按数值排序:先 .sort_values(),别在图上硬看。

10.7 期末大作业套路(直接抄这个框架)

题目方向:选一个主题 → 爬数据 → 分析 → 可视化 → 结论。

① 选题(如"某平台热门内容分析")
② 爬虫脚本:spider.py,抓 N 条数据存 CSV
③ 分析脚本:analyze.py,pandas 统计 + matplotlib 出图
④ 报告:背景 / 方法 / 数据图 / 结论 / 代码附上

评分要点通常包含:工作量(真实数据)+ 创新(分析角度)+ 文档(报告规范)。本讲的 20 道练习覆盖中间"清洗 → 统计 → 出报告"的完整流程。

10.8 合规提醒

  • 只爬公开数据,别碰需要登录/授权的数据。
  • 控制频率(加 time.sleep(1)),别打爆人家服务器。
  • 学习用途为主,爬来的数据别商用、别公开传播敏感信息。

📌 双语术语表(本讲)

中文 English
网络爬虫 web crawler / scraper
请求 request
解析 parse
选择器 selector
数据帧 DataFrame
可视化 visualization
清洗 cleaning
去重 deduplication
分组 group by
中位数 median
众数 mode
映射 mapping
筛选 filtering
聚合 aggregation
正则表达式 regular expression
格式化输出 formatted output
科学计算 scientific computing
统计 statistics

⭐ 本讲考点清单

  1. 爬虫三步:请求 → 解析 → 存储
  2. requests + User-Agent + timeout + status_code
  3. BeautifulSoup select / select_one
  4. JSON 接口优先(F12 Network),嵌套字典逐层取字段(v["owner"]["name"]
  5. 字符串/正则解析:re.findall、带引号 CSV、.*? 非贪婪
  6. 清洗四件套:去空 / 去重 / 类型转换(万/亿单位)/ 剔异常值
  7. 变换三板斧:映射 / 筛选 / 聚合(列表推导式)
  8. 统计:求和 / 均值 / 中位数 / 众数 / dict.get 分组计数
  9. 多条件排序:sorted(key=元组, reverse=True)
  10. 格式化输出:f"{x:>8,}"
  11. pandas 读 CSV + value_counts + sort_values;matplotlib 出图保存
  12. 大作业框架(爬 → 分析 → 可视化 → 报告)+ 合规

💡 记忆口诀

请求要证件,解析看类型;JSON 走捷径,HTML 用选择器。
清洗四件套:去空去重换格式、异常全剔掉。
变换三板斧:映射筛选再聚合,均值别忘先判空。
排序看 key,分组数 dict,中位先排序,输出用 f-string。
爬完别乱用:公开、慢速、学习用。