diff --git a/skills/archived/ozon-ru-scraper/SKILL.md b/skills/archived/ozon-ru-scraper/SKILL.md new file mode 100644 index 0000000..19691f2 --- /dev/null +++ b/skills/archived/ozon-ru-scraper/SKILL.md @@ -0,0 +1,238 @@ +--- +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×100px,PNG 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 自研验证码