8.9 KiB
8.9 KiB
name, description, version, tags
| name | description | version | tags | ||||||
|---|---|---|---|---|---|---|---|---|---|
| ozon-ru-scraper | 爬取Ozon.ru商品页面 — 俄罗斯代理+CamouFox+滑块验证码自动求解。含反爬策略、代理选型、验证码算法、避坑指南。 | 0.1 |
|
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×100px,PNG 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 #puzzle,4通道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(
/requestendpoint)使用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,能连但有验证码
陷阱与避坑
- 不要先访问首页再访问商品页 — 两次请求增加触发验证码概率。直接访问目标URL。
- 验证码最多试2次 — 第3次失败几乎必封IP。如果前2次偏移量有明显误差,放弃换代理。
- CCOEFF_NORMED 的 inf 值是假信号 — 目标区域全黑(mask=0)时会产生虚假高匹配,忽略。
- 验证码通过后不要立刻再请求 — Challenge page 会自动跳转,等它完成。
- IP被封后不要在同一代理上重试 — 封禁持续10-30分钟,换新代理。
- Jina Reader 也被拦 —
r.jina.ai无法绕过 Ozon captcha,返回 403。 - 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 自研验证码