第一次拿到《实验四 Python 网络爬虫——大数据采集》这个题目时,我下意识觉得它就是个"用Python把网页抠下来"的练习。真正做完以后才发现,这个实验的精髓根本不在于爬虫本身,而在于"采集"前面那两个字——大数据。一个只会在浏览器里看数据的人,和一个能从零构建数据采集链路的人,在数据岗面试时给面试官留下的印象是完全不同的。
这门实验适合所有正在学Python、但对"学了语法之后能干嘛"还感到迷茫的同学。如果你已经在啃Python基础,但没写过超过50行的程序,那这个实验就是你从"学语言"跨到"做项目"最合适的一道桥。我把整个实验从环境配置、页面分析、请求发送、HTML解析,到数据清洗、结果存储、常见坑位排查全部走了一遍,下面把这套完整链路拆开来讲,直接照着做就能跑通。
1. 拿到实验四,先别急着写代码:爬虫在大数据链路里的真实位置
1.1 为什么大数据项目的第一个环节总是数据采集
做数据分析的人经常遇到一个尴尬处境:模型、算法、可视化工具都准备就绪了,最缺的竟然是数据。
课堂上给的数据集通常都是清洗好的、字段规整的Excel或CSV,拿来练手没问题。但到了真实场景,数据散落在各个网站上:图书信息在图书网站里,商品价格在电商页面里,影评在影评平台里。没人会给你打包好一份数据文件,这时候就需要爬虫去把分散的数据"采"回来。
爬虫在整个大数据链路中扮演的角色,可以理解成"挖矿"。后面的数据清洗是把矿石里的杂质去掉,数据分析是从矿石里提炼贵金属,数据可视化是把提炼结果做成展品。如果第一步"挖矿"就挖不到或者挖坏了,后面所有环节都等于无源之水。这个实验叫"大数据采集"而不是简单叫"Python爬虫",就是要让你站在整条数据链路的起点去理解采集环节。
1.2 实验数据源的选择:网站选对了,实验就成功了一半
同样是写爬虫,选不同的目标网站,难度差距可以是十倍。
有的网站页面是JavaScript动态渲染的,翻页、内容都在浏览器端生成,requests发过去只能拿回一个空壳HTML;有的网站反爬机制很强,请求头校验、验证码、IP封禁轮番上阵,初学者连第一页数据都拿不到就开始怀疑人生。
我在做这个实验时,没有直接去爬日常用的那些大平台,而是选择了books.toscrape.com这个专门用于爬虫练习的公开网站。它是纯静态HTML,没有动态渲染,页面结构非常规整,一页展示20本书,带清晰的分页链接,字段也相当统一。它天然就是为"上手机器提取"设计的。
选这个网站不只是因为它好爬,更重要的是:实验的核心目标是把"采集链路"跑通,而不是和反爬机制斗智斗勇。先把链路在简单场景下跑通,再去面对复杂场景,这个顺序才是对的。如果你一上来就选了个难度过高的目标网站上头,大概率会卡在环境配置之外的问题上,反而得不偿失。
2. 环境准备:一次把Python、依赖库和编辑器配齐
2.1 Python解释器:用conda创建独立环境,别污染系统全局
爬虫这个方向对Python版本要求不苛刻,3.8、3.9、3.10、3.11都能跑。关键是别在系统全局环境里乱装包。我之前吃过大亏:因为多个项目挤在一个Python环境里,今天装这个库要降版本,明天装那个库又要升版本,最后整个环境搞得乌烟瘴气。
建议直接用Anaconda或者Miniconda,为这个实验建一个独立环境,哪怕后面把环境删了重来,也不影响其他项目。
conda create -n spider python=3.10 -y conda activate spider环境建好之后,命令行前面会出现(spider)提示符,说明已经进入隔离环境了。如果想要退出环境,执行conda deactivate就行。这个习惯一定要养成,它会在你后面同时处理多个项目时帮你省下大量排查环境冲突的时间。
2.2 依赖库安装:requests、beautifulsoup4、pandas、lxml
这个实验真正常用的库其实就四个:
| 库名 | 作用 | 为什么必须装 |
|---|---|---|
| requests | 发送HTTP请求,获取网页响应 | Python自带urllib太底层,requests封装得简洁易用 |
| beautifulsoup4 | 解析HTML结构,提取目标字段 | 从HTML里按标签、class定位数据,比正则稳定得多 |
| pandas | 数据清洗、结构化、导出文件 | 采集完的数据要变成DataFrame才能高效处理和存储 |
| lxml | BeautifulSoup的解析引擎 | 解析速度比Python内置的html.parser快很多 |
安装命令一行搞定:
pip install requests beautifulsoup4 pandas lxml有些同学装库总是失败,最常见的原因是pip默认源在国外,速度慢还容易超时。解决办法是换一个国内镜像源:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple requests beautifulsoup4 pandas lxml2.3 编辑器和调试环境:VSCode配置Python开发环境
编辑器我用的是VSCode,配合Python官方插件。Pycharm也很优秀,但VSCode更轻量,启动快,对内存不宽裕的机器更友好。
在VSCode里跑Python爬虫,有几个配置细节值得注意。
第一,必须确认左下角的Python解释器选的是刚才创建的conda环境,这一步非常关键。VSCode装好Python插件后会自动检测,但偶尔会选错解释器,导致你明明在终端里装好了库,VSCode运行时却报ModuleNotFoundError。所以运行之前一定看下状态栏右下角显示的Python版本和路径。
第二,调试爬虫时建议打开"断点调试"。在代码左侧行号旁边点一下设置红点,然后按F5启动调试,程序运行到这一行就会暂停,这时候可以查看每个变量的值,特别适合分析页面解析结果。我调试爬虫时基本不靠print一条条输出,而是直接在断点处看HTML解析后的对象结构。
3. 从单个列表页起步:requests请求与HTML解析的最小闭环
3.1 观察网站结构:先人工浏览页面,再动手写代码
拿到一个网站,第一件事不是写代码,而是打开浏览器慢慢翻。
打开https://books.toscrape.com/,你会看到首页是一个图书列表,每本书有封面图、书名、价格、评分、加入购物车按钮。点开一页除了展示20本书外,底部还有翻页按钮,下一页的链接指向catalogue/page-2.html。
这一步人工观察极其重要。你必须知道页面长什么样,才能确定要提取什么。鼠标右键点击书名,选择"检查",开发者工具里会定位到对应的HTML代码。观察发现书名在<h3><a>标签里,而且书名是放在title属性中的,比如title="A Light in the Attic";价格在<p class="price_color">£51.77</p>这种结构里。
把HTML结构搞清楚,后续解析代码就是按图索骥。
3.2 发送第一个请求:理解URL、Headers和状态码
用requests获取第一页HTML,代码很简单:
import requests url = "https://books.toscrape.com/catalogue/page-1.html" resp = requests.get(url) print(resp.status_code) # 200表示请求成功 print(resp.encoding) # 查看响应编码 print(resp.text[:500]) # 截取前500字符看下内容这里有个坑必须提:requests会根据HTTP响应头猜测编码,如果猜错了,中文内容就会乱码。books.toscrape.com是英文站可能不明显,但你拿去爬中文网站时,最好显式指定编码。通用的做法是先通过resp.apparent_encoding判断,再手动覆盖:
resp.encoding = resp.apparent_encoding关于状态码,遇到403表示被拒绝访问,404表示URL路径不对,500是服务器内部错误。做爬虫实验时看到200只是第一步,我们都希望拿到200,但更关键的是看返回内容符不符合预期。
3.3 用BeautifulSoup提取书名和价格
拿到HTML字符串后,解析的重点就来了:
from bs4 import BeautifulSoup soup = BeautifulSoup(resp.text, "lxml") books = soup.select("article.product_pod") for book in books: title = book.h3.a["title"] price = book.select_one("p.price_color").text print(title, price)代码逻辑拆开看很简单:select方法用CSS选择器定位所有"书籍卡片",然后从每个卡片里提取书名和价格。选择器article.product_pod的含义是"所有class为product_pod的article标签",这段分析来自于你在浏览器里看到的HTML结构。
为什么用CSS选择器而不是写正则?因为HTML是半结构化文档,标签嵌套有层级关系,正则去匹配标签结构非常笨拙,稍有改动就失效。BeautifulSoup把页面解析成DOM树之后,你可以像在浏览器里用选择器一样去搜索,这种思路更贴合网页本身的组织方式。
4. 从1页到N页:翻页逻辑、限速与反爬绕过经验
4.1 找分页规律:观察URL变化,而不是盲目点下一页
单页爬通之后,下一步是扩展到多页。回到浏览器观察第二页、第三页的URL:
page-1.html page-2.html page-3.html ... page-50.html规律非常明显。直接用循环拼接URL即可:
for page_num in range(1, 51): url = f"https://books.toscrape.com/catalogue/page-{page_num}.html" print(url)这个"找规律"的步骤看起来太简单,但它其实蕴含了爬虫设计的一个重要习惯:不要把所有目标URL硬编码写死在代码里,而是先分析出URL的生成规则,用循环去构造。真实项目里,很多站点的URL都有类似的参数化特征,比如?page=2、?offset=40,找到规律后整个采集范围就掌握了。
另外,页数上限从哪里来?我翻了网站底部的翻页导航,看到最后一页标记为Page 50,这就确定了循环边界。稳妥起见,也可以在代码里做个判断:如果某一页拿到的HTML里没有预期的书籍卡片,就停止循环。
4.2 限速:采集节奏体现工程素质
把循环套上请求之后,有个新手几乎都会踩的问题:拿着for循环对网站一顿猛拉,100个页面几秒钟就请求完了,然后IP被封了。
爬虫写出来不只是给自己跑着玩的。你对目标站点的每一次请求都会占用对方服务器的资源,如果完全不控制频率,对服务器就是一次小规模的攻击行为。
我的做法是每次请求之间至少间隔0.5到1.5秒,而且间隔时间要用随机值,因为固定间隔重复出现反而容易被识别为机器行为:
import time import random for page_num in range(1, 51): url = f"https://books.toscrape.com/catalogue/page-{page_num}.html" resp = requests.get(url) # ... 解析代码 ... time.sleep(random.uniform(0.5, 1.5))50页跑下来大约需要50秒左右,完全可接受。而且这种节奏在真实的爬虫任务中也是基本礼仪,遵守网站的robots协议和访问规则是每个爬虫开发者的底线。
4.3 反爬应对:从User-Agent伪装到代理池
books.toscrape.com本身几乎没有反爬,但实验报告里如果只写了"没有任何反爬处理"就显得太单薄了。我在实验过程中额外测试了常见的反爬应对手段。
最基础的反爬就是校验 User-Agent。有些网站不认识requests默认的User-Agent,直接返回403。解决办法是伪装成浏览器:
headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" } resp = requests.get(url, headers=headers)再进一步是频率限制。如果网站限制每IP每分钟的请求次数,光靠随机sleep还不够,需要引入代理IP池轮换出口IP。真正的代理池搭建是在另外一个专题了,实验阶段掌握到headers伪装和频率控制两层就足够了。
不过我要说句实在话:做实验练手,目标网站是允许你爬的;但如果你未来的业务需要采集某个真实站点,请先确认该站点的robots协议和用户条款,只在允许范围内采集,并且严格控制请求频率。数据采集不能突破法律和道德的边界。
5. 采集只是开始:字段抽取、清洗与落盘
5.1 同一批字段里隐藏的"不干净"
把50页、1000本书全部采集完成后,你可能会以为大功告成,但实际上拿到手里的数据远没有想象中规整。
我用前20本书打印了提取结果,几个问题立刻暴露出来:
- 价格字段是
"£51.77"这种字符串,带货币符号,没法直接做数值计算 - 评分字段在HTML里是
class="star-rating Three"这种形式,需要提取"Three"再映射成数字3 - 有些书有多个分类标签,有些书只有一个
这些"脏数据"不清理,后续的数据分析压根没法做。清理过程是整个采集链路里最枯燥但最不能省的一步。
5.2 用pandas完成清洗与结构化
采集时我建议把每本书的信息先存成字典,追加到一个大列表里:
all_books = [] for page_num in range(1, 51): url = f"https://books.toscrape.com/catalogue/page-{page_num}.html" resp = requests.get(url) soup = BeautifulSoup(resp.text, "lxml") for book in soup.select("article.product_pod"): title = book.h3.a["title"] price = book.select_one("p.price_color").text all_books.append({ "title": title, "price": price, }) time.sleep(random.uniform(0.5, 1.5))然后交给pandas清洗:
import pandas as pd df = pd.DataFrame(all_books) # 去掉价格里的货币符号,转成浮点数 df["price"] = df["price"].str.replace("£", "").astype(float) # 查看基本统计信息 print(df.describe()) # 检查有没有缺失值 print(df.isna().sum())清洗逻辑一目了然:£符号用str.replace去掉,astype(float)把字符串列转成数值列。清洗完后的DataFrame可以直接做分组、排序、画图,这才是"大数据采集"后半程需要的数据形态。
5.3 存储环节的选择:CSV、JSON还是SQLite
清洗完的数据必须落盘保存,有几种选择,适用场景各不相同:
| 存储方式 | 优点 | 适用场景 |
|---|---|---|
| CSV | 通用性强,Excel直接打开,简单直观 | 实验报告提交、小型数据量 |
| JSON | 保留嵌套结构,自带层级关系 | 需要保留字段间关系的数据 |
| SQLite | 单文件数据库,支持SQL查询,无服务依赖 | 数据量较大、需要多表关联时 |
实验阶段我建议用CSV,因为提交报告时老师可以直接打开看。但要注意编码问题:如果直接df.to_csv("books.csv", index=False),生成的文件用Excel打开中文可能乱码,因为pandas默认用UTF-8编码写入CSV,而Excel默认按GBK解析。解决办法是使用UTF-8 with BOM编码:
df.to_csv("books.csv", index=False, encoding="utf-8-sig")这个utf-8-sig后缀我每次写爬虫落盘都会用到,可以说是一行救命的代码。另外,为了后续做可视化,我一般会同时导出一份JSON:
df.to_json("books.json", orient="records", force_ascii=False)force_ascii=False保证了中文字符不被转义成\uXXXX。
6. 完整实验代码:从零到落盘的最短闭环
把前面所有步骤整合到一起,形成一份结构清晰的完整代码。我这里去掉了一些中间调试代码,只保留主流程:
import time import random import pandas as pd import requests from bs4 import BeautifulSoup BASE_URL = "https://books.toscrape.com/catalogue/page-{}.html" HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" } TOTAL_PAGES = 50 REQUEST_INTERVAL = (0.5, 1.5) def fetch_page(page_num): """请求单页HTML并解析出书籍列表""" url = BASE_URL.format(page_num) resp = requests.get(url, headers=HEADERS, timeout=10) resp.encoding = resp.apparent_encoding resp.raise_for_status() soup = BeautifulSoup(resp.text, "lxml") books = [] for book in soup.select("article.product_pod"): title = book.h3.a["title"] price = book.select_one("p.price_color").text rating = book.select_one("p.star-rating")["class"][1] books.append({ "title": title, "price": price, "rating": rating, }) return books def main(): all_books = [] for page_num in range(1, TOTAL_PAGES + 1): try: page_books = fetch_page(page_num) all_books.extend(page_books) print(f"第{page_num}页采集完成,累计{len(all_books)}条") except Exception as e: print(f"第{page_num}页采集失败: {e}") time.sleep(random.uniform(*REQUEST_INTERVAL)) df = pd.DataFrame(all_books) # 数据清洗 df["price"] = df["price"].str.replace("£", "").astype(float) rating_map = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5} df["rating"] = df["rating"].map(rating_map) # 落盘 df.to_csv("books_data.csv", index=False, encoding="utf-8-sig") df.to_json("books_data.json", orient="records", force_ascii=False) print(f"完成! 共采集{len(df)}条数据") if __name__ == "__main__": main()运行这段代码,终端会逐页输出采集进度。由于设置了0.5到1.5秒的随机延迟,50页大约需要1分钟才能跑完,别以为程序卡住了。跑完后目录下会多出books_data.csv和books_data.json两个文件。
把代码封装成函数而不是全部摊平写在主流程里,是我有意为之。fetch_page只负责"请求+解析单页",main只负责"组织循环+调用函数"。这样的好处是,如果以后换了目标网站,只需要改fetch_page内部逻辑,主流程完全不用动。可维护性在爬虫项目里很重要,爬虫代码改动的频率远比一般业务代码高。
7. 爬虫实验最容易翻车的五个坑:定位过程与避坑方案
这部分是我跑完整个实验后最想分享的内容。几乎每个坑我都踩过,排查过程本身也是实验的重要收获。
7.1 中文乱码:编码推断不可靠
第一次爬某个中文站点时,打印出来全是乱码,页面结构完全没法看。排查过程是这样的:先打印resp.encoding,发现requests猜测的编码是ISO-8859-1,但网页源码的<meta charset="utf-8">明确写了UTF-8。这是requests的已知问题——当服务器响应头没带charset时,它默认按ISO-8859-1解析,于是中文全废了。
解决办法不是全部一刀切设成UTF-8,而是用resp.apparent_encoding基于内容去推断。这个方法对绝大多数中文站都有效。在代码里,我通常在获取响应后立刻执行resp.encoding = resp.apparent_encoding。
7.2 请求超时导致整个循环卡死
爬虫的请求发到网络上,可能遇到服务器响应慢、连接超时、连接被重置各种状况。如果不设置超时时间,requests默认会一直等下去,程序好像"卡死"了。
我的代码里给requests.get传了timeout=10,表示10秒没响应就抛出异常。同时用try...except包住整个请求和解析过程,单页失败不影响整个实验的进度。
7.3 解析结果为空列表:先检查响应内容再检查选择器
有一次soup.select("article.product_pod")返回了空列表,当时第一反应是选择器写错了。但检查了很多遍HTML代码也没发现问题。后来我打印了resp.text[:200],发现拿到的根本不是预期的HTML页面,而是一段跳转提示——网站把我重定向到了别的页面。
这种情况在新手阶段经常发生。排查思路应该是:先确认"拿到了什么",再确认"怎么解析"。如果响应内容本身不对,再精确的CSS选择器也没有意义。我现在的习惯是,每换一个新页面写解析代码之前,先打印一下响应内容的前几百个字符,确认拿到的确实是目标HTML。
7.4 HTML实体字符:看到的和实际存的不是一回事
页面源码里经常出现 、&、"这类HTML实体字符。 是不换行空格,在页面上看不见,但存到字符串里就是一个实打实的实体文本,排序、去重时会影响结果。BeautifulSoup解析后的.text属性通常会帮我们把实体转为真实字符,但偶尔有些特殊实体不会被转换。清洗阶段我习惯做一次全表扫描,用正则把残留的实体替换掉:
import re df["title"] = df["title"].str.replace(r" ", " ", regex=True) df["title"] = df["title"].str.replace(r"&", "&", regex=True)7.5 不求甚解地"抄运行结果":实验价值归零
最后这个坑不涉及代码,但我觉得值得放在这里说。网上能搜到大量爬虫实验代码,直接复制粘贴也能跑出结果。但如果是这样完成实验,你丢失的恰恰是这个实验最值钱的部分——分析页面结构、设计解析策略、处理异常、清洗数据这一整套"采集思维"。我写爬虫时最常做的事情不是写代码,而是在浏览器开发者工具里研究HTML。这个习惯一旦养成,你遇到任何网站都不会发怵。
8. 实验之后的三个进阶方向
如果完成了上面这套流程,你其实已经掌握了爬虫的核心闭环。但把它作为起点,还有三个方向可以继续深挖。
第一个方向是并发加速。50页用同步循环加随机延迟,耗时约1分钟,但如果要采集5万页、50万页,这个速度就完全跟不上。Python协程(比如aiohttp)可以在同一时间发出上百个请求,配合semaphore控制并发量,能将采集效率提升一个数量级。这个方向适合数据量很大的采集任务。
第二个方向是JavaScript渲染页面的处理。我前面强调要选静态网站做实验,但真实场景中大量网站是前端渲染的,直接requests拿不到数据。这个方向就需要引入浏览器自动化工具,但对应的成本是资源占用更高、稳定性更复杂。
第三个方向是把采集链路平滑接到下游。比如爬完后直接用pandas做分析,用matplotlib或pyecharts画图,形成"采集-清洗-分析-可视化"的完整项目闭环。实验报告里如果能附上几张自己采集的数据画的图,说服力比贴代码强太多。
这三个方向都不需要换语言,依然是Python生态,你只要把基础爬虫跑熟了,进阶就是按需求去选工具的事。