
第一次拿到《实验四 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 python3.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才能高效处理和存储lxmlBeautifulSoup的解析引擎解析速度比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代码。观察发现书名在h3a标签里而且书名是放在title属性中的比如titleA Light in the Attic价格在p classprice_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变化而不是盲目点下一页单页爬通之后下一步是扩展到多页。回到浏览器观察第二页、第三页的URLpage-1.html page-2.html page-3.html ... page-50.html规律非常明显。直接用循环拼接URL即可for page_num in range(1, 51): url fhttps://books.toscrape.com/catalogue/page-{page_num}.html print(url)这个找规律的步骤看起来太简单但它其实蕴含了爬虫设计的一个重要习惯不要把所有目标URL硬编码写死在代码里而是先分析出URL的生成规则用循环去构造。真实项目里很多站点的URL都有类似的参数化特征比如?page2、?offset40找到规律后整个采集范围就掌握了。另外页数上限从哪里来我翻了网站底部的翻页导航看到最后一页标记为Page 50这就确定了循环边界。稳妥起见也可以在代码里做个判断如果某一页拿到的HTML里没有预期的书籍卡片就停止循环。4.2 限速采集节奏体现工程素质把循环套上请求之后有个新手几乎都会踩的问题拿着for循环对网站一顿猛拉100个页面几秒钟就请求完了然后IP被封了。爬虫写出来不只是给自己跑着玩的。你对目标站点的每一次请求都会占用对方服务器的资源如果完全不控制频率对服务器就是一次小规模的攻击行为。我的做法是每次请求之间至少间隔0.5到1.5秒而且间隔时间要用随机值因为固定间隔重复出现反而容易被识别为机器行为import time import random for page_num in range(1, 51): url fhttps://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, headersheaders)再进一步是频率限制。如果网站限制每IP每分钟的请求次数光靠随机sleep还不够需要引入代理IP池轮换出口IP。真正的代理池搭建是在另外一个专题了实验阶段掌握到headers伪装和频率控制两层就足够了。不过我要说句实在话做实验练手目标网站是允许你爬的但如果你未来的业务需要采集某个真实站点请先确认该站点的robots协议和用户条款只在允许范围内采集并且严格控制请求频率。数据采集不能突破法律和道德的边界。5. 采集只是开始字段抽取、清洗与落盘5.1 同一批字段里隐藏的不干净把50页、1000本书全部采集完成后你可能会以为大功告成但实际上拿到手里的数据远没有想象中规整。我用前20本书打印了提取结果几个问题立刻暴露出来价格字段是£51.77这种字符串带货币符号没法直接做数值计算评分字段在HTML里是classstar-rating Three这种形式需要提取Three再映射成数字3有些书有多个分类标签有些书只有一个这些脏数据不清理后续的数据分析压根没法做。清理过程是整个采集链路里最枯燥但最不能省的一步。5.2 用pandas完成清洗与结构化采集时我建议把每本书的信息先存成字典追加到一个大列表里all_books [] for page_num in range(1, 51): url fhttps://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, indexFalse)生成的文件用Excel打开中文可能乱码因为pandas默认用UTF-8编码写入CSV而Excel默认按GBK解析。解决办法是使用UTF-8 with BOM编码df.to_csv(books.csv, indexFalse, encodingutf-8-sig)这个utf-8-sig后缀我每次写爬虫落盘都会用到可以说是一行救命的代码。另外为了后续做可视化我一般会同时导出一份JSONdf.to_json(books.json, orientrecords, force_asciiFalse)force_asciiFalse保证了中文字符不被转义成\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, headersHEADERS, timeout10) 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, indexFalse, encodingutf-8-sig) df.to_json(books_data.json, orientrecords, force_asciiFalse) 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 charsetutf-8明确写了UTF-8。这是requests的已知问题——当服务器响应头没带charset时它默认按ISO-8859-1解析于是中文全废了。解决办法不是全部一刀切设成UTF-8而是用resp.apparent_encoding基于内容去推断。这个方法对绝大多数中文站都有效。在代码里我通常在获取响应后立刻执行resp.encoding resp.apparent_encoding。7.2 请求超时导致整个循环卡死爬虫的请求发到网络上可能遇到服务器响应慢、连接超时、连接被重置各种状况。如果不设置超时时间requests默认会一直等下去程序好像卡死了。我的代码里给requests.get传了timeout10表示10秒没响应就抛出异常。同时用try...except包住整个请求和解析过程单页失败不影响整个实验的进度。7.3 解析结果为空列表先检查响应内容再检查选择器有一次soup.select(article.product_pod)返回了空列表当时第一反应是选择器写错了。但检查了很多遍HTML代码也没发现问题。后来我打印了resp.text[:200]发现拿到的根本不是预期的HTML页面而是一段跳转提示——网站把我重定向到了别的页面。这种情况在新手阶段经常发生。排查思路应该是先确认拿到了什么再确认怎么解析。如果响应内容本身不对再精确的CSS选择器也没有意义。我现在的习惯是每换一个新页面写解析代码之前先打印一下响应内容的前几百个字符确认拿到的确实是目标HTML。7.4 HTML实体字符看到的和实际存的不是一回事页面源码里经常出现nbsp;、amp;、quot;这类HTML实体字符。nbsp;是不换行空格在页面上看不见但存到字符串里就是一个实打实的实体文本排序、去重时会影响结果。BeautifulSoup解析后的.text属性通常会帮我们把实体转为真实字符但偶尔有些特殊实体不会被转换。清洗阶段我习惯做一次全表扫描用正则把残留的实体替换掉import re df[title] df[title].str.replace(rnbsp;, , regexTrue) df[title] df[title].str.replace(ramp;, , regexTrue)7.5 不求甚解地抄运行结果实验价值归零最后这个坑不涉及代码但我觉得值得放在这里说。网上能搜到大量爬虫实验代码直接复制粘贴也能跑出结果。但如果是这样完成实验你丢失的恰恰是这个实验最值钱的部分——分析页面结构、设计解析策略、处理异常、清洗数据这一整套采集思维。我写爬虫时最常做的事情不是写代码而是在浏览器开发者工具里研究HTML。这个习惯一旦养成你遇到任何网站都不会发怵。8. 实验之后的三个进阶方向如果完成了上面这套流程你其实已经掌握了爬虫的核心闭环。但把它作为起点还有三个方向可以继续深挖。第一个方向是并发加速。50页用同步循环加随机延迟耗时约1分钟但如果要采集5万页、50万页这个速度就完全跟不上。Python协程比如aiohttp可以在同一时间发出上百个请求配合semaphore控制并发量能将采集效率提升一个数量级。这个方向适合数据量很大的采集任务。第二个方向是JavaScript渲染页面的处理。我前面强调要选静态网站做实验但真实场景中大量网站是前端渲染的直接requests拿不到数据。这个方向就需要引入浏览器自动化工具但对应的成本是资源占用更高、稳定性更复杂。第三个方向是把采集链路平滑接到下游。比如爬完后直接用pandas做分析用matplotlib或pyecharts画图形成采集-清洗-分析-可视化的完整项目闭环。实验报告里如果能附上几张自己采集的数据画的图说服力比贴代码强太多。这三个方向都不需要换语言依然是Python生态你只要把基础爬虫跑熟了进阶就是按需求去选工具的事。