Day 11: 电商数据爬取实战
学习目标
- 掌握电商网站的爬取策略
- 学会爬取商品列表和详情
- 实现评论数据采集
- 构建价格监控系统
技术原理
11.1 电商网站特点
常见特点:
- 页面结构复杂
- 动态加载内容多
- 反爬机制严格
- 数据量大
爬取难点:
- 需要处理分页
- 需要处理动态加载
- 需要处理登录验证
- 需要处理验证码
11.2 爬取策略
列表页爬取:
- 分析分页参数
- 提取商品链接
- 处理无限滚动
详情页爬取:
- 提取商品信息
- 处理图片下载
- 处理规格参数
评论爬取:
- 分析评论接口
- 处理分页加载
- 提取评论内容
11.3 反爬应对
常见反爬:
- User-Agent 检测
- IP 频率限制
- Cookie 验证
- 验证码
应对策略:
- 伪装请求头
- 使用代理 IP
- 控制请求频率
- 使用 Selenium
案例
案例1:商品列表爬取
python
import scrapy
class ProductListSpider(scrapy.Spider):
"""商品列表爬虫"""
name = 'product_list'
allowed_domains = ['example.com']
start_urls = ['https://example.com/products']
def parse(self, response):
"""解析商品列表"""
# 提取商品卡片
products = response.css('.product-card')
for product in products:
# 提取商品信息
item = {
'name': product.css('.product-name::text').get(),
'price': product.css('.product-price::text').get(),
'url': product.css('a::attr(href)').get(),
'image': product.css('img::attr(src)').get(),
}
# 跟踪详情页
detail_url = item['url']
if detail_url:
yield response.follow(
detail_url,
callback=self.parse_detail,
meta={'item': item}
)
# 处理分页
next_page = response.css('.next-page::attr(href)').get()
if next_page:
yield response.follow(next_page, callback=self.parse)
def parse_detail(self, response):
"""解析商品详情"""
item = response.meta['item']
# 提取详情信息
item['description'] = response.css('.product-description::text').get()
item['specs'] = response.css('.product-specs').get()
item['images'] = response.css('.product-image::attr(src)').getall()
yield item案例2:评论数据采集
python
import scrapy
import json
class ReviewSpider(scrapy.Spider):
"""评论爬虫"""
name = 'reviews'
def start_requests(self):
"""生成初始请求"""
# 假设评论接口
api_url = 'https://example.com/api/reviews'
for page in range(1, 10):
yield scrapy.FormRequest(
url=api_url,
formdata={'page': str(page), 'product_id': '12345'},
callback=self.parse_reviews
)
def parse_reviews(self, response):
"""解析评论数据"""
data = json.loads(response.text)
for review in data.get('reviews', []):
yield {
'user': review.get('username'),
'rating': review.get('rating'),
'content': review.get('content'),
'time': review.get('created_at'),
}应用场景
1. 商品信息采集
- 竞品分析
- 价格监控
- 市场调研
2. 评论数据分析
- 用户反馈分析
- 产品改进
- 舆情监控
3. 价格监控
- 价格变动提醒
- 促销活动监控
- 价格趋势分析
代码案例
案例3:完整的电商爬虫
python
import scrapy
from scrapy.loader import ItemLoader
from scrapy.loader.processors import TakeFirst, Join, MapCompose
import re
class ProductItem(scrapy.Item):
"""商品 Item"""
name = scrapy.Field()
price = scrapy.Field()
original_price = scrapy.Field()
sales = scrapy.Field()
reviews_count = scrapy.Field()
rating = scrapy.Field()
url = scrapy.Field()
image_urls = scrapy.Field()
description = scrapy.Field()
specs = scrapy.Field()
crawl_time = scrapy.Field()
class ProductLoader(ItemLoader):
"""商品加载器"""
default_output_processor = TakeFirst()
name_in = MapCompose(str.strip)
price_in = MapCompose(lambda x: re.sub(r'[^\d.]', '', x))
original_price_in = MapCompose(lambda x: re.sub(r'[^\d.]', '', x))
sales_in = MapCompose(lambda x: re.sub(r'[^\d]', '', x))
class EcommerceSpider(scrapy.Spider):
"""电商爬虫"""
name = 'ecommerce'
allowed_domains = ['example.com']
start_urls = ['https://example.com/products']
custom_settings = {
'DOWNLOAD_DELAY': 2,
'CONCURRENT_REQUESTS': 4,
'CONCURRENT_REQUESTS_PER_DOMAIN': 2,
}
def parse(self, response):
"""解析商品列表"""
products = response.css('.product-item')
for product in products:
loader = ProductLoader(item=ProductItem(), selector=product)
# 提取列表页信息
loader.add_css('name', '.product-name::text')
loader.add_css('price', '.current-price::text')
loader.add_css('original_price', '.original-price::text')
loader.add_css('sales', '.sales-count::text')
loader.add_css('url', 'a::attr(href)')
loader.add_css('image_urls', 'img::attr(src)')
item = loader.load_item()
# 跟踪详情页
if item.get('url'):
yield response.follow(
item['url'],
callback=self.parse_detail,
meta={'item': item}
)
# 处理分页
next_page = response.css('.pagination .next::attr(href)').get()
if next_page:
yield response.follow(next_page, callback=self.parse)
def parse_detail(self, response):
"""解析商品详情"""
item = response.meta['item']
# 提取详情信息
item['description'] = response.css('.product-description').get()
item['specs'] = response.css('.product-specs').get()
item['reviews_count'] = response.css('.reviews-count::text').get()
item['rating'] = response.css('.rating::text').get()
item['crawl_time'] = datetime.now().isoformat()
yield item案例4:价格监控系统
python
import scrapy
import json
from datetime import datetime
class PriceMonitorSpider(scrapy.Spider):
"""价格监控爬虫"""
name = 'price_monitor'
def __init__(self, product_ids=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.product_ids = product_ids.split(',') if product_ids else []
def start_requests(self):
"""生成初始请求"""
for product_id in self.product_ids:
url = f'https://example.com/product/{product_id}'
yield scrapy.Request(
url,
callback=self.parse_price,
meta={'product_id': product_id}
)
def parse_price(self, response):
"""解析价格"""
product_id = response.meta['product_id']
# 提取价格
price_text = response.css('.current-price::text').get()
price = self.extract_price(price_text)
# 提取原价
original_price_text = response.css('.original-price::text').get()
original_price = self.extract_price(original_price_text)
# 计算折扣
discount = None
if price and original_price and original_price > 0:
discount = round(price / original_price * 100, 2)
yield {
'product_id': product_id,
'price': price,
'original_price': original_price,
'discount': discount,
'timestamp': datetime.now().isoformat(),
'url': response.url,
}
def extract_price(self, price_text):
"""提取价格"""
if not price_text:
return None
import re
match = re.search(r'[\d.]+', price_text)
if match:
try:
return float(match.group())
except ValueError:
pass
return None案例5:图片下载管道
python
# pipelines.py
import scrapy
from scrapy.pipelines.images import ImagesPipeline
from urllib.parse import urlparse
class ProductImagesPipeline(ImagesPipeline):
"""商品图片管道"""
def get_media_requests(self, item, info):
"""获取图片下载请求"""
for image_url in item.get('image_urls', []):
yield scrapy.Request(
image_url,
meta={'product_id': item.get('product_id')}
)
def file_path(self, request, response=None, info=None, *, item=None):
"""生成文件路径"""
product_id = request.meta.get('product_id', 'unknown')
# 从URL提取文件名
url = urlparse(request.url)
filename = url.path.split('/')[-1]
# 按商品ID分目录
return f'products/{product_id}/{filename}'
def item_completed(self, results, item, info):
"""处理完成"""
image_paths = [x['path'] for ok, x in results if ok]
if image_paths:
item['image_paths'] = image_paths
return item
# settings.py
ITEM_PIPELINES = {
'myproject.pipelines.ProductImagesPipeline': 300,
}
IMAGES_STORE = './images'
IMAGES_MIN_HEIGHT = 200
IMAGES_MIN_WIDTH = 200案例6:数据存储与分析
python
# pipelines.py
import pymysql
from datetime import datetime
class EcommercePipeline:
"""电商数据管道"""
def __init__(self, db_config):
self.db_config = db_config
self.connection = None
@classmethod
def from_crawler(cls, crawler):
db_config = {
'host': crawler.settings.get('MYSQL_HOST', 'localhost'),
'port': crawler.settings.getint('MYSQL_PORT', 3306),
'user': crawler.settings.get('MYSQL_USER', 'root'),
'password': crawler.settings.get('MYSQL_PASSWORD', ''),
'database': crawler.settings.get('MYSQL_DATABASE', 'ecommerce'),
}
return cls(db_config)
def open_spider(self, spider):
"""连接数据库"""
self.connection = pymysql.connect(**self.db_config)
self.create_tables()
def close_spider(self, spider):
"""关闭连接"""
if self.connection:
self.connection.close()
def create_tables(self):
"""创建表"""
cursor = self.connection.cursor()
# 商品表
cursor.execute('''
CREATE TABLE IF NOT EXISTS products (
id INT AUTO_INCREMENT PRIMARY KEY,
product_id VARCHAR(50) UNIQUE,
name VARCHAR(255),
price DECIMAL(10,2),
original_price DECIMAL(10,2),
sales INT,
rating DECIMAL(3,2),
url VARCHAR(500),
crawl_time DATETIME,
INDEX idx_product_id (product_id),
INDEX idx_crawl_time (crawl_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
''')
# 价格历史表
cursor.execute('''
CREATE TABLE IF NOT EXISTS price_history (
id INT AUTO_INCREMENT PRIMARY KEY,
product_id VARCHAR(50),
price DECIMAL(10,2),
timestamp DATETIME,
INDEX idx_product_id (product_id),
INDEX idx_timestamp (timestamp)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
''')
self.connection.commit()
def process_item(self, item, spider):
"""存储数据"""
cursor = self.connection.cursor()
# 插入或更新商品
sql = '''
INSERT INTO products (product_id, name, price, original_price, sales, rating, url, crawl_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
price = VALUES(price),
original_price = VALUES(original_price),
sales = VALUES(sales),
rating = VALUES(rating),
crawl_time = VALUES(crawl_time)
'''
cursor.execute(sql, (
item.get('product_id'),
item.get('name'),
item.get('price'),
item.get('original_price'),
item.get('sales'),
item.get('rating'),
item.get('url'),
item.get('crawl_time'),
))
# 记录价格历史
if item.get('price'):
sql = '''
INSERT INTO price_history (product_id, price, timestamp)
VALUES (%s, %s, %s)
'''
cursor.execute(sql, (
item.get('product_id'),
item.get('price'),
datetime.now(),
))
self.connection.commit()
return item案例7:数据分析与可视化
python
import pandas as pd
import matplotlib.pyplot as plt
import pymysql
class PriceAnalyzer:
"""价格分析器"""
def __init__(self, db_config):
self.connection = pymysql.connect(**db_config)
def get_price_history(self, product_id):
"""获取价格历史"""
sql = '''
SELECT price, timestamp
FROM price_history
WHERE product_id = %s
ORDER BY timestamp
'''
df = pd.read_sql(sql, self.connection, params=[product_id])
return df
def plot_price_trend(self, product_id):
"""绘制价格趋势图"""
df = self.get_price_history(product_id)
if df.empty:
print(f'没有找到商品 {product_id} 的价格数据')
return
plt.figure(figsize=(12, 6))
plt.plot(df['timestamp'], df['price'], marker='o')
plt.title(f'商品 {product_id} 价格趋势')
plt.xlabel('时间')
plt.ylabel('价格')
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig(f'price_trend_{product_id}.png')
plt.show()
def analyze_price_changes(self, product_id):
"""分析价格变动"""
df = self.get_price_history(product_id)
if df.empty:
return None
analysis = {
'product_id': product_id,
'current_price': df['price'].iloc[-1],
'min_price': df['price'].min(),
'max_price': df['price'].max(),
'avg_price': df['price'].mean(),
'price_changes': len(df) - 1,
'first_recorded': df['timestamp'].iloc[0],
'last_recorded': df['timestamp'].iloc[-1],
}
return analysis
def close(self):
"""关闭连接"""
self.connection.close()
# 使用示例
db_config = {
'host': 'localhost',
'user': 'root',
'password': 'password',
'database': 'ecommerce',
}
analyzer = PriceAnalyzer(db_config)
# 分析价格变动
analysis = analyzer.analyze_price_changes('12345')
if analysis:
print(f'商品ID: {analysis["product_id"]}')
print(f'当前价格: ¥{analysis["current_price"]}')
print(f'最低价格: ¥{analysis["min_price"]}')
print(f'最高价格: ¥{analysis["max_price"]}')
print(f'平均价格: ¥{analysis["avg_price"]:.2f}')
# 绘制价格趋势
analyzer.plot_price_trend('12345')
analyzer.close()课后练习
练习1:爬取京东商品
实现一个爬取京东商品信息的爬虫。
练习2:实现价格提醒
实现一个价格低于阈值时发送提醒的功能。
练习3:竞品分析
实现一个竞品价格对比分析系统。
常见问题
Q1: 如何处理电商网站的反爬?
A: 使用 Selenium 模拟浏览器、使用代理 IP、控制请求频率、处理验证码。
Q2: 如何处理商品图片?
A: 使用 Scrapy 的 ImagesPipeline,配置图片存储路径。
Q3: 如何保证数据准确性?
A: 数据清洗、数据验证、异常处理、定期更新。