Вебкраулер на Python: як побудувати масштабований краулер на asyncio

Вебкраулер на Python є програмою, що завантажує сторінки, витягує дані й посилання та планує подальший обхід. Його використовують для технічного аудиту, моніторингу цін і підготовки наборів даних. У реальній роботі швидкість обмежують правила сайту, мережеві затримки, пам’ять браузера та якість дедуплікації URL. Тому збір даних починається не з кількості вкладок, а з черги з керованим навантаженням.
Масштабований краулер розділяє планування, завантаження, парсинг і зберігання. Такий поділ дозволяє замінити HTTP-клієнт на Playwright тільки для сторінок із JavaScript, не переписуючи решту системи. Нижче зібрано робочий каркас для asyncio, правила ввічливого обходу та способи визначити, коли збільшення кількості воркерів перестає прискорювати обхід.
З яких частин складається архітектура масштабованого вебкраулера
Масштабований вебкраулер складається з frontier-черги, набору воркерів, fetcher-рівня, парсера та сховища. Кожен компонент має окрему відповідальність, тому сайт із тривалим часом відповіді, помилка селектора або збій запису не зупиняють увесь обхід.
Frontier зберігає нормалізовані URL і вирішує, який хост можна опитати наступним. Для невеликого сайту достатньо asyncio.Queue. Для кількох доменів краще тримати окрему чергу на хост, власний ліміт конкурентності й час наступного дозволеного запиту. Цей принцип добре поєднується з візуальними сценаріями автоматизації, коли браузерні дії треба відокремити від планувальника.
Fetcher отримує URL і повертає HTTP-статус, фінальну адресу, заголовки та тіло відповіді. Спочатку він виконує менш ресурсомісткий HTTP-запит. Якщо контент формується у браузері, потрібен логін або порожній HTML не містить цільових даних, завдання переходить у Playwright. За такого підходу спочатку завантажують сторінки через HTTP, а браузер залучають лише за потреби, а не як універсальний засіб завантаження.
Парсер не повинен відкривати мережеві з’єднання. Він бере готовий документ, витягує canonical URL, JSON-LD, потрібні поля й нові посилання. Окремий класифікатор позначає сторінку як ok, blocked, not_found, parse_error або retryable. Для динамічних сторінок важливо врахувати ознаки headless browsing, бо WAF оцінює не лише User-Agent, а й поведінку сесії.
Сховище зберігає результати та стан обходу. Для одного процесу зазвичай достатньо SQLite у режимі WAL, а для кількох інстансів краулера підійде PostgreSQL. JSONL зручний для експорту, але слабкий як єдине сховище: після перезапуску складно визначити, які URL уже завершені, а які залишилися в роботі.
| Компонент | Що зберігає або виконує | Ознака готовності до масштабу |
|---|---|---|
| frontier | URL, пріоритет, хост, час наступної спроби | окремі ліміти й паузи для кожного хоста |
| worker | бере одне завдання та повертає результат | помилка сторінки не завершує корутину |
| fetcher | HTTP або браузерне завантаження | є timeout, retry і єдиний формат відповіді |
| parser | поля, посилання, JSON-LD, canonical | не виконує мережевих запитів |
| storage | записи, статуси, checkpoint | повторний запуск продовжує обхід |
Поділ здається зайвим, поки краулер обробляє двадцять сторінок. Під час обходу десятків тисяч URL такий поділ допомагає заощадити час: зависання Playwright не блокує HTTP-воркери, а зміни структури даних не потребують перероблення черги frontier.
У frontier корисно зберігати не просто рядок URL, а структурований запис завдання: url, depth, priority, attempt, ready_at, discovered_from і ознаку потреби в браузерному завантаженні. Пріоритет дозволяє раніше обробити картки товарів або свіжі статті, не чекаючи завершення всіх сторінок пагінації. Поле discovered_from допомагає визначити джерело циклічного обходу чи помилкового посилання.
Нормалізація URL виконується перед дедуплікацією. Наведений нижче код реалізує лише базову нормалізацію: схему, регістр хоста, стандартні порти, фрагмент і порожній шлях. Правила для завершального / та параметрів запиту залежать від конкретного сайту. Видаліть fragment після #, приведіть host до нижнього регістру, приберіть стандартні порти та застосуйте узгоджену політику до завершального /. Параметри на кшталт utm_source часто можна відкинути, але параметр page=2 змінює ресурс. Білий список значущих параметрів надійніший за спробу видалити все після знака питання.
Для великих обходів одного seen у RAM мало. У кількох процесах резервуйте завдання атомарно, наприклад через FOR UPDATE SKIP LOCKED у PostgreSQL, та використовуйте lease зі строком дії, щоб повертати незавершені завдання після збою. Bloom filter економить пам’ять, але допускає хибні спрацьовування, через які частина нових URL може бути пропущена. Якщо повнота важливіша за максимальну швидкість, зберігайте нормалізований URL або його криптографічний хеш у таблиці з унікальним індексом.
Як написати вебкраулер на asyncio і Playwright
Краулер на asyncio варто будувати навколо воркерів, які працюють протягом усього обходу і черги з керованим навантаженням. У навчальному прикладі нижче черга необмежена, але кількість доданих URL обмежує параметр limit.
Для робочої системи потрібні обмежена черга та окремий координатор frontier, який не допустить взаємного блокування воркерів. Не створюйте окрему задачу на кожен URL через необмежений asyncio.gather(): великий sitemap швидко перетворить список корутин на непотрібне споживання пам’яті.

