Scrapy电商全站爬取实战项目 - 从零构建多层级电商数据抓取系统
📂 所属阶段:第四阶段 — 实战演练(项目开发篇)
🔗 相关章节:Spider实战 · Selector选择器 · Pipeline管道实战 · 反爬对抗实战
目录
项目背景与目标
电商数据是许多业务决策的“燃料”——比价、选品、竞品监控都离不开它。可现实中,电商网站往往结构复杂:分类层层嵌套,商品列表一页接一页,还有各种反爬手段在暗中阻拦。全站爬取就是让爬虫像顾客一样从首页一路逛到详情页,把有价值的信息全部摘下来的过程。
本项目将带你搭建一个生产级的数据抓取系统,核心能力包括:
- 多级分类处理:自动识别并递归爬取网站的分类树,从一级类目深入到几百个子分类。
- 深度翻页:智能判断翻页链接或生成URL参数,确保不“漏页”,也能防止死循环。
- 精准数据提取:使用CSS选择器与正则表达式,从结构复杂的详情页中抽取出标题、价格、规格、图片等关键字段。
- 反爬虫对抗:集成随机User‑Agent、请求头伪装、代理轮换和延迟策略,让爬虫更“像人”。
- 数据质量保证:内置去重、清洗、有效性校验,产出的数据可以直接用于分析。
项目技术栈
- 爬虫框架:Scrapy 2.x,久经考验的异步爬虫引擎。
- 数据处理:Pydantic 或 Item 定义结构化数据,配合 ItemLoader 自动清洗转换。
- 存储方案:可选择 MongoDB、MySQL,甚至导出 CSV/JSON 文件。
- 代理管理:自建代理中间件,简单可控;也可对接 Scrapy‑Proxy‑Pool 等扩展。
准备好之后,我们就从分析目标网站的结构开始。
电商网站架构分析
无论目标网站长得多么花哨,它的内部结构通常都符合一套经典的四层模型:
- 首页:顶部导航栏、侧边分类菜单。所有子分类的入口都集中在这里。
- 分类页:要么展示下一级子分类,要么直接展示该分类下的商品列表,或者两者兼有。
- 列表页:由几十个商品卡片组成,通常带有分页器和排序/筛选控件。
- 详情页:单个商品的全部信息,包含标题、价格、属性、图片、评价等。
爬取流程就是沿着这个结构依次展开:
首页 → 获取分类链接 → 进入分类页 → 提取商品链接 → 进入详情页 → 抓取数据
真正的难点往往不在逻辑本身,而在于:
- 分类的层级不固定(可能2层,也可能4层),需要设计通用的递归解析器。
- 翻页的实现五花八门:可能是
?page=2的查询参数,也可能是/page/3/的路径形式,甚至是“点击加载更多”的按钮。
- 网站会悄悄检测爬虫:常见的反爬手段包括检查User‑Agent、限制请求频率、封禁IP等。
在动手编码前,我们先规划好项目的整体结构。
项目架构设计
清晰的目录划分会让后续开发更顺畅。下面这个结构是我们推荐的布局:
ecommerce_spider/
├── ecommerce_spider/
│ ├── spiders/ # 爬虫文件(核心逻辑)
│ ├── items/ # 数据模型定义
│ ├── pipelines/ # 管道(清洗、去重、存储)
│ ├── middlewares/ # 中间件(反爬、代理)
│ ├── utils/ # 工具类(翻页处理、分类解析等)
│ └── settings.py # 全局配置
├── scrapy.cfg # 项目配置文件
└── requirements.txt # 依赖列表
核心组件关系
下面的流程图概括了各组件是如何串联工作的:
graph TD
A[起始URL] --> B[分类解析模块]
B --> C[列表页解析模块]
C --> D[详情页解析模块]
D --> E[数据清洗管道]
E --> F[数据存储]
G[反爬中间件] -.-> B
H[代理中间件] -.-> C
说明:实线箭头表示数据/请求的流转,虚线表示中间件在背后“拦截”并处理请求。
接下来,我们从数据模型开始,一步步实现各个模块。
数据模型定义
在Scrapy中,通常用 Item 类描述我们要抓取的数据结构。配合 ItemLoader,可以很方便地在填充数据时完成清洗和格式转换。
# ecommerce_spider/items/product_item.py
import scrapy
from itemloaders.processors import TakeFirst, MapCompose, Join
from w3lib.html import remove_tags
import re
def clean_price(value):
"""清洗价格字符串,转为浮点数。例如 '¥1,299.00' -> 1299.0"""
if value:
cleaned = re.sub(r'[^\d.,]', '', value.strip())
try:
return float(cleaned.replace(',', ''))
except ValueError:
return None
return None
class ProductItem(scrapy.Item):
product_id = scrapy.Field(output_processor=TakeFirst())
title = scrapy.Field(output_processor=TakeFirst())
brand = scrapy.Field(output_processor=TakeFirst())
# 分类路径会用 ' > ' 连接多个层级
category_path = scrapy.Field(output_processor=Join(' > '))
current_price = scrapy.Field(
input_processor=MapCompose(clean_price),
output_processor=TakeFirst()
)
original_price = scrapy.Field(
input_processor=MapCompose(clean_price),
output_processor=TakeFirst()
)
main_image = scrapy.Field(output_processor=TakeFirst())
gallery_images = scrapy.Field() # 可能有多张图
rating = scrapy.Field(output_processor=TakeFirst())
review_count = scrapy.Field(output_processor=TakeFirst())
specifications = scrapy.Field() # 商品规格,通常为字典列表
url = scrapy.Field(output_processor=TakeFirst())
crawled_at = scrapy.Field()
解读:
clean_price 函数负责把“¥1,299.00”这类字符串变成可计算的浮点数。
input_processor 和 output_processor 定义了数据进出字段时的自动处理规则。
- 像
category_path 那样,多个同名字段的值会被自动拼接成字符串,方便后续存储。
有了数据蓝图,我们就可以开始编写爬虫逻辑了。
核心功能实现
主爬虫框架
主爬虫是整个系统的“调度中心”,它负责:
- 从起始URL拿到所有分类链接;
- 对每个分类,请求其商品列表页;
- 在列表页中抓取商品详情链接,并交给详情解析方法;
- 控制翻页,确保不会无限循环。
# ecommerce_spider/spiders/ecommerce_spider.py
import scrapy
from urllib.parse import urljoin
from ecommerce_spider.items import ProductItem
from datetime import datetime
class EcommerceSpider(scrapy.Spider):
name = 'ecommerce'
custom_settings = {
'DOWNLOAD_DELAY': 2, # 基础下载延迟,视网站情况调整
'CONCURRENT_REQUESTS': 8, # 全局并发请求数
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 允许通过命令行传入起始URL和最多翻页数
self.start_urls = kwargs.get('start_urls', ['https://example.com/categories'])
self.max_pages = int(kwargs.get('max_pages', 100))
def start_requests(self):
for url in self.start_urls:
yield scrapy.Request(url, callback=self.parse_categories)
def parse_categories(self, response):
"""解析分类页,提取所有子分类的链接"""
category_links = response.css('a.category-link::attr(href)').getall()
for link in category_links:
category_url = urljoin(response.url, link)
yield scrapy.Request(category_url, callback=self.parse_product_list)
def parse_product_list(self, response):
"""解析商品列表页:提取商品链接,并找到下一页"""
product_links = response.css('a.product-link::attr(href)').getall()
for link in product_links:
product_url = urljoin(response.url, link)
yield scrapy.Request(
product_url,
callback=self.parse_product_detail,
# 可以传递一些上下文信息,比如分类名
meta={'category': response.meta.get('category')}
)
# 处理翻页
current_page = response.meta.get('page', 1)
next_page = response.css('a.next::attr(href)').get()
if next_page and current_page < self.max_pages:
next_url = urljoin(response.url, next_page)
yield scrapy.Request(
next_url,
callback=self.parse_product_list,
meta={'page': current_page + 1}
)
def parse_product_detail(self, response):
"""解析商品详情页,构造 ProductItem"""
item = ProductItem()
item['product_id'] = self._extract_product_id(response)
item['title'] = response.css('h1::text').get().strip()
item['current_price'] = response.css('.price::text').get()
item['main_image'] = urljoin(
response.url,
response.css('.main-image img::attr(src)').get()
)
item['url'] = response.url
item['crawled_at'] = datetime.now().isoformat()
yield item
def _extract_product_id(self, response):
"""从URL或页面元素解析商品ID"""
match = re.search(r'/(\d+)/?$', response.url)
return match.group(1) if match else 'unknown'
💡 提示:如果在某个步骤中需要灵活传递上下文(比如分类名称),可以在meta字典中添加相应字段,后续方法通过response.meta取出即可。
多级分类解析
电商网站经常会用“面包屑导航”显示用户当前所在的分类层级。我们可以从这些元素中提取出完整的分类路径,用于后续数据分析。
# ecommerce_spider/utils/category_parser.py
from urllib.parse import urljoin
class CategoryParser:
def parse_hierarchy(self, response):
"""从面包屑导航中解析分类层级"""
breadcrumbs = response.css('.breadcrumb a')
hierarchy = []
for i, crumb in enumerate(breadcrumbs):
name = crumb.css('::text').get().strip()
url = urljoin(response.url, crumb.css('::attr(href)').get())
hierarchy.append({
'name': name,
'url': url,
'level': i # 0表示顶级分类
})
return hierarchy
用法:在parse_product_detail中调用该工具,将返回的路径信息存入item['category_path'](或单独字段),方便后续分组分析。
智能翻页处理
翻页逻辑不能只依赖一个“下一页”按钮。有些网站没有明确的下一页链接,就需要从当前URL中解析页码并构建下一页URL。下面的工具类兼顾了这两种情况:
# ecommerce_spider/utils/pagination_handler.py
import re
from urllib.parse import urlparse, parse_qs, urlencode
class PaginationHandler:
def handle(self, response, max_pages):
"""返回下一页URL,若无则返回None"""
current_page = self._extract_current_page(response.url)
if current_page >= max_pages:
return None
# 方式一:从页面中寻找 rel="next" 或 .next 的链接
next_link = response.css('a[rel="next"]::attr(href), a.next::attr(href)').get()
if next_link:
return urljoin(response.url, next_link)
# 方式二:通过URL参数自己生成
return self._generate_next_url(response.url, current_page)
def _extract_current_page(self, url):
parsed = urlparse(url)
query = parse_qs(parsed.query)
if 'page' in query:
return int(query['page'][0])
# 也可能页码在路径中,比如 /page/3/
match = re.search(r'/page/(\d+)', url)
return int(match.group(1)) if match else 1
def _generate_next_url(self, url, current_page):
parsed = urlparse(url)
query = parse_qs(parsed.query)
query['page'] = [current_page + 1]
new_query = urlencode(query, doseq=True)
return parsed._replace(query=new_query).geturl()
将这个工具集成到parse_product_list中,替换原来简单的.next查找,可以大幅提高翻页的健壮性。
反爬虫对抗策略
前面说到的所有逻辑,只有在请求能够成功返回的前提下才有意义。因此我们需要通过中间件武装我们的爬虫。
反爬虫中间件
这个中间件会在每个请求发出之前,随机更换 User‑Agent 和一些常见的请求头,同时加入一个随机延迟,让爬虫的行为更像普通浏览器。
# ecommerce_spider/middlewares/anti_crawler_middleware.py
import random
import time
from fake_useragent import UserAgent
class AntiCrawlerMiddleware:
def __init__(self):
self.ua = UserAgent()
def process_request(self, request, spider):
# 随机User-Agent
request.headers['User-Agent'] = self.ua.random
# 模拟浏览器的 Accept 头部
request.headers['Accept'] = (
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
)
request.headers['Accept-Language'] = 'zh-CN,zh;q=0.9,en;q=0.8'
# 微小随机延迟(注意:这里会阻塞请求,生产环境建议用 Downloader 中间件延迟)
time.sleep(random.uniform(1, 3))
return None
代理中间件
对于IP容易被封的网站,更换代理是最直接的应对手段。下面是一个极简的轮询代理实现:
# ecommerce_spider/middlewares/proxy_middleware.py
import random
class ProxyMiddleware:
def __init__(self):
self.proxies = [
'http://proxy1:port',
'http://proxy2:port',
]
def process_request(self, request, spider):
if self.proxies:
request.meta['proxy'] = random.choice(self.proxies)
return None
注意:
- 实际项目中,代理列表可能来自文件或付费代理API,你可以动态更新
self.proxies。
- 该中间件在请求发出前设置
request.meta['proxy'],Scrapy 会自动使用该代理进行下载。
- 需要配合
DOWNLOADER_MIDDLEWARES在settings.py中启用这些中间件。
数据去重与清洗
爬取过程中难免会出现重复数据,或者某次抓取返回了空白/无效信息。通过管道(Pipeline)可以集中处理这些问题。
数据清洗管道
利用之前定义的 ItemLoader 和 ProductItem,管道可以自动应用价格、标题等字段的清洗规则,同时剔除只有部分字段的“垃圾”Item。
# ecommerce_spider/pipelines/cleaning_pipeline.py
from ecommerce_spider.items import ProductItem
from itemloaders import ItemLoader
class CleaningPipeline:
def process_item(self, item, spider):
# 如果是用 ItemLoader 生成的 item 可以直接返回
# 这里演示手动校验
loader = ItemLoader(item=ProductItem(), response=item.get('response'))
title = item.get('title', '')
loader.add_value('title', title.strip())
price = item.get('current_price')
loader.add_value('current_price', price)
# 使用 loader 加载完成后,获取处理过的字段
cleaned_item = loader.load_item()
# 验证必需字段是否存在且有效
if not cleaned_item.get('title') or not cleaned_item.get('current_price'):
spider.logger.warning(
f"Missing required fields for item: {item.get('url')}"
)
# 返回 None 表示丢弃这个 item
return None
return cleaned_item
去重管道
最简单的去重就是在管道里维护一个已处理标识的集合。对于商品,product_id+url通常能唯一确定一条记录。
# ecommerce_spider/pipelines/deduplication_pipeline.py
class DeduplicationPipeline:
def __init__(self):
self.seen = set()
def process_item(self, item, spider):
identifier = f"{item.get('product_id')}_{item.get('url')}"
if identifier in self.seen:
spider.logger.info(f"Duplicate item: {item.get('title')}")
return None
self.seen.add(identifier)
return item
性能优化与部署
性能优化配置
在settings.py中,合理调整并发与延迟可以成倍提升爬取速度,同时避免触发反爬。
# ecommerce_spider/settings.py
# 全局并发请求数
CONCURRENT_REQUESTS = 32
# 对单个域名的并发请求数
CONCURRENT_REQUESTS_PER_DOMAIN = 8
# 下载延迟(秒),建议设为 1 或稍大
DOWNLOAD_DELAY = 1
# 在延迟基础上加入随机浮动 (0.5 * DOWNLOAD_DELAY)
RANDOMIZE_DOWNLOAD_DELAY = 0.5
# 启用自动限速,根据延迟和并发自动调整
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_TARGET_CONCURRENCY = 4.0
含义:AUTOTHROTTLE 会根据实际的响应时间动态调整请求速率,让你的爬虫既能跑得快,又不会把网站压垮。
Docker 部署
将整个项目打包成Docker镜像,可以省去环境配置的麻烦,也方便在服务器上横向扩展。
# Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["scrapy", "crawl", "ecommerce"]
# docker-compose.yml
version: '3.8'
services:
spider:
build: .
volumes:
- ./output:/app/output # 将输出目录挂载出来
mongodb:
image: mongo:4.4
ports:
- "27017:27017"
volumes:
- mongodb_data:/data/db
volumes:
mongodb_data:
部署时只需在目录下执行:
爬取的数据既可以存入MongoDB,也可以通过修改pipelines保存为文件。
至此,你已完整构建了一个可用的电商全站爬取系统。从理解网站结构,到设计数据模型,再到编写爬虫逻辑、反爬对抗、数据清洗和最终部署——每个环节都经过了实战打磨。把这个框架套用到不同的电商网站上,只需调整CSS选择器和部分规则,就能快速完成数据采集任务。开始你的爬虫之旅吧!