Afina

Скачати додаток

AppleWindows
UA
БлогГайди та навчання

7 листопада 2026 р.

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

Архітектура масштабованого вебкраулера на Python з asyncio і Playwright

Вебкраулер на 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 уже завершені, а які залишилися в роботі.

КомпонентЩо зберігає або виконуєОзнака готовності до масштабу
frontierURL, пріоритет, хост, час наступної спробиокремі ліміти й паузи для кожного хоста
workerбере одне завдання та повертає результатпомилка сторінки не завершує корутину
fetcherHTTP або браузерне завантаженняє 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 вебкраулера

Підготуйте Python 3.11 або новіший і встановіть залежності. Для HTML-парсингу в прикладі використано Beautiful Soup, а Playwright потрібен для сторінок, де цільовий контент з’являється після виконання JavaScript.

  1. створіть окреме віртуальне середовище командою python -m venv .venv
  2. активуйте середовище відповідно до вашої операційної системи
  3. встановіть пакети командою pip install aiohttp beautifulsoup4 playwright
  4. завантажте Chromium командою playwright install chromium
  5. додайте тестовий домен, який дозволяє автоматизований обхід

Каркас нижче показує головний механізм: дедуплікацію до постановки в чергу, 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 окремих транзакцій. Якщо база даних працює повільніше, черга результатів поступово заповнюється та обмежує швидкість завантаження нових сторінок. Це допомагає уникнути перевантаження системи.

Потік завдання в асинхронному Python краулері

У робочому проєкті замініть ознаку 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 не скасовує умови використання сайту, вимоги до персональних даних або обмеження доступу.

Порядок обробки відповіді краще зафіксувати явно:

  1. завершіть URL без повтору для статусів 400, 401, 403 і 404, якщо політика конкретного джерела не вимагає іншого
  2. прочитайте Retry-After для статусів 429 і 503: значення може бути кількістю секунд або HTTP-датою
  3. розрахуйте паузу як min(cap, base * 2 ** attempt) + jitter
  4. поверніть завдання у frontier з майбутнім часом готовності
  5. переведіть URL у failed після вичерпання ліміту спроб

Не тримайте воркер у asyncio.sleep() на десятки секунд. Коли час наступної спроби зберігається в черзі frontier як ready_at, воркер може перейти до завдань іншого хоста. Саме тут окремі черги на хост дають більший ефект, ніж просте збільшення workers.

Повторні запити та circuit breaker у Python краулері

Для Playwright задавайте два timeout:

  • коротший для навігації
  • довший для виконання всього завдання

Якщо page.goto() зависне, зовнішній asyncio.timeout() скасує операцію після заданого часу, але сам по собі не закриє сторінку. Обов’язково закривайте page або context у finally, щоб звільнити слот у пулі. Інакше кілька десятків невдалих переходів можуть залишити процеси Chromium у пам’яті.

Повторювати варто тільки ідемпотентні операції:

  • звичайний GET підходить для повторного виконання
  • натискання кнопки, що створює замовлення або змінює дані, не належить до краулінгу

Ведіть retry budget на хост, а не лише на окремий URL. Якщо за останні п’ять хвилин половина запитів повернула 429 або 503, зупиніть нові завдання цього домену на короткий cooldown. Інакше тисяча URL виконає по три повтори та перетворить тимчасову проблему на три тисячі зайвих запитів.

Circuit breaker можна реалізувати трьома станами:

  1. closed: запити йдуть за звичайним графіком
  2. open: після перевищення порога помилок frontier перестає видавати завдання цього хоста
  3. half_open: після cooldown дозволяється один тестовий запит

Успіх тестового запиту повертає систему в closed, а повторна помилка знову переводить її в open.

Зберігайте причину кожної невдачі окремо:

  • dns_error
  • connect_timeout
  • read_timeout
  • http_429
  • browser_timeout
  • parse_error

Ці помилки потребують різних рішень. Один лічильник failed нічого не пояснює, коли вранці треба зрозуміти, чому з десяти тисяч сторінок зібрано лише сім.

Як обходити sitemap, структуровані дані та оновлені сторінки

Для відомого сайту починайте із sitemap, а не з головної сторінки. Sitemap дає контрольований список URL, часто містить lastmod і не змушує краулер відкривати навігаційні сторінки лише заради пошуку посилань.

Підтримайте sitemap index, звичайні XML-файли та gzip. Перед розпакуванням задайте максимальний розмір результату, інакше маленький архів може розгорнутися в гігабайти. URL нормалізуйте, а <lastmod> зберігайте окремо від часу фактичного обходу. Значення <lastmod> може бути застарілим або неточним, тому не варто використовувати його як єдиний сигнал для планування повторного обходу.