Підготуйте Python 3.11 або новіший і встановіть залежності. Для HTML-парсингу в прикладі використано Beautiful Soup, а Playwright потрібен для сторінок, де цільовий контент з’являється після виконання JavaScript.
- створіть окреме віртуальне середовище командою
python -m venv .venv - активуйте середовище відповідно до вашої операційної системи
- встановіть пакети командою
pip install aiohttp beautifulsoup4 playwright - завантажте Chromium командою
playwright install chromium - додайте тестовий домен, який дозволяє автоматизований обхід
Каркас нижче показує головний механізм: дедуплікацію до постановки в чергу, task_done() у блоці finally, ліміт воркерів і один браузер, який використовують повторно. Це навчальний мінімум, а не готовий обхід будь-якого сайту. Функція нормалізації не перевіряє вбудовані облікові дані, DNS-відповіді чи доступність внутрішніх IP-адрес.
Для роботи з недовіреними URL потрібні окрема перевірка адрес, контроль DNS і мережеві обмеження, зокрема захист від SSRF. Приклад не реалізує перевірку robots.txt, затримки між запитами, повторні спроби та збереження стану. Виклик raise_for_status() передає всі HTTP-помилки загальному обробнику: навчальний код не розрізняє 404, 429 і 503 та не використовує Retry-After.
Правила класифікації статусів і повторних спроб, описані нижче, потрібно реалізувати окремо. Запускайте його лише на власному або явно дозволеному тестовому ресурсі, попередньо перевіривши правила доступу.
import asyncio
from dataclasses import dataclass
from urllib.parse import urldefrag, urljoin, urlsplit, urlunsplit
import aiohttp
from bs4 import BeautifulSoup
from playwright.async_api import Error as PlaywrightError, async_playwright
@dataclass
class Job:
url: str
depth: int = 0
def normalize(base: str, href: str) -> str | None:
absolute, _ = urldefrag(urljoin(base, href))
try:
parsed = urlsplit(absolute)
if parsed.scheme.lower() not in {"http", "https"} or not parsed.hostname:
return None
if parsed.username is not None or parsed.password is not None:
return None
scheme = parsed.scheme.lower()
host = parsed.hostname.lower()
port = parsed.port
if ":" in host: # IPv6
host = f"[{host}]"
if port and not ((scheme == "http" and port == 80) or
(scheme == "https" and port == 443)):
host = f"{host}:{port}"
# Query-параметри залишаємо: деякі з них визначають ресурс.
# Політику щодо завершального / і tracking-параметрів задають окремо.
return urlunsplit((scheme, host, parsed.path or "/", parsed.query, ""))
except ValueError:
return None
async def crawl(start_url: str, limit: int = 200, workers: int = 8):
start_url = normalize(start_url, start_url)
if not start_url:
raise ValueError("Некоректна початкова адреса")
if limit < 1 or workers < 1:
raise ValueError("limit і workers мають бути додатними")
queue: asyncio.Queue[Job] = asyncio.Queue()
seen = {start_url}
results = []
start_host = urlsplit(start_url).netloc
timeout = aiohttp.ClientTimeout(total=20)
connector = aiohttp.TCPConnector(limit=workers, limit_per_host=2)
browser_slots = asyncio.Semaphore(2)
async with aiohttp.ClientSession(
timeout=timeout,
connector=connector,
headers={"User-Agent": "ExampleAuditBot/1.0 contact@example.com"},
) as session, async_playwright() as pw:
browser = await pw.chromium.launch(headless=True)
try:
context = await browser.new_context()
try:
async def fetch(url: str) -> tuple[str, str]:
# Перевіряємо кожен HTTP-перехід до наступного запиту.
final_url = url
for _ in range(6):
if urlsplit(final_url).netloc != start_host:
raise ValueError("URL за межами дозволеного хоста")
async with session.get(final_url, allow_redirects=False) as response:
if response.status in {301, 302, 303, 307, 308}:
location = response.headers.get("Location")
if not location:
raise ValueError("Перенаправлення без Location")
next_url = normalize(final_url, location)
if not next_url or urlsplit(next_url).netloc != start_host:
raise ValueError("Перенаправлення за межі дозволеного хоста")
final_url = next_url
continue
response.raise_for_status()
html = await response.text(errors="replace")
final_url = str(response.url)
break
else:
raise ValueError("Забагато перенаправлень")
if "data-requires-javascript" not in html:
return final_url, html
async with browser_slots:
page = await context.new_page()
# Не дозволяємо браузеру запитувати інші хости.
async def restrict_route(route):
if urlsplit(route.request.url).netloc != start_host:
await route.abort()
else:
await route.continue_()
await page.route("**/*", restrict_route)
try:
async with asyncio.timeout(45):
await page.goto(final_url, wait_until="domcontentloaded", timeout=30_000)
if urlsplit(page.url).netloc != start_host:
raise ValueError("Браузер перейшов на інший хост")
return page.url, await page.content()
finally:
await page.close()
async def worker():
while True:
job = await queue.get()
try:
final_url, html = await fetch(job.url)
soup = BeautifulSoup(html, "html.parser")
results.append({
"url": final_url,
"title": soup.title.get_text(strip=True) if soup.title else None,
"depth": job.depth,
})
for tag in soup.select("a[href]"):
link = normalize(final_url, tag["href"])
if not link or urlsplit(link).netloc != start_host:
continue
if link not in seen and len(seen) < limit:
seen.add(link)
await queue.put(Job(link, job.depth + 1))
except Exception as error:
# Навчальний приклад: помилку записуємо, а воркер працює далі.
results.append({"url": job.url, "error": type(error).__name__})
finally:
queue.task_done()
tasks = [asyncio.create_task(worker()) for _ in range(workers)]
try:
await queue.put(Job(start_url))
await queue.join()
finally:
for task in tasks:
task.cancel()
await asyncio.gather(*tasks, return_exceptions=True)
finally:
await context.close()
finally:
await browser.close()
return results
if __name__ == "__main__":
data = asyncio.run(crawl("https://example.com"))
print(data)
Для швидкої перевірки нормалізації URL можна використати кілька простих тестів. Вони перевіряють стандартні порти, IPv6, фрагменти та збереження параметрів запиту:
assert normalize("https://EXAMPLE.com:443/a", "#section") == "https://example.com/a"
assert normalize("http://example.com", "/?page=2") == "http://example.com/?page=2"
assert normalize("http://[::1]:80/", "/path") == "http://[::1]/path"
assert normalize("https://example.com", "mailto:info@example.com") is None
Ці тести перевіряють лише перетворення адреси, а не безпечність мережевого доступу. Для недовірених URL додатково потрібні перевірки DNS і мережеві обмеження.
Ключова деталь тут не Playwright, а життєвий цикл завдання. Кожен queue.put() має завершитися одним queue.task_done(). Інакше queue.join() чекатиме вічно. Такий збій часто маскується під повільну мережу, хоча причина лежить у винятку всередині воркера.
У прикладі всі воркери використовують один BrowserContext, а семафор обмежує кількість одночасно відкритих сторінок. limit_per_host=2 у HTTP-клієнті обмежує одночасні з’єднання, але не кількість запитів за секунду: частоту обходу потрібно контролювати окремо. Для публічних сторінок без авторизації цього часто достатньо.
Якщо джерела потребують різних cookies або проксі:
- створіть окремий context на логічну сесію
- не запускайте окремий процес Chromium для кожного URL
- закривайте context після завершення групи завдань
Context потребує менше ресурсів, ніж окремий процес браузера. У робочій системі також контролюйте стан спільного контексту: після критичної помилки або втрати з’єднання з браузером його може знадобитися створити заново.
Передбачте:
- граничний час завершення завдання
- коректне скасування корутин
- очищення ресурсів
- зовнішній контроль і прибирання дочірніх процесів Chromium
Блок finally допомагає закрити сторінки й контексти за звичайного завершення або скасування корутин, але не виконується гарантовано, якщо процес Python аварійно завершено.
Перевірка хоста та перехоплення запитів через page.route() в прикладі обмежують частину переходів і запитів, але не гарантують повної ізоляції браузера.
Враховуйте такі обмеження:
- не всі мережеві звернення обов’язково проходять через цей обробник
- можливі особливості Service Worker
- DNS може повернути внутрішню IP-адресу навіть для дозволеного імені
Для недовірених джерел використовуйте ізольоване середовище з мережевими правилами, що блокують доступ до внутрішніх адрес і служб метаданих.
Параметр limit обмежує кількість унікальних URL, доданих до обходу, а не кількість успішно оброблених сторінок.
Обмежена черга, яку ті самі воркери одночасно читають і наповнюють, може спричинити deadlock: усі корутини чекатимуть вільного місця, але нікому буде забрати наступний URL.
У робочій версії:
- передайте додавання знайдених посилань окремому координатору черги frontier
- застосуйте зворотний тиск між координатором frontier, fetcher та writer
Для робочої системи також потрібні перевірка robots.txt перед запитом, обмеження частоти на хост, retry із затримками, постійне сховище frontier та окремий writer. Воркери передають готові записи в другу обмежену чергу, а writer пакетно додає їх у базу. Пакет із 100 записів зазвичай створює менше накладних витрат, ніж 100 окремих транзакцій. Якщо база даних працює повільніше, черга результатів поступово заповнюється та обмежує швидкість завантаження нових сторінок. Це допомагає уникнути перевантаження системи.

