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-proxiesscrapy-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

九、运行与测试

  1. 安装依赖:
pip install scrapy pymysql twisted
  1. 创建数据库和表(参考第六节SQL)。

  2. 运行爬虫:

cd scrapy_technews
scrapy crawl technews -o output.json  # 同时输出JSON用于调试
  1. 观察日志,检查MySQL中是否有数据。

十、优化与注意事项

  1. 代理池维护:生产环境应使用动态代理池,定期检测代理可用性,剔除失效IP。
  2. User-Agent轮换:可结合scrapy-fake-useragent库,自动生成真实UA。
  3. 请求重试:在settings.py中配置RETRY_TIMESRETRY_HTTP_CODES,配合代理中间件实现失败重试。
  4. 数据去重:除了数据库唯一索引,可在Pipeline中使用scrapy.dupefilters.RFPDupeFilter基于请求指纹去重。
  5. 分布式扩展:结合scrapy-redis实现分布式爬取,共享请求队列和去重集合。
  6. 异常处理:在中间件和Pipeline中捕获所有异常,避免爬虫崩溃。
  7. 日志管理:使用LOG_FILE将日志写入文件,便于排查问题。

十一、总结

本文从零构建了一个完整的Scrapy爬虫项目,涵盖了Spider、中间件、Pipeline、设置和扩展等核心组件。通过随机UA和代理池有效规避反爬,通过Pipeline实现数据清洗和MySQL异步入库,通过扩展实现运行统计。这套架构可灵活扩展,适用于大多数中小型爬虫需求。希望你能举一反三,根据实际业务调整各模块,打造属于自己的高效爬虫系统。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