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)¶
- 从零手写爬虫骨架:
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。 - 解析流程:
- 要点:
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(数据接口)做铺垫:本课是"最简单的情况"。