У робочому проєкті замініть ознаку data-requires-javascript на реальну перевірку. Наприклад, ескалуйте сторінку в браузер, коли потрібного CSS-селектора немає, HTML коротший за очікуваний мінімум або відповідь містить challenge-сторінку. Схожі конвеєри корисні для автоматизованого OSINT-збору, де джерела мають різні формати й режими завантаження.
Як обробляти помилки та дотримуватися robots.txt
Надійний краулер відрізняє постійні помилки від тимчасових і перевіряє правила доступу до завантаження URL. У наведеному вище навчальному коді перевірку robots.txt ще не реалізовано. 404 зазвичай не треба повторювати, а 429, 502, 503 і мережевий timeout потребують відкладеної спроби з exponential backoff та jitter.
Спершу завантажте /robots.txt для кожного хоста й кешуйте результат. Перевіряйте дозвіл для фактичного User-Agent, враховуйте Crawl-delay, якщо сайт його задає, та шукайте посилання на sitemap. Відсутність заборони в robots.txt не скасовує умови використання сайту, вимоги до персональних даних або обмеження доступу.
Порядок обробки відповіді краще зафіксувати явно:
- завершіть URL без повтору для статусів
400,401,403і404, якщо політика конкретного джерела не вимагає іншого - прочитайте
Retry-Afterдля статусів429і503: значення може бути кількістю секунд або HTTP-датою - розрахуйте паузу як
min(cap, base * 2 ** attempt) + jitter - поверніть завдання у frontier з майбутнім часом готовності
- переведіть URL у
failedпісля вичерпання ліміту спроб
Не тримайте воркер у asyncio.sleep() на десятки секунд. Коли час наступної спроби зберігається в черзі frontier як ready_at, воркер може перейти до завдань іншого хоста. Саме тут окремі черги на хост дають більший ефект, ніж просте збільшення workers.

