基于Python的爬虫毕设实战:从单机脚本到可维护系统的架构演进
最近在帮学弟学妹们看爬虫相关的毕业设计,发现一个挺普遍的现象:很多项目一开始只是一个简单的脚本,运行几次能抓到数据就算完成了。但稍微深入一点,比如要抓几千个页面、应对网站反爬、或者需要长期稳定运行,原来的脚本就各种“翻车”——数据漏抓、重复抓、程序莫名崩溃、日志找不到…… 这其实反映了从“玩具脚本”到“可维护系统”的思维转变。今天,我就结合自己的实践经验,聊聊如何把一个基础的Python爬虫,一步步演进成一个结构清晰、稳定可靠、便于扩展的毕业设计系统。

1. 背景与痛点:为什么你的爬虫总是“跑崩”?
很多同学起步时,代码通常是下面这样的“一锅炖”风格:
import requests from bs4 import BeautifulSoup import csv url = ‘某个列表页’ response = requests.get(url) soup = BeautifulSoup(response.text, ‘html.parser’) for item in soup.select(‘.item’): # 解析数据 title = item.find(‘h2’).text # 可能嵌套再发请求 detail_url = item[‘href’] # 直接再请求,没有间隔 detail_resp = requests.get(detail_url) # 解析详情... # 直接写入CSV with open(‘data.csv’, ‘a’) as f: writer = csv.writer(f) writer.writerow([title, ...]) 这种写法在初期验证想法时很快捷,但作为毕设,暴露了诸多工程问题:
- 无状态管理:脚本中断后,无法知道哪些URL抓过,哪些没抓,重跑会导致大量重复或遗漏。
- 硬编码与低扩展性:URL、解析规则、存储路径都写在代码里。想换个网站或增加字段,就得大改代码。
- 忽视网络礼仪与反爬:没有设置请求间隔(
time.sleep是随机的)、固定的User-Agent,极易触发IP封锁。很多同学甚至不知道要查看和尊重robots.txt。 - 脆弱的异常处理:网络波动、页面结构变化、数据缺失等情况,往往导致整个脚本崩溃,没有重试或降级机制。
- 缺乏监控与日志:程序运行时在做什么?出了什么错?没有任何记录,调试全靠“猜”。
一个合格的毕设系统,应该能系统地解决这些问题。
2. 技术选型对比:Requests, Scrapy, 还是Playwright?
选对工具是成功的一半。下面是几种主流方案的简单对比:
- Requests + BeautifulSoup/ lxml (手动组合):
- 优点:学习曲线平缓,灵活度极高,适合小规模、页面结构简单的抓取,或作为学习HTTP协议的起点。
- 缺点:所有高级功能(并发、去重、管道)都需要自己从头实现,工程化成本高,代码容易变得冗长。
- 适用场景:目标明确、规模小(几百页面)、反爬措施弱的快速任务。
- Scrapy 框架:
- 优点:真正的工业级框架。内置了异步请求引擎、请求调度器、中间件、管道(Item Pipeline)等一套完整架构。开发模式固定(Spider, Item, Pipeline),易于写出结构良好的代码。扩展性极强,有丰富的中间件库应对反爬(如
scrapy-user-agents,scrapy-proxies)。 - 缺点:有一定学习成本,框架本身有一定“黑盒”性。对于需要执行JavaScript的页面,需结合
Splash或Scrapy-Playwright。 - 适用场景:绝大多数爬虫毕设的首选。尤其是需要抓取数万以上页面、需要良好架构和可维护性的项目。
- 优点:真正的工业级框架。内置了异步请求引擎、请求调度器、中间件、管道(Item Pipeline)等一套完整架构。开发模式固定(Spider, Item, Pipeline),易于写出结构良好的代码。扩展性极强,有丰富的中间件库应对反爬(如
- Playwright / Selenium (浏览器自动化):
- 优点:能完美模拟真实浏览器行为,处理任何动态渲染(JS加载)的页面。Playwright性能优于Selenium,且支持多浏览器。
- 缺点:资源消耗大(启动浏览器实例),速度远慢于直接HTTP请求。主要用于解决“动态内容”问题,而非作为主要爬取框架。
- 适用场景:目标网站核心数据由JavaScript异步加载,且无法通过分析网络请求获取。通常作为Scrapy或Requests方案的补充(
scrapy-playwright)。
毕设建议:直接使用Scrapy。它迫使你以结构化的方式思考问题,其项目模板天然符合“可维护系统”的要求。把学习Scrapy的时间投资进去,在答辩时展示一个规范的项目目录结构,本身就是亮点。
3. 核心实现:模块化设计与关键组件
我们以Scrapy项目为基础,探讨如何构建系统。核心思想是解耦:下载、解析、存储各司其职。
3.1 项目结构 一个标准的Scrapy项目结构如下,这本身就是模块化的体现:
my_crawler/ ├── scrapy.cfg └── my_crawler/ ├── __init__.py ├── items.py # 定义数据结构 ├── middlewares.py # 下载器、爬虫中间件(代理、UA轮换在此) ├── pipelines.py # 数据处理管道(清洗、验证、存储) ├── settings.py # 全局配置(并发、延迟、中间件开关) └── spiders/ # 爬虫逻辑 ├── __init__.py └── example_spider.py 3.2 使用Redis实现去重与任务队列 Scrapy内置的去重和调度是基于内存的,重启后会丢失。为了持久化和支持分布式(加分项!),我们引入Redis。
- URL去重:利用Redis的
Set数据结构。在发送请求前,检查URL的指纹(如MD5)是否已在集合中。 - 请求调度队列:利用Redis的
List或Sorted Set作为优先级队列。Scrapy的Scheduler可以从Redis中拉取请求,而不是内存。
这需要用到scrapy-redis这个优秀的扩展库。配置后,你的爬虫就具备了“断点续爬”和“多机协同”的能力。
在settings.py中配置:
# 使用scrapy_redis的调度器 SCHEDULER = “scrapy_redis.scheduler.Scheduler” # 使用scrapy_redis的去重组件 DUPEFILTER_CLASS = “scrapy_redis.dupefilter.RFPDupeFilter” # 允许暂停/恢复,保持爬虫状态 SCHEDULER_PERSIST = True # Redis连接信息 REDIS_HOST = ‘localhost’ REDIS_PORT = 6379 在你的爬虫中,起始URL不再用start_urls列表,而是从Redis队列读取,或者用redis-cli手动添加种子URL,非常灵活。
3.3 编写健壮的Spider与Pipeline Spider负责生成请求和解析响应,要保持简洁。
import scrapy from my_crawler.items import ArticleItem # 从items.py导入定义好的数据结构 class NewsSpider(scrapy.Spider): name = ‘news’ allowed_domains = [‘news.example.com’] # start_urls 可被redis替代 def parse(self, response): # 解析列表页,提取文章链接 for article_url in response.css(‘.article-list a::attr(href)’).getall(): yield scrapy.Request(url=response.urljoin(article_url), callback=self.parse_article, errback=self.errback_handle) # 指定错误回调 # 翻页 next_page = response.css(‘.next-page::attr(href)’).get() if next_page: yield scrapy.Request(url=response.urljoin(next_page), callback=self.parse) def parse_article(self, response): # 使用Item封装数据,确保结构一致 item = ArticleItem() item[‘title’] = response.css(‘h1::text’).get(‘’).strip() item[‘content’] = ‘ ‘.join(response.css(‘.content p::text’).getall()).strip() item[‘url’] = response.url item[‘crawl_time’] = datetime.now().isoformat() # 记录抓取时间 yield item # 交给Pipeline处理 def errback_handle(self, failure): # 记录请求失败的URL和原因,可用于后续重试 self.logger.error(f’Request failed: {failure.request.url}, {failure.value}‘) Pipeline负责后续处理,一个项目可以配置多个Pipeline,按顺序执行。
import pymongo import logging class MongoPipeline: “””将数据存入MongoDB的Pipeline“”” def __init__(self, mongo_uri, mongo_db): self.mongo_uri = mongo_uri self.mongo_db = mongo_db @classmethod def from_crawler(cls, crawler): # 从settings读取配置的经典方法 return cls( mongo_uri=crawler.settings.get(‘MONGO_URI’), mongo_db=crawler.settings.get(‘MONGO_DATABASE’, ‘items’) ) def open_spider(self, spider): # 爬虫启动时连接数据库 self.client = pymongo.MongoClient(self.mongo_uri) self.db = self.client[self.mongo_db] def close_spider(self, spider): # 爬虫关闭时断开连接 self.client.close() def process_item(self, item, spider): # 核心处理逻辑:去重、清洗、存储 # 例如,根据url去重 if self.db[spider.name].find_one({‘url’: item[‘url’]}): spider.logger.warning(f’Duplicate item found: {item[“url”]}‘) raise DropItem(f’Duplicate item: {item[“url”]}‘) # 数据清洗:例如,如果标题为空,则丢弃 if not item.get(‘title’): raise DropItem(“Item has no title”) # 存入数据库 self.db[spider.name].insert_one(dict(item)) spider.logger.info(f’Item saved to MongoDB: {item[“url”]}‘) return item 在settings.py中启用并设置Pipeline优先级。
4. 性能与安全:稳健爬取的关键
4.1 并发控制与请求延迟 在settings.py中调整,避免对目标服务器造成压力。
CONCURRENT_REQUESTS = 16 # 全局并发数 CONCURRENT_REQUESTS_PER_DOMAIN = 2 # 对单个域名的并发数,更友好 DOWNLOAD_DELAY = 1.0 # 两次请求间的间隔(秒) AUTOTHROTTLE_ENABLED = True # 启用自动限速,根据服务器响应动态调整,非常推荐 4.2 反爬应对策略
- User-Agent轮换:在
middlewares.py中编写下载器中间件,从一个预定义的列表中随机选择UA。 - IP代理池:集成开源代理池项目(如
proxy_pool),或在中间件中从付费代理API获取IP。同样在下载器中间件中为请求设置proxy。 - Cookies处理:对于需要登录的网站,使用
scrapy.Request的cookies参数或FormRequest。 - 动态渲染:对于JS加载的内容,使用
scrapy-playwright中间件。它为部分请求启用Playwright,其他请求仍用普通方式,兼顾效率与功能。
4.3 法律与合规边界这是毕设中必须严肃讨论的部分!
- 查看
robots.txt:使用Python的urllib.robotparser解析目标网站的robots.txt,并让你的爬虫遵守它。在答辩中展示这部分代码,体现你的合规意识。 - 数据使用目的:在项目文档中明确声明,数据仅用于学术研究和毕业设计演示,不会用于商业用途或侵犯个人隐私。
- 控制爬取速度:如前所述,使用延迟和并发控制,体现对网站资源的尊重。
- 关注网站条款:有些网站的
Terms of Service明确禁止爬虫。
5. 生产环境避坑指南
即使本地测试顺利,真正“跑起来”还可能遇到这些问题:
- 冷启动延迟与超时:数据库连接、代理池初始化可能较慢。确保在
open_spider方法中做好初始化,并设置合理的DOWNLOAD_TIMEOUT。 - 内存泄漏:长时间运行后内存增长。检查是否在Pipeline或中间件中创建了未释放的大对象(如数据库连接池)。确保使用
close_spider进行清理。对于自定义的中间件,注意request.meta中不要累积过大数据。 - 数据库写入瓶颈:当爬取速度很快时,逐条插入数据库可能成为瓶颈。可以在Pipeline中实现批量插入(如每100条插入一次),但要注意异常时的数据一致性。
- 日志管理:不要只打印到控制台。配置
settings.py中的LOG_LEVEL,LOG_FILE,将日志写入文件,便于后期排查问题。 - 异常恢复:除了
errback,充分利用Scrapy的重试中间件(RETRY_ENABLED,RETRY_TIMES)。对于因页面结构变化导致的解析失败,应在解析函数中使用更健壮的CSS选择器或try…except,并记录错误页面,而不是让整个爬虫停止。

6. 总结与展望:让你的毕设更进一步
通过以上步骤,我们完成了一个爬虫系统的架构演进:从脚本到模块化,再到引入外部组件(Redis)增强其状态管理和扩展性。这样的系统已经足够支撑一个优秀的本科毕业设计。
在论文和答辩中,你可以进一步展示思考的深度:
- 如何扩展为增量抓取? 思路:在Item中增加
update_time字段。爬虫不仅从种子开始,还可以定期抓取“最新”页面。解析时,与数据库中已有记录的update_time比较,只抓取和更新有变化的条目。这需要更精细的URL发现和对比逻辑。 - 如何将系统API化? 思路:使用
Flask或FastAPI包装你的爬虫。提供RESTful API,例如:POST /api/crawl接收一个种子URL和配置参数,启动一个爬虫任务;GET /api/results?task_id=xxx查询抓取结果。这涉及到任务队列(如Celery+RabbitMQ)的管理,展示了将离线系统升级为在线服务的能力。 - 数据可视化与分析:将抓取到的数据(如新闻情感、商品价格趋势)用
ECharts或Pyecharts进行可视化,作为毕设的“数据分析”章节,让项目更加丰满。
爬虫项目的价值不止于“抓到数据”,更在于如何可靠、高效、合规地管理和利用数据流。希望这篇笔记能帮你理清思路,打造一个既扎实又有亮点的毕业设计。动手搭起来吧,遇到具体问题再去深挖,这才是最好的学习路径。