【爬虫入门第14讲】Scrapy 框架深度解析(五)综合实战
Scrapy综合实践:从爬虫脚本到数据入库的完整指南
一、前言
Scrapy是一个功能强大的异步爬虫框架,广泛应用于数据采集、内容聚合和自动化测试。本文将通过一个完整的实战项目,带你掌握Scrapy的核心组件:Spider脚本、请求中间件、代理池、随机User-Agent、Pipeline数据清洗与MySQL入库、设置文件优化以及自定义扩展。我们将以爬取某新闻网站(示例为虚构的“TechNews”)的标题、摘要和发布时间为例,展示从请求到存储的全流程。
二、项目结构
scrapy_technews/
├── scrapy.cfg
├── technews/
│ ├── __init__.py
│ ├── items.py
│ ├── middlewares.py
│ ├── pipelines.py
│ ├── settings.py
│ ├── extensions.py
│ ├── spiders/
│ │ ├── __init__.py
│ │ └── news_spider.py
│ └── utils/
│ ├── proxy_pool.py
│ └── user_agent.py
└── requirements.txt
三、Item定义(items.py)
首先定义爬取的数据结构:
import scrapy
class TechnewsItem(scrapy.Item):
title = scrapy.Field() # 新闻标题
summary = scrapy.Field() # 摘要
publish_time = scrapy.Field() # 发布时间
url = scrapy.Field() # 原文链接
source = scrapy.Field() # 来源网站
crawl_time = scrapy.Field() # 爬取时间
四、Spider脚本(spiders/news_spider.py)
Spider是爬虫的核心,负责解析响应并提取数据。我们使用CrawlSpider自动跟踪链接,同时结合自定义解析逻辑。
import scrapy
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from technews.items import TechnewsItem
from datetime import datetime
class NewsSpider(CrawlSpider):
name = 'technews'
allowed_domains = ['technews.example.com']
start_urls = ['https://technews.example.com/latest']
# 定义爬取规则:只提取新闻详情页链接,并限制深度
rules = (
Rule(LinkExtractor(allow=r'/article/\d+'), callback='parse_article', follow=False),
Rule(LinkExtractor(allow=r'/page/\d+'), follow=True), # 翻页
)
def parse_article(self, response):
item = TechnewsItem()
item['title'] = response.css('h1.article-title::text').get(default='').strip()
item['summary'] = response.css('p.article-summary::text').get(default='').strip()
item['publish_time'] = response.css('time.article-date::attr(datetime)').get()
item['url'] = response.url
item['source'] = 'TechNews'
item['crawl_time'] = datetime.now().isoformat()
yield item
关键点:
- 使用
CrawlSpider自动处理分页和详情页链接。 Rule中的callback指定解析函数,follow控制是否继续跟踪该链接。- 通过CSS选择器提取数据,并做空值处理。
五、请求中间件(middlewares.py)
请求中间件在请求发送前或响应返回后执行自定义逻辑。我们实现两个中间件:随机User-Agent和代理添加。
5.1 随机User-Agent中间件
import random
from scrapy import signals
from technews.utils.user_agent import USER_AGENTS
class RandomUserAgentMiddleware:
"""随机更换User-Agent"""
def process_request(self, request, spider):
ua = random.choice(USER_AGENTS)
request.headers['User-Agent'] = ua
spider.logger.debug(f'User-Agent: {ua}')
utils/user_agent.py中维护一个UA列表:
USER_AGENTS = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
# 更多UA...
]
5.2 代理添加中间件
from technews.utils.proxy_pool import get_proxy
class ProxyMiddleware:
"""为每个请求添加代理IP"""
def process_request(self, request, spider):
proxy = get_proxy() # 从代理池获取一个可用代理
if proxy:
request.meta['proxy'] = proxy
spider.logger.debug(f'Using proxy: {proxy}')
utils/proxy_pool.py模拟代理池(实际可对接付费API或自建池):
import random
PROXY_LIST = [
'http://123.45.67.89:8080',
'http://98.76.54.32:3128',
# 实际应从数据库或API动态获取
]
def get_proxy():
return random.choice(PROXY_LIST) if PROXY_LIST else None
注意:生产环境建议使用scrapy-proxies或scrapy-rotating-proxies等成熟库,并处理代理失效重试。
六、Pipeline数据清洗与MySQL入库(pipelines.py)
Pipeline负责处理Spider产出的Item,可进行数据清洗、去重、存储等。我们实现两个Pipeline:一个用于清洗数据,一个用于写入MySQL。
6.1 数据清洗Pipeline
import re
from datetime import datetime
class DataCleaningPipeline:
"""清洗数据:去除空白、格式化时间"""
def process_item(self, item, spider):
# 清洗标题
if item.get('title'):
item['title'] = re.sub(r'\s+', ' ', item['title']).strip()
# 清洗摘要
if item.get('summary'):
item['summary'] = re.sub(r'\s+', ' ', item['summary']).strip()
# 格式化时间(假设原始格式为ISO 8601)
if item.get('publish_time'):
try:
dt = datetime.fromisoformat(item['publish_time'])
item['publish_time'] = dt.strftime('%Y-%m-%d %H:%M:%S')
except ValueError:
item['publish_time'] = None
return item
6.2 MySQL入库Pipeline
import pymysql
from twisted.enterprise import adbapi
class MySQLPipeline:
"""异步写入MySQL"""
def open_spider(self, spider):
db_params = {
'host': spider.settings.get('MYSQL_HOST', 'localhost'),
'port': spider.settings.get('MYSQL_PORT', 3306),
'user': spider.settings.get('MYSQL_USER', 'root'),
'password': spider.settings.get('MYSQL_PASSWORD', ''),
'database': spider.settings.get('MYSQL_DATABASE', 'scrapy_technews'),
'charset': 'utf8mb4',
}
self.dbpool = adbapi.ConnectionPool('pymysql', **db_params)
def close_spider(self, spider):
self.dbpool.close()
def process_item(self, item, spider):
query = self.dbpool.runInteraction(self._insert_item, item)
query.addErrback(self._handle_error, item, spider)
return item
def _insert_item(self, cursor, item):
sql = """INSERT INTO news (title, summary, publish_time, url, source, crawl_time)
VALUES (%s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE title=VALUES(title)"""
cursor.execute(sql, (
item.get('title'),
item.get('summary'),
item.get('publish_time'),
item.get('url'),
item.get('source'),
item.get('crawl_time')
))
def _handle_error(self, failure, item, spider):
spider.logger.error(f'Insert failed: {failure}, item: {item}')
说明:
- 使用
adbapi实现异步数据库操作,避免阻塞事件循环。 ON DUPLICATE KEY UPDATE实现去重(假设url字段设为唯一索引)。- 数据库表结构需提前创建:
CREATE TABLE news (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(500),
summary TEXT,
publish_time DATETIME,
url VARCHAR(1000) UNIQUE,
source VARCHAR(100),
crawl_time DATETIME
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
七、设置文件(settings.py)
设置文件集中管理爬虫配置,包括中间件、Pipeline、并发数、下载延迟等。
# -*- coding: utf-8 -*-
BOT_NAME = 'technews'
SPIDER_MODULES = ['technews.spiders']
NEWSPIDER_MODULE = 'technews.spiders'
# 遵守robots.txt(生产环境可设为False)
ROBOTSTXT_OBEY = True
# 并发请求数
CONCURRENT_REQUESTS = 16
# 下载延迟(秒)
DOWNLOAD_DELAY = 1.0
# 启用中间件
DOWNLOADER_MIDDLEWARES = {
'technews.middlewares.RandomUserAgentMiddleware': 400,
'technews.middlewares.ProxyMiddleware': 500,
# Scrapy内置的RetryMiddleware等保持默认
}
# 启用Pipeline
ITEM_PIPELINES = {
'technews.pipelines.DataCleaningPipeline': 200,
'technews.pipelines.MySQLPipeline': 300,
}
# MySQL配置
MYSQL_HOST = 'localhost'
MYSQL_PORT = 3306
MYSQL_USER = 'root'
MYSQL_PASSWORD = 'your_password'
MYSQL_DATABASE = 'scrapy_technews'
# 扩展配置(见下一节)
EXTENSIONS = {
'technews.extensions.StatsExtension': 500,
}
# 日志级别
LOG_LEVEL = 'INFO'
# 自动限速(可选)
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 1.0
AUTOTHROTTLE_MAX_DELAY = 10.0
配置要点:
- 中间件优先级数字越小越早执行,
RandomUserAgentMiddleware设为400,ProxyMiddleware设为500,确保UA先设置,代理后设置。 - Pipeline优先级数字越小越先处理,清洗Pipeline(200)先于入库Pipeline(300)。
- 开启
AUTOTHROTTLE可动态调整请求速度,降低被封风险。
八、扩展(extensions.py)
扩展可以在爬虫生命周期中执行自定义逻辑,例如统计爬取数量、发送通知、记录性能指标等。下面实现一个简单的统计扩展,在爬虫结束时打印爬取总数。
from scrapy import signals
from scrapy.exceptions import NotConfigured
class StatsExtension:
"""统计爬取数量并输出"""
def __init__(self, stats):
self.stats = stats
@classmethod
def from_crawler(cls, crawler):
if not crawler.settings.getbool('STATS_EXTENSION_ENABLED', True):
raise NotConfigured
ext = cls(crawler.stats)
crawler.signals.connect(ext.spider_closed, signal=signals.spider_closed)
return ext
def spider_closed(self, spider):
item_count = self.stats.get_value('item_scraped_count', 0)
request_count = self.stats.get_value('downloader/request_count', 0)
spider.logger.info(f'爬虫结束:共爬取 {item_count} 条数据,发送 {request_count} 个请求')
# 可在此处发送邮件或写入日志文件
在settings.py中启用扩展(已在上节配置),并添加开关:
STATS_EXTENSION_ENABLED = True
九、运行与测试
- 安装依赖:
pip install scrapy pymysql twisted
-
创建数据库和表(参考第六节SQL)。
-
运行爬虫:
cd scrapy_technews
scrapy crawl technews -o output.json # 同时输出JSON用于调试
- 观察日志,检查MySQL中是否有数据。
十、优化与注意事项
- 代理池维护:生产环境应使用动态代理池,定期检测代理可用性,剔除失效IP。
- User-Agent轮换:可结合
scrapy-fake-useragent库,自动生成真实UA。 - 请求重试:在
settings.py中配置RETRY_TIMES和RETRY_HTTP_CODES,配合代理中间件实现失败重试。 - 数据去重:除了数据库唯一索引,可在Pipeline中使用
scrapy.dupefilters.RFPDupeFilter基于请求指纹去重。 - 分布式扩展:结合
scrapy-redis实现分布式爬取,共享请求队列和去重集合。 - 异常处理:在中间件和Pipeline中捕获所有异常,避免爬虫崩溃。
- 日志管理:使用
LOG_FILE将日志写入文件,便于排查问题。
十一、总结
本文从零构建了一个完整的Scrapy爬虫项目,涵盖了Spider、中间件、Pipeline、设置和扩展等核心组件。通过随机UA和代理池有效规避反爬,通过Pipeline实现数据清洗和MySQL异步入库,通过扩展实现运行统计。这套架构可灵活扩展,适用于大多数中小型爬虫需求。希望你能举一反三,根据实际业务调整各模块,打造属于自己的高效爬虫系统。
更多推荐
所有评论(0)