Scrapy 是 Python 生态中功能强大的爬虫框架,能高效抓取网页数据并进行结构化处理。本文将从基础用法出发,结合可落地的实战案例,重点讲解正则表达式在 Scrapy 中的应用,并拓展至高级功能,帮助你快速掌握并灵活运用。
一、Scrapy 基础入门
1. 安装 Scrapy
bash
pip install scrapy
核心概念速览
- 项目(Project):爬虫的整体工程目录,包含所有配置和代码。
- 爬虫(Spider):自定义的爬虫类,定义抓取规则(起始 URL、解析逻辑等)。
- Item:用于结构化存储爬取的数据(类似字典,但更规范)。
- 管道(Pipeline):处理爬取到的 Item(如去重、存储到文件 / 数据库)。
- 中间件(Middleware):介于 Scrapy 引擎和爬虫 / 下载器之间的钩子,可处理请求 / 响应(如添加代理、修改请求头)。
二、实战案例:爬取豆瓣电影 Top250
以爬取豆瓣电影 Top250(https://movie.douban.com/top250)为例,完整演示 Scrapy 工作流程,重点融入正则表达式的应用。
步骤 1:创建项目和爬虫
- 创建项目:
bash
scrapy startproject douban_movie # 项目名:douban_movie
cd douban_movie
2**.创建爬虫**:
bash
scrapy genspider top250 movie.douban.com # 爬虫名:top250,域名:movie.douban.com
此时项目结构如下:
bash
douban_movie/
├── douban_movie/ # 项目核心目录
│ ├── spiders/ # 爬虫目录
│ │ ├── __init__.py
│ │ └── top250.py # 刚创建的爬虫
│ ├── __init__.py
│ ├── items.py # 定义数据结构
│ ├── middlewares.py # 中间件
│ ├── pipelines.py # 数据处理管道
│ └── settings.py # 配置文件
└── scrapy.cfg # 项目配置
步骤 2:定义 Item(数据结构)
在 items.py
中定义需要爬取的数据字段(如电影名、评分、简介等):
python
import scrapy
class DoubanMovieItem(scrapy.Item):
# 电影名称
title = scrapy.Field()
# 评分
rating = scrapy.Field()
# 评价人数
comment_count = scrapy.Field()
# 电影简介(可能包含年份、国家、类型等信息)
info = scrapy.Field()
# 从简介中提取的年份(用正则处理)
year = scrapy.Field()
# 从简介中提取的国家(用正则处理)
country = scrapy.Field()
# 从简介中提取的类型(用正则处理)
genre = scrapy.Field()
步骤 3:编写爬虫逻辑(核心,含正则案例)
打开 spiders/top250.py
,实现爬取和解析逻辑。豆瓣电影列表页的 HTML 结构中,每部电影信息包含在 div.item
标签内,我们需要从中提取数据,并通过正则从复杂文本中提取结构化信息。
完整爬虫代码:
python
import scrapy
import re # 引入正则模块
from douban_movie.items import DoubanMovieItem
class Top250Spider(scrapy.Spider):
name = "top250" # 爬虫名(唯一)
allowed_domains = ["movie.douban.com"]
start_urls = ["https://movie.douban.com/top250"] # 起始 URL
def parse(self, response):
"""解析列表页,提取每部电影的信息,并翻页"""
# 1. 提取当前页所有电影节点(div.item)
movies = response.xpath('//div[@class="item"]')
for movie in movies:
item = DoubanMovieItem() # 实例化 Item
# 2. 提取基础信息(用 XPath)
# 电影名(可能包含中文名和英文名,取第一个)
item['title'] = movie.xpath('.//span[@class="title"][1]/text()').get()
# 评分
item['rating'] = movie.xpath('.//span[@class="rating_num"]/text()').get()
# 评价人数(格式如"150.0万人评价",用正则提取数字)
comment_text = movie.xpath('.//div[@class="star"]/span[4]/text()').get()
# 正则案例1:提取评价人数中的数字(如"150.0万人评价"→"150.0万")
item['comment_count'] = re.findall(r'(\d+\.\d+万|\d+万)', comment_text)[0] if comment_text else None
# 3. 提取简介(包含年份、国家、类型,格式如"1994 / 美国 / 剧情 犯罪")
info_text = movie.xpath('.//p[@class=""]/text()').getall()
# 清洗简介文本(去除换行和空格)
info_text = ''.join(info_text).strip() if info_text else ''
item['info'] = info_text
# 4. 用正则从简介中提取年份、国家、类型(核心正则案例)
# 正则案例2:提取年份(如"1994 / 美国 / 剧情 犯罪"→"1994")
year_match = re.search(r'(\d{4})', info_text) # \d{4} 匹配4位数字(年份)
item['year'] = year_match.group(1) if year_match else None
# 正则案例3:提取国家(如"1994 / 美国 / 剧情 犯罪"→"美国")
# 逻辑:年份后用"/"分隔,国家在年份和类型之间
country_match = re.search(r'\d{4}\s*/\s*([^/]+)\s*/', info_text)
item['country'] = country_match.group(1).strip() if country_match else None
# 正则案例4:提取类型(如"1994 / 美国 / 剧情 犯罪"→"剧情 犯罪")
# 逻辑:最后一个"/"后的内容为类型
genre_match = re.search(r'/.*/\s*(.*)$', info_text)
item['genre'] = genre_match.group(1).strip() if genre_match else None
yield item # 提交 Item 到管道处理
# 5. 翻页逻辑(获取下一页 URL 并继续爬取)
next_page = response.xpath('//span[@class="next"]/a/@href').get()
if next_page:
# 拼接完整 URL(豆瓣分页 URL 是相对路径,如"?start=25&filter=")
next_url = response.urljoin(next_page)
yield scrapy.Request(next_url, callback=self.parse) # 递归调用 parse 解析下一页
正则表达式详解(核心):
-
提取评价人数 :原始文本:
"150.0万人评价"
,正则r'(\d+\.\d+万|\d+万)'
匹配带小数点或不带小数点的 "万" 单位数字,findall
返回匹配列表,取第一个结果。 -
提取年份 :原始文本:
"1994 / 美国 / 剧情 犯罪"
,正则r'(\d{4})'
匹配 4 位数字(年份特征),search
找到第一个匹配,group(1)
提取分组内容。 -
提取国家 :正则
r'\d{4}\s*/\s*([^/]+)\s*/'
中:\d{4}\s*/\s*
:匹配年份(4 位数字)+ 可能的空格 + "/" + 可能的空格;([^/]+)
:匹配非 "/" 的任意字符(国家名称),用分组捕获;\s*/
:匹配后续的空格和 "/"。
-
提取类型 :正则
r'/.*/\s*(.*)$'
中:/.*/
:匹配到最后一个 "/"(因为.*
是贪婪匹配);\s*(.*)$
:匹配剩余内容(类型),$
表示字符串结尾。
步骤 4:配置管道(存储数据)
在 pipelines.py
中定义数据处理逻辑,这里演示保存到 CSV 文件:
python
import csv
from itemadapter import ItemAdapter
class DoubanMoviePipeline:
def __init__(self):
# 打开文件并写入表头
self.file = open('douban_top250.csv', 'w', newline='', encoding='utf-8')
self.writer = csv.DictWriter(self.file, fieldnames=[
'title', 'rating', 'comment_count', 'info', 'year', 'country', 'genre'
])
self.writer.writeheader()
def process_item(self, item, spider):
# 写入每一行数据
self.writer.writerow(ItemAdapter(item).asdict())
return item # 传递给下一个管道(如果有的话)
def close_spider(self, spider):
# 关闭文件
self.file.close()
在 settings.py
中启用管道(取消注释并修改):
python
ITEM_PIPELINES = {
"douban_movie.pipelines.DoubanMoviePipeline": 300, # 300 是优先级(0-1000,越小越先执行)
}
步骤 5:设置请求头(避免被反爬)
在 settings.py
中添加 User-Agent(模拟浏览器请求):
python
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
# 可选:禁用 Cookie(部分网站反爬会检测 Cookie)
# COOKIES_ENABLED = False
步骤 6:运行爬虫
python
scrapy crawl top250 # 爬虫名是 top250
运行后,项目根目录会生成 douban_top250.csv
文件,包含所有爬取的电影数据。
三、扩展性:高级功能与实战技巧
1. 处理动态加载数据(AJAX)
如果目标网站数据通过 AJAX 动态加载(如滚动加载、点击加载),可通过以下方式处理:
- 打开浏览器开发者工具(F12),在 "Network" 页找到 AJAX 请求的 API 接口(通常是 JSON 格式);
- 在 Scrapy 中直接请求 API 接口,解析 JSON 数据(无需解析 HTML)。
示例 :爬取某网站动态加载的列表,API 接口为 https://example.com/api/list?page=1
:
python
def parse(self, response):
data = response.json() # 解析 JSON
for item in data['results']:
# 提取数据
yield {'title': item['title'], 'url': item['url']}
# 翻页
next_page = data.get('next_page')
if next_page:
yield scrapy.Request(f'https://example.com/api/list?page={next_page}', callback=self.parse)
2. 使用代理 IP 突破反爬
在 middlewares.py
中添加代理中间件:
python
class ProxyMiddleware:
def process_request(self, request, spider):
# 代理 IP 列表(可从代理服务商获取)
proxies = [
'http://123.45.67.89:8080',
'http://98.76.54.32:8888'
]
# 随机选择一个代理
request.meta['proxy'] = random.choice(proxies)
在 settings.py
中启用:
python
DOWNLOADER_MIDDLEWARES = {
"douban_movie.middlewares.ProxyMiddleware": 543,
}
3. 分布式爬取(大规模数据)
使用 scrapy-redis
实现分布式爬取(多台机器共享任务队列):
bash
pip install scrapy-redis
修改 settings.py
:
python
# 启用 scrapy-redis 调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 启用 scrapy-redis 去重
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# Redis 连接配置
REDIS_URL = "redis://localhost:6379/0"
四、学习建议与总结
-
多练正则 :正则是 Scrapy 解析复杂文本的核心工具,推荐通过 Regex101 在线练习,重点掌握:
- 元字符(
\d
数字、\w
单词字符、.
任意字符); - 量词(
*
0 次或多次、+
1 次或多次、?
0 次或 1 次、{n}
恰好 n 次); - 分组(
()
捕获分组、(?:)
非捕获分组); - 贪婪与非贪婪匹配(
.*
贪婪、.*?
非贪婪)。
- 元字符(
-
调试技巧:
使用
scrapy shell
调试 XPath 和正则:
bash
scrapy shell "https://movie.douban.com/top250" # 在终端交互式调试
在爬虫中添加 self.log(item)
打印数据,查看解析是否正确。
3.反爬应对 :除了代理和 User-Agent,还可设置请求间隔(DOWNLOAD_DELAY = 2
在 settings.py
中)、随机请求头、模拟登录(通过 FormRequest
)等。
通过豆瓣电影案例,你已掌握 Scrapy 的核心流程和正则应用,结合扩展性技巧,可应对大部分爬虫场景。实际项目中,需根据目标网站结构灵活调整解析逻辑,不断优化反爬策略。
四、正则表达式高阶技巧与实战场景
1. 非贪婪匹配与分组捕获的碰撞
在爬取电影简介时,常遇到类似"导演: 弗兰克·德拉邦特 / 主演: 蒂姆·罗宾斯 / 摩根·弗里曼 / 鲍勃·冈顿"
的文本,需要分别提取导演和主演。若用贪婪匹配.*
会一口气匹配到末尾,此时非贪婪模式.*?
+ 分组捕获就能精准拆分:
python
info_text = "导演: 弗兰克·德拉邦特 / 主演: 蒂姆·罗宾斯 / 摩根·弗里曼 / 鲍勃·冈顿"
# 提取导演
director_match = re.search(r'导演:\s*(.*?)\s*/', info_text)
director = director_match.group(1) if director_match else None
# 结果:'弗兰克·德拉邦特'
# 提取主演(所有主演用逗号拼接)
cast_match = re.search(r'主演:\s*(.*)$', info_text)
cast = ','.join(re.split(r'\s*/\s*', cast_match.group(1))) if cast_match else None
# 结果:'蒂姆·罗宾斯,摩根·弗里曼,鲍勃·冈顿'
- 解析:
\s*
匹配空格,(.*?)
非贪婪捕获导演名,遇到/
就停止;主演部分用split
按/
分割后拼接,处理多主演场景。
2. 零宽断言:不捕获文本却能定位
爬取网页中隐藏的邮箱时,常遇到"联系我们:xxx[AT]example.com"
(用[AT]
替代@
防爬),需匹配[AT]
前后的字符并替换:
python
hidden_email = "联系我们:xxx[AT]example.com"
# 零宽断言定位[AT]前后内容,替换为@
email = re.sub(r'(?<=\w)\[AT\](?=\w)', '@', hidden_email)
# 结果:'联系我们:xxx@example.com'
- 零宽断言
(?<=\w)
表示前面必须是单词字符(字母 / 数字 / 下划线),(?=\w)
表示后面必须是单词字符,只做位置判断不捕获内容,完美解决替换边界问题。
五、动态渲染页面爬取(应对 JavaScript 加载数据)
部分网站用 JavaScript 动态生成内容(如滚动加载的商品列表),此时直接爬取 HTML 只能拿到空数据。可通过以下两种方式解决:
1. 分析 AJAX 接口
打开浏览器开发者工具(F12)→ Network → XHR,找到动态加载的 API 接口(通常返回 JSON)。例如某电商网站的商品列表接口为https://api.example.com/goods?page=1
,直接请求接口更高效:
python
def start_requests(self):
for page in range(1, 11): # 爬1-10页
url = f'https://api.example.com/goods?page={page}'
yield scrapy.Request(url, callback=self.parse_api)
def parse_api(self, response):
data = response.json()
for item in data['goods_list']:
yield {
'name': item['name'],
'price': item['price'],
'sales': item['sales']
}
2. 集成 Selenium 处理渲染
若接口加密难以解析,可用 Selenium 驱动浏览器加载完整页面。需先安装scrapy-selenium
和浏览器驱动:
bash
pip install scrapy-selenium
在settings.py
配置:
python
DOWNLOADER_MIDDLEWARES = {
'scrapy_selenium.SeleniumMiddleware': 800
}
SELENIUM_DRIVER_NAME = 'chrome'
SELENIUM_DRIVER_EXECUTABLE_PATH = '/path/to/chromedriver'
爬虫中使用:
python
from scrapy_selenium import SeleniumRequest
def start_requests(self):
yield SeleniumRequest(
url='https://example.com/dynamic-page',
callback=self.parse_dynamic
)
def parse_dynamic(self, response):
# 此时response已包含JS渲染后的内容
for goods in response.xpath('//div[@class="goods-item"]'):
yield {
'name': goods.xpath('.//h3/text()').get(),
'price': goods.xpath('.//span[@class="price"]/text()').get()
}
六、分布式爬取与去重优化(基于 Scrapy-Redis)
当数据量达到百万级,单台机器爬取效率极低,分布式爬取可让多台机器共享任务队列。
1. 环境配置
bash
pip install scrapy-redis
修改settings.py
:
python
# 替换调度器和去重器为Scrapy-Redis实现
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# Redis连接地址(多台机器需指向同一Redis服务器)
REDIS_URL = "redis://192.168.1.100:6379/0"
# 允许暂停后重启继续爬取
SCHEDULER_PERSIST = True
2. 爬虫代码调整
python
from scrapy_redis.spiders import RedisSpider
class DistributedSpider(RedisSpider):
name = 'distributed'
redis_key = 'start_urls' # 从Redis的start_urls列表获取起始URL
def parse(self, response):
# 解析逻辑与普通爬虫一致
for item in response.xpath('//div[@class="item"]'):
yield {
'title': item.xpath('.//h2/text()').get(),
'url': item.xpath('.//a/@href').get()
}
# 翻页链接
next_page = response.xpath('//a[@class="next"]/@href').get()
if next_page:
yield response.follow(next_page, callback=self.parse)
启动方式:先在 Redis 中添加起始 URL:
bash
redis-cli lpush start_urls https://example.com/page1
然后多台机器分别运行爬虫:
bash
scrapy crawl distributed
机器会自动从 Redis 获取任务,避免重复爬取,极大提升效率。
七、反爬策略进阶:从伪装到动态调整
1. 随机请求头池
在middlewares.py
添加随机 User-Agent 中间件:
python
import random
class RandomUserAgentMiddleware:
def __init__(self):
self.user_agents = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.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/16.5 Safari/605.1.15",
"Mozilla/5.0 (X11; Linux x86_64) Firefox/113.0"
]
def process_request(self, request, spider):
request.headers['User-Agent'] = random.choice(self.user_agents)
在settings.py
启用:
python
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.RandomUserAgentMiddleware': 400,
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None # 禁用默认中间件
}
2. 动态调整请求间隔
根据网站响应速度自动调整爬取频率,在settings.py
设置:
python
DOWNLOAD_DELAY = 1 # 基础间隔1秒
RANDOMIZE_DOWNLOAD_DELAY = True # 随机加减0.5-1.5倍,避免固定间隔被识别
八、数据存储多样化:从 CSV 到数据库
1. 存储到 MySQL
在pipelines.py
编写数据库管道:
python
import pymysql
from itemadapter import ItemAdapter
class MySQLPipeline:
def __init__(self):
self.conn = None
self.cursor = None
def open_spider(self, spider):
self.conn = pymysql.connect(
host='localhost',
user='root',
password='123456',
database='scrapy_data',
charset='utf8mb4'
)
self.cursor = self.conn.cursor()
# 创建表
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS goods (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price DECIMAL(10,2) NOT NULL,
sales INT NOT NULL
)
''')
def process_item(self, item, spider):
adapter = ItemAdapter(item)
self.cursor.execute('''
INSERT INTO goods (name, price, sales) VALUES (%s, %s, %s)
''', (adapter['name'], adapter['price'], adapter['sales']))
self.conn.commit()
return item
def close_spider(self, spider):
self.cursor.close()
self.conn.close()
在settings.py
启用:
python
ITEM_PIPELINES = {
'myproject.pipelines.MySQLPipeline': 300
}
2. 存储到 MongoDB
安装pymongo
后编写管道:
python
import pymongo
from itemadapter import ItemAdapter
class MongoPipeline:
def __init__(self):
self.client = pymongo.MongoClient('mongodb://localhost:27017/')
self.db = self.client['scrapy_db']
self.collection = self.db['goods']
def process_item(self, item, spider):
self.collection.insert_one(ItemAdapter(item).asdict())
return item
九、总结与学习路径
Scrapy 的核心魅力在于其模块化设计 ------ 从请求发送、数据解析到存储,每个环节都可灵活扩展。入门时可从单页静态网站练手,熟练掌握 XPath 和正则表达式(重点练分组、非贪婪匹配、断言);进阶阶段攻克动态渲染和反爬,尝试用 Selenium 或分析 AJAX 接口;最后通过 Scrapy-Redis 实现分布式,应对大规模数据爬取。
推荐资源:
- 官方文档:Scrapy Documentation(最权威的参考资料)
- 实战项目:爬取豆瓣图书、知乎话题、电商商品列表,用不同存储方式落地数据
- 正则练习:Regex101(实时验证正则表达式,支持 Python 语法)
记住,爬虫的核心是 "尊重网站规则,合理控制频率",避免对服务器造成过度压力。随着技术迭代,反爬与绕过的博弈会持续升级,但扎实的基础和灵活的思路,永远是应对变化的底气。