跳转至

13.3 Web分析与爬虫——静态页面爬取示例

内容梗概

本节课是静态页面爬取的完整实战。老师以自己开发的演示网站(上一节课用过的静态网站,本地 8000 端口)为爬取对象,"左右互博"避免法律风险。课程先演示了环境准备过程(克隆仓库、pipenv 虚拟环境、安装 requests 和 beautifulsoup4),再从零手写爬虫:用 requests.get 发 HTTP 请求、设置 r.encoding = 'utf-8' 解决中文乱码、用 BeautifulSoup 解析并用 CSS 选择器 select('li') 提取学生名单。最后讲解一个约 61 行的完整多页爬虫框架:用字典做 URL 去重与爬取状态管理、循环发现新链接直至爬完全部页面,期间还现场调试了 for...else 逻辑的一个 bug。

知识点详解

环境准备(约 00:00–09:30)

  • 爬取对象是老师自己仓库里的演示网站(static website spider 项目),用 python -m http.server 一类的 Python 模块在本地 8000 端口启动静态网站(0.0.0.0 改成 localhost 访问完全可行)。
  • 运行示例代码前要注意**终端当前路径**(cd 到 spiders/static 目录下再运行 simple_spider.py)。
  • 依赖安装(pipenv 虚拟环境):
  • pipenv install requests —— 注意是 requests 带 s,多个同学因少打 s 报错。
  • pipenv install beautifulsoup4 —— 查官方文档:Python 3 应安装 beautifulsoup4,而**不是** bs4,也不叫 beautifulsoup;安装后 from bs4 import BeautifulSoup 导入。
  • 老师反省示例仓库没提供 pip freeze 的依赖清单,课后补推 GitHub;pip 源已改为阿里云镜像加速。
  • 写爬虫用不到 flask,可将其从依赖中删掉。

requests 发送请求与编码处理(约 11:30–16:50)

  • 从零手写爬虫骨架:
    import requests
    from bs4 import BeautifulSoup
    
    url = 'http://localhost:8000'  # 或 127.0.0.1,等价
    r = requests.get(url)          # 发送 HTTP GET 请求,参数是网址字符串
    
  • requests.get() 返回值是一个 Response 对象(变量名 r 自取);取返回文本用 r.text 属性。
  • 中文乱码问题:直接打印 r.text 时 title 里的中文是乱码,需设置 r.encoding = 'utf-8' 告诉 requests 返回结果使用的编码,之后内容正常(呼应之前讲过的编码知识)。
  • 拿到 HTML 后先与浏览器看到的页面比对,确认拿到的是**完整页面**。

BeautifulSoup 解析与 CSS 选择器提取(约 16:50–23:20)

  • 需求一:获取页面中所有学生名字(学号 ID)。正确做法是**到浏览器里右键检查元素**观察结构,发现学生名字都存在 li 元素里 → CSS 选择器为 li。
  • 解析流程:
    soup = BeautifulSoup(r.text, 'html.parser')  # 参数1:HTML 字符串;参数2:解析器
    li_list = soup.select('li')                  # 按 CSS 选择器选元素,返回列表
    for li in li_list:
        print(li.text)                           # .text 取元素的文本内容(去掉标签)
    
  • 要点:BeautifulSoup(html串, 'html.parser') 初始化解析器返回 soup 对象;select(选择器字符串) 返回匹配元素的**列表**;HTML 元素有文本内容、属性等多个维度,取文本访问 .text 属性。
  • BeautifulSoup 官方文档**有中文版**,鼓励查文档。

