引言
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.Spider、scrapy.CrawlSpider等。
2.1.1 基本Spider
基本Spider是最简单的Spider类型,适用于简单的抓取任务。上面的例子就是一个基本Spider。
2.1.2 CrawlSpider
CrawlSpider是scrapy.Spider的子类,适用于需要从多个页面抓取数据的复杂网站。它使用Rule和LinkExtractor来定义抓取规则。
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 问题一:爬虫被网站封禁
原因:网站检测到爬虫行为,如请求频率过高、缺少请求头等。
解决方案:
设置下载延迟:在
settings.py中设置DOWNLOAD_DELAY,降低请求频率。DOWNLOAD_DELAY = 1 # 延迟1秒使用代理IP:通过中间件添加代理IP。
class ProxyMiddleware: def process_request(self, request, spider): request.meta['proxy'] = 'http://your-proxy:port'设置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'使用随机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 问题二:数据提取失败
原因:网页结构变化、选择器错误或动态内容。
解决方案:
- 检查选择器:使用浏览器开发者工具验证选择器。
- 使用XPath:有时XPath比CSS选择器更灵活。
response.xpath('//div[@class="quote"]/span/text()').get() - 处理动态内容:使用Splash或Selenium(如前所述)。
- 添加重试机制:在
settings.py中设置重试次数。RETRY_TIMES = 3
5.3 问题三:内存泄漏
原因:爬虫长时间运行,内存占用过高。
解决方案:
- 限制爬取深度:在
settings.py中设置DEPTH_LIMIT。DEPTH_LIMIT = 10 - 使用Item Pipeline及时处理数据:避免在内存中积累过多数据。
- 分批处理:将数据分批写入文件或数据库。
- 监控内存使用:使用
scrapy的stats命令或第三方工具监控。
5.4 问题四:爬虫速度慢
原因:网络延迟、并发设置不当等。
解决方案:
- 增加并发请求:在
settings.py中设置CONCURRENT_REQUESTS。CONCURRENT_REQUESTS = 64 - 使用异步IO:Scrapy默认使用Twisted异步IO,确保没有阻塞操作。
- 优化选择器:避免使用过于复杂的选择器。
- 使用缓存:启用HTTP缓存,避免重复下载。
HTTPCACHE_ENABLED = True
5.5 问题五:处理反爬虫机制
原因:网站使用验证码、IP限制、行为分析等反爬虫措施。
解决方案:
- 验证码识别:使用第三方库如
pytesseract或captcha识别验证码。 - IP轮换:使用代理池,动态切换IP。
- 模拟人类行为:添加随机延迟、鼠标移动等(使用Selenium)。
- 使用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的学习和使用中取得成功!