Перший прохід доцільно організувати так:

  1. завантажте robots.txt і знайдіть оголошені sitemap
  2. розберіть sitemap index до кінцевих XML-файлів
  3. відфільтруйте URL за дозволеним хостом і потрібним path-префіксом
  4. поставте сторінки в frontier з пріоритетом за типом і lastmod
  5. завантажте HTML через HTTP
  6. передайте складні сторінки у Playwright лише після перевірки сигналів ескалації
  7. запишіть контент-хеш, ETag, Last-Modified і час наступного обходу

Спершу шукайте JSON-LD. Структуровані дані часто містять назву, ціну, валюту, доступність або дату публікації в стабільнішому форматі, ніж CSS-селектори. Потім використовуйте microdata, Open Graph і лише після цього селектори сторінки. Така послідовність зменшує кількість поломок після редизайну.

Черга повторного обходу сторінок за sitemap і змінами контенту

Повторний обхід не повинен мати однаковий інтервал для всіх URL. Сторінка товару з частими змінами потребує коротшого циклу, ніж архівна стаття. Просте правило: скорочуйте інтервал після зміни контент-хешу й поступово збільшуйте після кількох обходів без змін. Для парсингу динамічних сторінок додатково зберігайте версію селекторів, щоб відрізнити реальну відсутність даних від поломки парсера.

У запитах повторного обходу використовуйте If-None-Match з ETag або If-Modified-Since, якщо сервер підтримує ці заголовки. Відповідь 304 Not Modified економить трафік і процесорні ресурси на парсинг. Вона також не повинна перезаписувати останній успішний запис порожнім тілом.

Після завершення строку lease завдання може бути видане повторно, тому запис результатів має бути ідемпотентним: використовуйте унікальний ключ URL і операцію оновлення або вставлення, щоб не створювати дублікати.

Чим вебкраулер відрізняється від вебскрапера і браузерної автоматизації

Вебкраулер знаходить і планує URL, вебскрапер витягує поля зі сторінки, а браузерна автоматизація відтворює дії в інтерфейсі. Один проєкт може містити всі три частини, але змішувати їх в одному класі незручно для тестування та масштабування.

Інструмент або підхідГоловна задачаТиповий вхідТиповий результатКоли потрібен браузер
вебкраулерзнайти й обійти URLseed, 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 залишаються прийнятними.

Вибір кількості воркерів для Python краулера

Для попередньої консервативної оцінки одного хоста можна використати concurrency ≈ target_requests_per_second × p95_latency_seconds. Це орієнтир, а не точний розрахунок: p95 відображає затримку 95-го процентиля, тоді як для оцінки середньої кількості одночасних запитів потрібна середня затримка.

Якщо сайт дозволяє один запит на секунду, а p95 дорівнює трьом секундам, три одночасні запити можна взяти як початковий орієнтир для тесту, а не як гарантовано достатній рівень конкурентності. Частоту запуску запитів обмежуйте окремо від кількості одночасних з’єднань і перевіряйте результат вимірюваннями.

Показники варто писати у структуровані логи:

  • queued і active для стану frontier
  • ok, 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 limitp95 latency, DNS, кількість з’єднань на хост
завантаження процесора наближається до максимумупарсинг або серіалізаціячас роботи парсера, профіль функцій, пул процесів
RSS росте без падіннянезакриті page чи responsefinally, кількість context, відкриті сокети
багато 429надмірна частотаRetry-After, per-host interval, cooldown
база відстаєдрібні транзакції або індексиbatch size, write latency, план запиту

Таблиця допомагає визначити можливу причину проблеми, але підтвердити її можна лише повторним тестуванням зі зміною одного параметра. Якщо одночасно збільшити workers, замінити parser і вимкнути картинки, джерело приросту залишиться невідомим.

Пошук вузького місця Python краулера за метриками навантаження

Після кожного тесту зберігайте конфігурацію поруч із результатами: версію 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, що змінюються часто, і збільшуйте після кількох обходів без змін.

Схожі терміни

Читати далі:Web scraping — автоматизація збору даних | Afina Browser
Тимур Приказниченко

Я один із найраніших учасників Afina та фахівець зі скриптів і Web3-автоматизації. Проєктую масштабовані системи, що дозволяють керувати 2 000+ акаунтами та великим парком гаманців без втрати контролю й якості виконання. Паралельно навчаю інших через стрими та прямі ефіри, пояснюючи підходи до скриптингу й архітектуру рішень. Співзаснував AfinaDAO, де опубліковані скрипти сьогодні підтримують 20 000–30 000 гаманців у спільноті