Files

8.9 KiB
Raw Permalink Blame History

name, description, version, tags
name description version tags
ozon-ru-scraper 爬取Ozon.ru商品页面 — 俄罗斯代理+CamouFox+滑块验证码自动求解。含反爬策略、代理选型、验证码算法、避坑指南。 0.1
ozon
russia
scraping
captcha
camoufox
proxy

Ozon.ru 商品爬取

目标

爬取 https://www.ozon.ru/product/{id} 商品页,提取名称、价格、描述、评分等信息。

三层防御(必须理解)

第1层:GeoIP 检测

Ozon 对非俄罗斯 IP 直接返回 307 重定向循环或 Access Denied。

对策CamouFox + 俄罗斯出口代理 + geoip=True

第2层:Antibot Captcha(自研滑块拼图)

标题变为 Antibot Captcha,页面显示:

  • 背景图(400×300px,含缺口)
  • 拼图块(~133×100pxPNG with alpha
  • 滑块条(slider-background 512px 宽,slider 40px 宽)
  • 提示文字:"Сопоставьте пазл, двигая ползунок"(移动滑块对齐拼图)

第3层:IP 快速封禁

验证码失败 2-3 次 → IP 被临时封("Доступ ограничен"),封禁时间约 10-30 分钟。页面会显示被封 IP 地址和时间戳。

代码模板

完整爬取流程

from camoufox.sync_api import Camoufox
import cv2, numpy as np, time, json, subprocess

PROXY = {"server": "socks4://RU_PROXY_IP:PORT"}
TARGET = "https://www.ozon.ru/product/PRODUCT_ID"

with Camoufox(headless=True, geoip=True, humanize=True, proxy=PROXY) as browser:
    page = browser.new_page()
    
    # 直接访问商品页(不要先访问首页,减少请求次数)
    page.goto(TARGET, timeout=25000)
    page.wait_for_load_state("networkidle", timeout=15000)
    time.sleep(2)
    
    title = page.title()
    if "ограничен" in title:
        print("IP被封,换代理")
    elif "Captcha" in title:
        # 解验证码(见下方)
        pass
    else:
        # 直接提取商品信息
        pass

滑块验证码求解

def solve_puzzle(bg_path, puzzle_path):
    """用 OpenCV 模板匹配计算滑块偏移量"""
    bg = cv2.imread(bg_path, cv2.IMREAD_UNCHANGED)
    puzzle = cv2.imread(puzzle_path, cv2.IMREAD_UNCHANGED)
    
    bg_rgb = bg[:, :, :3]
    puzzle_rgb = puzzle[:, :, :3]
    
    # 关键:利用拼图块的alpha通道作为掩码
    mask = (puzzle[:, :, 3] > 10).astype(np.uint8) * 255 if puzzle.shape[2] == 4 else None
    
    # TM_CCORR_NORMED + mask 是最佳方法(相似度 0.84-0.86)
    result = cv2.matchTemplate(bg_rgb, puzzle_rgb, cv2.TM_CCORR_NORMED, mask=mask)
    _, max_val, _, max_loc = cv2.minMaxLoc(result)
    
    return max_loc[0]  # 图片坐标系下的 X 偏移

def get_captcha_info(page):
    """获取验证码 DOM 参数"""
    return page.evaluate('''() => {
        const c = document.querySelector("#captcha");
        const p = document.querySelector("#captcha #puzzle");
        const s = document.querySelector("#slider");
        if (!p || !s || !c) return null;
        return {
            scale: parseFloat(getComputedStyle(c).getPropertyValue("--scale")) || 1,
            puzzleLeft: parseFloat(p.style.left) || 0,  // 拼图块当前CSS left
            sliderContW: document.querySelector("#slider-container")?.getBoundingClientRect().width || 0,
            sliderW: s.getBoundingClientRect().width,
        };
    }''')

def solve_and_drag(page):
    """完整求解+拖动流程"""
    info = get_captcha_info(page)
    if not info:
        return False
    
    # 下载验证码图片
    bg_src = page.evaluate('document.querySelector("#captcha #image")?.src')
    pz_src = page.evaluate('document.querySelector("#captcha #puzzle")?.src')
    for url, path in [(bg_src, "/tmp/oz_bg.png"), (pz_src, "/tmp/oz_pz.png")]:
        subprocess.run(["curl", "-s", "-o", path, url], capture_output=True, timeout=10)
    
    # 计算偏移
    target_x = solve_puzzle("/tmp/oz_bg.png", "/tmp/oz_pz.png")
    
    # 核心公式:拖动距离 = 目标渲染X - 拼图当前left
    # 目标渲染X = 图片坐标偏移 × CSS scale
    drag_px = target_x * info['scale'] - info['puzzleLeft']
    max_drag = info['sliderContW'] - info['sliderW']
    drag_px = max(0, min(drag_px, max_drag))
    
    # 人类化拖动
    slider = page.query_selector("#slider")
    box = slider.bounding_box()
    sx, sy = box["x"] + box["width"]/2, box["y"] + box["height"]/2
    
    page.mouse.move(sx, sy)
    time.sleep(0.3)
    page.mouse.down()
    time.sleep(0.1)
    
    steps = 25
    for i in range(1, steps + 1):
        t = i / steps
        ease = 1 - (1 - t) ** 3  # ease-out cubic
        x = sx + drag_px * ease
        y = sy + (np.random.random() - 0.5) * 1.0  # 微小Y抖动
        page.mouse.move(x, y)
        time.sleep(np.random.uniform(0.015, 0.04))
    
    time.sleep(0.1)
    page.mouse.up()
    return True

验证码通过后等跳转

# 验证码通过后标题变为 "Antibot Challenge Page"
# 会自动 JS 跳转到商品页
if "Challenge" in page.title():
    try:
        page.wait_for_url("**/product/**", timeout=10000)
    except:
        page.goto(TARGET, timeout=20000)
    page.wait_for_load_state("networkidle", timeout=15000)
    time.sleep(3)

商品信息提取

# 方法1: LD+JSON(最可靠)
ld = page.evaluate('''() => {
    const el = document.querySelector('script[type="application/ld+json"]');
    return el ? el.textContent : null;
}''')
if ld:
    data = json.loads(ld)
    name = data.get('name')
    price = data.get('offers', {}).get('price')
    currency = data.get('offers', {}).get('priceCurrency')
    description = data.get('description')
    rating = data.get('aggregateRating', {}).get('ratingValue')
    review_count = data.get('aggregateRating', {}).get('reviewCount')
    brand = data.get('brand')
    sku = data.get('sku')

验证码关键参数

参数 说明
--scale 1.28 captcha 容器的 CSS 缩放因子
背景图 400×300px #captcha #image
拼图块 ~133×100px #captcha #puzzle4通道PNG
slider-background 512px 渲染宽度 = 400 × 1.28
slider 40px 宽 可拖动滑块
拖动范围 containerW - sliderW = ~440px slider-container 宽480 - slider宽40
拼图初始left 随机(14-67px puzzle.style.left
拖动公式 target_x × scale - puzzleLeft 从初始位置到目标位置的像素距离

模板匹配方法对比

方法 相似度 说明
TM_CCORR_NORMED + alpha mask 0.84-0.86 最佳,推荐
TM_CCOEFF_NORMED + mask 不稳定(0.39-inf 受拼图块透明区域干扰
Canny边缘 + TM_CCOEFF_NORMED 0.45-0.67 ⚠️ 可作辅助验证
HSV V通道 + mask 0.85 ≈方法1,可交叉验证

代理选型

Bright Data 详细配置见参考文件: references/brightdata-proxy-guide.md
包含完整凭证、所有可用zone、/request API 使用方式、价格对比、避坑指南。

推荐:付费住宅代理(首选)

  • Bright Data — 通过 Web Access API/request endpoint)使用
    • zone9:数据中心 DC,$0.11/GB(比住宅便宜70倍)
    • zone47:住宅,$8/GB,支持 country=us/kz 切换
  • 住宅IP几乎不触发验证码
  • 轮换IP避免单IP被封
  • 策略: 优先用 DC zone$0.11/GB),被封再换住宅 zone47

免费 SOCKS4(不推荐生产使用)

  • 来源:https://api.proxyscrape.com/v4/free-proxy-list/get?request=display_proxies&proxy_format=protocolipport&format=text&country=ru&protocol=socks4
  • 可用率:<5%,寿命短,同IP多人共用易触发封禁
  • 已验证的代理(可能已失效):
    • 77.232.142.77:31336 — SOCKS4,曾成功过一次,后 IP 被封
    • 89.169.168.25:1080 — SOCKS4,能连但有验证码

陷阱与避坑

  1. 不要先访问首页再访问商品页 — 两次请求增加触发验证码概率。直接访问目标URL。
  2. 验证码最多试2次 — 第3次失败几乎必封IP。如果前2次偏移量有明显误差,放弃换代理。
  3. CCOEFF_NORMED 的 inf 值是假信号 — 目标区域全黑(mask=0)时会产生虚假高匹配,忽略。
  4. 验证码通过后不要立刻再请求 — Challenge page 会自动跳转,等它完成。
  5. IP被封后不要在同一代理上重试 — 封禁持续10-30分钟,换新代理。
  6. Jina Reader 也被拦r.jina.ai 无法绕过 Ozon captcha,返回 403。
  7. curl 裸连返回 307 — 无代理根本连不上 Ozon。

依赖安装

pip install camoufox[geoip] opencv-python-headless -i https://pypi.org/simple/
python -m camoufox fetch  # 下载 Firefox v135 浏览器二进制

未来改进方向

  • 付费住宅代理集成(Bright Data / Oxylabs
  • 验证码偏移量用深度学习模型替代 OpenCV 模板匹配
  • 代理池自动轮换,单IP失败自动切换
  • 是用 YesCaptcha 打码平台 API 解 Ozon 自研验证码