Když blokování sklízečů selže: tichá odpověď místo 403

Pokračování článku o sedmi vlnách sklizně. Rezidenční pool se na 403 přizpůsobil za tři hodiny. Proč mu místo odmítnutí posílám stránku bez toho podstatného a kdy přestat stavět další vrstvy?
Nálepky:
Minulý díl o sklízečích na najdi-film.cz jsem končil přiznáním, že rezidenční pool spolehlivě zablokovat neumím, jen ho zatím nepouštím k obsahu. Vlny, které přišly po uzávěrce, jsem do něj už nedopisoval a redakci jsem místo toho nabídl pokračování o tom, co dělat, když blokování selže. Za čtyři týdny od uzávěrky prvního dílu jsem si to vyzkoušel víc, než bych chtěl. Blok selhal pětkrát a pokaždé jinak.
Jednou za to mohly chyby v mých skriptech, jednou pravidlo, které jsem nastavil schválně, jednou sklízeč, který se přizpůsobil, jednou sklízeč, který adresu nikdy nezopakoval, a jednou to, že jsem chránil jen část výstupů. Odpověď na otázku, co dělat, se proto liší podle toho, který z těch případů nastal, a první krok je to rozlišit.
| Kdy | Kdo | Proč blok selhal | Co pomohlo |
|---|---|---|---|
| 24. 9. | 49 adres v cloudu, 1 879 stránek | chyba v hlídce a v blokovacím skriptu | oprava obou |
| 29. a 30. 9. | návrat poolu z 1. září, 160 adres | 88 adres bez cloudového reverzního záznamu | úzký řez na jeho otisk |
| 12. a 13. 10. | rezidenční pool, ~4 000 stránek za den | přizpůsobil se 403 za tři hodiny | tichá verze stránky |
| 15. a 16. 10. | HTTP knihovna, stovky stránek za noc | každá adresa jen jednou | tichá verze stránky i výpisů |
| 17. 10. | klient, který hledal přes API | API vydávalo stejná data jako stránka | tichá verze API |
Text pokrývá události od 21. září do 20. října 2026. Stejně jako minule nepíšu, podle čeho přesně klienty poznávám. Kdo to ví, obejde pravidlo jednou hlavičkou.
Kdy za selháním bloku stojím já
24. září v půl čtvrté ráno stahovalo katalog 49 adres v cloudu Amazonu. Hlídka je viděla a nahlásila, blokovací skript zablokoval osm a zbylých 41 stahovalo dál, dohromady 1 879 stránek. Vypadalo to na sklízeče, který bloky obchází, jenže ho pustily dál dvě chyby v mých skriptech.
Hlídka seskupovala klienty podle otisku hlaviček a přibírala do skupiny i adresy, které v daném okně nic nestáhly a byly tam jen z týdenní historie. Otisk byl obecný, takže místo 49 adres hlásila 406. Samo o sobě by to tolik nevadilo. Blokovací skript ale z nich prověřoval jen prvních 100 v lexikografickém pořadí, a protože už zablokované adresy ve výběru zůstávaly, prověřoval v každém dalším běhu znovu ty samé. Mezi nimi bylo osm strojů sklízeče a ty dostaly blok. Zbytek sklízeče se do výběru nikdy nedostal.
O pět dní později se vrátil pool z 1. září a tentokrát se dalo říct, že jde o stejného klienta. Ze 72 serverů na OVH jich skript zablokoval 68 a 64 z nich dostalo blok už 1. září. Eskalace fungovala, jak měla, a místo dvou dnů dostaly týden. Jenže pool si mezitím přibral dalších 88 adres od firem, které pronajímají adresní rozsahy a proxy. Cloudový reverzní záznam nemají a můj skript adresu bez něj zásadně neblokuje, protože za ní může být domácnost. Když skript zablokoval servery z OVH, běžela sklizeň přes zbylé adresy dál zhruba polovičním tempem.
Tady jsem málem udělal chybu v úsudku. Nejdřív jsem pool chtěl nechat běžet, protože jsem škodu považoval za nulovou a statický řez na jeho hlavičky jsem už dřív zamítl. Obojí bylo špatně. Zamítl jsem jen široký řez, který by trefil i lidi. Úzký řez na jeden konkrétní otisk lidi netrefí. A škoda nulová nebyla. Pool stahoval detaily filmů i s informací, kde se dá film pustit, a s mými štítky nálad a témat. Právě na tom web stojí. Úzký řez na jeho otisk jsem nasadil za pár minut. Do té doby odteklo zhruba 1 030 stránek.
Když teď sklizeň projde blokem, kontroluju nejdřív, co s nálezem hlídky udělal můj kód, a teprve potom sklízeče. Pravidlo, které schválně někoho pouští, je taky selhání bloku, jen záměrné, a stojí za to ho jednou za čas porovnat s tím, co skutečně propustilo.
Odmítnutí je pro sklízeče informace
12. října se objevil rezidenční pool, který posílal hlavičky jako prohlížeč a mluvil HTTP/2. Nespadl do žádného z košů, kam padají podezřelí klienti, a běžný access log adresy ani User-Agent neukládá, takže jsem je neviděl. Hlídka ho chytila jen podle objemu, protože do druhého dne ráno stáhl kolem 3 900 stránek. Krátkodobá sonda pak ukázala, že za 11 minut poslal 114 požadavků ze 104 adres v 55 různých sítích, bez reverzních záznamů, tedy z domácích linek.
Jeho nástroj měl vadu, kterou skutečný prohlížeč nemá, a na tu jsem v noci nasadil řez s odpovědí 403. Od té chvíle dostával jen odmítnutí. O tři hodiny později jel znovu, tentokrát z headless Chromu s pluginem, který maskuje automatizaci. Hlavičky měl úplné a vada z noci zmizela. Stahoval zase kolem 500 stránek za hodinu a za ten den jich bylo zhruba 4 000.
Že je to opravdový prohlížeč, prozradil web sám. Moje stránky mají zapnuté hlášení porušení CSP a na každý detail filmu přišla od klienta dvě hlášení, protože prohlížeč vyhodnocoval inline skripty. Knihovna, která jen stahuje HTML, žádné hlášení nepošle.
Odpověď 403 sklízeči přesně řekne, od kdy ho poznávám. Kdo se o sklizeň stará, má z odmítnutí zpětnou vazbu zadarmo a další verzi nástroje za pár hodin. Blok podle adresy proti rezidenční síti nefunguje a blok podle tvaru požadavku vydrží do nejbližší úpravy nástroje. Další 403 by jen otočila stejné kolečko.
Odpověď, která sklízeči nic neprozradí
Místo dalšího odmítnutí jsem se zeptal, co na stránce vlastně chráním. Název filmu, rok, režie, herci, synopse a hodnocení jsou veřejně na mnoha místech a sklízeč by je sehnal jinde. Moje práce je informace, kde se film dá v Česku pustit, přímé odkazy na konkrétní titul u služeb a štítky, které k filmům dělám sám.
Klient, kterého poznám jako sklízeče, teď dostane stavový kód 200 a stránku bez téhle vrstvy: bez bloku „kde sledovat“, bez přímých odkazů, bez mých štítků a bez nabídek ve strukturovaných datech. Všechno ostatní na stránce zůstává, protože holá kostra by byla stejně hlasitý signál jako 403. Tichá verze přitom nesmí tvrdit nic, co není pravda. Kdyby místo prázdného bloku napsala, že film není v žádném předplatném, lhal bych tím i člověku, který do ní omylem spadne. Proto má vlastní větev šablony. Plná šablona by z prázdných dat napsala právě tohle.
Na tom posledním záleží, protože pravidlo občas trefí i člověka. Kdo do tiché verze spadne, tomu chybějící blok doplní skript na stránce z malého endpointu, který vrací stejný fragment ze stejné šablony. Samotné doplnění trvá v Chromiu kolem čtvrt sekundy a doplněný blok je znak po znaku stejný jako v plné verzi.
S doplněním jsem se dvakrát spletl. Nejdřív jsem předpokládal, že pool skripty nespouští. Platilo to jen pro nástroj, který pool používal v noci. Headless Chrome skripty spouštěl a hlášení CSP mi to ukazovala už ráno, jen jsem se nepodíval. Pak jsem doplnění podmínil příznakem navigator.webdriver, který řízený prohlížeč nastavuje. Maskovací plugin ho skryje a pool si dostupnost doplnil během dvou minut od nasazení. Co se počítá v prohlížeči, si prohlížeč může zfalšovat. Rozhodovat musí server.
Kdy přestat přidávat vrstvy
Další dva dny jsem proti poolu stavěl vrstvu po vrstvě. Nejdřív jsem na serveru přidal brzdu. Profil klienta, který si za hodinu doplní příliš mnoho tichých stránek, dostane prázdný fragment. Brzda za celou noc nesepnula ani jednou. Pool zpomalil na desítky stránek za hodinu, ještě než brzda mohla cokoli udělat, a navíc se objevil v jiném jazykovém nastavení. Tichá verze tehdy chytala jen to původní, takže nový profil dostával plnou stránku. Ráno jsem pravidlo rozšířil a pool tichou verzi zase dostal. Dostupnost si ale doplnil pokaždé. Doplnění jsem v tom samém kroku odložil až na první pohyb nebo uplynutí krátkého časovače a v 89 doplněních, která jsem ten den změřil, pool nepohnul myší ani jednou. Prostě nechal stránku otevřenou, dokud časovač nevypršel.
Dalo se to čekat, protože klient, který stránku renderuje a skript spouští sám, na krátký časovač vždycky počká. Časovač zastaví jen klienta, který stránku zavře dřív, než vyprší.
Večer jsem se rozhodl obranu proti tomuhle poolu dál nerozšiřovat. Bral kolem 25 stránek za hodinu, provozní škoda byla nulová, každá vrstva mě stála den práce a on ji obešel za pár hodin. Tichá verze zůstala jako měřák. Počet tichých stránek proti počtu doplnění po jednotlivých profilech přesně ukazuje, kolik dostupnosti odtéká, a to je víc, než mi dával kterýkoli blok.
Během těch dvou dnů jsem zamítl ještě několik nápadů, které vypadají dobře na papíře. Otisk prohlížeče přes WebGL nebo rozměry okna maskuje stejný plugin, který maskoval navigator.webdriver. Token vázaný na adresu nefunguje, protože fragment si pool stahoval ze stejné adresy jako stránku ve všech čtyřech případech, které jsem změřil. Proof-of-work za desítky milisekund nic nestojí klienta, který bere 25 stránek za hodinu. A skrývat odkazy v HTML přes CSS je blízko cloakingu a moc to nepomůže, protože sklízeč, který text bere z vyrenderované stránky, skrytý obsah stejně vynechá.
Kanárek a měření ve stínu
Místo dalších řezů jsem nasadil měření, která nic neblokují.
První je kanárek. Na každém výpisu je skrytý odkaz, který člověk nevidí, klávesnicí se na něj nedostane a robots.txt ho zakazuje. Kdo ho stáhne, sbírá odkazy přímo z HTML výpisů, a URL kanárka prozradí, ze kterého výpisu. Pool z prohlížeče ho za šest dní nestáhl ani jednou, takže odkazy z HTML výpisů nesbírá. Tichá verze výpisů by ho nezasáhla a pro něj jsem ji nestavěl.
Druhé měření hlásí, jestli na detailu přišel dřív pohyb nebo vypršel časovač. Na tiché stránce tím začíná doplnění, na plné se jen zapíše, co nastalo dřív. Chtěl jsem zjistit, jestli se čekání na časovač dá použít jako otisk stroje. U lidí z Česka a Slovenska skončilo za šest dní časovačem 868 z 2 211 zobrazení, tedy 39 %, a den po dni to drželo mezi 36 a 44 %. Otisk podle chování by trefil skoro dvě pětiny lidí. Bez toho měření bych ho možná nasadil, protože u poolu to vypadalo přesvědčivě.
Změřit bylo potřeba i to, koho tichá verze trefí. Pravidlo, které jsem 14. října ráno rozšířil, posílalo tichou stránku i čtenářům z Česka a Slovenska, kteří přišli bez Refereru, třeba ze záložky nebo z aplikace. Podle zobrazení detailu za předchozí týden šlo o 5,4 % všech zobrazení. Doplnění skriptem je mělo zachránit, jenže ze sedmi tichých stránek, které takoví čtenáři 14. října do večera dostali, se doplnily jen dvě. Proč ne ostatní, z logu nevyčtu. Pravidlo jsem pro ně ještě ten večer upravil a pool tím o nic nepřišel, protože se za takového čtenáře nevydával.
Ve stínu měřím i hustotu. Profil klienta, který za hodinu projde nezvykle mnoho různých detailů, se jen zapíše a stránku dostane plnou. Ostře ji nezapnu. Sepnula na oba sklízeče z další kapitoly, ale taky 15. října na ClaudeBota od Anthropicu, který prošel přes 6 000 detailů z jedné adresy, 19. října na crawler vyhledávače Exa, který ten den poslal přes 3 500 požadavků, a 20. října na GPTBota od OpenAI, který za 20 minut prošel přes 1 800 detailů. Všichni tři se představují a jsou na webu vítaní. Ostrá hustota by jim dala ochuzenou stránku.
Dostupnost odchází i jinými dveřmi
15. října hlídka nahlásila jinou sklizeň. Tentokrát to byla obyčejná HTTP knihovna s podvrženým User-Agentem prohlížeče, bez JavaScriptu, z rezidenční sítě, každá IP adresa jen jednou. Za noc brala stovky stránek a pokračovala v seznamu. Ze 764 stránek za dvě noci po sobě se překrývala jediná. Takovým tempem by katalog prošla zhruba za měsíc. Pravidla tiché verze do té doby mířila na prohlížeč, takže tuhle knihovnu minula. Od skutečného prohlížeče se ale dala spolehlivě odlišit, a tak na ni stačilo jedno pevné pravidlo navíc.
Pak se ukázalo, že detail filmu není jediné místo, kde dostupnost leží. Výpisy, třeba žebříčky typu „horory na Netflixu“, nesou přímé odkazy u každé karty. Na 310 výpisech ze sitemapy je dohromady 3 783 titulů s přímým odkazem. Výpis služby má odkazy i na jiné služby u stejných titulů, takže velký výpis vydá tolik jako desítky detailů. Knihovna brala i výpisy, takže tichou verzi nakonec dostaly i ony.
17. října se ozvala hlídka API. Během čtvrt hodiny přišlo přes 600 vyhledávacích dotazů bez jediné hlavičky prohlížeče, na filmy a seriály z jednoho seznamu názvů. API vracelo celé tituly i s dostupností, protože ho používá aplikace na webu, a tichou verzi do té doby dostávaly jen stránky. Stejný klient si stáhl i JavaScript aplikace a URL detailů si skládal z identifikátorů v odpovědích. Takhle se chová někdo, kdo web teprve rozebírá.
Tichou verzi teď vrací i API, ale až po inventuře a jen klientovi, který se nechová jako prohlížeč. Dostupnost vydávalo kolem deseti endpointů a mezi nimi i ten, který vrací fragment pro doplnění tiché stránky a na který odkazoval skript přímo v její šabloně. Kdo chrání jeden výstup, přesune sklízeče na druhý.
Všechno vypadá jako reakce na mě
V noci z 16. na 17. října přišla knihovna jen na tři detaily a skončila. Předchozí dvě noci poslala 344 a 420 požadavků a tohle byla první noc s tichou verzí, takže se nabízel závěr, že knihovna kontroluje obsah, všimla si toho a vzdala to. Další den odpoledne jela znovu, v noci prošla kolem 700 detailů a s tichou verzí ta pauza zjevně nesouvisela.
Podobně jsem si jeden večer zapsal, že pool utichl po nasazení kanárka. Přesné časy to vyvrátily. Poslední požadavek poslal pět minut před restartem serveru a to, co přicházelo potom, byl jiný klient, který chodil už dřív, než jsem ten večer cokoli nasadil. Když jsem prošel všechny „reakce“, které jsem do té doby zaznamenal, obstála jedna jistá (přizpůsobení na 403) a jedna pravděpodobná (nové jazykové nastavení necelou hodinu po první tiché verzi). K tomu stačí, když sklízeč kontroluje, co z odpovědi vyparsoval. Ostatní domnělé reakce měly jednodušší vysvětlení.
Problém je v tom, kdy se dívám. Nejpečlivěji čtu logy právě po každém nasazení a pool mění chování i sám od sebe, protože si po hodinách točí profily. Změnu kolem nasazení proto najdu skoro vždycky. Rozhodl by jen slepý pokus, třeba restart bez jakékoli změny v náhodnou chvíli, případně srovnání s počtem změn v hodinách, kdy jsem nic nenasadil.
A co roboti, kteří se představí
Nejjednodušší případ je klient, který se představí, a i tam se rozhodnutí neopírá o jméno. Exa a ShapBot provozují vyhledávací indexy pro AI aplikace a prodávají přístup k nim přes API. Účel mají stejný a na webu jsem nechal jen jednoho z nich.
Exa si robots.txt stahuje jednou denně a jezdí pomalu, i v nejsilnější hodině kolem deseti požadavků za minutu. Měření hustoty na ni sepnulo přesto, protože i takové tempo dá za hodinu stovky různých detailů. Blokovací skript ji zablokoval jednou, než jsem ji ručně vyjmul. ShapBot 10. října za šest minut stáhl 1 854 stránek z osmi adres v Google Cloudu. Na každou poslal nejdřív HEAD a hned potom GET, takže to dělalo zhruba 630 požadavků za minutu. Jeho adresy jsou ve zveřejněném seznamu, takže pravost se dala ověřit. Robots.txt si za sedm dní nestáhl ani jednou, i když jeho dokumentace tvrdí, že ho respektuje. Tempo mu tedy nepřikážu ani přes robots.txt a rate limit na stránky jsem zamítl hned po první vlně v srpnu, protože by trefil lidi za sdílenou adresou operátora. Zbývá jen blok, a tak jsem ShapBota na rozdíl od Exy z bloku nevyjmul.
Co si z toho vzít
Když blok selže, prověřte nejdřív vlastní kód a pravidla, až potom sklízeče. Z pěti selhání za ty čtyři týdny jsem dvě zavinil sám, chybami ve skriptech a nechráněným API, a u třetího jsem sklízeče málem nechal běžet.
Odmítnutí je zpětná vazba pro toho, koho odmítáte. Proti klientovi, který se umí přizpůsobit, funguje lépe odpověď, ze které se nic nedozví: stránka bez té části, která je vaše práce. Chce to ale vědět, co přesně chráníte, a projít všechna místa, kudy to odchází, včetně API a výpisů.
Každou novou vrstvu stavte proti tomu, jak sklízeč chodí právě teď, a počítejte s tím, že ji obejde. Když vás vrstva stojí den a sklízeče hodinu, je čas přestat a začít měřit. Měřák, který přesně ukazuje, kolik odtéká, je užitečnější než další past.
Pravidla na chování nejdřív pusťte naprázdno na lidech. Čekání na časovač vypadalo jako spolehlivý otisk stroje a stejně se chovají skoro dvě pětiny čtenářů.
Rezidenční pool pořád zablokovat neumím. Jen už vím, že ho blokovat nepotřebuju. Stačí, když si jednoduché skripty z mé práce nic neodnesou, a u těch složitějších přesně vidím, kolik se jim odnést podařilo.