Для Playwright задавайте два timeout:
- коротший для навігації
- довший для виконання всього завдання
Якщо page.goto() зависне, зовнішній asyncio.timeout() скасує операцію після заданого часу, але сам по собі не закриє сторінку. Обов’язково закривайте page або context у finally, щоб звільнити слот у пулі. Інакше кілька десятків невдалих переходів можуть залишити процеси Chromium у пам’яті.
Повторювати варто тільки ідемпотентні операції:
- звичайний
GETпідходить для повторного виконання - натискання кнопки, що створює замовлення або змінює дані, не належить до краулінгу
Ведіть retry budget на хост, а не лише на окремий URL. Якщо за останні п’ять хвилин половина запитів повернула 429 або 503, зупиніть нові завдання цього домену на короткий cooldown. Інакше тисяча URL виконає по три повтори та перетворить тимчасову проблему на три тисячі зайвих запитів.
Circuit breaker можна реалізувати трьома станами:
closed: запити йдуть за звичайним графікомopen: після перевищення порога помилок frontier перестає видавати завдання цього хостаhalf_open: після cooldown дозволяється один тестовий запит
Успіх тестового запиту повертає систему в closed, а повторна помилка знову переводить її в open.
Зберігайте причину кожної невдачі окремо:
dns_errorconnect_timeoutread_timeouthttp_429browser_timeoutparse_error
Ці помилки потребують різних рішень. Один лічильник failed нічого не пояснює, коли вранці треба зрозуміти, чому з десяти тисяч сторінок зібрано лише сім.
Як обходити sitemap, структуровані дані та оновлені сторінки
Для відомого сайту починайте із sitemap, а не з головної сторінки. Sitemap дає контрольований список URL, часто містить lastmod і не змушує краулер відкривати навігаційні сторінки лише заради пошуку посилань.
Підтримайте sitemap index, звичайні XML-файли та gzip. Перед розпакуванням задайте максимальний розмір результату, інакше маленький архів може розгорнутися в гігабайти. URL нормалізуйте, а <lastmod> зберігайте окремо від часу фактичного обходу. Значення <lastmod> може бути застарілим або неточним, тому не варто використовувати його як єдиний сигнал для планування повторного обходу.
Перший прохід доцільно організувати так:
- завантажте
robots.txtі знайдіть оголошені sitemap - розберіть sitemap index до кінцевих XML-файлів
- відфільтруйте URL за дозволеним хостом і потрібним path-префіксом
- поставте сторінки в frontier з пріоритетом за типом і
lastmod - завантажте HTML через HTTP
- передайте складні сторінки у Playwright лише після перевірки сигналів ескалації
- запишіть контент-хеш, ETag, Last-Modified і час наступного обходу
Спершу шукайте JSON-LD. Структуровані дані часто містять назву, ціну, валюту, доступність або дату публікації в стабільнішому форматі, ніж CSS-селектори. Потім використовуйте microdata, Open Graph і лише після цього селектори сторінки. Така послідовність зменшує кількість поломок після редизайну.

