What goes wrong when this scraper fails
Rank tracking outside the Google monoculture means scraping engines that guard their SERPs just as hard, with different tells. Yandex dominates Russia and much of the CIS, Baidu leads China, Naver leads South Korea, and Seznam holds real share in the Czech market - none expose a usable public organic-ranking API, so trackers read the HTML pages that human searchers see. Yandex in particular answers suspicious traffic with its showcaptcha interstitial (a SmartCaptcha 'are you a robot' page served at /showcaptcha), scores exits on IP reputation and request velocity, and personalizes every result set by region and language. Get the exit country or region code wrong and you are not blocked - worse, you silently store rankings that no real user in that market would ever see.
Why this failure mode happens
Yandex fingerprints incoming traffic across several axes at once: the reputation of the exit IP (datacenter ranges and known proxy pools are pre-scored poorly), the velocity and regularity of requests from that IP, and the completeness of the request - a bare User-Agent with no Accept-Language, no cookies, and no realistic header ordering reads as automation instantly. When the combined score crosses a threshold Yandex redirects to /showcaptcha or embeds a captcha token in the response instead of results. Because rankings are computed per region via lr= and per language via the interface params, two identical queries from a Moscow exit and a Frankfurt exit return genuinely different orderings - the personalization is a feature of the product, not an anti-bot trick, but it means location correctness is as important as not getting blocked.
Challenges that make this hard to automate
- The showcaptcha interstitial: once your exit trips the score, Yandex serves a SmartCaptcha page at /showcaptcha with a 200 status, so a naive scraper stores an empty or challenge page as if it were results.
- Region personalization: the same keyword ranks differently by lr= region code and by exit country, so a wrong-country exit produces plausible-but-wrong data that is hard to notice.
- IP reputation scoring: datacenter and flagged proxy ranges start with a poor score and hit captchas within a handful of requests regardless of pacing.
- Velocity detection: bursts of regular-interval requests from one exit are scored as automation even at modest per-minute rates.
- Fragile, shifting markup: Yandex rotates class names and DOM structure, and Baidu/Naver each have their own layout quirks, so brittle selectors break parsing silently and return zero results.
Approaches that usually fail
- Datacenter proxy lists: cheap and fast, but Yandex has pre-scored most datacenter ranges poorly, so they hit showcaptcha almost immediately and burn out.
- Headless browser farms with no IP strategy: Playwright or Puppeteer can render the page, but without residential exits in the target market they still trip reputation scoring and just render the captcha instead.
- Third-party SERP APIs: convenient, but coverage of Yandex and regional engines is thin, region-code control is often coarse, and you inherit their block-recovery and freshness rather than owning it.
- Manual captcha-solving pipelines: routing every showcaptcha through a human/solver service is slow, costly, and treats the symptom - a good exit strategy means you rarely see the challenge at all.
When residential proxies fix this — and when they cannot
The single biggest lever is where the request exits and how often that exit is reused. Residential exits inside the target market (a Moscow or St. Petersburg IP for a Russian query) carry the reputation of a real subscriber line, so they start with a clean score instead of the pre-flagged datacenter penalty, and they match the region personalization you actually want to measure. Rotating to a fresh exit on every request keeps any single IP far below Yandex's velocity threshold, so no one address accumulates the request regularity that triggers scoring. Combined with a complete, realistic header set and the correct lr= region code, per-request residential rotation turns rank tracking from a captcha-fighting exercise into a data-collection one.
How Aethyn residential proxies help here
Aethyn gives you residential exits in the exact markets these engines personalize for, addressed with one compact username convention. For Yandex work you point at a Russian exit pool and let the network rotate a fresh IP per query, which is precisely the pattern that keeps you under velocity scoring while matching the region you are trying to measure. The Elite tier is the right pick for SERP work because its exit reputation and pool depth hold up against Yandex's IP scoring far better than shared datacenter ranges.
- Residential exits in Russia and the CIS (and other regional markets) so your SERP matches the lr= region you are tracking, addressed with -country-ru.
- Per-request rotation by default: omit the session token and every request leaves on a fresh exit, staying under Yandex's velocity threshold automatically.
- Elite-tier exit reputation (port 5499) tuned for high-scrutiny targets, so exits arrive with a clean score instead of a pre-flagged datacenter penalty.
- Optional sticky sessions (-session-TOKEN-lifetime-MINUTES, up to 30 min) for the rare multi-request flow that must stay on one exit, such as paging deep into a single result set.
- City-level targeting (-city-moscow) when a keyword's ranking is metro-specific and the region code alone is too coarse.
- One username format across every engine you track - Yandex, Baidu, Naver, Seznam - so the same client code just changes the country code.
How to implement this with residential proxies
- 1
Build the region-correct proxy exit
Rankings are personalized, so the exit country and the lr= region code must agree with the market you are measuring. For Russian rank tracking, exit through a Russian residential IP and pass the matching Yandex region code (lr=213 is Moscow, lr=2 is St. Petersburg, lr=225 is all of Russia). Use per-request rotation - no session token - so each query leaves on a fresh exit. The Elite tier uses port 5499.
pythonimport requests # Elite tier -> port 5499. Per-request rotation: no session token. PROXY = "http://aethyn-XXXXX-country-ru:PASSWORD@proxy.aethyn.io:5499" proxies = {"http": PROXY, "https": PROXY} params = { "text": "купить кроссовки", # query "lr": 213, # Yandex region code: 213 = Moscow "lang": "ru", "p": 0, # page index (0-based) } resp = requests.get( "https://yandex.ru/search/", params=params, proxies=proxies, timeout=20, ) print(resp.status_code, resp.url)Field note: Keep a small lookup of Yandex lr= codes per city you track and store it alongside every stored ranking. A ranking row without its region code is unreconstructable later - you will not be able to tell whether position 4 was a Moscow result or a nationwide one.
- 2
Send a complete, human-realistic header set
A bare User-Agent with no Accept-Language is the classic bot tell, and for a region-personalized engine the language header also shapes results. Send a full, coherent header set: a current browser User-Agent, an Accept-Language that matches the market (ru-RU), a real Accept, and an Accept-Encoding. Keep the headers internally consistent - a Chrome User-Agent paired with a Firefox-style Accept ordering is itself a fingerprint.
pythonHEADERS = { "User-Agent": ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/126.0.0.0 Safari/537.36" ), "Accept": ( "text/html,application/xhtml+xml,application/xml;q=0.9," "image/avif,image/webp,*/*;q=0.8" ), "Accept-Language": "ru-RU,ru;q=0.9,en;q=0.5", "Accept-Encoding": "gzip, deflate, br", "Upgrade-Insecure-Requests": "1", } resp = requests.get( "https://yandex.ru/search/", params=params, headers=HEADERS, proxies=proxies, timeout=20, )Field note: Match Accept-Language to the exit country, not to your own locale. A Russian exit sending Accept-Language: en-US is a contradiction Yandex can score against - the header should look like the browser of a real user in that market.
- 3
Detect the showcaptcha challenge before trusting the response
Yandex returns the SmartCaptcha challenge with a 200 status, so status code alone means nothing. Treat a response as blocked if it redirects to /showcaptcha, contains the showcaptcha markup, or (on the XML/JSON surfaces) carries a captcha field. Never write a challenged response to your database - classify it, back off, and retry on a fresh exit.
pythondef is_blocked(resp): # Yandex serves the SmartCaptcha page with HTTP 200. if "/showcaptcha" in resp.url: return True body = resp.text markers = ( "showcaptcha", "SmartCaptcha", "captcha-required", "я не робот", # 'I am not a robot' ) return any(m in body for m in markers) if is_blocked(resp): raise RuntimeError("showcaptcha challenge - rotate exit and back off")Field note: Log the block rate per region and per hour, not just per run. A creeping showcaptcha rate is the earliest signal that your pacing or header set has drifted into detection - catch it at 5% before it becomes 60%.
- 4
Parse defensively and assert a non-empty result set
Yandex rotates class names and DOM structure, so anchor parsing on stable containers (organic result items carry a serp-item / li.serp-item structure) and extract title, URL, and position with fallbacks. Then assert you actually got results before storing - a 200 OK with zero parsed organic results on a common query is almost never a real empty page, it is a soft block or a markup change.
pythonfrom bs4 import BeautifulSoup def parse_organic(html): soup = BeautifulSoup(html, "html.parser") results = [] for pos, item in enumerate(soup.select("li.serp-item"), start=1): link = item.select_one("a.organic__url, a.Link") title = item.select_one("h2, .organic__title") if not link or not link.get("href"): continue results.append({ "position": pos, "url": link["href"], "title": title.get_text(strip=True) if title else "", }) return results rows = parse_organic(resp.text) if not rows: raise RuntimeError("zero organic results - treat as soft block, do not store")Field note: Always assert a non-empty result set before writing to your database. A 200 OK with zero parsed results is almost never a real empty page - it is a soft block or a silent markup change, and storing it is how a rank dashboard goes quietly wrong for weeks.
- 5
Pace, rotate, and back off on blocks
Per-request rotation handles velocity, but add jittered delays so your traffic does not arrive on a machine-regular cadence, and implement exponential backoff when you do see a challenge. On a block, rotate to a fresh exit (which happens automatically with no session token) and increase the delay before retrying rather than hammering the same query. Keep concurrency modest - a few workers per region, not hundreds.
pythonimport random, time def fetch_with_backoff(params, max_tries=4): delay = 2.0 for attempt in range(max_tries): # Fresh exit every call (no session token in the username). resp = requests.get( "https://yandex.ru/search/", params=params, headers=HEADERS, proxies=proxies, timeout=20, ) if not is_blocked(resp): return resp time.sleep(delay + random.uniform(0, 1.5)) # jittered backoff delay *= 2 raise RuntimeError("exhausted retries - persistent challenge") time.sleep(random.uniform(1.0, 4.0)) # jitter between queries resp = fetch_with_backoff(params)Field note: Cap retries at 3-4 and record persistent failures rather than looping forever. A query that will not clear after four fresh exits is telling you something structural - a bad region code, a flagged pool, or a real markup change - and infinite retries just burn exits and inflate your block rate.
- 6
Reuse the same pattern for other regional engines
The exit-in-market plus complete-headers plus defensive-parse pattern ports directly to Baidu (China, -country-cn), Naver (South Korea, -country-kr), and Seznam (Czech Republic, -country-cz). Only three things change per engine: the exit country code in the username, the query/region parameters, and the result selectors. Keep one engine adapter interface so adding a market is a config change, not a rewrite.
pythonENGINES = { "yandex": {"country": "ru", "url": "https://yandex.ru/search/", "text_param": "text", "lang": "ru-RU"}, "baidu": {"country": "cn", "url": "https://www.baidu.com/s", "text_param": "wd", "lang": "zh-CN"}, "naver": {"country": "kr", "url": "https://search.naver.com/search.naver", "text_param": "query", "lang": "ko-KR"}, } def proxy_for(country): # Elite tier -> port 5499, per-request rotation. user = "aethyn-XXXXX-country-" + country return "http://" + user + ":PASSWORD@proxy.aethyn.io:5499" cfg = ENGINES["naver"] p = {"http": proxy_for(cfg["country"]), "https": proxy_for(cfg["country"])} r = requests.get(cfg["url"], params={cfg["text_param"]: "운동화"}, proxies=p, timeout=20)Field note: Give every engine adapter its own is_blocked and parse function - Baidu's challenge and Naver's empty-state look nothing like Yandex's showcaptcha. A shared block detector tuned only for Yandex will silently misclassify the others and quietly poison those markets' data.
Best practices that keep scrapers reliable
- Collect only public, non-personal SERP data - the ranking of public pages for a keyword - keep request volume proportionate to real rank-tracking needs, and respect each engine's Terms of Service; consult counsel for your jurisdiction.
- Always store the region context (exit country plus lr= or engine region code) with every ranking row, so results are reproducible and never ambiguous between metro and national rankings.
- Default to per-request rotation and reserve sticky sessions for the rare flow that must stay on one exit, such as paging deep into one result set within the 30-minute lifetime cap.
- Assert a non-empty, sane result set before persisting, and treat zero-result or challenge responses as soft blocks to retry, never as data to store.
- Match the exit country, Accept-Language, and region code to each other and to the target market - any mismatch is both a fingerprint and a data-correctness bug.
- Track block rate and result-count distributions per region and hour as first-class metrics, so pacing or markup drift surfaces early instead of after weeks of bad data.
- Keep engine-specific block detectors and parsers isolated behind an adapter so one engine's markup change or challenge style never corrupts another market's results.
Common mistakes that burn proxy budget
- Trusting the HTTP status: Yandex serves showcaptcha with a 200, so treating status 200 as success stores challenge pages as rankings.
- Using datacenter exits for Yandex: those ranges are pre-scored poorly and hit showcaptcha within a few requests no matter how slowly you pace.
- Ignoring the lr= region code (or using an exit outside the target country), producing plausible but wrong rankings that no user in that market sees.
- Sending a bare User-Agent with no Accept-Language, or an Accept-Language that contradicts the exit country - both read as automation.
- Storing zero-result responses as legitimate empty SERPs instead of flagging them as soft blocks or markup changes.
- Reusing a Yandex-tuned block detector and selectors for Baidu, Naver, or Seznam, which silently misclassifies and mis-parses those engines.