Sedm vln sklizně a jeden access log: jak poznat sklízeče bez jména

Sedm vln sklizně za měsíc, žádný Cloudflare a vlastní access log jako jediné měřidlo. Jak poznat sklízeče, který se nepředstaví, proč blokem práce nekončí a co s poolem, který adresu nikdy nezopakuje?
Nálepky:
Na katalog najdi-film.cz přišlo mezi 26. srpnem a 20. zářím sedm vln sklizně a schovávaly se čím dál líp. První klient se nepředstavil, ale ani se neschovával: začal u robots.txt a za 2,5 hodiny si vzal 5 885 stránek. Poslední jel nejméně z 602 adres, každou použil právě jednou, a rezidenční síť, kterou si k tomu koupil, zablokovat pořád neumím.
Mezi tím je měsíc práce s vlastním access logem, bez Cloudflare a bez jediného zakoupeného nástroje. Shrnu, co z toho funguje, co jsem zamítl a kde jsem se spletl, protože chyby na měřidle byly nakonec užitečnější než samotné bloky.
Začal jsem s otázkou „jak je zablokovat“ a skončil u otázky „jak je spolehlivě poznat a spočítat“. Blokování je ta snazší část a u některých klientů nemá vůbec smysl.
| Kdy | Objem | Co ho prozradilo | Co ho zastavilo |
|---|---|---|---|
| 26. 8. | 5 885 stránek za 2,5 h, 7 souborů statiky | nic, pravidlo na něj ještě nebylo | nic, skončil sám |
| 27. až 29. 8. | 6 014 stránek, 41 adres v cloudu | 90 % sitemapy, statika nula | řádek v robots.txt, jmenný blok |
| 1. 9. | ~40 serverů, 3 až 6 požadavků z adresy v jedné dávce | rozpor v hlavičkách | blok 69 adres v cloudu |
| 4. 9. | 148 požadavků, 148 adres, 147 podsítí | nedokonalé maskování za prohlížeč | řez na tvar požadavku |
| 5. 9. | 5 016 odmítnutých požadavků proti 114 obslouženým | stejný tvar, větší dávka | řez ze 4. 9. |
| 11. 9. | 2 554 požadavků, 806 adres, 79 zemí | hlavička, kterou prohlížeč neposílá | úzké pravidlo na tu hlavičku |
| 15. až 20. 9. | 1 206 požadavků, 602 viditelných adres, každá jednou | nic, jen tvar sklizně | řez ze 4. 9., obslouženo nula |
Čísla v tabulce i v textu odpovídají stavu k 21. 9. 2026.
Mimochodem, o zátěž tu nejde. 11. září odbavil server 3 276 stránek s detaily filmů místo obvyklých čtyř až osmi stovek a medián odbavení zůstal na 19 ms, stejně jako každý jiný den v tom týdnu. Data pro stránky čte ze souboru SQLite ve vlastním procesu, takže tady není databázový server, do kterého by šlo zatlačit. Web mi sklízeč nepoloží, zato mi bere obsah a rozbíjí čísla, podle kterých se rozhoduju.
Jak sklízeče poznám, když se nepředstaví
Signatury konkrétních nástrojů jsem zavrhl hned. Kdo posílá vlastní User-Agent, ten se dá vyřešit řádkem v robots.txt, a kdo se maskuje za prohlížeč, tomu stačí jeden řádek v konfiguraci, aby moje pravidlo přestalo platit. Chtěl jsem kritérium, které nepůjde obejít zadarmo.
Čím robots.txt je a čím není, tady na Zdrojáku v květnu rozebral Ondřej Dolejš v článku Robots.txt nestačí. Že je robots.txt prosba, a ne zámek, píše i norma RFC 9309: pravidla v něm „nejsou formou autorizace přístupu“. Dolejš mluví o botech, které jde oslovit jménem. Já začínám tam, kde jméno není.
Tím kritériem je tvar sklizně. Sklízeč jde po obsahu, takže každou stránku bere jednou a statiku nechává ležet. Renderující robot si ke stránce stáhne CSS a JavaScript, člověk v prohlížeči taky a navíc se na stejnou stránku vrací. Cache prohlížeče podíl statiky snižuje, ale nevynuluje, protože i podmíněný požadavek je v logu řádek jako každý jiný. Provoz skutečných lidí měřím přímo z ostrého webu, takže vliv cache už v číslech je. Na první hrubé roztřídění stačí dvě čísla: kolik různých stránek a kolik statiky připadá na jeden požadavek. Není to můj objev. Stejný princip najdete hotový třeba v CrowdSecu, ve scénáři http-crawl-non_statics. Ten ale počítá po jednotlivých adresách, což proti poolu, který každou adresu použije jen jednou, nestačí.
Důležitější než to pravidlo je negativní kontrola, kterou jsem k němu udělal. Klienti, které jsem si při prvním třídění odložil jako podezřelé, si za jediný den vzali dohromady necelých 1 400 různých stránek, což zní jako sklizeň, ale taky 827 statických souborů. Byli to lidé. Bez toho druhého čísla bych postavil pravidlo, které odmítá návštěvníky.
První půlku kritéria mi pak jeden ze sklízečů vyvrátil. 11. září přišel pool z rezidenční sítě, 806 adres ve 474 autonomních systémech a 79 zemích, v první dvacítce ani jedno datacentrum, samí koncoví operátoři včetně Starlinku. Chodil jako pavouk a Referer měl pravý: odkazující stránku stáhl dřív než cíl, a to ve všech 1 818 případech. A opakoval. 1 715 požadavků na detail filmu mířilo jen na 1 007 různých filmů, 413 z nich si vzal dvakrát a titulní stranu 237krát, protože každý z jeho pěti workerů si vede vlastní seznam navštívených. Věta „sklízeč neopakuje stránky“, ze které jsem sám vycházel, tedy obecně neplatí. Statiku nebere, to ano, ale opakovat umí.
Kde to kritérium končí, mi ukázala druhá půlka září. Vyhledávač Seznamu mi od 16. září bere mezi 1 000 a 3 000 různých stránek denně a statiku nebere vůbec. Má přesně ten tvar, podle kterého sklízeče hledám. Je to legitimní robot, který se představuje a kterého si na webu přeju. Tvar sklizně proto jen třídí. Rozhoduje až ověřené jméno, u vyhledávačů přes reverzní DNS.
Další past je v okně měření. Moje hodinová hlídka označila za sklízeče poctivý crawler jedné SEO služby jen proto, že se dívala na kratší úsek, než kolik trvá celá návštěva. Za hodinu vypadal jako záplava, za den jako slušně rozložený průchod.
Blokem to nekončí
5. září mi přehled návštěvnosti ukázal rekordní den, který se nestal. Tvořilo ho 5 016 odmítnutých požadavků proti 114 obslouženým. Sklízeč, kterého jsem právě úspěšně blokoval, mi dál kazil čísla, podle kterých se rozhoduju, co na webu postavit. Od té doby počítám odmítnuté požadavky zvlášť a do sklizně zahrnuju jen to, co dostalo obsah.
Podobně se rozbíjejí prahy. Druhý sklízeč v pořadí si z každé adresy vzal 244 až 299 stránek, zatímco hlídka měla práh 300 na klienta. V součtu to byla skoro celá sitemapa a hlídka mlčela. Prahy měřené na jednom klientovi jsou proti rozprostřené sklizni slepé. Hlídka proto dnes sčítá flotily a součet přebíjí jednotlivé členy, aby o jedné sklizni nepřišlo 40 hlášení.
Nejvíc mě ale poučilo, co blok se sklízečem udělá. Pool ze 4. září po odmítnutí zkusil stejnou stránku během pěti sekund ještě dvakrát, vždy z jiné adresy a z jiné země. Ten z 11. září naopak návratové kódy nečetl vůbec a po zablokování zrychlil čtyřikrát, protože odmítnutí odbavím řádově rychleji než vyrenderovanou stránku a jeho vlákna se tím protočí častěji. Stejný blok je pro jednoho signál a pro druhého zrychlovač a bez měření nevíte, kterého z nich máte.
Proč jsem nešel do Cloudflare
Dva důvody, oba provozní. První byl dočasný: zrovna mi propadla indexace u Bingu a výměna IP adresy, TLS otisku a hlaviček by v tu chvíli zamlžila, co by stálo za jejím návratem.
Druhý důvod je praktický a platí obecně. Proxy před webem píše do logu vlastní adresu. Pravou si vrátíte hlavičkou CF-Connecting-IP a seznamem důvěryhodných proxy na straně serveru, což je pár řádků konfigurace. Kdo to udělá až po zapnutí, přijde mezitím o ověření robotů. Pravého robota poznám tak, že jeho adresu přeložím na jméno a jméno zpátky na adresu. S adresou proxy to selže u všech botů naráz, tedy přesně tam, kde padá rozhodnutí o odmítnutí. Cloudflare kvůli tomu zavrhovat nemusíte, jen měřidlo se musí přenastavit dřív, než se ochrana zapne.
Cloudflare je nejsilnější proti zátěži, a tu jsem neměl. I 11. září zůstal medián odbavení na 19 ms. Jestli by jeho ochrana proti botům chytila i rezidenční pool, nevím, nezkoušel jsem to.
Tři chyby, které jsem udělal na měřidle
Server si provoz rozděluje do pěti logů podle třídy klienta a k tomu drží archiv. Vypadá to jako dobrý nápad a taky to je dobrý nápad, ale 11. září mi to za jediný den třikrát zkreslilo měření.
Nejdřív jsem o té sklizni ohlásil poloviční objem. Počítal jsem ho z logu podezřelých klientů, protože jen tam zůstává adresa, a přehlédl jsem, že do něj padá jen část požadavků. Hlavní log má všechny, jen kvůli soukromí bez adresy. Pak na stejné rozštěpení doplatila hlídka, protože klienty sčítá přes adresu a v jednom z těch logů adresa není. V hlášení mi tak přišla třetina skutečného objemu.
Třetí chyba byla nejhorší. Tomu sklízeči prosakovala hlavička, kterou prohlížeč na web nikdy neposílá, a já na ni postavil pravidlo. Obhájil jsem ho tím, že se ta hlavička za 34 dní neobjevila u nikoho jiného. To měření ale sáhlo jen do živých logů a minulo archiv. V archivu byli dva skuteční lidé, kteří tu hlavičku poslali taky, protože jsou za proxy operátora nebo VPN, která hlavičky přeposílá naslepo. Jeden z nich přišel z prohlížeče vestavěného do Facebooku a rovnou se připojil do párovací místnosti, kde spolu dva lidé vybírají film. Široké pravidlo by ho odtamtud vyhodilo uprostřed výběru. Ještě ten den jsem k hlavičce přidal další podmínky, ve kterých se knihovna sklízeče liší od prohlížeče v mobilu. Takhle zúžené pravidlo pokrylo 95 % sklizně a oba ty lidi pustí.
Platí to pro každé podobné rozhodnutí: nula z vlastního dotazu není důkaz, dokud neukážete, že ten dotaz umí vrátit i nenulu.
Co z toho nakonec stojí v konfiguraci
Provoz se podle třídy klienta rozděluje do několika logů, takže vyhledávače, AI roboti a klienti, kteří se prozradili rozporem v hlavičkách, nepadají do jedné hromady s lidmi. Odmítám pak ve dvou vrstvách, které o sobě nevědí.
Dole, v konfiguraci webserveru (u mě Caddy), stojí statické řezy na tvar požadavku. Rozhodují podle jediného požadavku, jméno neřeší a ověřit ho neumějí, protože reverzní dotaz do DNS nemá co dělat v cestě každé odpovědi.
Nad nimi jede hodinová hlídka, která čte logy, a jen ta umí ověřit jméno a sečíst flotilu. Do Telegramu hlásí klienta, který přeleze práh a zároveň má tvar sklizně. Klienty, kteří patří k sobě, spojuje podle čtyř vodítek naráz: stejné jméno z různých adres, jedna adresa pod různými jmény, jedna podsíť a u klientů z podezřelého logu i stejný otisk hlaviček. Podle jména přitom smí sčítat jen klienta, který se za prohlížeč nevydává nebo se prozradil rozporem. Jinak by se jedna verze Chromu slila do flotily o tisíci lidech a součet by vypadal jako sklizeň. Hotové nástroje na bany tady míjejí cíl, protože fail2ban hledá opakovaný neúspěch z jedné adresy, zatímco sklízeč sbírá samé dvoustovky z adres, které se neopakují.
Hlídka sama neblokuje. Bloky zapisuje samostatný skript, který běží každých 15 minut. Pojmenovaného sklízeče z hlášení hlídky zablokuje automaticky. První blok vyprší po 48 hodinách, protože trvalé bloky nikdo nereviduje, a kdo se vrátí, dostane týden a pak měsíc. Vyhledávače ani AI roboty automaticky neblokuju. Platí to ale jen tak dlouho, dokud jim jméno sedí. Adresu přeložím na jméno a jméno zpátky na adresu a komu to nevyjde, ten dostane blok na adresu, i kdyby měl v hlavičce napsáno Googlebot. Jinak podle adresy blokuju jen to, čemu reverzní záznam ukazuje na cloud. Domácí a mobilní linky ho buď nemají vůbec, nebo v něm stojí dynamický pool operátora. Uříznout člověka za sdílenou adresou je dražší omyl než pustit jeden stroj, a proto skript proti rezidenčnímu poolu nic nezmůže.
Že to nestřílí do vlastních řad, jsem si ověřil auditem 11. září. Za předchozích sedm dní padlo 21 447 odmítnutí a nebyl mezi nimi ani jeden vyhledávač. Adres, které odmítlo hlavní pravidlo, bylo 9 152 a ani jedna z nich si za celý týden nevzala statický soubor, jaký si stahuje každý prohlížeč.
Jak to skončilo
Mezi 15. a 20. zářím přišel třikrát rezidenční pool, který už neposílá hlavičku z posledního pravidla. Jestli za ním stojí stejný provozovatel jako 11. září, říct neumím. Adresy se nepřekrývají a návratové kódy tenhle čte, chová se spíš jako pool ze 4. září. Zastavilo ho pravidlo starší a hrubší, postavené na tvaru požadavku. Dohromady si řekl o 216 různých stránek a nedostal ani jednu.
Zajímavější je jeho tvar. Adresu vidím u poloviny požadavků, protože zůstává jen v podezřelém logu. Viditelných adres je 602, každá použitá právě jednou. Mezi jednotlivými dny se neopakuje ani jedna adresa a ani jedna stránka. Uvnitř jednoho dne naopak opakuje pilně: na každou stránku šest pokusů během pěti sekund. Adresu vidím u tří z nich a pokaždé je jiná. Návratové kódy čte a odpovídá na ně rotací.
Proti takovému klientovi je blokování podle adresy k ničemu. Blok adresy, která se už nikdy nevrátí, je jen řádek v konfiguraci, který stárne. Zabral řez na tvar požadavku, nasazený 11 dní předtím, než na něj tenhle pool narazil. Iluze si o něm nedělám. Obejít se dá levně a drží jen do dne, kdy si toho provozovatel všimne, takže pool zatím zastavila spíš chyba v jeho nástroji než moje obrana.
Obrana má taky cenu, kterou je fér přiznat. 18. a 19. září odmítl ten řez Storebota od Googlu, jeden požadavek denně. Tím přestal platit závěr auditu z 11. září, že mezi odmítnutými není ani jeden vyhledávač. Výjimka pro ověřené roboty bydlí o patro výš, v hlídce, a ta o jednotlivém požadavku nerozhoduje. Řez vidí jen tvar požadavku a ten měl Storebot stejný jako sklízeč. Samotné ověření by ho navíc nezachránilo. Nechodí z domény googlebot.com jako zbytek Googlu a slovo googlebot v jeho jménu není, takže jsem mu tu kontrolu musel dopsat ručně.
16. září ohlásila hlídka dalšího sklízeče. Byl jsem to já s kontrolou sitemapy, 310 stránek proti prahu 300. Stalo se to potřetí, protože kontrola má přesně ten tvar, který hledám: každá stránka jednou, žádná statika. Zablokovaný jsem nebyl ani jednou, takže šlo jen o šum v hlášení. Curl podle jména neblokuju, používá ho kdekdo, a blok podle adresy domácí linku mine, protože nemá reverzní záznam cloudu. Vlastní adresu na výjimku dát nemůžu, je dynamická, a práh zvedat nechci, protože 300 už jednou bylo málo. Zbyla jediná cesta: kontrola teď jede pod vlastním jménem, které hlídka zná a nehlásí. To jméno musí znát i blokovací skript. Bez výjimky by kontrolu zablokoval jako kteréhokoli jiného pojmenovaného klienta, takže představit se by bylo horší než nepředstavit se vůbec.
Co si z toho vzít
Sklízeče poznáte i bez signatur, protože tvar sklizně se obchází dráž než hlavička. Sám o sobě ale nestačí: rozhodovat podle něj bez ověření jména znamená odmítat vyhledávače.
Po bloku se měří dál. Odmítnuté požadavky patří do vlastního koše, jinak vám zablokovaný sklízeč zkreslí čísla.
Prahy stavte na flotilu, ne na klienta. Když pak přijde hlášení, ověřte nejdřív, jestli to nejste vy.
Vítězný konec tenhle příběh nemá. Rezidenční pool spolehlivě zablokovat neumím, jen ho zatím nepouštím k obsahu. Měřit ho umím, a to je rozdíl, o kterém stojí za to vědět dřív, než si člověk koupí ochranu s pocitem, že tím problém zmizel.