--- 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 自研验证码