引言

Scrapy是一个强大的Python开源网络爬虫框架,专为高效、大规模的网页抓取而设计。它由Python编写,基于Twisted异步网络库,能够处理数百万个网页的抓取任务。Scrapy提供了完整的爬虫生命周期管理,包括请求调度、数据提取、数据存储和错误处理。本指南将带你从Scrapy的基础概念开始,逐步深入到高级应用,并提供实战案例和常见问题的解决方案。

第一部分:Scrapy入门基础

1.1 Scrapy简介与安装

Scrapy是一个用于网络爬虫的框架,它简化了爬虫的开发过程,提供了许多内置功能,如自动处理请求、数据提取、数据存储等。Scrapy的核心组件包括:

  • Spiders(爬虫):定义如何抓取特定网站(或一组网站)的规则。
  • Scheduler(调度器):管理请求队列,决定下一个要处理的请求。
  • Downloader(下载器):执行网络请求,下载网页内容。
  • Item Pipeline(数据管道):处理从爬虫中提取的数据,如清洗、验证、存储等。
  • Middleware(中间件):在请求和响应之间进行处理,如添加请求头、代理等。

安装Scrapy: Scrapy可以通过pip安装。建议在虚拟环境中安装,以避免依赖冲突。

# 创建虚拟环境(可选)
python -m venv scrapy_env
source scrapy_env/bin/activate  # Linux/Mac
# scrapy_env\Scripts\activate  # Windows

# 安装Scrapy
pip install scrapy

安装完成后,可以通过以下命令验证安装:

scrapy --version

1.2 创建第一个Scrapy项目

使用Scrapy命令行工具创建一个新项目:

scrapy startproject tutorial

这将创建一个名为tutorial的目录,包含以下结构:

tutorial/
    scrapy.cfg
    tutorial/
        __init__.py
        items.py
        middlewares.py
        pipelines.py
        settings.py
        spiders/
            __init__.py
  • scrapy.cfg:项目配置文件。
  • tutorial/:项目Python模块。
  • items.py:定义数据模型。
  • middlewares.py:中间件配置。
  • pipelines.py:数据管道配置。
  • settings.py:全局设置。
  • spiders/:存放爬虫代码的目录。

1.3 编写第一个爬虫

spiders/目录下创建一个爬虫文件,例如quotes_spider.py

import scrapy

class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        'http://quotes.toscrape.com/page/1/',
    ]

    def parse(self, response):
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css('span.text::text').get(),
                'author': quote.css('small.author::text').get(),
                'tags': quote.css('div.tags a.tag::text').getall(),
            }

        # 翻页
        next_page = response.css('li.next a::attr(href)').get()
        if next_page is not None:
            yield response.follow(next_page, self.parse)

代码解释

  • name:爬虫的唯一标识符。
  • start_urls:爬虫开始抓取的URL列表。
  • parse:默认的回调函数,用于处理响应。
  • response.css:使用CSS选择器提取数据。
  • yield:生成数据项,Scrapy会自动处理这些数据。
  • response.follow:用于跟进链接,自动处理相对URL。

1.4 运行爬虫

在项目根目录下运行爬虫:

scrapy crawl quotes

这将启动爬虫,并输出抓取的数据。默认情况下,数据会打印到控制台。要将数据保存到文件,可以使用-o选项:

scrapy crawl quotes -o quotes.json

这将把抓取的数据保存为JSON格式的文件。

第二部分:Scrapy核心组件详解

2.1 Spiders(爬虫)

Spiders是Scrapy的核心,定义了如何抓取网站。Scrapy提供了多种内置的Spider类,如scrapy.Spiderscrapy.CrawlSpider等。

2.1.1 基本Spider

基本Spider是最简单的Spider类型,适用于简单的抓取任务。上面的例子就是一个基本Spider。

2.1.2 CrawlSpider

CrawlSpider是scrapy.Spider的子类,适用于需要从多个页面抓取数据的复杂网站。它使用RuleLinkExtractor来定义抓取规则。

import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule

class BookSpider(CrawlSpider):
    name = 'book'
    allowed_domains = ['books.toscrape.com']
    start_urls = ['http://books.toscrape.com/']

    rules = (
        Rule(LinkExtractor(allow=r'/catalogue/'), callback='parse_item', follow=True),
    )

    def parse_item(self, response):
        for book in response.css('article.product_pod'):
            yield {
                'title': book.css('h3 a::attr(title)').get(),
                'price': book.css('p.price_color::text').get(),
                'availability': book.css('p.instock.availability::text').getall()[1].strip(),
            }

