🔎大家好,我是ZTLJQ,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流
📝个人主页-ZTLJQ的主页
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝📣系列果你对这个系列感兴趣的话
专栏 - Python从零到企业级应用:短时间成为市场抢手的程序员
✔说明⇢本人讲解主要包括Python爬虫、JS逆向、Python的企业级应用
如果你对这个系列感兴趣的话,可以关注订阅哟👋
从脚本到工程的飞跃
在数据驱动的时代,从海量的网页中高效、稳定地提取所需信息已成为一项核心技能。当您的爬虫需求从一个简单的几行脚本,演进到需要处理千万级URL、应对复杂的反爬策略、实现数据的实时清洗与入库时,一个成熟的爬虫框架便成为您不可或缺的工具。
它就像一座精心设计的工厂,您只需将原材料(URL)投入,就能高效地产出标准化的产品(结构化数据),而无需关心工厂内部复杂的运转机制。本篇博客将带您深入Scrapy等主流框架的殿堂,掌握从单兵作战到集团军协同的爬虫开发艺术。
第一部分:Scrapy - 爬虫界的"工业标准"
Scrapy (pip install scrapy) 是Python生态中首屈一指的爬虫框架。它以异步非阻塞的高性能著称,其"事件驱动"的架构使其能够以惊人的效率处理大量并发请求,是构建大型、复杂爬虫项目的首选。
1.1 核心架构:各司其职的协作系统
Scrapy的成功源于其精妙的组件化设计。整个数据流转过程由以下几个核心部件协同完成:
- 引擎 (Engine): 框架的大脑和中枢神经,负责在各组件间传递信号和数据,驱动整个系统有序运行。
- 调度器 (Scheduler) : 一个高效的请求队列,接收来自引擎的
Request对象并排队,待引擎请求时再将其返回。它是URL的"仓库管理员"。 - 下载器 (Downloader) : 一个高效的HTTP客户端,负责获取页面。它接收引擎传来的
Request,执行网络请求,然后将生成的Response对象交还给引擎。 - 蜘蛛 (Spiders) : 您编写的核心业务逻辑所在地。蜘蛛定义了如何爬取某个(或某些)网站,包括起始URL、如何跟进链接以及如何从页面中提取结构化数据(
Items)。 - 项目管道 (Item Pipelines) : 一条流水线,负责处理由蜘蛛提取出的
Item对象。在这里,您可以进行数据清洗、验证、去重,并将其持久化到数据库或文件中。 - 下载器中间件 (Downloader Middlewares): 一层可插拔的钩子系统,允许您处理请求(如添加代理、修改Headers)和响应(如处理重定向、模拟JavaScript渲染),是应对反爬虫的主力。
- 蜘蛛中间件 (Spider Middlewares) : 在引擎和蜘蛛之间添加自定义逻辑,如修改发送给蜘蛛的响应或从蜘蛛返回的
Item/Request。
1.2 实战演练:从零构建一个书籍信息爬虫
理论枯燥,实战方能出真知。让我们通过一个完整的项目来感受Scrapy的强大。
项目目标 :爬取 books.toscrape.com 网站,提取每本书的书名、价格、评分和库存信息。
第一步:初始化项目
在您的命令行中执行:
bash
scrapy startproject book_crawler
cd book_crawler
这将生成一个标准的项目骨架。
第二步:定义数据结构 (items.py)
在 book_crawler/book_crawler/ 目录下,编辑 items.py。这里我们定义了要抓取数据的"蓝图"。
python
import scrapy
class BookItem(scrapy.Item):
"""
定义书籍信息的数据结构
"""
name = scrapy.Field() # 书名
price = scrapy.Field() # 价格
stock = scrapy.Field() # 库存
rating = scrapy.Field() # 评分 (星级)
image_urls = scrapy.Field() # 封面图片链接 (列表)
images = scrapy.Field() # 下载后的图片信息 (由ImagesPipeline填充)
第三步:编写核心逻辑 (spiders/books_spider.py)
在 spiders 文件夹下创建 books_spider.py 文件。
python
import scrapy
from urllib.parse import urljoin
from book_crawler.book_crawler.items import BookItem
class BooksToScrapeSpider(scrapy.Spider):
name = 'books_to_scrape' # 蜘蛛的唯一标识,必须唯一
allowed_domains = ['books.toscrape.com'] # 限制爬取的域名范围
start_urls = ['http://books.toscrape.com/'] # 初始URL列表
def parse(self, response):
"""
解析首页,提取书籍列表页的链接
"""
# 使用CSS选择器提取所有书籍详情页的链接
book_detail_links = response.css('article.product_pod h3 a::attr(href)').getall()
for relative_link in book_detail_links:
# 将相对链接转为绝对链接,并发起新的请求
# callback=self.parse_book 指定了处理响应的函数
absolute_url = urljoin(response.url, relative_link)
yield response.follow(absolute_url, callback=self.parse_book)
# 提取下一页链接,实现翻页
next_page_relative_url = response.css('li.next a::attr(href)').get()
if next_page_relative_url:
next_page_absolute_url = urljoin(response.url, next_page_relative_url)
# 递归地请求下一页
yield response.follow(next_page_absolute_url, callback=self.parse)
def parse_book(self, response):
"""
解析书籍详情页,提取具体信息
"""
item = BookItem()
# 提取书名
item['name'] = response.css('div.product_main h1::text').get()
# 提取价格 (例如 £51.77),使用正则表达式提取数字部分
price_text = response.css('p.price_color::text').get()
if price_text:
item['price'] = price_text.replace('£', '') # 去掉英镑符号
# 提取库存 (例如 In stock (5 available))
stock_text = response.css('p.instock.availability::text').re_first(r'\((\d+) available\)')
item['stock'] = stock_text or 'Unknown' # 如果没找到,则设为'Unknown'
# 提取评分 (通过CSS类名,例如 'Star-Rating Three')
rating_class = response.css('p.star-rating::attr(class)').re_first(r'Star-Rating\s+(\w+)')
item['rating'] = rating_class.capitalize() if rating_class else 'None'
# 提取封面图片链接 (注意这里获取的是缩略图的src)
image_url = response.css('#product_gallery img::attr(src)').get()
if image_url:
# 同样需要转换为绝对URL
item['image_urls'] = [urljoin(response.url, image_url)]
yield item # 将提取好的item发送到Item Pipeline
第四步:配置数据处理管道 (pipelines.py)
编辑 book_crawler/book_crawler/pipelines.py。这里我们配置一个简单的JSON导出管道和一个图片下载管道。
python
import json
import os
from itemadapter import ItemAdapter
from scrapy.pipelines.images import ImagesPipeline
import scrapy
class JsonWriterPipeline:
"""
将Item写入JSON Lines文件
"""
def open_spider(self, spider):import json
import os
from itemadapter import ItemAdapter
from scrapy.pipelines.images import ImagesPipeline
import scrapy
class JsonWriterPipeline:
"""
将Item写入JSON Lines文件
"""
def open_spider(self, spider):
self.file = open('books.jsonl', 'w', encoding='utf-8')
def close_spider(self, spider):
self.file.close()
def process_item(self, item, spider):
adapter = ItemAdapter(item)
line = json.dumps(adapter.asdict(), ensure_ascii=False) + "\n"
self.file.write(line)
return item
class MyImagesPipeline(ImagesPipeline):
"""
自定义图片下载管道,重写file_path方法来控制文件保存路径和名称
"""
def file_path(self, request, response=None, info=None, *, item=None):
# 从item中获取书名,用作图片文件名
book_name = item.get('name', 'default')
# 清理文件名中的非法字符
clean_name = "".join(c for c in book_name if c.isalnum() or c in (' ', '-', '_')).rstrip()
return f'{clean_name}.jpg'
def get_media_requests(self, item, info):
# 为每个图片URL生成一个Request
if item.get('image_urls'):
for image_url in item['image_urls']:
yield scrapy.Request(image_url, meta={'item': item})
self.file = open('books.jsonl', 'w', encoding='utf-8')
def close_spider(self, spider):
self.file.close()
def process_item(self, item, spider):
adapter = ItemAdapter(item)
line = json.dumps(adapter.asdict(), ensure_ascii=False) + "\n"
self.file.write(line)
return item
class MyImagesPipeline(ImagesPipeline):
"""
自定义图片下载管道,重写file_path方法来控制文件保存路径和名称
"""
def file_path(self, request, response=None, info=None, *, item=None):
# 从item中获取书名,用作图片文件名
book_name = item.get('name', 'default')
# 清理文件名中的非法字符
clean_name = "".join(c for c in book_name if c.isalnum() or c in (' ', '-', '_')).rstrip()
return f'{clean_name}.jpg'
def get_media_requests(self, item, info):
# 为每个图片URL生成一个Request
if item.get('image_urls'):
for image_url in item['image_urls']:
yield scrapy.Request(image_url, meta={'item': item})
第五步:激活管道 (settings.py)
编辑 book_crawler/book_crawler/settings.py,启用我们定义的管道。
python
# 项目管道配置
ITEM_PIPELINES = {
'book_crawler.book_crawler.pipelines.MyImagesPipeline': 1, # 数字越小,优先级越高
'book_crawler.book_crawler.pipelines.JsonWriterPipeline': 2,
}
# 配置图片存储路径
IMAGES_STORE = 'images' # 图片将被下载到项目根目录下的 'images' 文件夹
# 设置下载延迟,遵守robots.txt,尊重网站
DOWNLOAD_DELAY = 1
RANDOMIZE_DOWNLOAD_DELAY = 0.5
# 设置并发请求数
CONCURRENT_REQUESTS = 16
CONCURRENT_REQUESTS_PER_DOMAIN = 8
# 设置User-Agent
USER_AGENT = 'book_crawler (+http://www.yourdomain.com)'
第六步:运行爬虫
一切就绪,在项目根目录下执行命令:
bash
scrapy crawl books_to_scrape
Scrapy会自动启动,按照您的定义开始抓取。您会看到日志滚动,最终在项目目录下生成 books.jsonl 文件和 images 文件夹,里面包含了所有书籍的数据和封面图片。
代码解析:
response.follow(): 这是Scrapy提供的便捷方法,它会自动处理相对URL到绝对URL的转换,并生成Request对象。css()和re_first():css()用于提取文本,re_first()则直接对提取结果应用正则表达式,非常适合提取数字或特定格式的数据。yield: 在Scrapy中,yield不仅用于生成Item,也用于生成Request。引擎会接收这些对象并进行下一步处理。ImagesPipeline: 这是Scrapy内置的强大功能,只需指定image_urls字段,它就能自动下载图片并处理。
第二部分:超越Scrapy的选项
虽然Scrapy功能强大,但在某些特定场景下,其他工具可能更为合适。
Scrapy-Redis: 如前文所述,它是Scrapy的增强版,通过将调度器和去重器替换为Redis后端,实现了多台机器的分布式爬取,是处理海量任务的利器。Celery+requests/httpx: 这种组合更适合构建高度定制化的分布式任务系统。您可以将URL抓取定义为一个Celery任务,然后分发到多个工作节点并发执行,灵活性极高,但需要自行处理调度和状态管理。AioScrapy: 一个对Scrapy进行异步重构的实验性项目,旨在解决原生Scrapy在CPU密集型解析任务上的瓶颈。
第三部分:反爬虫与最佳实践
面对日益严峻的反爬形势,Scrapy的中间件机制是您的"防弹衣"。
- User-Agent轮换 : 使用
scrapy-fake-useragent等库,让每个请求看起来都来自不同的浏览器。 - IP代理池 : 集成
scrapy-proxy-pool,通过代理IP池隐藏真实IP。 - 请求频率控制 : 通过
DOWNLOAD_DELAY和CONCURRENT_REQUESTS等设置,模拟人类访问节奏。
最佳实践:
- 遵守规则 : 始终检查并遵守
robots.txt。 - 优雅降级 : 妥善处理
404 Not Found、500 Internal Server Error等异常响应。 - 数据验证: 在Pipeline中对数据进行二次校验,确保入库数据的准确性。
- 日志监控: 充分利用Scrapy的日志系统,记录关键信息,便于排查问题。
