Смерть CSS-селектора: архитектура отказоустойчивых веб-скрапингов на базе ИИ
-
Добавлено пользователем weblaby - 16.02.2026 - 16:03
Традиционный веб-скрапинг переживает кризис. Разберемся, как спроектировать самовосстанавливающиеся пайплайны данных, используя стек из Playwright, AWS Bedrock и Pydantic для семантической извлечения данных.
Введение: высокая цена хрупких пайплайнов данных
Более десяти лет веб-скрапинг был игрой в кошки-мышки. Вы пишете скрипт для сбора вакансий, нацеливаясь на конкретные DOM-элементы вроде div.job-title или span#salary. Всё работает идеально месяц. Затем сайт выкатывает обновление фронтенда. Имена классов меняются на случайные хэши (что типично для приложений на React/Next.js), селекторы ломаются — и ваш пайплайн падает.
Скрытая стоимость веб-скрапинга — это не вычисления, а инженерные часы, потраченные на отладку и починку сломанных селекторов.
С появлением больших языковых моделей (LLM) мы подошли к переломному моменту. Нам больше не нужно указывать скрейперу, где находятся данные (XPath или селектор); достаточно сказать ему, что это за данные.
В этой статье мы разберём архитектурный паттерн построения агентных скрайперов — систем, которые используют визуальный рендеринг и семантическое понимание для извлечения структурированных данных с любого сайта независимо от его HTML-структуры.
Архитектура: стек «семантического веб-скрапинга»
Чтобы создать скрейпер, имитирующий человеческое понимание, нужны три уровня:
- Слой рендеринга (Playwright) — для работы с динамическим JavaScript и SPA.
- Слой рассуждения (AWS Bedrock + LangChain) — для интерпретации HTML и извлечения смысла.
- Слой валидации (Pydantic) — чтобы заставить недетерминированную LLM возвращать строго типизированный JSON, готовый для API.
Разберём, как эти слои решают проблему «хрупкого скрейпера».
1. Слой валидации: разработка от контракта
В традиционном скрапинге сначала пишется логика извлечения. В AI-подходе сначала определяется контракт данных.
С помощью Pydantic мы описываем, как должна выглядеть «Вакансия». Эта схема:
- валидирует данные в Python;
- генерирует инструкции форматирования для LLM.
from typing import Union, List
from pydantic import BaseModel, Field
class JobInformationSchema(BaseModel):
job_title: Union[str, None] = Field(description="The official title of the role")
company_name: Union[str, None] = Field(description="Name of the hiring company")
location_type: Union[str, None] = Field(description="Must be 'remote', 'onsite', or 'hybrid'")
location: Union[List[str], None] = Field(description="List of physical locations")
commitment: Union[str, None] = Field(description="e.g., 'full-time', 'contract'")
description_summary: Union[str, None] = Field(description="A concise summary of the role, stripping HTML tags")Определяя location_type и commitment, мы фактически «программируем» LLM нормализовать данные (например, преобразовывать «Work from home» в remote).
2. Слой рассуждения: баланс стоимости и интеллекта
Главный аргумент против LLM в скрайпинге — цена. Отправка «сырого» HTML в GPT-4 дорого обходится при больших объёмах.
Однако экономика изменилась с выходом компактных и эффективных моделей, таких как Claude 3 Haiku через AWS Bedrock. Для задач извлечения нам не нужно сложное «рассуждение», нам нужно качественное понимание.
LangChain используется для оркестрации запроса. Ключевой приём — внедрение Pydantic-схемы в инструкции:
async def extract_job_information(html_document, apply_url):
# Setup the parser based on our Pydantic schema
parser = PydanticOutputParser(pydantic_object=JobInformationSchema)
prompt = PromptTemplate(
template="""
You are a data extraction agent. Analyze the following HTML snippet.
Extract the job title, company, and location details.
Strictly follow these format instructions: {format_instructions}
HTML Content: {html_document}
""",
input_variables=['html_document'],
partial_variables={'format_instructions': parser.get_format_instructions()},
)
# Use Bedrock with Claude 3 Haiku for speed and low cost
bedrock_client = boto3.client('bedrock-runtime', region_name='us-east-1')
llm = ChatBedrock(
model_id='anthropic.claude-3-haiku-20240307-v1:0',
client=bedrock_client,
model_kwargs={"temperature": 0.0} # Temperature 0 ensures deterministic output
)
chain = prompt | llm | parser
return await chain.ainvoke({"html_document": html_document})Важно: temperature: 0.0 критически важен — модель должна быть детерминированным механизмом извлечения, а не креативным автором.
3. Слой рендеринга: современный веб
BeautifulSoup и Requests больше недостаточны. Большинство сайтов с вакансиями (Greenhouse, Lever, Workday) — это React-приложения, где контент загружается через JavaScript. Простой GET возвращает пустую HTML-оболочку.
Используем Playwright в асинхронном режиме:
async def scrape_dynamic_content(url: str):
async with async_playwright() as p:
# Launch Chromium in headless mode
browser = await p.chromium.launch(headless=True, args=['--no-sandbox'])
page = await browser.new_page()
# Wait for the DOM to settle (network idle)
await page.goto(url, wait_until="domcontentloaded")
# Extract the raw HTML content
content = await page.content()
await browser.close()
return contentБраузер рендерит страницу полностью, после чего можно извлекать данные.
4. Интеграция: микросервис на FastAPI
Для продакшена архитектура оборачивается в FastAPI, что позволяет развернуть её как масштабируемый микросервис (например, в AWS Fargate или Lambda).
Асинхронная природа FastAPI хорошо сочетается с Playwright:
@app.post("/extract-jobs", status_code=200)
async def extract_jobs_endpoint(target_url: URL):
# 1. Scrape raw HTML (Browser Layer)
raw_html = await scraper.scrape(target_url.url)
# 2. Clean HTML to save tokens (Optimization)
cleaned_html = remove_script_tags(raw_html)
# 3. Extract structured data (Reasoning Layer)
structured_data = await extract_job_information(cleaned_html)
return {"status": "success", "data": structured_data}Оптимизация: подготовка к продакшену
Новая архитектура решает проблему хрупкости, но создаёт новые — задержки и стоимость токенов.
1. Очистка HTML
Удаляйте <script>, <style>, SVG и прочий шум перед отправкой в LLM. Это снижает потребление токенов на 60–80%.
2. Работа с пагинацией
Не отправляйте всю страницу списка вакансий в модель. Сначала собирайте URL через Playwright, затем обрабатывайте страницы вакансий параллельно.
3. Гибридный подход
AI нужен не всегда. Можно один раз попросить LLM определить селектор (h2.css-1234), а затем использовать обычный скрапинг для 1000 страниц. Если селектор ломается — снова вызвать LLM для «самоисцеления».
Заключение
Эпоха поддержки regex-паттернов и CSS-селекторов подходит к концу. Рассматривая веб-страницы как неструктурированный текст и применяя LLM для придания семантической структуры, можно строить по-настоящему устойчивые пайплайны данных.
Хотя вычислительные затраты выше, чем у традиционного сккрапинга, снижение затрат на поддержку и высокая надёжность делают паттерн семантического скрейпера предпочтительным выбором для современных команд по работе с данными.
Источник: dzone