Python爬虫实战:手把手教你如何从零构建高可用静态数据采集流水线!
㊗️本期内容已收录至专栏《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加上BeautifulSoup,把一个在线书店的所有图书数据扒个底朝天,最后稳稳当当地存进本地的 SQLite 数据库和 CSV 文件里。
读完这篇你将获得什么?
- 一套直接能跑的、具备企业级容错思维的爬虫脚手架代码。
- 彻底搞懂“请求 -> 解析 -> 清洗 -> 存储”的闭环逻辑。
- 掌握应对网络波动、字段缺失等恶心问题的“防身术”。
1️⃣ 摘要(Abstract)
本文以静态网站数据采集为切入点,基于 Python3 +requests+BeautifulSoup4栈,构建了一个轻量级但高可用的爬虫项目。最终产出包含书名、价格、评分、库存状态等核心维度的结构化数据(CSV/SQLite 双驱存储)。
核心收获:
- 掌握构建带重试机制的健壮 Fetcher 层。
- 学会优雅地处理数据流转与去重策略。
- 获得向高并发(Asyncio)演进的架构视野。
2️⃣ 背景与需求(Why)
为什么要干这个?
在当今这个数据为王的时代,无论是做行业竞品分析、训练个人大模型,还是简单地想做个信息聚合网站,数据都是原油。咱们这次实战,旨在为你打造一把顺手的“抽油泵”。
目标靶场:http://books.toscrape.com/(官方测试靶场,无反爬,适合练手与架构搭建)
目标字段清单(Data Schema):
book_id:图书唯一标识(基于 URL Hash)title:书名全称price:价格(需清洗掉货币符号)rating:星级评分(需转为数字 1-5)stock_status:库存状态(布尔值或具体数量)detail_url:详情页链接
3️⃣ 合规与注意事项(必看!保命指南 ⚠️)
写爬虫不讲武德,迟早进去喝茶。咱们作为专业选手,规矩必须懂:
- 遵守
robots.txt:动手前先看网站根目录/robots.txt,人家不让爬的绝对不碰。 - 克制你的并发:别上来就开 1000 个线程把人家服务器打挂了,那是 DDoS,不是爬虫。本文代码默认加入
time.sleep()频率控制。 - 不越界:绝不采集涉及个人隐私的敏感信息(如手机号、身份证),绝不破坏性绕过付费墙或登录限制。咱们只搬运公开的公共数据。技术中立,向善而行。
4️⃣ 技术选型与整体流程(What/How)
本次靶场是一个纯静态服务端渲染(SSR)的网站,不需要执行 JS 代码。
- 技术栈选型:抛弃了笨重的 Selenium/Playwright,也没用杀鸡用牛刀的 Scrapy。咱们用最原生的
requests做请求,用lxml驱动的BeautifulSoup做解析。轻量、透明、好控制。 - 整体流水线(Pipeline):
URL队列池➡️采集层 (Fetcher: 负责网络请求/重试/伪装)➡️解析层 (Parser: XPath/CSS 抽提数据)➡️清洗层 (Cleaner: 脏数据规整)➡️存储层 (Storage: 去重并落盘)
5️⃣ 环境准备与依赖安装(可复现)
老规矩,先搞定环境。建议使用 Python 3.9+。
# 创建虚拟环境(好习惯)python-mvenv venvsourcevenv/bin/activate# Mac/Linux# venv\Scripts\activate # Windows# 安装核心依赖pipinstallrequests beautifulsoup4 lxml tenacity pandas推荐项目结构:
book_scraper/ │ ├── main.py # 启动入口 ├── fetcher.py # 网络请求层 ├── parser.py # 数据解析层 ├── storage.py # 存储与去重层 ├── settings.py # 配置文件(UA池、延迟配置) ├── logs/ # 运行日志 └── data/ # 结果输出(CSV/DB)6️⃣ 核心实现:请求层(Fetcher)
这一层是爬虫的装甲车,必须足够皮实。我们要解决网络抖动、超时、基本的伪装。我引入了牛逼的tenacity库来做异常重试,这比你自己写while True优雅一万倍。
# fetcher.pyimportrequestsfromrequests.adaptersimportHTTPAdapterfromurllib3.util.retryimportRetryfromtenacityimportretry,stop_after_attempt,wait_exponentialimportrandom# 简易 UA 池USER_AGENTS=["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.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"]classRobustFetcher:def__init__(self):self.session=requests.Session()# 底层 TCP 级别的重试retry_strategy=Retry(total=3,backoff_factor=1,status_forcelist=[429,500,502,503,504],)adapter=HTTPAdapter(max_retries=retry_strategy)self.session.mount("http://",adapter)self.session.mount("https://",adapter)@retry(stop=stop_after_attempt(3),wait=wait_exponential(multiplier=1,min=2,max=10))defget_html(self,url):""" 获取 HTML 内容,包含应用层重试机制 (Tenacity) """headers={"User-Agent":random.choice(USER_AGENTS),"Accept-Language":"en-US,en;q=0.9","Referer":"http://books.toscrape.com/"}try:# timeout=(连接超时, 读取超时)response=self.session.get(url,headers=headers,timeout=(3.05,10))response.raise_for_status()# 抛出非 200 异常response.encoding='utf-8'# 防止乱码returnresponse.textexceptrequests.exceptions.RequestExceptionase:print(f"[Fetcher Error] 请求失败:{url}-{str(e)}")raise# 抛出异常触发 @retry💡 细节解析:咱们不仅用了requests.Session维持连接池加速,还用了urllib3的底层的 Retry 和tenacity的应用层退避重试,双重保险!
7️⃣ 核心实现:解析层(Parser)
抓到了 HTML,接下来就是庖丁解牛。这里采用CSS 选择器为主,它比 XPath 写起来更清爽。
# parser.pyfrombs4importBeautifulSoupfromurllib.parseimporturljoinimportreclassBookParser:def__init__(self,base_url="http://books.toscrape.com/"):self.base_url=base_url# 建立星级映射字典self.rating_map={"One":1,"Two":2,"Three":3,"Four":4,"Five":5}defparse_list_page(self,html,current_url):"""解析列表页,提取详情页 URL 和下一页 URL"""soup=BeautifulSoup(html,'lxml')detail_urls=[]# 1. 抽详情链接forarticleinsoup.select('article.product_pod'):a_tag=article.select_one('h3 a')ifa_tagand'href'ina_tag.attrs:# 拼接绝对路径full_url=urljoin(current_url,a_tag['href'])detail_urls.append(full_url)# 2. 抽下一页链接next_btn=soup.select_one('li.next a')next_page_url=urljoin(current_url,next_btn['href'])ifnext_btnelseNonereturndetail_urls,next_page_urldefparse_detail_page(self,html,url):"""解析详情页,抽取核心字段,带容错处理"""soup=BeautifulSoup(html,'lxml')data={'detail_url':url}# 书名title_tag=soup.select_one('div.product_main h1')data['title']=title_tag.text.strip()iftitle_tagelse"Unknown"# 价格清洗 (去掉 £ 符号)price_tag=soup.select_one('div.product_main p.price_color')ifprice_tag:price_text=price_tag.text.replace('£','').strip()data['price']=float(price_text)ifprice_text.replace('.','',1).isdigit()else0.0else:data['price']=0.0# 评分rating_tag=soup.select_one('div.product_main p.star-rating')data['rating']=0ifrating_tag:classes=rating_tag.get('class',[])forcinclasses:ifcinself.rating_map:data['rating']=self.rating_map[c]break# 库存stock_tag=soup.select_one('div.product_main p.instock.availability')ifstock_tag:text=stock_tag.text.strip()# 用正则提取数字match=re.search(r'\d+',text)data['stock_status']=int(match.group())ifmatchelse0else:data['stock_status']=0returndata💡 灵魂拷问:缺失字段怎么办?看到上面大量的if xxx else了吗?这就是爬虫的日常。防御性编程,永远假设标签可能会消失!
8️⃣ 数据存储与导出(Storage)
不能光爬不存啊兄弟。我们要实现 URL 去重(避免重复爬取同一本书),并且把结果落盘到 CSV。
# storage.pyimportcsvimportosimporthashlibclassDataStorage:def__init__(self,output_dir="data"):self.output_dir=output_dirifnotos.path.exists(self.output_dir):os.makedirs(self.output_dir)self.csv_file=os.path.join(self.output_dir,"scraped_books_data.csv")self.seen_urls=set()# 简单的内存级去重,生产环境推荐 Redis# 初始化 CSV 表头self.fields=['book_id','title','price','rating','stock_status','detail_url']ifnotos.path.exists(self.csv_file):withopen(self.csv_file,'w',newline='',encoding='utf-8-sig')asf:writer=csv.DictWriter(f,fieldnames=self.fields)writer.writeheader()defgenerate_id(self,url):"""对 URL 进行 MD5 散列作为唯一主键"""returnhashlib.md5(url.encode('utf-8')).hexdigest()defis_duplicate(self,url):book_id=self.generate_id(url)ifbook_idinself.seen_urls:returnTrueself.seen_urls.add(book_id)returnFalsedefsave_data(self,data_dict):# 写入前二次校验去重ifself.is_duplicate(data_dict['detail_url']):returnFalsedata_dict['book_id']=self.generate_id(data_dict['detail_url'])# 追加写入withopen(self.csv_file,'a',newline='',encoding='utf-8-sig')asf:writer=csv.DictWriter(f,fieldnames=self.fields)writer.writerow(data_dict)returnTrue9️⃣ 运行方式与结果展示(起飞!🛫)
拼图完成,我们把它在main.py里组装起来!
# main.pyfromfetcherimportRobustFetcherfromparserimportBookParserfromstorageimportDataStorageimporttimeimportrandomdefmain():start_url="http://books.toscrape.com/catalogue/category/books_1/page-1.html"fetcher=RobustFetcher()parser=BookParser()storage=DataStorage()current_page=start_url page_count=1print("🚀 爬虫引擎启动...")whilecurrent_page:print(f"\n[INFO] 正在抓取第{page_count}页:{current_page}")html=fetcher.get_html(current_page)ifnothtml:print("[WARN] 页面抓取失败,跳过...")break# 1. 解析出详情页列表detail_urls,next_page=parser.parse_list_page(html,current_page)# 2. 遍历详情页forurlindetail_urls:# 去重拦截前置,节约网络请求ifstorage.generate_id(url)instorage.seen_urls:continueprint(f" -> 抓取详情:{url}")detail_html=fetcher.get_html(url)ifdetail_html:book_data=parser.parse_detail_page(detail_html,url)storage.save_data(book_data)# 基础礼貌:每次请求停顿 0.5-1.5 秒time.sleep(random.uniform(0.5,1.5))current_page=next_page page_count+=1# 为了测试,我们只跑前 2 页ifpage_count>2:print("🛑 达到测试页数限制,停止抓取。")breakprint("🎉 抓取任务圆满完成!数据已保存至 data/scraped_books_data.csv")if__name__=="__main__":main()如何启动?
在终端运行:python main.py
结果展示(示例 3 行):
| book_id | title | price | rating | stock_status | detail_url |
|---|---|---|---|---|---|
| a3b… | A Light in the Attic | 51.77 | 3 | 22 | http://books… |
| 5c6… | Tipping the Velvet | 53.74 | 1 | 20 | http://books… |
| 9d2… | Soumission | 50.10 | 1 | 20 | http://books… |
🔟 常见问题与排错(老鸟踩坑录 😭)
你以为跑通了就万事大吉了?Too young too simple!生产环境的破事儿多着呢:
无限报 403 Forbidden 怎么破?
- 诊断:对方 WAF(防火墙)识别出你是爬虫了。
- 解法:检查 headers 里有没有带
User-Agent;尝试加入更完整的头,如Accept,Sec-Ch-Ua;再不行,上代理池(Proxy Pool)。
HTML 抓下来全是 JS 代码,没有数据?
- 诊断:遇到动态渲染(SPA)的网站了(如 Vue/React 写的页面)。
- 解法:赶紧按 F12 打开浏览器 Network 面板,找
XHR/Fetch里的 API 接口!直接请求 JSON 接口比解析 HTML 爽一万倍。如果接口加密了,才考虑用 Playwright 挂浏览器硬跑。
解析报错
AttributeError: 'NoneType' object has no attribute 'text'- 诊断:你写的 CSS 选择器失效了,或者当前页面结构改变,提取到了空值。
- 解法:容错处理一定要做好(参考我写的
if title_tag else "Unknown")。平时多打 Log。
中文乱码?
response.encoding = response.apparent_encoding或者强制response.encoding = 'utf-8'。
1️⃣1️⃣ 进阶优化(秀操作环节 😎)
目前我们写的是单线程同步模型,爬完一页要好几分钟,太慢了!下一步优化的方向:
- 并发加速:把
requests换成aiohttp,配合asyncio协程,并发量轻松上百。(注意控制并发,别搞崩人家服务器) - 断点续跑:现在的去重是基于内存的
set(),重启就没了。进阶玩法是把 MD5 哈希存进 Redis 或 SQLite,每次启动先从数据库加载已抓取集合。 - 分布式:当你有一千万条数据要爬,单机扛不住时,可以拥抱 Scrapy-Redis 生态,让多台机器共享同一个 URL 队列。
1️⃣2️⃣ 总结与延伸阅读
呼~ 写到这里,键盘都快冒烟了!🔥 我们今天手搓了一个五脏俱全的 Python 爬虫,它有自动重试、有优雅的提取、还有完整的数据落盘机制。更重要的是,你学到了一种工程化的思维。
🌟 文末
好啦~以上就是本期的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持!❤️🔥
✅ 专栏持续更新中|建议收藏 + 订阅
墙裂推荐订阅专栏 👉 《Python爬虫实战》,本专栏秉承着以“入门 → 进阶 → 工程化 → 项目落地”的路线持续更新,争取让每一期内容都做到:
✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)
📣想系统提升的小伙伴:强烈建议先订阅专栏 《Python爬虫实战》,再按目录大纲顺序学习,效率十倍上升~
✅ 互动征集
想让我把【某站点/某反爬/某验证码/某分布式方案】等写成某期实战?
评论区留言告诉我你的需求,我会优先安排实现(更新)哒~
⭐️ 若喜欢我,就请关注我叭~(更新不迷路)
⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)
⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)
✅ 免责声明
本文爬虫思路、相关技术和代码仅用于学习参考,对阅读本文后的进行爬虫行为的用户本作者不承担任何法律责任。
使用或者参考本项目即表示您已阅读并同意以下条款:
- 合法使用: 不得将本项目用于任何违法、违规或侵犯他人权益的行为,包括但不限于网络攻击、诈骗、绕过身份验证、未经授权的数据抓取等。
- 风险自负: 任何因使用本项目而产生的法律责任、技术风险或经济损失,由使用者自行承担,项目作者不承担任何形式的责任。
- 禁止滥用: 不得将本项目用于违法牟利、黑产活动或其他不当商业用途。
- 使用或者参考本项目即视为同意上述条款,即 “谁使用,谁负责” 。如不同意,请立即停止使用并删除本项目。!!!