代码解释

  • allowed_domains:限制爬虫只抓取指定域名的页面。
  • rules:定义抓取规则,每个规则是一个Rule对象。
  • LinkExtractor:用于提取链接,allow参数指定要匹配的URL模式。
  • callback:指定处理匹配链接的回调函数。
  • follow:是否跟进链接,如果为True,则继续从这些链接中提取更多链接。

2.2 Items(数据项)

Items定义了要抓取的数据结构。在items.py中定义:

import scrapy

class TutorialItem(scrapy.Item):
    text = scrapy.Field()
    author = scrapy.Field()
    tags = scrapy.Field()

在爬虫中使用:

from tutorial.items import TutorialItem

class QuotesSpider(scrapy.Spider):
    # ... 其他代码 ...
    def parse(self, response):
        for quote in response.css('div.quote'):
            item = TutorialItem()
            item['text'] = quote.css('span.text::text').get()
            item['author'] = quote.css('small.author::text').get()
            item['tags'] = quote.css('div.tags a.tag::text').getall()
            yield item

2.3 Item Pipeline(数据管道)

Item Pipeline用于处理抓取的数据,如清洗、验证、存储等。在pipelines.py中定义:

import json

class JsonWriterPipeline:
    def open_spider(self, spider):
        self.file = open('items.json', 'w')

    def close_spider(self, spider):
        self.file.close()

    def process_item(self, item, spider):
        line = json.dumps(dict(item)) + "\n"
        self.file.write(line)
        return item

settings.py中启用Pipeline:

ITEM_PIPELINES = {
    'tutorial.pipelines.JsonWriterPipeline': 300,
}

数字表示Pipeline的执行顺序,数字越小优先级越高。

2.4 Middleware(中间件)

Middleware在请求和响应之间进行处理,如添加请求头、代理、处理错误等。在middlewares.py中定义:

class ProxyMiddleware:
    def process_request(self, request, spider):
        request.meta['proxy'] = 'http://your-proxy:port'

settings.py中启用:

DOWNLOADER_MIDDLEWARES = {
    'tutorial.middlewares.ProxyMiddleware': 350,
}

2.5 Settings(设置)

Settings是Scrapy的配置文件,用于控制爬虫的行为。在settings.py中设置:

# 设置请求头
DEFAULT_REQUEST_HEADERS = {
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'en',
}

# 设置并发请求
CONCURRENT_REQUESTS = 32

# 设置下载延迟
DOWNLOAD_DELAY = 0.5

# 设置User-Agent
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'

第三部分:Scrapy高级应用

3.1 处理JavaScript渲染的页面

Scrapy本身不支持JavaScript渲染,但可以通过集成Splash或Selenium来处理动态内容。

3.1.1 使用Splash

Splash是一个轻量级的JavaScript渲染服务。首先安装Splash:

pip install scrapy-splash

然后在settings.py中配置:

SPLASH_URL = 'http://localhost:8050'

DOWNLOADER_MIDDLEWARES = {
    'scrapy_splash.SplashCookiesMiddleware': 723,
    'scrapy_splash.SplashMiddleware': 725,
    'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 810,
}

SPIDER_MIDDLEWARES = {
    'scrapy_splash.SplashDeduplicateArgsMiddleware': 100,
}

DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter'

HTTPCACHE_STORAGE = 'scrapy_splash.SplashAwareFSCacheStorage'

在爬虫中使用:

import scrapy
from scrapy_splash import SplashRequest

class JSSpider(scrapy.Spider):
    name = 'js'
    start_urls = ['http://example.com']

    def start_requests(self):
        for url in self.start_urls:
            yield SplashRequest(url, self.parse, args={'wait': 0.5})

    def parse(self, response):
        # 处理JavaScript渲染后的页面
        pass

3.1.2 使用Selenium

Selenium是一个浏览器自动化工具,可以模拟真实浏览器行为。首先安装:

pip install selenium

然后在爬虫中使用:

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import scrapy

