在电商数据采集领域,淘宝作为国内最大的电商平台之一,其数据采集面临着复杂的反爬机制和分页限制。本文将深入探讨如何通过技术手段突破这些限制,实现淘宝商品详情数据的高效采集。
1. 淘宝反爬机制分析
淘宝的反爬体系主要包括以下几个方面:
- 请求签名验证:所有 API 请求需要通过特定算法生成签名
- 频率限制:单 IP 请求频率超过阈值会触发验证码或封禁
- 设备指纹:分析请求头信息识别异常请求
- Cookie/Session 管理:需要维护有效的会话状态
- 动态页面渲染:部分数据通过 JavaScript 动态加载
2. 分页采集策略
淘宝商品列表通常采用分页展示,每页限制 20-40 条记录。高级分页策略包括:
- 多维度分页:结合时间、分类、销量等维度
- 智能断点续传:记录已采集页码,支持中断后继续
- 自适应分页大小:根据 API 限制动态调整每页请求量
- 异步并发采集:使用协程或多线程提高采集效率
3. 核心代码实现
以下是一个完整的淘宝商品详情采集解决方案,包含反爬处理和分页策略:
import requests
import time
import random
import hashlib
import json
import logging
import base64
import re
from urllib.parse import urlencode
from concurrent.futures import ThreadPoolExecutor, as_completed
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class TaobaoSpider:
def __init__(self):
# 基础配置
self.session = requests.Session()
self.ua_list = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Safari/605.1.15"
]
self.headers = {
"User-Agent": random.choice(self.ua_list),
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Connection": "keep-alive",
}
# 反爬配置
self.cookies = {}
self.token = None
self.x_csrf_token = None
self.proxy_pool = [] # 代理IP池
self.current_proxy = None
# 分页配置
self.page_size = 20 # 默认每页20条
self.max_retries = 3 # 最大重试次数
self.delay = (1, 3) # 请求间隔范围(秒)
def load_proxy_pool(self, proxy_file="proxies.txt"):
"""加载代理IP池"""
try:
with open(proxy_file, "r") as f:
self.proxy_pool = [line.strip() for line in f.readlines() if line.strip()]
logger.info(f"成功加载 {len(self.proxy_pool)} 个代理IP")
except Exception as e:
logger.error(f"加载代理失败: {e}")
def get_random_proxy(self):
"""获取随机代理"""
if not self.proxy_pool:
return None
return random.choice(self.proxy_pool)
def rotate_proxy(self):
"""切换代理"""
self.current_proxy = self.get_random_proxy()
logger.info(f"切换代理: {self.current_proxy}")
return self.current_proxy
def generate_sign(self, data, timestamp, token):
"""生成请求签名"""
# 淘宝签名算法通常涉及token、时间戳和请求参数
# 这里是简化示例,实际需要逆向分析淘宝JS代码
sign_str = f"{token}{timestamp}{json.dumps(data)}"
return hashlib.md5(sign_str.encode()).hexdigest()
def update_headers(self):
"""更新请求头,添加必要信息"""
self.headers["User-Agent"] = random.choice(self.ua_list)
if self.x_csrf_token:
self.headers["x-csrf-token"] = self.x_csrf_token
if self.cookies:
self.session.cookies.update(self.cookies)
def handle_captcha(self, response):
"""处理验证码"""
# 检测是否需要验证码
if "verify" in response.url or "captcha" in response.text:
logger.warning("触发验证码,需要人工处理或使用打码平台")
# 这里可以集成打码平台API
return False
return True
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=10),
retry=retry_if_exception_type((requests.exceptions.RequestException,))
)
def send_request(self, method, url, params=None, data=None, headers=None):
"""发送请求,带重试和代理机制"""
if not headers:
headers = self.headers
# 使用代理
proxies = None
if self.current_proxy:
proxies = {
"http": f"http://{self.current_proxy}",
"https": f"https://{self.current_proxy}"
}
# 添加随机延迟,避免请求过于频繁
delay = random.uniform(*self.delay)
logger.debug(f"等待 {delay:.2f} 秒后发送请求")
time.sleep(delay)
try:
response = self.session.request(
method, url, params=params, data=data,
headers=headers, proxies=proxies, timeout=15
)
# 检查状态码
response.raise_for_status()
# 处理验证码
if not self.handle_captcha(response):
# 切换代理并重试
self.rotate_proxy()
raise requests.exceptions.RequestException("需要验证码")
# 更新cookies
self.cookies.update(dict(response.cookies))
return response
except requests.exceptions.RequestException as e:
logger.error(f"请求异常: {e},尝试切换代理")
self.rotate_proxy()
raise # 重新抛出异常,触发重试
def login(self, username, password):
"""模拟登录获取有效会话"""
logger.info("开始登录淘宝...")
# 访问登录页面,获取初始cookies
login_page_url = "https://login.taobao.com/member/login.jhtml"
self.send_request("GET", login_page_url)
# 登录API
login_api = "https://login.taobao.com/member/loginByPassword.jhtml"
data = {
"TPL_username": username,
"TPL_password": password,
# 实际登录需要处理加密和验证码
}
response = self.send_request("POST", login_api, data=data)
if "登录成功" in response.text:
logger.info("登录成功")
# 提取token等信息
self.token = self.extract_token(response.text)
self.x_csrf_token = self.extract_csrf_token(response.text)
return True
else:
logger.error("登录失败")
return False
def extract_token(self, html):
"""从HTML中提取token"""
# 实际需要根据淘宝页面结构调整
match = re.search(r'"token":"(.*?)"', html)
return match.group(1) if match else None
def extract_csrf_token(self, html):
"""从HTML中提取CSRF token"""
match = re.search(r'name="_csrf_token" value="(.*?)"', html)
return match.group(1) if match else None
def get_item_detail(self, item_id):
"""获取商品详情"""
logger.info(f"获取商品详情: {item_id}")
# 构建商品详情API URL
url = f"https://h5api.m.taobao.com/h5/mtop.taobao.detail.getdetail/6.0/"
# 构建请求参数
data = {
"itemNumId": item_id,
"detail_v": "3.1.1",
"ut_sk": "xx", # 用户token
"type": "jsonp",
"callback": "mtopjsonp1"
}
# 生成时间戳和签名
timestamp = int(time.time() * 1000)
sign = self.generate_sign(data, timestamp, self.token)
# 构建请求参数
params = {
"jsv": "2.6.1",
"appKey": "12574478",
"t": timestamp,
"sign": sign,
"api": "mtop.taobao.detail.getdetail",
"v": "6.0",
"dataType": "jsonp",
"callback": "mtopjsonp1",
"data": json.dumps(data)
}
# 发送请求
response = self.send_request("GET", url, params=params)
# 解析JSONP响应
json_str = response.text
json_str = re.search(r'mtopjsonp1((.*))', json_str).group(1)
try:
result = json.loads(json_str)
return result.get("data", {})
except json.JSONDecodeError as e:
logger.error(f"解析商品详情失败: {e}")
return None
def get_item_list(self, keyword, page=1):
"""获取商品列表(分页)"""
logger.info(f"获取商品列表: 关键词={keyword}, 页码={page}")
# 淘宝搜索API
url = "https://s.taobao.com/search"
# 构建请求参数
params = {
"q": keyword,
"s": (page - 1) * self.page_size, # 偏移量
"ie": "utf8",
"ajax": "true"
}
# 发送请求
response = self.send_request("GET", url, params=params)
try:
result = response.json()
items = result.get("mods", {}).get("itemlist", {}).get("data", {}).get("auctions", [])
return items
except json.JSONDecodeError:
# 可能需要处理HTML格式响应
items = self.parse_item_list_html(response.text)
return items
def parse_item_list_html(self, html):
"""解析HTML格式的商品列表"""
# 使用正则表达式或BeautifulSoup解析商品信息
items = []
# 这里是简化示例,实际需要根据淘宝页面结构调整
item_pattern = re.compile(r'"nid":"(.*?)","title":"(.*?)","price":"(.*?)"')
for match in item_pattern.finditer(html):
item_id, title, price = match.groups()
items.append({
"item_id": item_id,
"title": title,
"price": price
})
return items
def crawl_items_by_keyword(self, keyword, max_pages=10):
"""根据关键词采集商品信息"""
all_items = []
for page in range(1, max_pages + 1):
try:
items = self.get_item_list(keyword, page)
if not items:
logger.info(f"第 {page} 页没有商品,结束采集")
break
all_items.extend(items)
logger.info(f"成功获取第 {page} 页,共 {len(items)} 个商品")
# 处理每个商品详情(异步)
self.process_items_concurrently(items)
# 随机延迟,避免被反爬
time.sleep(random.uniform(2, 5))
except Exception as e:
logger.error(f"采集第 {page} 页失败: {e}")
# 发生异常时适当延长等待时间
time.sleep(random.uniform(5, 10))
return all_items
def process_items_concurrently(self, items, max_workers=5):
"""并发处理商品详情"""
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = []
for item in items:
item_id = item.get("nid") or item.get("item_id")
if item_id:
futures.append(executor.submit(self.process_item_detail, item_id))
# 等待所有任务完成
for future in as_completed(futures):
try:
future.result()
except Exception as e:
logger.error(f"处理商品详情失败: {e}")
def process_item_detail(self, item_id):
"""处理单个商品详情"""
detail = self.get_item_detail(item_id)
if detail:
# 保存商品详情到数据库
self.save_item_detail(detail)
return True
return False
def save_item_detail(self, detail):
"""保存商品详情到数据库"""
# 这里应该实现数据库存储逻辑
# 简化示例,打印商品标题和价格
item_info = detail.get("itemInfoModel", {})
logger.info(f"保存商品: {item_info.get('title')} - {item_info.get('price')}")
# 实际应用中,这里应该将数据存入数据库
# 例如:
# with self.db_connection.cursor() as cursor:
# sql = "INSERT INTO items VALUES (%s, %s, %s)"
# cursor.execute(sql, (item_id, title, price))
# self.db_connection.commit()
# 使用示例
if __name__ == "__main__":
spider = TaobaoSpider()
# 加载代理池(如果有)
spider.load_proxy_pool()
# 可选:登录获取更完整的数据
# spider.login("your_username", "your_password")
# 开始采集
keyword = "手机"
spider.crawl_items_by_keyword(keyword, max_pages=5)
4. 高级反爬突破技巧
-
Cookie/Session 管理
- 维护长期有效的会话状态
- 定期刷新 Cookie,避免过期
- 分析 Cookie 生成规则,模拟生成
-
请求头定制
- 随机更换 User-Agent,模拟不同浏览器
- 添加必要的请求头字段(如 Referer、Accept 等)
- 分析请求头指纹,确保一致性
-
动态内容处理
- 使用 Selenium/WebDriver 处理 JavaScript 渲染内容
- 逆向分析 JS 代码,提取数据加载逻辑
- 直接调用 AJAX 接口获取数据
-
分布式采集
- 使用代理 IP 池分散请求源
- 部署多节点采集系统,分担压力
- 实现任务队列,均衡负载
5. 分页优化策略
- 智能分页控制
def smart_pagination(self, keyword):
"""智能分页策略,自动检测结束条件"""
page = 1
while True:
items = self.get_item_list(keyword, page)
if not items:
break
# 处理商品
self.process_items_concurrently(items)
# 检查是否有下一页
if not self.has_next_page(items):
break
page += 1
# 控制采集频率
time.sleep(random.uniform(2, 5))
2.断点续传实现
def resume_crawling(self, keyword, last_page=1):
"""从指定页码开始继续采集"""
logger.info(f"从第 {last_page} 页开始继续采集")
for page in range(last_page, 100): # 设置最大页数限制
items = self.get_item_list(keyword, page)
if not items:
break
# 处理商品
self.process_items_concurrently(items)
# 记录当前页码
self.save_crawler_state(keyword, page)
# 控制采集频率
time.sleep(random.uniform(2, 5))
6. 数据存储与分析
- 数据库设计
CREATE TABLE `taobao_items` (
`id` BIGINT PRIMARY KEY,
`title` TEXT NOT NULL,
`price` DECIMAL(10, 2) NOT NULL,
`original_price` DECIMAL(10, 2),
`sales_volume` INT,
`seller_id` VARCHAR(50),
`category_id` VARCHAR(50),
`brand` VARCHAR(100),
`keywords` VARCHAR(255),
`crawl_time` DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE `taobao_item_details` (
`item_id` BIGINT PRIMARY KEY,
`description` TEXT,
`specifications` JSON,
`images` JSON,
`reviews_count` INT,
`good_review_rate` DECIMAL(5, 2),
`attributes` JSON,
`crawl_time` DATETIME DEFAULT CURRENT_TIMESTAMP
);
2.数据分析示例
def analyze_prices(self, items):
"""分析商品价格分布"""
prices = [float(item.get("price", 0)) for item in items if item.get("price")]
if not prices:
return
avg_price = sum(prices) / len(prices)
min_price = min(prices)
max_price = max(prices)
logger.info(f"价格分析: 平均={avg_price:.2f}, 最低={min_price:.2f}, 最高={max_price:.2f}")
# 可以进一步分析价格区间分布
price_ranges = {
"0-100": 0, "100-500": 0, "500-1000": 0,
"1000-2000": 0, "2000+": 0
}
for price in prices:
if price < 100:
price_ranges["0-100"] += 1
elif price < 500:
price_ranges["100-500"] += 1
elif price < 1000:
price_ranges["500-1000"] += 1
elif price < 2000:
price_ranges["1000-2000"] += 1
else:
price_ranges["2000+"] += 1
logger.info(f"价格区间分布: {price_ranges}")
7. 部署与运维建议
- 配置环境变量
# .env 文件示例
PROXY_ENABLED=True
PROXY_FILE=proxies.txt
LOG_LEVEL=INFO
MAX_WORKERS=5
DELAY_MIN=1
DELAY_MAX=3
2.异常监控与告警
def setup_monitoring(self):
"""设置监控和告警"""
# 集成Sentry监控异常
import sentry_sdk
sentry_sdk.init(
dsn="https://your-sentry-dsn@example.com/1",
traces_sample_rate=1.0,
)
# 设置邮件告警
self.alert_email = "admin@example.com"
3.性能优化
- 使用异步请求库(如 aiohttp)替代 requests
- 实现连接池复用 HTTP 连接
- 优化数据库插入操作,使用批量插入
8. 合规与伦理考量
- 遵守 robots.txt 规则
- 控制采集频率,避免影响目标网站性能
- 仅用于合法的数据分析和研究目的
- 妥善保护采集到的数据,避免泄露
总结
通过本文介绍的技术和策略,开发者可以有效突破淘宝的反爬机制和分页限制,实现高效、稳定的商品数据采集。关键要点包括:
- 深入分析目标网站的反爬机制
- 实现智能的分页采集策略
- 构建健壮的反爬应对体系
- 合理设计数据存储与分析方案
- 确保采集行为的合规性
实际应用中,应根据淘宝网站的更新及时调整采集策略,保持代码的灵活性和可维护性。通过持续优化,可以构建出一套稳定、高效的电商数据采集系统。