
wxpath - декларативный веб-скрапинг с XPath; язык веб-запросов (WQL)
НОВИНКА: TUI — интерактивный терминальный интерфейс (на основе Textual) для тестирования выражений wxpath и экспорта данных.

Требуется Python 3.10+.
pip install wxpath
# Для поддержки TUI:
pip install "wxpath[tui]"
# Быстрый запуск TUI через uv:
uvx --from "wxpath[tui]" wxpath-tui
wxpath — это декларативный веб-сканер, в котором обход выражается непосредственно в XPath. Вместо написания императивных циклов сканирования, wxpath позволяет описать, что отслеживать и что извлекать, в одном выражении. wxpath выполняет это выражение конкурентно, в стиле поиска в ширину, и передаёт результаты по мере их обнаружения.
Это выражение загружает страницу, извлекает ссылки и передаёт их конкурентно — без цикла сканирования:
import wxpath
expr = "url('https://quotes.toscrape.com')//a/@href"
for link in wxpath.wxpath_async_blocking_iter(expr):
print(link)
Благодаря оператору url(...) и синтаксису ///, движок wxpath может выполнять рекурсивный (или постраничный) веб-сканнинг и извлечение:
import wxpath
path_expr = """
url('https://quotes.toscrape.com')
///url(//a/@href)
//a/@href
"""
for item in wxpath.wxpath_async_blocking_iter(path_expr, max_depth=1):
print(item)
Большинство веб-скраперов заставляют сначала писать управляющий код сканирования, а затем извлечение.
wxpath объединяет эти два шага в один:
Извлекайте чистые, структурированные иерархии JSON непосредственно из графа — подавайте в LLM сигнал, а не шум. Подробнее в разделе Интеграция с LangChain.
wxpath детерминирован (читайте: не на основе LLM). Мы не можем гарантировать стабильность сети, но можем гарантировать предсказуемость обхода.
Документация доступна здесь.
url(...) и ///url(...)import wxpath
from wxpath.settings import CRAWLER_SETTINGS
# Пользовательские заголовки для вежливости; необходимы для некоторых сайтов (например, Wikipedia)
CRAWLER_SETTINGS.headers = {'User-Agent': 'my-app/0.4.0 (contact: [email protected])'}
# Сканирование, извлечение полей, построение графа знаний
path_expr = """
url('https://en.wikipedia.org/wiki/Expression_language')
///url(
//main//a/@href[
starts-with(., '/wiki/') and not(contains(., ':'))
]
)
/map{
'title': (//span[contains(@class, "mw-page-title-main")]/text())[1] ! string(.),
'url': string(base-uri(.)),
'short_description': //div[contains(@class, 'shortdescription')]/text() ! string(.),
'forward_links': //div[@id="mw-content-text"]//a/@href ! string(.)
}
"""
for item in wxpath.wxpath_async_blocking_iter(path_expr, max_depth=1):
print(item)
Примечание: Некоторые сайты (включая Wikipedia) могут блокировать запросы без правильных заголовков.
См. раздел Продвинутая настройка движка и краулера для установки пользовательского User-Agent.
Приведённое выше выражение делает следующее:
https://en.wikipedia.org/wiki/Expression_language.<main>, которые начинаются с /wiki/ и не содержат двоеточие (:).url(...) и ///url(...)url(...) — пользовательский оператор, который загружает содержимое указанного пользователем или созданного внутри URL и возвращает его как lxml.html.HtmlElement для дальнейшей обработки XPath.///url(...) указывает на глубокое сканирование. Он сообщает движку продолжать переход по ссылкам до указанного max_depth. В отличие от повторяющихся шагов url(), он позволяет одному выражению описывать более глубокое исследование графа. ВНИМАНИЕ: Используйте с осторожностью и ограничениями (через max_depth или предикаты XPath), чтобы избежать взрывного роста обхода.Подробности проектирования языка см. в DESIGN.md. Вы увидите основные концепции и сможете спроектировать язык с нуля.
wxpath вычисляет выражение как список шагов обхода и извлечения (внутренне называемых Segment).
url(...) создаёт задачи сканирования либо статически (через фиксированный URL), либо динамически (через URL, полученный из выражения XPath). URL дедуплицируются глобально, по принципу best-effort, а не по глубине.
Сегменты XPath работают с загруженными документами (загруженными через непосредственно предшествующие операции url(...)).
///url(...) указывает на глубокое сканирование — оно выполняется примерно в ширину до max_depth.
Результаты передаются, как только они готовы.
wxpath использует asyncio/aiohttp в первую очередь, предоставляя асинхронный API для сканирования и извлечения данных.
import asyncio
from wxpath import wxpath_async
items = []
async def main():
path_expr = "url('https://en.wikipedia.org/wiki/Expression_language')///url(//@href[starts-with(., '/wiki/')])//a/@href"
async for item in wxpath_async(path_expr, max_depth=1):
items.append(item)
asyncio.run(main())
wxpath также предоставляет API asyncio-in-sync, позволяя сканировать несколько страниц конкурентно, сохраняя простоту синхронного кода. Это особенно полезно для сканирований в строго синхронных средах выполнения (т.е. не внутри цикла событий asyncio), где важна производительность.
from wxpath import wxpath_async_blocking_iter
path_expr = "url('https://en.wikipedia.org/wiki/Expression_language')///url(//@href[starts-with(., '/wiki/')])//a/@href"
items = list(wxpath_async_blocking_iter(path_expr, max_depth=1))
wxpath по умолчанию соблюдает robots.txt через конструктор WXPathEngine(..., robotstxt=True).
Python API wxpath возвращает структурированные объекты.
В зависимости от выражения, результаты могут включать:
lxml.* и lxml.html.*elementpath.datatypes.* (для функций XPath 3.1)WxStr (строковые значения с информацией о происхождении)CLI преобразует эти объекты в простой JSON для отображения. Python API по умолчанию сохраняет структуру.
wxpath использует библиотеку elementpath для поддержки XPath 3.1, что позволяет использовать продвинутые возможности, такие как карты, массивы и другое. Это позволяет писать более мощные запросы XPath.
path_expr = """
url('https://en.wikipedia.org/wiki/Expression_language')
///url(//div[@id='mw-content-text']//a/@href)
/map{
'title':(//span[contains(@class, "mw-page-title-main")]/text())[1],
'short_description':(//div[contains(@class, "shortdescription")]/text())[1],
'url'://link[@rel='canonical']/@href[1]
}
"""
# [...
# {'title': 'Computer language',
# 'short_description': 'Formal language for communicating with a computer',
# 'url': 'https://en.wikipedia.org/wiki/Computer_language'},
# {'title': 'Machine-readable medium and data',
# 'short_description': 'Medium capable of storing data in a format readable by a machine',
# 'url': 'https://en.wikipedia.org/wiki/Machine-readable_medium_and_data'},
# {'title': 'Domain knowledge',
# 'short_description': 'Specialist knowledge within a specific field',
# 'url': 'https://en.wikipedia.org/wiki/Domain_knowledge'},
# ...]
wxpath предоставляет индикатор прогресса (через tqdm) для отслеживания хода сканирования. Это особенно полезно для длительных сканирований.
Включите, установив engine.run(..., progress=True), или передайте progress=True в любую из функций wxpath_async*(...).
items = wxpath.wxpath_async_blocking("...", progress=True)
> 100%|██████████████████████████████████████████████████████████▎| 469/471 [00:05<00:00, 72.00it/s, depth=2, yielded=457]
wxpath предоставляет интерфейс командной строки (CLI) для быстрого экспериментирования и выполнения выражений wxpath прямо из терминала.
Следующий пример демонстрирует, как сканировать Wikipedia, начиная со страницы "Expression language", извлекать ссылки на другие вики-страницы и получать определённые поля с каждой связанной страницы.
ПРИМЕЧАНИЕ: Из-за постоянно меняющегося содержимого веб-страниц вывод может со временем меняться.
> wxpath --depth 1 \
--header "User-Agent: my-app/0.1 (contact: [email protected])" \
"url('https://en.wikipedia.org/wiki/Expression_language') \
///url(//div[@id='mw-content-text']//a/@href[starts-with(., '/wiki/') \
and not(matches(@href, '^(?:/wiki/)?(?:Wikipedia|File|Template|Special|Template_talk|Help):'))]) \
/map{ \
'title':(//span[contains(@class, 'mw-page-title-main')]/text())[1], \
'short_description':(//div[contains(@class, 'shortdescription')]/text())[1], \
'url':string(base-uri(.)), \
'backlink':wx:backlink(.), \
'depth':wx:depth(.) \
}"
{"title": "Computer language", "short_description": "Formal language for communicating with a computer", "url": "https://en.wikipedia.org/wiki/Computer_language", "backlink": "https://en.wikipedia.org/wiki/Expression_language", "depth": 1.0}
{"title": "Machine-readable medium and data", "short_description": "Medium capable of storing data in a format readable by a machine", "url": "https://en.wikipedia.org/wiki/Machine_readable", "backlink": "https://en.wikipedia.org/wiki/Expression_language", "depth": 1.0}
{"title": "Domain knowledge", "short_description": "Specialist knowledge within a specific field", "url": "https://en.wikipedia.org/wiki/Domain_knowledge", "backlink": "https://en.wikipedia.org/wiki/Expression_language", "depth": 1.0}
{"title": "Advanced Boolean Expression Language", "short_description": "Hardware description language and software", "url": "https://en.wikipedia.org/wiki/Advanced_Boolean_Expression_Language", "backlink": "https://en.wikipedia.org/wiki/Expression_language", "depth": 1.0}
{"title": "Data Analysis Expressions", "short_description": "Formula and data query language", "url": "https://en.wikipedia.org/wiki/Data_Analysis_Expressions", "backlink": "https://en.wikipedia.org/wiki/Expression_language", "depth": 1.0}
{"title": "Jakarta Expression Language", "short_description": "Computer programming language", "url": "https://en.wikipedia.org/wiki/Jakarta_Expression_Language", "backlink": "https://en.wikipedia.org/wiki/Expression_language", "depth": 1.0}
{"title": "Rights Expression Language", "short_description": [], "url": "https://en.wikipedia.org/wiki/Rights_Expression_Language", "backlink": "https://en.wikipedia.org/wiki/Expression_language", "depth": 1.0}
{"title": "Computer science", "short_description": "Study of computation", "url": "https://en.wikipedia.org/wiki/Computer_science", "backlink": "https://en.wikipedia.org/wiki/Expression_language", "depth": 1.0}
Параметры командной строки:
--depth <depth> Максимальная глубина сканирования
--verbose [true|false] Отображает поверхностную информацию CLI
--debug [true|false] Отображает подробный вывод и информацию времени выполнения
--concurrency <concurrency> Количество одновременных запросов
--concurrency-per-host <concurrency> Количество одновременных запросов на хост
--header "Key:Value" Добавить пользовательский заголовок (например, 'Key:Value'). Можно использовать несколько раз.
--respect-robots [true|false] (По умолчанию: True) Соблюдать robots.txt
--cache [true|false] (По умолчанию: False) Сохранять результаты сканирования в локальную базу данных
wxpath предоставляет терминальный интерфейс (TUI) для интерактивного тестирования выражений и извлечения данных.
См. TUI Quickstart для подробностей.
wxpath опционально сохраняет результаты сканирования в локальную базу данных. Это особенно полезно, когда вы сканируете большое количество URL и решаете приостановить сканирование, изменить выражения извлечения или перезапустить сканирование.
wxpath поддерживает два бэкенда: sqlite и redis. SQLite отлично подходит для небольших сканирований с одним рабочим процессом (т.е. engine.crawler.concurrency == 1). Redis хорошо подходит для крупномасштабных сканирований с несколькими рабочими процессами. Вы получите предупреждение, если min(engine.crawler.concurrency, engine.crawler.per_host) > 1 при использовании бэкенда sqlite.
Для использования необходимо установить соответствующую опциональную зависимость:
pip install wxpath[cache-sqlite]
pip install wxpath[cache-redis]
После установки зависимости необходимо включить кэш:
from wxpath.settings import SETTINGS
# Включить кэширование; по умолчанию sqlite
SETTINGS.http.client.cache.enabled = True
# Для бэкенда redis
SETTINGS.http.client.cache.enabled = True
SETTINGS.http.client.cache.backend = "redis"
SETTINGS.http.client.cache.redis.address = "redis://localhost:6379/0"
# Запуск wxpath как обычно
items = list(wxpath_async_blocking_iter('...', max_depth=1, engine=engine))
Подробности настроек см. в settings.py.
wxpath поддерживает систему подключаемых хуков, позволяющую изменять поведение сканирования и извлечения. Вы можете регистрировать хуки для предварительной обработки URL, постобработки HTML, фильтрации извлечённых значений и т.д. Хуки выполняются в порядке их регистрации. Хуки могут влиять на производительность.
from wxpath import hooks
@hooks.register
class OnlyEnglish:
def post_parse(self, ctx, elem):
lang = elem.xpath('string(/html/@lang)').lower()[:2]
return elem if lang in ("en", "") else None
ПРИМЕЧАНИЕ: Хуки могут быть синхронными или асинхронными, но все хуки в проекте должны следовать одному стилю. Смешивание синхронных и асинхронных хуков не поддерживается и может привести к неожиданному поведению.
from wxpath import hooks
@hooks.register
class OnlyEnglish:
async def post_parse(self, ctx, elem):
lang = elem.xpath('string(/html/@lang)').lower()[:2]
return elem if lang in ("en", "") else None
JSONLWriter (псевдоним NDJSONWriter) — встроенный хук, записывающий извлечённые данные в JSON-файл с разделителями строк. Это полезно для хранения результатов в структурированном формате, который можно легко обработать позже.
from wxpath import hooks
hooks.register(hooks.JSONLWriter)
Требуется Python 3.10+.
pip install wxpath
Для персистентности/кэширования wxpath поддерживает следующие бэкенды:
pip install wxpath[cache-sqlite]
pip install wxpath[cache-redis]
См. EXAMPLES.md для дополнительных примеров использования.
См. COMPARISONS.md для сравнения с другими инструментами веб-скрапинга.
Вы можете изменить поведение движка и краулера следующим образом:
from wxpath import wxpath_async_blocking_iter
from wxpath.core.runtime import WXPathEngine
from wxpath.http.client.crawler import Crawler
crawler = Crawler(
concurrency=8,
per_host=2,
timeout=10,
respect_robots=False,
headers={
"User-Agent": "my-app/0.1.0 (contact: [email protected])", # Такие сайты, как Wikipedia, оценят это
},
)
# Если `crawler` не указан, будет создан Crawler по умолчанию
# с переданными значениями concurrency, per_host, respect_robots или значениями по умолчанию.
engine = WXPathEngine(
# concurrency: int = 16,
# per_host: int = 8,
# respect_robots: bool = True,
# allowed_response_codes: set[int] = {200},
# allow_redirects: bool = True,
crawler=crawler,
)
path_expr = "url('https://en.wikipedia.org/wiki/Expression_language')//url(//main//a/@href)"
items = list(wxpath_async_blocking_iter(path_expr, max_depth=1, engine=engine))
wxpath_async*)max_depth: int = 1progress: bool = Falseengine: WXPathEngine | None = Noneyield_errors: bool = FalseВы также можете использовать settings.py для включения кэширования, троттлинга, конкурентности и т.д.
max_depth.Следующие функции пока не поддерживаются:
Этот проект находится на ранней стадии разработки. Основные концепции стабильны, но API и функции могут измениться. Пожалуйста, сообщайте об ошибках — особенно о взаимоблокировках сканирования или неожиданном поведении — и о любых функциях, которые вы хотели бы видеть (нет гарантии, что они будут реализованы).
///) требует дисциплины пользователя, чтобы избежать неограниченного расширения (взрывного роста обхода).max_depth, а также предикатов и фильтров XPath для ограничения области сканирования.Если вам нужна помощь в создании или эксплуатации сканеров/потоков данных с wxpath (извлечение, планирование, мониторинг, исправление поломок) или другие потребности веб-скрапинга, свяжитесь со мной по адресу: [email protected].
Если вам нравится wxpath и вы хотите поддержать его развитие, рассмотрите возможность пожертвования.
wxpath следует semver: <MAJOR>.<MINOR>.<PATCH>.
Однако до версии 1.0.0 используется 0.<MAJOR>.<MINOR|PATCH>.
AGPL-3.0