class SeleniumSpider(scrapy.Spider):
    name = 'selenium'
    start_urls = ['http://example.com']

    def __init__(self):
        options = Options()
        options.add_argument('--headless')
        self.driver = webdriver.Chrome(options=options)

    def parse(self, response):
        self.driver.get(response.url)
        # 使用Selenium提取数据
        content = self.driver.find_element_by_id('content').text
        yield {'content': content}
        self.driver.quit()

3.2 分布式爬虫

Scrapy-Redis是一个基于Redis的分布式爬虫框架,可以实现多机协同抓取。

首先安装:

pip install scrapy-redis

然后在settings.py中配置:

# 使用Scrapy-Redis的调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"

# 使用Scrapy-Redis的去重过滤器
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

# Redis服务器地址
REDIS_URL = 'redis://localhost:6379/0'

# 设置爬虫队列
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'

在爬虫中使用:

from scrapy_redis.spiders import RedisSpider

class MyRedisSpider(RedisSpider):
    name = 'my_redis_spider'
    redis_key = 'my_redis_spider:start_urls'

    def parse(self, response):
        # 解析逻辑
        pass

启动爬虫后,需要向Redis中添加起始URL:

redis-cli
LPUSH my_redis_spider:start_urls "http://example.com"

3.3 数据存储到数据库

Scrapy可以将数据存储到各种数据库,如MySQL、MongoDB等。

3.3.1 存储到MySQL

首先安装MySQL驱动:

pip install mysql-connector-python

然后在pipelines.py中定义:

import mysql.connector

class MySQLPipeline:
    def __init__(self):
        self.conn = mysql.connector.connect(
            host='localhost',
            user='root',
            password='password',
            database='scrapy_data'
        )
        self.cursor = self.conn.cursor()
        self.create_table()

    def create_table(self):
        self.cursor.execute("""
            CREATE TABLE IF NOT EXISTS quotes (
                id INT AUTO_INCREMENT PRIMARY KEY,
                text VARCHAR(1000),
                author VARCHAR(255),
                tags VARCHAR(255)
            )
        """)
        self.conn.commit()

    def process_item(self, item, spider):
        sql = "INSERT INTO quotes (text, author, tags) VALUES (%s, %s, %s)"
        tags = ', '.join(item['tags'])
        self.cursor.execute(sql, (item['text'], item['author'], tags))
        self.conn.commit()
        return item

    def close_spider(self, spider):
        self.cursor.close()
        self.conn.close()

settings.py中启用:

ITEM_PIPELINES = {
    'tutorial.pipelines.MySQLPipeline': 300,
}

3.3.2 存储到MongoDB

首先安装MongoDB驱动:

pip install pymongo

然后在pipelines.py中定义:

import pymongo

class MongoDBPipeline:
    def __init__(self):
        self.client = pymongo.MongoClient('mongodb://localhost:27017/')
        self.db = self.client['scrapy_db']
        self.collection = self.db['quotes']

    def process_item(self, item, spider):
        self.collection.insert_one(dict(item))
        return item

    def close_spider(self, spider):
        self.client.close()

settings.py中启用:

ITEM_PIPELINES = {
    'tutorial.pipelines.MongoDBPipeline': 300,
}

3.4 处理登录和会话

Scrapy可以处理需要登录的网站,通过模拟登录获取会话Cookie。

3.4.1 使用FormRequest

import scrapy

class LoginSpider(scrapy.Spider):
    name = 'login'
    start_urls = ['http://example.com/login']

    def parse(self, response):
        # 提取CSRF token
        token = response.css('input[name="csrf_token"]::attr(value)').get()
        
        # 提交登录表单
        return scrapy.FormRequest.from_response(
            response,
            formdata={
                'username': 'your_username',
                'password': 'your_password',
                'csrf_token': token,
            },
            callback=self.after_login
        )

    def after_login(self, response):
        # 登录成功后,抓取需要登录才能访问的页面
        yield scrapy.Request('http://example.com/protected', self.parse_protected)

    def parse_protected(self, response):
        # 解析受保护页面
        pass

3.4.2 使用Cookie

class CookieSpider(scrapy.Spider):
    name = 'cookie'
    start_urls = ['http://example.com/protected']

    def start_requests(self):
        # 手动设置Cookie
        cookies = {'session_id': 'your_session_id'}
        for url in self.start_urls:
            yield scrapy.Request(url, cookies=cookies, callback=self.parse)

    def parse(self, response):
        # 解析页面
        pass

