Python爬虫实战:京东/淘宝搜索多页爬虫实战 - 从反爬对抗到数据入库的完整工程化方案(附CSV导出 + SQLite持久化存储)!

㊙️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~持续更新中!

㊗️爬虫难度指数:⭐⭐

🚫声明:本数据&代码仅供学习交流,严禁用于商业用途、倒卖数据或违反目标站点的服务条款等,一切后果皆由使用者本人承担。公开榜单数据一般允许访问,但请务必遵守"君子协议",技术无罪,责任在人。

全文目录:

🌟 开篇语

哈喽,各位小伙伴们你们好呀~我是【喵手】。

运营社区: 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页?

原因:未登录账号的限制

解决方案

  1. 使用已登录的cookie(推荐)
  2. 切换到API逆向(需要破解sign签名)
  3. 接受限制,只爬前几页(适合快速测试)

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秒)
稳定性 高(配合延迟) 中(易触发验证码)
学习曲线 中等 较陡

下一步可以做什么?

技术深化方向

  1. 破解淘宝Sign签名

    • 使用Chrome DevTools定位sign生成逻辑
    • 逆向混淆的JavaScript代码
    • 用Python复现签名算法(推荐库:execjs)
  2. 分布式爬虫架构

    • 使用Celery任务队列分发爬取任务
    • Redis存储任务状态和去重集合
    • Docker容器化部署多个Worker
  3. 验证码自动识别

    • 滑块验证码:轨迹模拟 + OpenCV缺口识别
    • 文字验证码:Tesseract OCR / 付费打码平台
    • 行为验证:模拟人类操作轨迹(贝塞尔曲线)
  4. 实时价格监控系统

    • Airflow定时调度(每小时/每天)
    • 价格变动告警(邮件/史价格曲线可视化(ECharts)

业务拓展方向

  • 扩展到拼多多、苏宁、唯品会等平台
  • 增加商品详情页爬取(规格参数、用户评价)
  • 构建商品知识图谱(品牌-型号-参数关系)
  • 开发Chrome插件:一键比价

延伸阅读推荐

反爬对抗进阶

JavaScript逆向

分布式架构

法律合规

  • 《中华人民共和国网络安全法》
  • 爬虫合规指南
  • 《反不正当竞争法》关于数据抓取的规定

最后的话:电商爬虫是一个技术门槛较高但应用价值极大的领域。在掌握技术的同时,请务必:

  1. 尊重平台规则:不做恶意爬取和数据倒卖
  2. 控制爬取频率:避免影响网站正常运营
  3. 数据仅供学习:不用于商业目的或不正当竞争
  4. 关注法律边界:了解《网络安全法》等相关法规

技术是中性的工具,关键在于使用者的初心。希望这篇教程能帮助你在合法合规的前提下,提升数据获取能力,创造更多价值!

如果本教程对你有帮助,欢迎分享给更多朋友!有问题随时交流,祝爬虫愉快!

🌟 文末

好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥

📌 专栏持续更新中|建议收藏 + 订阅

专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:

✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)

📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集

想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?

评论区留言告诉我你的需求,我会优先安排更新 ✅


⭐️ 若喜欢我,就请关注我叭~(更新不迷路)

⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)

⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)


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

相关推荐
B站_计算机毕业设计之家12 小时前
猫眼电影数据可视化与智能分析平台 | Python Flask框架 Echarts 推荐算法 爬虫 大数据 毕业设计源码
python·机器学习·信息可视化·flask·毕业设计·echarts·推荐算法
PPPPPaPeR.12 小时前
光学算法实战:深度解析镜片厚度对前后表面折射/反射的影响(纯Python实现)
开发语言·python·数码相机·算法
JaydenAI12 小时前
[拆解LangChain执行引擎] ManagedValue——一种特殊的只读虚拟通道
python·langchain
骇城迷影12 小时前
Makemore 核心面试题大汇总
人工智能·pytorch·python·深度学习·线性回归
长安牧笛12 小时前
反传统学习APP,摒弃固定课程顺序,根据用户做题正确性,学习速度,动态调整课程难度,比如某知识点学不会,自动推荐基础讲解和练习题,学习后再进阶,不搞一刀切。
python·编程语言
码界筑梦坊12 小时前
330-基于Python的社交媒体舆情监控系统
python·mysql·信息可视化·数据分析·django·毕业设计·echarts
森焱森13 小时前
详解 Spring Boot、Flask、Nginx、Redis、MySQL 的关系与协作
spring boot·redis·python·nginx·flask
0思必得013 小时前
[Web自动化] Selenium获取元素的子元素
前端·爬虫·selenium·自动化·web自动化