Повторний обхід не повинен мати однаковий інтервал для всіх URL. Сторінка товару з частими змінами потребує коротшого циклу, ніж архівна стаття. Просте правило: скорочуйте інтервал після зміни контент-хешу й поступово збільшуйте після кількох обходів без змін. Для парсингу динамічних сторінок додатково зберігайте версію селекторів, щоб відрізнити реальну відсутність даних від поломки парсера.
У запитах повторного обходу використовуйте If-None-Match з ETag або If-Modified-Since, якщо сервер підтримує ці заголовки. Відповідь 304 Not Modified економить трафік і процесорні ресурси на парсинг. Вона також не повинна перезаписувати останній успішний запис порожнім тілом.
Після завершення строку lease завдання може бути видане повторно, тому запис результатів має бути ідемпотентним: використовуйте унікальний ключ URL і операцію оновлення або вставлення, щоб не створювати дублікати.
Чим вебкраулер відрізняється від вебскрапера і браузерної автоматизації
Вебкраулер знаходить і планує URL, вебскрапер витягує поля зі сторінки, а браузерна автоматизація відтворює дії в інтерфейсі. Один проєкт може містити всі три частини, але змішувати їх в одному класі незручно для тестування та масштабування.
| Інструмент або підхід | Головна задача | Типовий вхід | Типовий результат | Коли потрібен браузер |
|---|---|---|---|---|
| вебкраулер | знайти й обійти URL | seed, sitemap, frontier | граф сторінок і статуси | коли посилання з’являються після JavaScript |
| вебскрапер | витягти задані поля | HTML або JSON | структуровані записи | коли даних немає у відповіді HTTP |
| браузерна автоматизація | виконати сценарій у UI | кроки, профіль, сесія | зміна стану або знімок | майже завжди |
| API-клієнт | отримати дані через контракт | endpoint і параметри | JSON або інша схема | не потрібен |
Для каталогу з серверним HTML достатньо aiohttp та парсера. Для SPA, де ціна надходить після XHR-запиту, спершу перевірте мережевий API. Playwright залиште для випадків, коли потрібні рендеринг, cookies, сесія або взаємодія зі сторінкою. Браузер потребує більше пам’яті, процесорного часу й часу на запуск, ніж HTTP-клієнт.
HTTP-first архітектура дає ще одну перевагу: код парсингу не знає, звідки прийшов HTML. Можна підмінити fetcher у тесті локальним файлом із тестовими даними, перевірити селектори без мережі й відтворити помилку через місяць.
Як знайти вузькі місця за трафіком, пам’яттю та CPU
Межу конкурентності визначає найповільніший ресурс: ліміт сайту, пул з’єднань, пропускна здатність мережі, RAM браузера, CPU парсера або швидкість запису в базу. Кількість воркерів не треба вгадувати. Її підбирають серією коротких вимірювань на дозволеному тестовому наборі.
Почніть з 1, 2, 4 і 8 воркерів. Для кожного запуску виміряйте pages/s, p50 і p95 часу відповіді, частку помилок, RSS усього дерева процесів, CPU та розмір черги. Додайте наступний рівень конкурентності лише тоді, коли пропускна здатність помітно зростає, а помилки й p95 залишаються прийнятними.