第四部分:实战案例

4.1 案例一:抓取电商网站商品信息

假设我们要抓取一个电商网站的商品信息,包括商品名称、价格、库存等。

步骤1:创建项目

scrapy startproject ecommerce
cd ecommerce

步骤2:定义Item

items.py中:

import scrapy

class ProductItem(scrapy.Item):
    name = scrapy.Field()
    price = scrapy.Field()
    stock = scrapy.Field()
    url = scrapy.Field()

步骤3:编写爬虫

spiders/目录下创建product_spider.py

import scrapy
from ecommerce.items import ProductItem

class ProductSpider(scrapy.Spider):
    name = 'product'
    allowed_domains = ['example.com']
    start_urls = ['http://example.com/products']

    def parse(self, response):
        for product in response.css('div.product'):
            item = ProductItem()
            item['name'] = product.css('h3.product-name::text').get()
            item['price'] = product.css('span.price::text').get()
            item['stock'] = product.css('span.stock::text').get()
            item['url'] = response.urljoin(product.css('a::attr(href)').get())
            yield item

        # 翻页
        next_page = response.css('li.next a::attr(href)').get()
        if next_page:
            yield response.follow(next_page, self.parse)

步骤4:配置Pipeline

pipelines.py中定义数据存储:

import json

class JsonWriterPipeline:
    def open_spider(self, spider):
        self.file = open('products.json', 'w')

    def close_spider(self, spider):
        self.file.close()

    def process_item(self, item, spider):
        line = json.dumps(dict(item)) + "\n"
        self.file.write(line)
        return item

settings.py中启用:

ITEM_PIPELINES = {
    'ecommerce.pipelines.JsonWriterPipeline': 300,
}

步骤5:运行爬虫

scrapy crawl product -o products.json

4.2 案例二:抓取新闻网站并存储到数据库

假设我们要抓取一个新闻网站的新闻标题、链接和发布时间,并存储到MySQL数据库。

步骤1:创建项目

scrapy startproject news
cd news

步骤2:定义Item

items.py中:

import scrapy

class NewsItem(scrapy.Item):
    title = scrapy.Field()
    link = scrapy.Field()
    publish_time = scrapy.Field()

步骤3:编写爬虫

spiders/目录下创建news_spider.py

import scrapy
from news.items import NewsItem

class NewsSpider(scrapy.Spider):
    name = 'news'
    allowed_domains = ['example.com']
    start_urls = ['http://example.com/news']

    def parse(self, response):
        for article in response.css('article.news-item'):
            item = NewsItem()
            item['title'] = article.css('h2.title::text').get()
            item['link'] = response.urljoin(article.css('a::attr(href)').get())
            item['publish_time'] = article.css('span.time::text').get()
            yield item

        # 翻页
        next_page = response.css('li.next a::attr(href)').get()
        if next_page:
            yield response.follow(next_page, self.parse)

步骤4:配置Pipeline

pipelines.py中定义MySQL存储:

import mysql.connector

class MySQLPipeline:
    def __init__(self):
        self.conn = mysql.connector.connect(
            host='localhost',
            user='root',
            password='password',
            database='news_db'
        )
        self.cursor = self.conn.cursor()
        self.create_table()

    def create_table(self):
        self.cursor.execute("""
            CREATE TABLE IF NOT EXISTS news (
                id INT AUTO_INCREMENT PRIMARY KEY,
                title VARCHAR(255),
                link VARCHAR(500),
                publish_time VARCHAR(50)
            )
        """)
        self.conn.commit()

    def process_item(self, item, spider):
        sql = "INSERT INTO news (title, link, publish_time) VALUES (%s, %s, %s)"
        self.cursor.execute(sql, (item['title'], item['link'], item['publish_time']))
        self.conn.commit()
        return item

    def close_spider(self, spider):
        self.cursor.close()
        self.conn.close()

settings.py中启用:

ITEM_PIPELINES = {
    'news.pipelines.MySQLPipeline': 300,
}

步骤5:运行爬虫

scrapy crawl news

第五部分:常见问题解决方案

5.1 问题一:爬虫被网站封禁

原因:网站检测到爬虫行为,如请求频率过高、缺少请求头等。

