㊙️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~持续更新中!
㊗️爬虫难度指数:⭐⭐
🚫声明:本数据&代码仅供学习交流,严禁用于商业用途、倒卖数据或违反目标站点的服务条款等,一切后果皆由使用者本人承担。公开榜单数据一般允许访问,但请务必遵守"君子协议",技术无罪,责任在人。

全文目录:
-
- [🌟 开篇语](#🌟 开篇语)
- [1️⃣ 摘要(Abstract)](#1️⃣ 摘要(Abstract))
- [2️⃣ 背景与需求(Why)](#2️⃣ 背景与需求(Why))
- [3️⃣ 合规与注意事项(必写)](#3️⃣ 合规与注意事项(必写))
- [4️⃣ 技术选型与整体流程(What/How)](#4️⃣ 技术选型与整体流程(What/How))
- [5️⃣ 环境准备与依赖安装(可复现)](#5️⃣ 环境准备与依赖安装(可复现))
- [6️⃣ 核心实现:请求层(Fetcher)](#6️⃣ 核心实现:请求层(Fetcher))
-
- 配置文件(config.py)
- [京东请求层(jd_scraper.py - 第一部分)](#京东请求层(jd_scraper.py - 第一部分))
- [7️⃣ 核心实现:解析层(Parser)](#7️⃣ 核心实现:解析层(Parser))
-
- [京东解析器(jd_scraper.py - 第二部分)](#京东解析器(jd_scraper.py - 第二部分))
- 淘宝爬虫(taobao_scraper.py)
- [8️⃣ 数据存储与导出(Storage)](#8️⃣ 数据存储与导出(Storage))
- [9️⃣ 运行方式与结果展示(必写)](#9️⃣ 运行方式与结果展示(必写))
- [🔟 常见问题与排错](#🔟 常见问题与排错)
-
- [Q1: 京东返回空页面或"没有找到"?](#Q1: 京东返回空页面或"没有找到"?)
- [Q2: 淘宝Selenium被检测到"自动化控制"?](#Q2: 淘宝Selenium被检测到"自动化控制"?)
- [Q3: XPath找不到元素,但浏览器能看到?](#Q3: XPath找不到元素,但浏览器能看到?)
- [Q4: 价格解析错误,出现0.0或异常值?](#Q4: 价格解析错误,出现0.0或异常值?)
- [Q5: SQLite报错"database is locked"?](#Q5: SQLite报错"database is locked"?)
- [Q6: 淘宝只能爬前3页?](#Q6: 淘宝只能爬前3页?)
- [1️⃣1️⃣ 进阶优化](#1️⃣1️⃣ 进阶优化)
- [1️⃣2️⃣ 总结与延伸阅读](#1️⃣2️⃣ 总结与延伸阅读)
- [🌟 文末](#🌟 文末)
-
- [📌 专栏持续更新中|建议收藏 + 订阅](#📌 专栏持续更新中|建议收藏 + 订阅)
- [✅ 互动征集](#✅ 互动征集)
🌟 开篇语
哈喽,各位小伙伴们你们好呀~我是【喵手】。
运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO
欢迎大家常来逛逛,一起学习,一起进步~🌟
我长期专注 Python 爬虫工程化实战 ,主理专栏 《Python爬虫实战》:从采集策略 到反爬对抗 ,从数据清洗 到分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上。
📌 专栏食用指南(建议收藏)
- ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
- ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
- ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
- ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用
📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅/关注专栏👉《Python爬虫实战》👈
💕订阅后更新会优先推送,按目录学习更高效💯~
1️⃣ 摘要(Abstract)
目标:使用Python爬取京东和淘宝搜索结果页的商品数据(以"机械键盘"为例),支持多页翻页,提取商品名、价格、店铺、评论数等核心字段,最终存储为SQLite数据库并导出CSV文件。
你将获得:
- 掌握电商平台两大典型反爬场景的应对策略:京东的API接口逆向 vs 淘宝的动态渲染对抗
- 学会处理分页逻辑、价格格式清洗、评论数转换等电商数据特有的工程问题
- 构建可扩展的爬虫架构,轻松迁移到其他搜索场景(拼多多、苏宁等)
2️⃣ 背景与需求(Why)
为什么要爬取电商搜索数据?
作为一名长期关注数码产品的消费者和数据分析爱好者,我在选购商品时经常需要对比不同平台的价格、销量、用户评价。手动浏览几十页商品列表效率太低,而且无法直观看出价格趋势、热门店铺分布等规律。
更重要的是,电商搜索数据包含丰富的商业价值:
- 价格监控:追踪同一商品在不同时段的价格波动,找到最佳购买时机
- 竞品分析:对比同类商品的定价策略、销量分布,辅助商家决策
- 市场趋势:分析某品类的品牌集中度、用户偏好变化
京东和淘宝作为国内电商双巨头,它们的搜索结果覆盖了绝大多数商品类目,是数标。
目标站点与字段清单
主站点 :京东搜索(https://search.jd.com/Search?keyword=xxx)
对比站点 :淘宝搜索(https://s.taobao.com/search?q=xxx)
目标字段:
| 字段名称 | 数据类型 | 示例值 | 说明 |
|---|---|---|---|
| platform | VARCHAR(20) | "JD" / "Taobao" | 平台标识 |
| product_name | TEXT | "Cherry MX8.0机械键盘" | 商品标题 |
| price | DECIMAL(10,2) | 599.00 | 当前价格(元) |
| original_price | DECIMAL(10,2) | 899.00 | 原价(如有) |
| shop_name | VARCHAR(200) | "Cherry官方旗舰店" | 店铺名称 |
| comment_count | INTEGER | 15000 | 评论数/销量 |
| product_url | TEXT | "https://item.jd.com/xxx.html" | 商品详情页链接 |
| image_url | TEXT | "https://img.jd.com/xxx.jpg" | 商品主图URL |
| sku_id | VARCHAR(50) | "100012345678" | 商品唯一ID |
3️⃣ 合规与注意事项(必写)
robots.txt基本说明
电商平台对爬虫的态度较为复杂:
- 京东:robots.txt允许抓取搜索结果页,但禁止高频访问
- 淘宝:robots.txt禁止大部分爬虫,但实际执行依赖反爬策略
python
# 京东 robots.txt(简化版)
User-agent: *
Disallow: /cart/
Disallow: /order/
Allow: /search/
# 淘宝 robots.txt(更严格)
User-agent: *
Disallow: /
重要提醒 :虽然技术上可以绕过robots限制,但并不意味着法律上允许。我们的爬虫仅用于个人学习和价格对比,严禁用于:
- 批量倒卖数据
- 恶意竞价监控
- 干扰平台正常运营
频率控制的生死线
电商平台的反爬系统非常敏感,我在实测中发现:
京东:
- 单IP每秒请求超过2次 → 返回空数据
- 短时间内(5分钟)请求超过100次 → 触发验证码
- 连续多次403 → IP被封禁1-24小时
淘宝:
- 未登录状态只能查看前2-3页
- 使用Selenium时必须加载完整页面(包括图片),否则被识别
- 滑块验证码出现频率极高
安全建议:
- 单线程爬取,每页间隔3-5秒
- 使用代理IP池(推荐快代理、芝麻代理等服务)
- 设置合理的超时和重试次数
- 避开流量高峰期(上午10-12点,晚上8-10点)
数据使用边界
我们只爬取公开展示的搜索结果,不涉及:
- 需要登录才能看到的会员价
- 商家后台的销售数据
- 用户的购物车、订单信息
- 评论中的用户昵称、头像等隐私信息
4️⃣ 技术选型与整体流程(What/How)
网站类型判断
通过开发者工具分析两大平台的架构差异:
京东搜索:
- 列表页是服务端渲染(SSR),HTML中直接包含商品基本信息
- 价格数据通过异步API接口 加载(https://p.3.cn/prices/...)
- 分页参数清晰:
&page=2&s=30
淘宝搜索:
- 整个页面是React单页应用(SPA),初始HTML几乎为空
- 数据通过加密的API接口返回,参数包含sign签名
- 必须执行JavaScript才能看到商品列表
结论:
- 京东用 requests + lxml + API逆向(效率高)
- 淘宝用 Selenium + Stealth插件(稳定但慢)
技术栈选择理由
json
核心库:
requests==2.31.0 # 京东接口请求
lxml==5.1.0 # 京东页面解析
selenium==4.16.0 # 淘宝动态渲染
selenium-stealth==1.0.6 # 绕过webdriver检测
fake-useragent==1.4.0 # UA随机生成
Pillow==10.1.0 # 验证码处理(备用)
存储:
sqlite3(内置) # 轻量数据库
pandas==2.1.4 # CSV导出
为什么不全用Selenium?
虽然Selenium通用性更好,但速度比requests慢10-20倍。京东的搜索页面已经足够友好,没必要杀鸡用牛刀。
为什么不用Scrapy?
Scrapy在处理异步API和动态页面时需要配置复杂的中间件,对于这个中小型项目,灵活性反而不如原生requests。
整体流程设计
json
┌──────────────┐
│ 用户输入关键词 │ ("机械键盘")
└───────┬──────┘
│
┌───┴────┐
│ 京东分支 │
└───┬────┘
│
├─→ ┌──────────────┐
│ │构造搜索URL │ (keyword + page)
│ └──────┬───────┘
│ │
│ ┌──────▼───────┐
│ │请求列表页 │ (requests.get)
│ └──────┬───────┘
│ │
│ ┌──────▼───────┐
│ │解析商品ID │ (XPath提取SKU)
│ └──────┬───────┘
│ │
│ ┌──────▼───────┐
│ │请求价格API │ (批量获取价格)
│ └──────┬───────┘
│ │
│ ┌──────▼───────┐
│ │组装完整数据 │
│ └──────┬───────┘
│ │
┌───┴────┐ │
│ 淘宝分支 │ │
└───┬────┘ │
│ │
├─→ ┌──────▼───────┐
│ │启动Selenium │
│ └──────┬───────┘
│ │
│ ┌──────▼───────┐
│ │输入关键词搜索 │
│ └──────┬───────┘
│ │
│ ┌──────▼───────┐
│ │等待页面加载 │ (WebDriverWait)
│ └──────┬───────┘
│ │
│ ┌──────▼───────┐
│ │提取商品元素 │ (find_elements)
│ └──────┬───────┘
│ │
│ ┌──────▼───────┐
│ │点击下一页 │ (循环翻页)
│ └──────┬───────┘
│ │
└──────────┴───────────┐
│
┌──────▼───────┐
│数据清洗 │ (价格/评论数)
└──────┬───────┘
│
┌──────▼───────┐
│去重 │ (SKU/URL)
└──────┬───────┘
│
┌──────▼───────┐
│存入SQLite │
└──────┬───────┘
│
┌──────▼───────┐
│导出CSV │
└──────────────┘
5️⃣ 环境准备与依赖安装(可复现)
Python版本要求
bash
Python 3.9+ # 本教程在Python 3.11.5测试通过
依赖安装
创建虚拟环境:
bash
python -m venv ecommerce_env
source ecommerce_env/bin/activate # Windows: ecommerce_env\Scripts\activate
安装依赖:
bash
pip install requests==2.31.0
pip install lxml==5.1.0
pip install selenium==4.16.0
pip install selenium-stealth==1.0.6
pip install fake-useragent==1.4.0
pip install pandas==2.1.4
pip install Pillow==10.1.0
安装Chrome浏览器驱动(Selenium必需):
bash
# 方式1:自动下载(推荐)
pip install webdriver-manager==4.0.1
# 方式2:手动下载
# 访问 https://chromedriver.chromium.org/
# 下载对应Chrome版本的驱动,放到PATH路径
项目目录结构
json
ecommerce_scraper/
│
├── config.py # 配置参数
├── jd_scraper.py # 京东爬虫
├── taobao_scraper.py # 淘宝爬虫
├── data_cleaner.py # 数据清洗
├── storage.py # 数据库操作
├── main.py # 主入口
├── requirements.txt # 依赖清单
│
├── data/
│ ├── products.db # SQLite数据库
│ └── products.csv # CSV导出
│
├── logs/
│ └── scraper.log # 运行日志
│
└── screenshots/ # Selenium截图(调试用)
└── error_*.png
创建目录:
bash
mkdir -p ecommerce_scraper/{data,logs,screenshots}
cd ecommerce_scraper
6️⃣ 核心实现:请求层(Fetcher)
配置文件(config.py)
python
import random
from fake_useragent import UserAgent
# ========== 通用配置 ==========
KEYWORD = "机械键盘" # 搜索关键词
MAX_PAGES = 20 # 爬取页数
TIMEOUT = 15 # 请求超时(秒)
RETRY_TIMES = 3 # 失败重试次数
DELAY_RANGE = (3, 6) # 请求间隔(秒)
# ========== 京东配置 ==========
JD_SEARCH_URL = "https://search.jd.com/Search"
JD_PRICE_API = "https://p.3.cn/prices/mgets" # 价格接口
JD_HEADERS = {
'User-Agent': UserAgent().random,
'Referer': 'https://www.jd.com/',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Connection': 'keep-alive'
}
# ========== 淘宝配置 ==========
TB_SEARCH_URL = "https://s.taobao.com/search"
TB_HEADERS = {
'User-Agent': UserAgent().random,
'Referer': 'https://www.taobao.com/',
'Accept-Language': 'zh-CN,zh;q=0.9'
}
# ========== Selenium配置 ==========
CHROME_OPTIONS = [
'--headless', # 无头模式(可注释掉以调试)
'--disable-gpu',
'--no-sandbox',
'--disable-dev-shm-usage',
'--disable-blink-features=AutomationControlled',
'--window-size=1920,1080',
f'--user-agent={UserAgent().random}'
]
# ========== 数据库配置 ==========
DB_PATH = 'data/products.db'
CSV_PATH = 'data/products.csv'
京东请求层(jd_scraper.py - 第一部分)
python
import requests
import time
import random
import logging
from lxml import etree
from typing import List, Dict, Optional
from urllib.parse import quote
from config import *
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - [%(levelname)s] - %(message)s',
handlers=[
logging.FileHandler('logs/scraper.log', encoding='utf-8'),
logging.StreamHandler()
]
)
class JDScraper:
"""京东搜索爬虫"""
def __init__(self):
self.session = requests.Session()
self.session
"""随机延迟"""
delay = random.uniform(*DELAY_RANGE)
logging.debug(f"延迟 {delay:.2f} 秒")
time.sleep(delay)
def search_products(self, keyword: str, page: int = 1) -> Optional[str]:
"""
搜索商品列表页
Args:
keyword: 搜索关键词
page: 页码(从1开始)
Returns:
HTML文本 或 None
"""
# 计算偏移量:京东每页显示30个商品,s参数表示起始位置
s_param = (page - 1) * 30 + 1
params = {
'keyword': keyword,
'page': page,
's': s_param,
'click': 0
}
for attempt in range(RETRY_TIMES):
try:
response = self.session.get(
JD_SEARCH_URL,
params=params,
timeout=TIMEOUT
)
if response.status_code == 200:
# 检查是否被反爬(返回空页面)
if '没有找到' in response.text or len(response.text) < 1000:
logging.warning(f"京东返回空结果,可能被限流")
time.sleep(10)
continue
logging.info(f"✓ 成功获取京东第 {page} 页")
self._random_delay()
return response.text
elif response.status_code == 302:
logging.warning(f"触发302重定向,可能需要验证")
time.sleep(30)
else:
logging.error(f"状态码异常: {response.status_code}")
except requests.Timeout:
logging.error(f"请求超时(尝试 {attempt+1}/{RETRY_TIMES})")
time.sleep(5)
except Exception as e:
logging.error(f"请求异常: {e}")
return None
def get_prices(self, sku_ids: List[str]) -> Dict[str, float]:
"""
批量获取价格(调用京东价格API)
Args:
sku_ids: 商品SKU列表,如 ['100012345678', '100087654321']
Returns:
{sku: price} 字典
"""
# 构造API参数:skuIds=J_100012345678,J_100087654321
skuids_param = ','.join([f'J_{sku}' for sku in sku_ids])
params = {
'skuIds': skuids_param,
'type': 1 # 1=普通价格,2=秒杀价
}
try:
response = self.session.get(
JD_PRICE_API,
params=params,
timeout=10
)
if response.status_code == 200:
data = response.json()
# 返回格式:[{"id":"J_100012345678","p":"599.00"}]
prices = {}
for item in data:
sku = item['id'].replace('J_', '')
price = float(item['p'])
prices[sku] = price
logging.info(f"✓ 获取 {len(prices)} 个商品价格")
return prices
except Exception as e:
logging.error(f"价格API请求失败: {e}")
return {}
7️⃣ 核心实现:解析层(Parser)
京东解析器(jd_scraper.py - 第二部分)
python
def parse_product_list(self, html: str) -> List[Dict]:
"""
解析商品列表页
Returns:
商品数据列表
"""
products = []
tree = etree.HTML(html)
# 定位商品列表容器
items = tree.xpath('//div[@id="J_goodsList"]//li[@class="gl-item"]')
if not items:
logging.warning("未找到商品元素,XPath可能失效")
return products
logging.info(f"找到 {len(items)} 个商品")
# 收集所有SKU,用于批量获取价格
sku_list = []
for item in items:
try:
# SKU ID(核心标识)
sku = item.xpath('./@data-sku')
sku = sku[0] if sku else None
if not sku:
continue
sku_list.append(sku)
# 商品名称
name = item.xpath('.//div[@class="p-name"]//em/text()')
name = ''.join(name).strip() if name else None
# 店铺名称
shop = item.xpath('.//div[@class="p-shop"]/@data-shop_name')
shop = shop[0] if shop else None
# 评论数(需要清洗)
comment = item.xpath('.//div[@class="p-commit"]//a/text()')
comment = comment[0] if comment else "0"
comment_count = self._parse_comment_count(comment)
# 商品链接
url = item.xpath('.//div[@class="p-img"]//a/@href')
url = url[0] if url else None
if url and not url.startswith('http'):
url = 'https:' + url
# 商品图片
img = item.xpath('.//div[@class="p-img"]//img/@src | .//div[@class="p-img"]//img/@data-lazy-img')
img = img[0] if img else None
if img and not img.startswith('http'):
img = 'https:' + img
products.append({
'platform': 'JD',
'sku_id': sku,
'product_name': name,
'price稍后填充
'shop_name': shop,
'comment_count': comment_count,
'product_url': url,
'image_url': img
})
except Exception as e:
logging.error(f"解析商品出错: {e}")
continue
# 批量获取价格
if sku_list:
prices = self.get_prices(sku_list)
for product in products:
product['price'] = prices.get(product['sku_id'], 0.0)
return products
def _parse_comment_count(self, text: str) -> int:
"""
解析评论数
Examples:
"1.2万+" → 12000
"5000+" → 5000
"50条评价" → 50
"""
import re
# 去除中文字符
text = text.replace('条评价', '').replace('+', '').strip()
# 处理"万"
if '万' in text:
num = float(re.findall(r'(\d+\.?\d*)', text)[0])
return int(num * 10000)
# 直接提取数字
match = re.search(r'(\d+)', text)
return int(match.group(1)) if match else 0
淘宝爬虫(taobao_scraper.py)
python
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.options import Options
from selenium_stealth import stealth
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome.service import Service
import time
import logging
from typing import List, Dict
from config import *
class TaobaoScraper:
"""淘宝搜索爬虫(Selenium方案)"""
def __init__(self):
self.driver = None
self._init_driver()
def _init_driver(self):
"""初始化Chrome驱动"""
chrome_options = Options()
for opt in CHROME_OPTIONS:
chrome_options.add_argument(opt)
# 自动下载匹配的ChromeDriver
service = Service(ChromeDriverManager().install())
self.driver = webdriver.Chrome(service=service, options=chrome_options)
# 使用stealth插件隐藏webdriver特征
stealth(
self.driver,
languages=["zh-CN", "zh"],
vendor="Google Inc.",
platform="Win32",
webgl_vendor="Intel Inc.",
renderer="Intel Iris OpenGL Engine",
fix_hairline=True,
)
logging.info("✓ Selenium驱动初始化完成")
def search_products(self, keyword: str, max_pages: int = 3) -> List[Dict]:
"""
搜索商品(淘宝限制未登录只能看前几页)
Args:
keyword: 搜索关键词
max_pages: 最大页数(建议不超过5)
"""
all_products = []
try:
# 访问搜索页
search_url = f"{TB_SEARCH_URL}?q={quote(keyword)}"
self.driver.get(search_url)
# 等待商品加载
WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "item"))
)
for page in range(1, max_pages + 1):
logging.info(f"正在爬取淘宝第 {page} 页")
# 解析当前页
products = self._parse_current_page()
all_products.extend(products)
# 点击下一页
if page < max_pages:
if not self._click_next_page():
logging.warning("无法翻页,停止爬取")
break
time.sleep(random.uniform(2, 4))
except Exception as e:
logging.error(f"淘宝爬取异常: {e}")
self._save_screenshot("error")
return all_products
def _parse_current_page(self) -> List[Dict]:
"""解析当前页面的商品"""
products = []
# 淘宝商品元素定位(需根据实际页面调整)
items = self.driver.find_elements(By.CSS_SELECTOR, 'div.item')
logging.info(f"找到 {len(items)} 个商品")
for item in items:
try:
# 商品标题
title_elem = item.find_element(By.CSS_SELECTOR, 'a.title')
title = title_elem.text.strip()
url = title_elem.get_attribute('href')
# 价格
price_elem = item.find_element(By.CSS_SELECTOR, 'strong')
price_text = price_elem.text.strip()
price = self._parse_price(price_text)
# 店铺
shop_elem = item.find_element(By.CSS_SELECTOR, 'div.shop a')
shop = shop_elem.text.strip()
# 销量(淘宝显示为"xx人付款")
sales_elem = item.find_element(By.CSS_SELECTOR, 'div.deal-cnt')
sales_text = sales_elem.text.strip()
sales = self._parse_sales(sales_text)
# 图片
img_elem = item.find_element(By.CSS_SELECTOR, 'img')
img = img_elem.get_attribute('src')
if not img.startswith('http'):
img = 'https:' + img
products.append({
'platform': 'Taobao',
'sku_id': None, # 淘宝不直接暴露SKU
'product_name': title,
'price': price,
'shop_name': shop,
'comment_count': sales,
'product_url': url,
'image_url': img
})
except Exception as e:
logging.debug(f"解析单个商品失败: {e}")
continue
return products
def _click_next_page(self) -> bool:
"""点击下一页按钮"""
try:
next_btn = WebDriverWait(self.driver, 5).until(
EC.element_to_be_clickable((By.CSS_SELECTOR, 'button.next'))
)
next_btn.click()
# 等待新页面加载
time.sleep(2)
return True
except:
return False
def _parse_price(self, text: str) -> float:
"""解析价格:599.00 或 199-599"""
import re
numbers = re.findall(r'\d+\.?\d*', text)
return float(numbers[0]) if numbers else 0.0
def _parse_sales(self, text: str) -> int:
"""解析销量:5000人付款 → 5000"""
import re
match = re.search(r'(\d+)', text)
return int(match.group(1)) if match else 0
def _save_screenshot(self, name: str):
"""保存截图(调试用)"""
filename = f"screenshots/{name}_{int(time.time())}.png"
self.driver.save_screenshot(filename)
logging.info(f"截图已保存: {filename}")
def close(self):
"""关闭浏览器"""
if self.driver:
self.driver.quit()
logging.info("Selenium驱动已关闭")
8️⃣ 数据存储与导出(Storage)
数据库操作(storage.py)
python
import sqlite3
import pandas as pd
import logging
from typing import List, Dict
from pathlib import Path
from config import DB_PATH, CSV_PATH
class ProductStorage:
"""商品数据存储"""
def __init__(self):
Path(DB_PATH).parent.mkdir(exist_ok=True)
self.conn = sqlite3.connect(DB_PATH)
self.cursor = self.conn.cursor()
self._create_table()
def _create_table(self):
"""创建products表"""
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
platform VARCHAR(20) NOT NULL,
sku_id VARCHAR(50),
product_name TEXT NOT NULL,
price DECIMAL(10,2),
original_price DECIMAL(10,2),
shop_name VARCHAR(200),
comment_count INTEGER,
product_url TEXT UNIQUE,
image_url TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 创建索引
self.cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_platform_price
ON products(platform, price)
''')
self.cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_sku
ON products(sku_id)
''')
self.conn.commit()
logging.info("数据库表初始化完成")
def insert_products(self, products: List[Dict]) -> int:
"""
批量插入商品
Returns:
成功插入的数量
"""
success_count = 0
for product in products:
try:
self.cursor.execute('''
INSERT OR IGNORE INTO products
(platform, sku_id, product_name, price, shop_name,
comment_count, product_url, image_url)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (
product.get('platform'),
product.get('sku_id'),
product.get('product_name'),
product.get('price'),
product.get('shop_name'),
product.get('comment_count'),
product.get('product_url'),
product.get('image_url')
))
if self.cursor.rowcount > 0:
success_count += 1
except sqlite3.IntegrityError:
logging.debug(f"商品已存在: {product.get('product_url')}")
except Exception as e:
logging.error(f"插入失败: {e}")
self.conn.commit()
logging.info(f"✓ 成功插入 {success_count} 条数据")
return success_count
def export_to_csv(self):
"""导出为CSV"""
df = pd.read_sql_query('''
SELECT
platform AS 平台,
product_name AS 商品名称,
price AS 价格,
shop_name AS 店铺,
comment_count AS 评论数,
product_url AS 链接
FROM products
ORDER BY platform, price
''', self.conn)
df.to_csv(CSV_PATH, index=False, encoding='utf-8-sig')
logging.info(f"✓ 已导出 {len(df)} 条数据到 {CSV_PATH}")
def get_stats(self) -> Dict:
"""获取统计信息"""
stats = {}
# 总商品数
self.cursor.execute('SELECT COUNT(*) FROM products')
stats['total'] = self.cursor.fetchone()[0]
# 按平台统计
self.cursor.execute('''
SELECT platform, COUNT(*), AVG(price), MIN(price), MAX(price)
FROM products
GROUP BY platform
''')
platform_stats = {}
for row in self.cursor.fetchall():
platform_stats[row[0]] = {
'count': row[1],
'avg_price': round(row[2], 2) if row[2] else 0,
'min_price': row[3],
'max_price': row[4]
}
stats['by_platform'] = platform_stats
return stats
def close(self):
"""关闭连接"""
self.conn.commit()
self.conn.close()
9️⃣ 运行方式与结果展示(必写)
主程序(main.py)
python
import logging
from jd_scraper import JDScraper
from taobao_scraper import TaobaoScraper
from storage import ProductStorage
from config import KEYWORD, MAX_PAGES
def main():
"""主流程"""
logging.info("=" * 60)
logging.info(f"电商爬虫启动 | 关键词: {KEYWORD} | 页数: {MAX_PAGES}")
logging.info("=" * 60)
storage = ProductStorage()
all_products = []
# ========== 爬取京东 ==========
logging.info("\n【1/2】开始爬取京东...")
jd = JDScraper()
for page in range(1, MAX_PAGES + 1):
html = jd.search_products(KEYWORD, page)
if html:
products = jd.parse_product_list(html)
all_products.extend(products)
logging.info(f"京东第{page}页: 获取{len(products)}个商品")
else:
logging.warning(f"京东第{page}页获取失败")
break
# ========== 爬取淘宝 ==========
logging.info("\n【2/2】开始爬取淘宝...")
tb = TaobaoScraper()
try:
tb_products = tb.search_products(KEYWORD, max_pages=3)
all_products.extend(tb_products)
logging.info(f"淘宝共获取{len(tb_products)}个商品")
except Exception as e:
logging.error(f"淘宝爬取失败: {e}")
finally:
tb.close()
# ========== 存储数据 ==========
logging.info("\n开始存储数据...")
storage.insert_products(all_products)
# ========== 导出CSV ==========
storage.export_to_csv()
# ========== 输出统计 ==========
stats = storage.get_stats()
logging.info("\n" + "=" * 60)
logging.info("爬取完成!统计信息:")
logging.info(f"总商品数: {stats['total']}")
for platform, data in stats['by_platform'].items():
logging.info(f"\n{platform}:")
logging.info(f" 数量: {data['count']}")
logging.info(f" 平均价格: ¥{data['avg_price']}")
logging.info(f" 价格区间: ¥{data['min_price']} - ¥{data['max_price']}")
logging.info("=" * 60)
storage.close()
if __name__ == "__main__":
main()
启动命令
bash
# 激活虚拟环境
source ecommerce_env/bin/activate
# 运行爬虫
python main.py
运行日志示例
json
2024-01-28 16:20:15 - [INFO] - ============================================================
2024-01-28 16:20:15 - [INFO] - 电商爬虫启动 | 关键词: 机械键盘 | 页数: 20
2024-01-28 16:20:15 - [INFO] - ============================================================
2024-01-28 16:20:15 - [INFO] - 数据库表初始化完成
2024-01-28 16:20:15 - [INFO] - 【1/2】开始爬取京东...
2024-01-28 16:20:18 - [INFO] - ✓ 成功获取京东第 1 页
2024-01-28 16:20:18 - [INFO] - 找到 30 个商品
2024-01-28 16:20:19 - [INFO] - ✓ 获取 30 个商品价格
2024-01-28 16:20:19 - [INFO] - 京东第1页: 获取30个商品
...
2024-01-28 16:35:42 - [INFO] - ✓ 成功获取京东第 20 页
2024-01-28 16:35:42 - [INFO] - 京东第20页: 获取28个商品
2024-01-28 16:35:45 - [INFO] - 【2/2】开始爬取淘宝...
2024-01-28 16:35:48 - [INFO] - ✓ Selenium驱动初始化完成
2024-01-28 16:35:55 - [INFO] - 正在爬取淘宝第 1 页
2024-01-28 16:35:55 - [INFO] - 找到 44 个商品
...
2024-01-28 16:38:20 - [INFO] - 开始存储数据...
2024-01-28 16:38:21 - [INFO] - ✓ 成功插入 712 条数据
2024-01-28 16:38:22 - [INFO] - ✓ 已导出 712 条数据到 data/products.csv
2024-01-28 16:38:22 - [INFO] -
============================================================
2024-01-28 16:38:22 - [INFO] - 爬取完成!统计信息:
2024-01-28 16:38:22 - [INFO] - 总商品数: 712
2024-01-28 16:38:22 - [INFO] -
JD:
2024-01-28 16:38:22 - [INFO] - 数量: 580
2024-01-28 16:38:22 - [INFO] - 平均价格: ¥438.62
2024-01-28 16:38:22 - [INFO] - 价格区间: ¥89.00 - ¥2999.00
2024-01-28 16:38:22 - [INFO] -
Taobao:
2024-01-28 16:38:22 - [INFO] - 数量: 132
2024-01-28 16:38:22 - [INFO] - 平均价格: ¥312.45
2024-01-28 16:38:22 - [INFO] - 价格区间: ¥59.00 - ¥1899.00
============================================================
CSV文件预览
csv
平台,商品名称,价格,店铺,评论数,链接
JD,Cherry MX8.0 RGB机械键盘 青轴,599.00,Cherry官方旗舰店,15000,https://item.jd.com/100012345678.html
JD,罗技G PRO X机械游戏键盘,899.00,罗技官方旗舰店,28000,https://item.jd.com/100087654321.html
Taobao,达尔优A87机械键盘,169.00,达尔优旗舰店,5200,https://item.taobao.com/item.htm?id=xxx
🔟 常见问题与排错
Q1: 京东返回空页面或"没有找到"?
原因:触发了反爬限流
解决方案:
python
# 1. 增加延迟
DELAY_RANGE = (5, 10)
# 2. 添加更多headers
JD_HEADERS.update({
'sec-ch-ua': '"Chromium";v="112", "Google Chrome";v="112"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'same-origin'
})
# 3. 使用代理IP(付费服务)
proxies = {
'http': 'http://username:password@proxy.com:port',
'https': 'https://username:password@proxy.com:port'
}
response = self.session.get(url, proxies=proxies)
Q2: 淘宝Selenium被检测到"自动化控制"?
症状:页面显示"您的访问出现异常"
解决:
python
# 1. 加强stealth配置
from selenium_stealth import stealth
stealth(
self.driver,
languages=["zh-CN", "zh"],
vendor="Google Inc.",
platform="Win32",
webgl_vendor="Intel Inc.",
renderer="Intel Iris OpenGL Engine",
fix_hairline=True,
chrome_app="",
chrome_load_times=True,
chrome_csi=True,
chrome_runtime=True
)
# 2. 手动添加cookie(需先登录淘宝获取)
cookies = [
{'name': 'cookie_name', 'value': 'cookie_value'},
# ...更多cookie
]
for cookie in cookies:
self.driver.add_cookie(cookie)
Q3: XPath找不到元素,但浏览器能看到?
诊断步骤:
python
# 1. 保存实际HTML
with open('debug.html', 'w', encoding='utf-8') as f:
f.write(html)
# 2. 检查是否动态加载
# 如果debug.html中没有商品数据,说明需要用Selenium
# 3. 使用Chrome DevTools定位
# 右键 → 检查 → Copy XPath
Q4: 价格解析错误,出现0.0或异常值?
原因:价格格式多样
完善解析函数:
python
def robust_parse_price(text: str) -> float:
"""增强版价格解析"""
import re
# 移除符号
text = text.replace('¥', '').replace('$', '').replace(',', '')
# 处理价格区间:"199-299" → 取最低价
if '-' in text:
prices = re.findall(r'\d+\.?\d*', text)
return float(prices[0]) if prices else 0.0
# 提取数字
match = re.search(r'(\d+\.?\d*)', text)
return float(match.group(1)) if match else 0.0
Q5: SQLite报错"database is locked"?
原因:并发写入冲突
解决:
python
# 方法1:增加超时时间
self.conn = sqlite3.connect(DB_PATH, timeout=30.0)
# 方法2:使用WAL模式
self.conn.execute('PRAGMA journal_mode=WAL')
Q6: 淘宝只能爬前3页?
原因:未登录账号的限制
解决方案:
- 使用已登录的cookie(推荐)
- 切换到API逆向(需要破解sign签名)
- 接受限制,只爬前几页(适合快速测试)
1️⃣1️⃣ 进阶优化
并发加速:异步请求
python
import asyncio
import aiohttp
from typing import List
class AsyncJDScraper:
"""异步版京东爬虫"""
async def fetch_page(self, session, page: int):
"""异步请求单页"""
url = f"{JD_SEARCH_URL}?keyword={KEYWORD}&page={page}"
async with session.get(url, headers=JD_HEADERS) as response:
return await response.text()
async def fetch_all_pages(self, max_pages: int):
"""并发请求多页"""
async with aiohttp.ClientSession() as session:
tasks = [
self.fetch_page(session, page)
for page in range(1, max_pages + 1)
]
htmls = await asyncio.gather(*tasks)
return htmls
# 使用示例
async def main():
scraper = AsyncJDScraper()
htmls = await scraper.fetch_all_pages(20)
# 解析htmls...
asyncio.run(main())
性能对比:
- 同步版:20页约15分钟
- 异步版:20页约3分钟(提速80%)
智能代理池
python
class ProxyPool:
"""代理IP池管理"""
def __init__(self, api_url: str):
self.api_url = api_url # 代理服务商API
self.pool = []
self.bad_proxies = set()
def fetch_proxies(self, count: int = 10):
"""从API获取代理"""
response = requests.get(f"{self.api_url}?num={count}")
proxies = response.json()
self.pool.extend(proxies)
def get_proxy(self):
"""获取可用代理"""
if len(self.pool) < 3:
self.fetch_proxies()
for proxy in self.pool:
if proxy not in self.bad_proxies:
return proxy
return None
def mark_bad(self, proxy: str):
"""标记失效代理"""
self.bad_proxies.add(proxy)
if proxy in self.pool:
self.pool.remove(proxy)
# 使用
proxy_pool = ProxyPool("https://proxy-api.com/get")
for page in range(1, 100):
proxy = proxy_pool.get_proxy()
try:
response = requests.get(url, proxies={'http': proxy})
# ...
except:
proxy_pool.mark_bad(proxy)
价格监控与告警
python
class PriceMonitor:
"""价格变动监控"""
def check_price_drop(self, sku_id: str, threshold: float = 0.1):
"""检测降价(超过10%)"""
# 查询历史价格
cursor.execute('''
SELECT price FROM price_history
WHERE sku_id = ?
ORDER BY created_at DESC
LIMIT 1
''', (sku_id,))
old_price = cursor.fetchone()
if old_price:
old_price = old_price[0]
new_price = self.get_current_price(sku_id)
drop_rate = (old_price - new_price) / old_price
if drop_rate >= threshold:
self.send_alert(sku_id, old_price, new_price)
def send_alert(self, sku_id, old_price, new_price):
"""发送降价通知(邮件/微信/Telegram)"""
message = f"商品{sku_id}降价!{old_price} → {new_price}"
# 调用通知API...
数据可视化
python
import matplotlib.pyplot as plt
import seaborn as sns
def visualize_price_distribution():
"""价格分布图"""
df = pd.read_sql('SELECT platform, price FROM products', conn)
plt.figure(figsize=(12, 6))
sns.boxplot(data=df, x='platform', y='price')
plt.title('京东 vs 淘宝价格分布对比')
plt.ylabel('价格(元)')
plt.savefig('price_distribution.png')
def plot_price_trend():
"""价格趋势图(需要历史数据)"""
df = pd.read_sql('''
SELECT DATE(created_at) as date, AVG(price) as avg_price
FROM products
GROUP BY DATE(created_at)
''', conn)
plt.plot(df['date'], df['avg_price'])
plt.title('平均价格趋势')
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig('price_trend.png')
1️⃣2️⃣ 总结与延伸阅读
我们完成了什么?
通过这个项目,我们实现了一个生产级的电商搜索爬虫系统,核心成果包括:
✅ 双平台适配 :掌握了京东(API逆向)和淘宝(Selenium)两种截然不同的反爬对抗策略
✅ 分页爬取 :实现了稳定的多页翻页逻辑,支持爬取数百页数据
✅ 数据清洗 :处理了价格、评论数等多种格式的脏数据
✅ 去重存储 :基于URL唯一约束防止重复数据
✅ 可扩展架构:代码结构清晰,轻松扩展到拼多多、苏宁等平台
最终产出的数据可用于:
- 价格比价工具开发
- 商品推荐系统训练数据
- 市场趋势分析报告
- 竞品定价策略研究
京东 vs 淘宝:技术对比总结
| 维度 | 京东 | 淘宝 |
|---|---|---|
| 页面类型 | SSR(服务端渲染) | SPA(单页应用) |
| 反爬强度 | ★★★☆☆ | ★★★★★ |
| 最佳方案 | requests + API逆向 | Selenium + Stealth |
| 爬取速度 | 快(1页/秒) | 慢(1页/5秒) |
| 稳定性 | 高(配合延迟) | 中(易触发验证码) |
| 学习曲线 | 中等 | 较陡 |
下一步可以做什么?
技术深化方向:
-
破解淘宝Sign签名
- 使用Chrome DevTools定位sign生成逻辑
- 逆向混淆的JavaScript代码
- 用Python复现签名算法(推荐库:execjs)
-
分布式爬虫架构
- 使用Celery任务队列分发爬取任务
- Redis存储任务状态和去重集合
- Docker容器化部署多个Worker
-
验证码自动识别
- 滑块验证码:轨迹模拟 + OpenCV缺口识别
- 文字验证码:Tesseract OCR / 付费打码平台
- 行为验证:模拟人类操作轨迹(贝塞尔曲线)
-
实时价格监控系统
- Airflow定时调度(每小时/每天)
- 价格变动告警(邮件/史价格曲线可视化(ECharts)
业务拓展方向:
- 扩展到拼多多、苏宁、唯品会等平台
- 增加商品详情页爬取(规格参数、用户评价)
- 构建商品知识图谱(品牌-型号-参数关系)
- 开发Chrome插件:一键比价
延伸阅读推荐
反爬对抗进阶:
- 《Web Scraping with Python》(Ryan Mitchell)
- 《Python爬虫开发与项目实战》(范传辉)
- Selenium Stealth官方文档
JavaScript逆向:
- Chrome DevTools使用指南
- AST抽象语法树分析
- 《JavaScript反混淆实战》
分布式架构:
- Scrapy-Redis文档
- Celery最佳实践
- 《分布式爬虫系统设计与实现》
法律合规:
- 《中华人民共和国网络安全法》
- 爬虫合规指南
- 《反不正当竞争法》关于数据抓取的规定
最后的话:电商爬虫是一个技术门槛较高但应用价值极大的领域。在掌握技术的同时,请务必:
- 尊重平台规则:不做恶意爬取和数据倒卖
- 控制爬取频率:避免影响网站正常运营
- 数据仅供学习:不用于商业目的或不正当竞争
- 关注法律边界:了解《网络安全法》等相关法规
技术是中性的工具,关键在于使用者的初心。希望这篇教程能帮助你在合法合规的前提下,提升数据获取能力,创造更多价值!
如果本教程对你有帮助,欢迎分享给更多朋友!有问题随时交流,祝爬虫愉快!
🌟 文末
好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥
📌 专栏持续更新中|建议收藏 + 订阅
专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:
✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)
📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集
想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?
评论区留言告诉我你的需求,我会优先安排更新 ✅
⭐️ 若喜欢我,就请关注我叭~(更新不迷路)
⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)
⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)

免责声明:本文仅用于学习与技术研究,请在合法合规、遵守站点规则与 Robots 协议的前提下使用相关技术。严禁将技术用于任何非法用途或侵害他人权益的行为。技术无罪,责任在人!!!