Для попередньої консервативної оцінки одного хоста можна використати concurrency ≈ target_requests_per_second × p95_latency_seconds. Це орієнтир, а не точний розрахунок: p95 відображає затримку 95-го процентиля, тоді як для оцінки середньої кількості одночасних запитів потрібна середня затримка.
Якщо сайт дозволяє один запит на секунду, а p95 дорівнює трьом секундам, три одночасні запити можна взяти як початковий орієнтир для тесту, а не як гарантовано достатній рівень конкурентності. Частоту запуску запитів обмежуйте окремо від кількості одночасних з’єднань і перевіряйте результат вимірюваннями.
Показники варто писати у структуровані логи:
queuedіactiveдля стану frontierok,retry,blocked,parse_errorдля результатівhttp_seconds,browser_seconds,parse_secondsдля етапівrss_mbіcpu_percentдля ресурсів процесуrecords_writtenіwrite_secondsдля сховища
Якщо парсер надовго блокує цикл подій, перенесіть ресурсомістку обробку в ProcessPoolExecutor. Потоки допоможуть лише тоді, коли бібліотека відпускає GIL. Перевіряйте вимірюванням: один і той самий HTML-набір обробіть у event loop, thread pool та process pool.
Якщо споживання пам’яті Chromium зростає, обмежте кількість одночасних сторінок, блокуйте непотрібні зображення, шрифти й відео та перевикористовуйте context у межах сумісної сесії. Не тримайте сотні відкритих page. Під час збору даних через проксі окремо рахуйте трафік: браузер може завантажувати ресурси, яких немає в HTTP-first сценарії.
Діагностику варто починати з аналізу графіків, а не з припущень про бібліотеку. Зростання queued при стабільному active означає, що fetcher або ліміти хоста не встигають. Зростання черги запису при порожньому frontier вказує на базу. Поступове збільшення RSS після завершення кожної сторінки може свідчити про незакриті ресурси, але також буває наслідком кешування або особливостей керування пам’яттю Chromium.
| Симптом | Імовірне вузьке місце | Що перевірити першим |
|---|---|---|
| низький CPU і довгі відповіді | мережа або rate limit | p95 latency, DNS, кількість з’єднань на хост |
| завантаження процесора наближається до максимуму | парсинг або серіалізація | час роботи парсера, профіль функцій, пул процесів |
| RSS росте без падіння | незакриті page чи response | finally, кількість context, відкриті сокети |
багато 429 | надмірна частота | Retry-After, per-host interval, cooldown |
| база відстає | дрібні транзакції або індекси | batch size, write latency, план запиту |
Таблиця допомагає визначити можливу причину проблеми, але підтвердити її можна лише повторним тестуванням зі зміною одного параметра. Якщо одночасно збільшити workers, замінити parser і вимкнути картинки, джерело приросту залишиться невідомим.