解决方案

  1. 设置下载延迟:在settings.py中设置DOWNLOAD_DELAY,降低请求频率。

    DOWNLOAD_DELAY = 1  # 延迟1秒
    
  2. 使用代理IP:通过中间件添加代理IP。

    class ProxyMiddleware:
       def process_request(self, request, spider):
           request.meta['proxy'] = 'http://your-proxy:port'
    
  3. 设置User-Agent:模拟浏览器请求。

    USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
    
  4. 使用随机User-Agent:安装scrapy-user-agents

    pip install scrapy-user-agents
    

    settings.py中启用:

    DOWNLOADER_MIDDLEWARES = {
       'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
       'scrapy_user_agents.middlewares.RandomUserAgentMiddleware': 400,
    }
    

5.2 问题二:数据提取失败

原因:网页结构变化、选择器错误或动态内容。

解决方案

  1. 检查选择器:使用浏览器开发者工具验证选择器。
  2. 使用XPath:有时XPath比CSS选择器更灵活。
    
    response.xpath('//div[@class="quote"]/span/text()').get()
    
  3. 处理动态内容:使用Splash或Selenium(如前所述)。
  4. 添加重试机制:在settings.py中设置重试次数。
    
    RETRY_TIMES = 3
    

5.3 问题三:内存泄漏

原因:爬虫长时间运行,内存占用过高。

解决方案

  1. 限制爬取深度:在settings.py中设置DEPTH_LIMIT
    
    DEPTH_LIMIT = 10
    
  2. 使用Item Pipeline及时处理数据:避免在内存中积累过多数据。
  3. 分批处理:将数据分批写入文件或数据库。
  4. 监控内存使用:使用scrapystats命令或第三方工具监控。

5.4 问题四:爬虫速度慢

原因:网络延迟、并发设置不当等。

解决方案

  1. 增加并发请求:在settings.py中设置CONCURRENT_REQUESTS
    
    CONCURRENT_REQUESTS = 64
    
  2. 使用异步IO:Scrapy默认使用Twisted异步IO,确保没有阻塞操作。
  3. 优化选择器:避免使用过于复杂的选择器。
  4. 使用缓存:启用HTTP缓存,避免重复下载。
    
    HTTPCACHE_ENABLED = True
    

5.5 问题五:处理反爬虫机制

原因:网站使用验证码、IP限制、行为分析等反爬虫措施。

解决方案

  1. 验证码识别:使用第三方库如pytesseractcaptcha识别验证码。
  2. IP轮换:使用代理池,动态切换IP。
  3. 模拟人类行为:添加随机延迟、鼠标移动等(使用Selenium)。
  4. 使用Headless浏览器:如Selenium或Playwright,模拟真实浏览器。

第六部分:最佳实践

6.1 遵守robots.txt

settings.py中设置:

ROBOTSTXT_OBEY = True

6.2 设置合理的请求头

settings.py中设置:

DEFAULT_REQUEST_HEADERS = {
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'en',
}

6.3 使用Item Pipeline进行数据清洗

pipelines.py中定义清洗逻辑:

class DataCleaningPipeline:
    def process_item(self, item, spider):
        # 清洗数据
        if item.get('price'):
            item['price'] = float(item['price'].replace('$', ''))
        return item

6.4 错误处理和日志记录

settings.py中设置日志级别:

LOG_LEVEL = 'INFO'

在爬虫中添加错误处理:

def parse(self, response):
    try:
        # 解析逻辑
    except Exception as e:
        self.logger.error(f"Error parsing {response.url}: {e}")

6.5 测试爬虫

使用Scrapy的shell命令测试选择器:

scrapy shell "http://quotes.toscrape.com"

在shell中测试:

response.css('div.quote').get()

第七部分:总结

Scrapy是一个功能强大且灵活的网络爬虫框架,适用于各种规模的抓取任务。通过本指南,你已经学习了Scrapy的基础知识、核心组件、高级应用以及实战案例。同时,我们也提供了解决常见问题的方案。掌握Scrapy需要实践和不断学习,希望本指南能帮助你快速上手并精通Scrapy。

在实际应用中,根据具体需求调整配置和代码,遵守网站的使用条款,合理使用爬虫技术。祝你在Scrapy的学习和使用中取得成功!