完整多页爬虫框架(约 23:20–36:30)

  • 需求二:合格爬虫要爬完所有分页(第 2、3、4 页)。程序约 61 行,结构如下:
  • URL 字典去重:初始化字典 {目标url: False},key 是待抓 URL,value 是爬取状态(False 未爬 / True 已爬)。利用字典 key 唯一做去重,发现已爬过(True)就跳过。
  • get_next_url() 函数:遍历字典找到第一个 value 为 False 的 URL 返回;全部爬完(for 循环跑完没 return)时返回 None,主循环结束。
  • 抓取函数:传入 URL → requests.get + encoding='utf-8' → BeautifulSoup 解析 → find_all('li')(与 select 返回值一样,老师这里换用 find_all 演示另一个接口)取出学生信息存列表;同时找出页面所有 a 元素,取其 href 属性拼上 http://localhost:8000 前缀得到完整链接,一并返回。返回值是字典 {'students': [...], 'links': [...]}。
  • 主循环:全局 students 列表用 extend(或 +=)汇总每页结果;新发现的链接加入 URL 字典(已存在则跳过,实现去重)并置状态 False;当前 URL 置 True;再取下一个待爬 URL,为 None 则循环结束,打印全部学生 ID。
  • 现场调试插曲(约 31:40–36:30):
  • 老师想在 get_next_url 里给 if 补一个 else: return None 来"修复"只爬第一页的问题,结果逻辑错误:遇到第一个 True 就提前 return None 退出,后面还有 False 也不爬了。正确逻辑是 for...else:for 循环完整跑完(没找到任何 False)才进 else 返回 None。
  • 进一步讨论:else 其实**不写也行**——函数跑完 for 循环且没有 return 时默认返回 None,效果相同("小黄鸭同学你赢了")。
  • 遗留小 bug:首页被抓两次,因为 http://localhost:8000 和 http://localhost:8000/index.html 字符串不同被视为两个 URL,但网站逻辑上是同一页面。

示例与演示

  • 演示对象:老师自己开发的静态演示网站(本地 8000 端口,多页学生名单,页间有 a 链接互相跳转);爬虫项目与网站项目分别从 GitHub 仓库克隆。
  • 环境演示:克隆仓库 → 启动本地网站 → pipenv 安装 requests、beautifulsoup4 → pipenv shell 进虚拟环境 → 运行 python simple_spider.py。
  • 单页提取演示:手写代码抓首页,select('li') 得到 li 元素列表,遍历取 .text 打印学生名字,目标达成。
  • 完整爬虫演示:61 行程序自动从首页出发,发现新链接、依次抓取第 1–4 页,把所有学生名字存入列表打印;运行输出中的"抓取第 X 页 / 找到新链接"等信息都是老师自己 print 的调试信息。
  • 调试信息细节:首页因 index.html 链接指向自身被重复抓取一次。

重点与难点

  • 重点:静态爬取三步曲——requests.get(url) 拿 HTML → r.encoding='utf-8' 解决乱码 → BeautifulSoup + CSS 选择器(select/find_all)提取内容、.text 取文本。
  • 重点:多页爬虫的经典骨架——字典管理 URL 队列与爬取状态(去重)、从页面提取 a[href] 发现新链接、循环直到无新 URL。
  • 易错点:
  • 包名是 requests(带 s)和 beautifulsoup4(导入名 bs4),装错名字是常见报错来源。
  • 不设 encoding 中文会乱码。
  • for...else 的语义:else 在循环**正常跑完**(未被 break/return 打断)时执行;在循环体内乱放 else: return None 会导致提前退出——这是本课现场翻车又修复的点。
  • 函数无显式 return 时默认返回 None,可利用这一点省略 else 分支。
  • URL 字符串层面不同(带不带 index.html)会绕过去重,实际指向同一页面。
  • 老师强调随时注意终端当前路径,运行脚本前先确认所在目录。

关联内容

  • 直接应用 13.1 的爬虫两步框架(requests 通信 + BeautifulSoup 解析)和 13.2 的 CSS 选择器知识(li、a 选择器、右键检查元素)。
  • 演示网站来自上一周 Web 开发课程(静态网站章节);pipenv 虚拟环境、pip install、国内镜像源对应第 10 章内容;r.encoding 呼应先前排过的字符编码知识;字典 key 唯一去重呼应字典章节。
  • 为 13.4(动态页面,requests 拿不到完整 HTML 的情形)和 13.5(数据接口)做铺垫:本课是"最简单的情况"。