Після кожного тесту зберігайте конфігурацію поруч із результатами: версію Python, версію Playwright, тип машини, кількість воркерів і URL-набір. Інакше цифри з двох запусків не можна чесно порівняти.
Як організувати безперервний краулінг і браузерні сесії з Afina
Безперервний обхід потребує checkpoint, сталих браузерних середовищ і зрозумілої відповідальності за кожен хост. Зберігайте frontier та статуси у базі, а секрети, проксі й сесійні дані не додавайте в код або логи.
Коли частина дозволених джерел потребує постійної браузерної сесії, профілі Afina можна використати як окремий інфраструктурний шар для браузерних задач, проксі та повторюваних сценаріїв. Сам краулер при цьому залишається відповідальним за чергу, robots.txt, ліміти та збереження результатів. Матеріал надано виключно в ознайомчих та освітніх цілях.
Не передавайте весь sitemap у браузерний шар. Спочатку виконайте дешевий HTTP-прохід, а в профіль направляйте лише URL із підтвердженою потребою в JavaScript або сталій сесії. Так простіше прогнозувати витрати й знайти причину збою.
СкачатиFAQ — Часті запитання
Що таке вебкраулер на Python?
Вебкраулер на Python автоматично завантажує сторінки, витягує посилання й планує подальший обхід. Парсер усередині краулера перетворює HTML або JSON на структуровані записи.
Як написати асинхронний краулер на asyncio?
Використайте asyncio.Queue, фіксований пул воркерів і дедуплікацію URL до постановки в чергу. Кожне завдання завершуйте викликом task_done() у блоці finally.
Коли вебкраулеру потрібен Playwright?
Playwright потрібен, коли цільовий контент з’являється після JavaScript, залежить від cookies або вимагає браузерної сесії. Для звичайного серверного HTML швидший HTTP-клієнт.
Чим вебкраулер відрізняється від вебскрапера?
Вебкраулер знаходить і планує URL, а вебскрапер витягує конкретні дані зі сторінки. У практичному проєкті вони часто працюють разом.
Як краулер має дотримуватися robots.txt?
Краулер має перевіряти правила для свого User-Agent перед завантаженням URL і враховувати паузу між запитами. Умови використання сайту та правові обмеження діють окремо.
Чому asyncio краулер зависає на queue.join()?
Найчастіша причина полягає у пропущеному queue.task_done() після винятку. Розмістіть цей виклик у finally, щоб кожне отримане завдання коректно закривалося.
Скільки воркерів потрібно для Python краулера?
Кількість воркерів залежить від p95 затримки, дозволеної частоти запитів і доступної пам’яті. Почніть із малого значення та збільшуйте його лише після вимірювання throughput і помилок.
Як безпечно відновити краулер після збою?
Зберігайте статус URL, кількість спроб і час наступного запуску в SQLite або PostgreSQL. Після перезапуску повертайте в frontier тільки незавершені та відкладені завдання.
Як часто повторно обходити сторінки?
Інтервал залежить від фактичної частоти змін контенту. Скорочуйте його для URL, що змінюються часто, і збільшуйте після кількох обходів без змін.
