Files

239 lines
8.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: ozon-ru-scraper
description: 爬取Ozon.ru商品页面 — 俄罗斯代理+CamouFox+滑块验证码自动求解。含反爬策略、代理选型、验证码算法、避坑指南。
version: 0.1
tags: [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 地址和时间戳。
## 代码模板
### 完整爬取流程
```python
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
```
### 滑块验证码求解
```python
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
```
### 验证码通过后等跳转
```python
# 验证码通过后标题变为 "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)
```
### 商品信息提取
```python
# 方法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`/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。
## 依赖安装
```bash
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 自研验证码