㊗️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~
㊙️本期爬虫难度指数:⭐⭐⭐ (进阶)
🉐福利:一次订阅后,专栏内的所有文章可永久免费看,持续更新中,保底1000+(篇)硬核实战内容。
全文目录:
- 🌟 开篇语
- 0️⃣ 前言(Preface)
- 1️⃣ 摘要(Abstract)
- 2️⃣ 背景与需求(Why)
- 3️⃣ 合规与注意事项(必写)
- 4️⃣ 技术选型与整体流程(What/How)
- 5️⃣ 环境准备与依赖安装(可复现)
- 6️⃣ 核心实现:请求层(Fetcher)
- 7️⃣ 核心实现:解析层(Parser)
- 8️⃣ 数据存储与导出(Storage)
- 9️⃣ 运行方式与结果展示(必写)
- 🔟 常见问题与排错(强烈建议写)
- 1️⃣1️⃣ 进阶优化(加分项)
- 1️⃣2️⃣ 总结与延伸阅读
- 🌟 文末
- ✅ 专栏持续更新中|建议收藏 + 订阅
- ✅ 互动征集
- ✅ 免责声明
🌟 开篇语
哈喽,各位小伙伴们你们好呀~我是【喵手】。
运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO
欢迎大家常来逛逛,一起学习,一起进步~🌟
我长期专注Python 爬虫工程化实战,主理专栏 《Python爬虫实战》:从采集策略到反爬对抗,从数据清洗到分布式调度,持续输出可复用的方法论与可落地案例。内容主打一个“能跑、能用、能扩展”,让数据价值真正做到——抓得到、洗得净、用得上。
📌专栏食用指南(建议收藏)
- ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
- ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
- ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
- ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用
📣专栏推广时间:如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅专栏👉《Python爬虫实战》👈,一次订阅后,专栏内的所有文章可永久免费阅读,持续更新中。
💕订阅后更新会优先推送,按目录学习更高效💯~
0️⃣ 前言(Preface)
在这篇文章里,我们将使用 Python 的requests和BeautifulSoup4库,带你完整抓取一个名言警句网站,并最终将数据导出为结构化的 CSV 文件。
读完这篇硬核干货,你将获得:
- 掌握针对静态网页的“请求-解析-存储”全链路标准开发范式。
- 学会如何编写具备基础反爬应对能力(如随机 UA、异常重试)的健壮代码。
- 拥有一份随时可复用到其他静态站点抓取任务中的工程化代码模板。
1️⃣ 摘要(Abstract)
本文以抓取经典沙盒站点 Quotes to Scrape 为例,详细拆解了使用 Python + Requests + BS4 工具栈实现数据采集的全过程,最终产出规范的 CSV 数据集。
通过阅读本文,你不仅能收获一份拿来即用的高质量源码,还能深刻理解爬虫背后的容错机制设计与合规边界原则,助你从爬虫“脚本小子”进阶为“数据工程师”。
2️⃣ 背景与需求(Why)
为什么要爬?
在日常工作中,我们经常需要进行信息聚合与自动化数据分析。在这个案例中,假设我们正在训练一个情感分析模型,或者需要为一个“每日一句”的 Bot 积累语料库,手动复制粘贴显然不够 Geek。自动化采集不仅解放双手,还能保证数据的时效性与完整性。
目标站点与字段清单:
目标站点:
http://quotes.toscrape.com/(包含多页结构)目标字段:
Quote_Text(名言内容)Author(作者名字)Tags(关联标签,多个标签以逗号拼接)
3️⃣ 合规与注意事项(必写)
说实话,写爬虫最关键的不是技术多牛,而是要知道“边界”在哪。进去喝茶可不是闹着玩的。
- 关于 robots.txt:动手前永远记得看一眼
domain.com/robots.txt。这是一种君子协定,如果对方明确Disallow了特定目录,请保持尊重,不要硬闯。 - 频率控制与礼貌并发:不要把爬虫写成 DDoS 攻击器!在请求之间加入
time.sleep(),模拟人类正常的浏览频率。这不仅是保护目标服务器,也是保护你自己的 IP 不被秒封。 - 底线原则:绝不采集涉及个人隐私的敏感信息(如手机号、身份证、私密聊天);在遇到明确的登录墙或付费墙时,不要尝试使用黑客手段绕过获取未授权数据。咱们技术人,要用中立、克制的态度对待数据。
4️⃣ 技术选型与整体流程(What/How)
我们这次面对的是一个纯静态网页——这意味着所有的数据都直接写在了页面的 HTML 源码里,不需要执行 JavaScript,也不需要去抓 XHR 接口。
整体流程:目标URL发现➡️采集(Fetcher)发起网络请求➡️解析(Parser)提取DOM节点➡️清洗(Cleaner)数据去空去重➡️存储(Storage)落盘CSV
为什么选 requests + BS4?
对于静态页面,requests的简单易用加上BeautifulSoup的链式调用,是快速验证需求的最优解。杀鸡焉用牛刀,在这个量级下如果上 Scrapy 或 Playwright 反而显得臃肿。我们的目标是:用最少的代码,干最漂亮的事。
5️⃣ 环境准备与依赖安装(可复现)
开始码代码前,先把环境拾掇利索。
Python 版本:建议使用 Python 3.8 或以上版本。
依赖安装:打开你的终端,敲下这行命令:
pipinstallrequests beautifulsoup4项目结构推荐:好的代码从目录开始。
quote_spider/ ├── spider.py # 主程序入口 ├── user_agents.py # 存放UA池(可选) └── data/ # 存放最终产出的CSV文件
6️⃣ 核心实现:请求层(Fetcher)
网络请求是爬虫的“先头部队”,这部分代码必须够稳!
为了防止被防爬机制误杀,我加入了随机User-Agent,并设置了timeout防止程序假死。踩过坑的都知道,不加重试机制的请求都是耍流氓,这里演示一个简单的退避重试思路。
importrequestsimporttimeimportrandom# 简易版 UA 池USER_AGENTS=["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36","Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/115.0"]deffetch_page(url,retries=3):headers={"User-Agent":random.choice(USER_AGENTS),"Referer":"http://quotes.toscrape.com/"}forattemptinrange(retries):try:# 必须设置 timeout,5秒连不上直接进异常response=requests.get(url,headers=headers,timeout=5)response.raise_for_status()# 状态码不是200就抛异常returnresponse.textexceptrequests.exceptions.RequestExceptionase:print(f"请求失败 [{url}], 错误:{e}. 正在进行第{attempt+1}次重试...")time.sleep(2**attempt)# 指数退避,越重试等越久print(f"放弃抓取该页面:{url}")returnNone7️⃣ 核心实现:解析层(Parser)
拿到 HTML 源码后,轮到BeautifulSoup4上场了。这其实是一个找规律的游戏。
我们打开网页的开发者工具(F12),发现每一条名言都包裹在一个<div class="quote">里。
容错是解析的灵魂:页面结构随时可能微调,如果某个节点没找到,不要让整个程序崩溃,给个默认值即可。
frombs4importBeautifulSoupdefparse_page(html):ifnothtml:return[],Nonesoup=BeautifulSoup(html,'html.parser')quotes_data=[]# 提取当前页的所有名言块quote_blocks=soup.find_all('div',class_='quote')forblockinquote_blocks:# 容错处理:安全地抽取文本text_elem=block.find('span',class_='text')author_elem=block.find('small',class_='author')tags_elems=block.find_all('a',class_='tag')quote_text=text_elem.get_text(strip=True)iftext_elemelse"无名言"author=author_elem.get_text(strip=True)ifauthor_elemelse"佚名"tags=",".join([tag.get_text(strip=True)fortagintags_elems])iftags_elemselse"无标签"quotes_data.append({"Quote_Text":quote_text,"Author":author,"Tags":tags})# 获取下一页的链接逻辑next_li=soup.find('li',class_='next')next_url=Noneifnext_li:next_tag=next_li.find('a')ifnext_tagand'href'innext_tag.attrs:# 拼接完整的下一页URLnext_url="http://quotes.toscrape.com"+next_tag['href']returnquotes_data,next_url8️⃣ 数据存储与导出(Storage)
辛辛苦苦抓下来的数据,总得有个家。对于中小型爬虫,CSV 格式拥有极佳的便携性和兼容性。
去重策略在这里就不上布隆过滤器了,对于这种小站,咱们可以基于Quote_Text在内存里做一个简单的Set去重。
importcsvimportosdefsave_to_csv(data_list,filename="data/quotes_result.csv"):# 确保目录存在os.makedirs(os.path.dirname(filename),exist_ok=True)# 字段映射表定义fieldnames=["Quote_Text","Author","Tags"]file_exists=os.path.isfile(filename)# newline='' 防止Windows下产生空行,encoding='utf-8-sig'防止Excel打开乱码withopen(filename,mode='a',newline='',encoding='utf-8-sig')asf:writer=csv.DictWriter(f,fieldnames=fieldnames)ifnotfile_exists:writer.writeheader()# 首次写入表头forrowindata_list:writer.writerow(row)print(f"成功保存{len(data_list)}条数据到{filename}")9️⃣ 运行方式与结果展示(必写)
万事俱备,开始组装主引擎!
defmain():start_url="http://quotes.toscrape.com/page/1/"current_url=start_url total_scraped=0seen_quotes=set()# 简单内存去重集合print("🚀 爬虫启动,目标锁定:Quotes to Scrape")whilecurrent_url:print(f"正在抓取:{current_url}")html=fetch_page(current_url)data,next_url=parse_page(html)# 去重与清洗逻辑unique_data=[]foritemindata:ifitem['Quote_Text']notinseen_quotes:seen_quotes.add(item['Quote_Text'])unique_data.append(item)ifunique_data:save_to_csv(unique_data)total_scraped+=len(unique_data)current_url=next_url# 翻页跟进# 频率控制:做个文明的爬虫time.sleep(random.uniform(1.5,3.0))print(f"🎉 抓取任务圆满完成!共获取{total_scraped}条去重后的名言。")if__name__=="__main__":main()如何启动:
在终端中进入项目根目录,输入python spider.py。
最终输出的 CSV 示例结果(展示前3行):
| Quote_Text | Author | Tags |
|---|---|---|
| “The world as we have created it is a process of our thinking…” | Albert Einstein | change,deep-thoughts,thinking,world |
| “It is our choices, Harry, that show what we truly are…” | J.K. Rowling | abilities,choices |
| “There are only two ways to live your life. One is as though…” | Albert Einstein | inspirational,life,live,miracle,miracles |
🔟 常见问题与排错(强烈建议写)
在真实的爬虫战场上,往往是“写代码一小时,Debug一整天”。以下是几个高频天坑:
遭遇 403 Forbidden 怎么办?
- 绝大部分是因为你的 Headers 太“裸露”了。检查 UA 是否带上,偶尔还要补齐
Accept-Language甚至特定网站需要的Cookie。实在不行,就得上代理池(Proxy Pool)换 IP 跑了。
- 绝大部分是因为你的 Headers 太“裸露”了。检查 UA 是否带上,偶尔还要补齐
状态码 200,但 HTML 抓下来是个“空壳”?
- 恭喜你遇到了动态渲染网站(Vue/React)。这时候 Requests 就歇菜了,你需要去 Network 面板抓
XHR/Fetch真实的数据接口,或者换用Playwright这种浏览器自动化工具直接渲染页面再抓。
- 恭喜你遇到了动态渲染网站(Vue/React)。这时候 Requests 就歇菜了,你需要去 Network 面板抓
解析频繁报错
AttributeError: 'NoneType' object has no attribute 'get_text'?- 这意味着你的选择器(CSS/XPath)失效了,目标节点结构不稳定或者不存在。务必养成使用
if element is not None:的防御性编程习惯。
- 这意味着你的选择器(CSS/XPath)失效了,目标节点结构不稳定或者不存在。务必养成使用
数据落盘乱码?
- 记住,写入 CSV 加上
encoding='utf-8-sig',在 Windows 环境里直接用 Excel 打开绝对治好你的乱码焦虑症。
- 记住,写入 CSV 加上
1️⃣1️⃣ 进阶优化(加分项)
当你把上面的流程跑通,你就已经及格了。想拿满分?试试这些思路:
- 火力全开(并发):引入
concurrent.futures.ThreadPoolExecutor或者直接用asyncio + aiohttp,速度能飙升十几倍。不过切记,并发越高,离“封IP”越近。 - 断点续跑机制:如果抓了 10 万条突然断网了怎么办?把抓取过的 URL 存入 Redis 的 Set 里做持久化已抓集合,下次启动只抓未抓过的。
- 脱离脚本,走向工程:尝试把这套逻辑迁移到Scrapy框架里,体验一下工业级爬虫流水线的美感。
1️⃣2️⃣ 总结与延伸阅读
呼~长舒一口气。在这篇文章中,我们从最基础的 HTTP 请求起手,到精细的 DOM 节点解析,再到结构化的数据落盘,完整地构建了一个“小而美”的静态页面爬虫引擎。
如果你觉得意犹未尽,下一步可以做什么?
我强烈建议你去了解一下Playwright,掌握了它,你就掌握了对付所有复杂动态网页的终极武器;如果你对大规模分布式爬虫感兴趣,Scrapy-Redis绝对是你的必修课。
🌟 文末
好啦~以上就是本期的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持!❤️🔥
✅ 专栏持续更新中|建议收藏 + 订阅
墙裂推荐订阅专栏 👉 《Python爬虫实战》,本专栏秉承着以“入门 → 进阶 → 工程化 → 项目落地”的路线持续更新,争取让每一期内容都做到:
✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)
📣想系统提升的小伙伴:强烈建议先订阅专栏 《Python爬虫实战》,再按目录大纲顺序学习,效率十倍上升~
✅ 互动征集
想让我把【某站点/某反爬/某验证码/某分布式方案】等写成某期实战?
评论区留言告诉我你的需求,我会优先安排实现(更新)哒~
⭐️ 若喜欢我,就请关注我叭~(更新不迷路)
⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)
⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)
✅ 免责声明
本文爬虫思路、相关技术和代码仅用于学习参考,对阅读本文后的进行爬虫行为的用户本作者不承担任何法律责任。
使用或者参考本项目即表示您已阅读并同意以下条款:
- 合法使用: 不得将本项目用于任何违法、违规或侵犯他人权益的行为,包括但不限于网络攻击、诈骗、绕过身份验证、未经授权的数据抓取等。
- 风险自负: 任何因使用本项目而产生的法律责任、技术风险或经济损失,由使用者自行承担,项目作者不承担任何形式的责任。
- 禁止滥用: 不得将本项目用于违法牟利、黑产活动或其他不当商业用途。
- 使用或者参考本项目即视为同意上述条款,即 “谁使用,谁负责” 。如不同意,请立即停止使用并删除本项目。!!!