Scrapy з asyncio: Telegram як тригер для асинхронного скрейпінгу
На мою думку, Scrapy — це досить недооцінений фреймворк, який часто вважають занадто складним, особливо для створення невеликих сканерів. Проте чим більше ви ним користуєтеся, тим менше у вас виникатиме бажання переходити на щось інше.
⚠️ Важливо: усе, що показано нижче — виключно в навчальних цілях.
Не ламайте сайти, не перевантажуйте сервери і поважайте працю інших. Хтось ці сайти створював, і для когось вони мають велику цінність.
(Окрім, звісно, сайтів з оркостану.)
Проблема
Scrapy під капотом використовує Twisted — асинхронний фреймворк, який не сумісний з asyncio «із коробки». Через це виникає чимало плутанини та обмежень, особливо якщо ви хочете інтегрувати Scrapy з іншими сучасними async-інструментами, як-от asyncio, aiohttp, чи будь-який Telegram-бот на aiogram.
У реальних проектах це часто призводить до обхідних рішень:
- створення окремих сервісів,
- запуск Scrapy як окремого процесу,
- складні тригери для запуску.
Все це ускладнює розробку — особливо якщо проєкт невеликий і не потребує складної архітектури.
Що я пропоную
Нижче я покажу, як на мою думку, можна зробити простіше: запускати Scrapy за допомогою Telegram-бота, який реагує на команди користувача — наприклад, щоб у реальному часі сканувати акції в супермаркетах.
Ми обійдемо типові проблеми взаємодії asyncio з Scrapy і отримаємо зручний, легкий у використанні інтерфейс запуску скрейпінгу без додаткових процесів і складної логіки.
Ну що спробуємо
У документації scrapy зазначено, що для сумісності з asyncio потрібно змінити reactor на:
'twisted.internet.asyncioreactor.AsyncioSelectorReactor'
Зі сторони нашого бота — це буде виглядати як:
from scrapy.utils.reactor import install_reactor
install_reactor("twisted.internet.asyncioreactor.AsyncioSelectorReactor")
Telegram-бот
Я не зосереджуюсь тут на реалізації Telegram-бота. Якщо вам цікаво побачити повний код — напишіть мені. З радістю поділюсь ним у відповідь на донат на один з актуальних зборів (DOU або Стерненку).
Ось базовий код запуску бота:
class PriceBot:
"""Main bot class."""
def __init__(self):
self.app = ApplicationBuilder().token(PriceBotConfig.TELEGRAM_API_TOKEN).build()
self._setup_handlers()
def _setup_handlers(self):
"""Setup conversation handlers."""
conv_handler = ConversationHandler(
entry_points=[CommandHandler("start", PriceBotHandlers.start)],
states={
SELECT_SHOP: [MessageHandler(filters.TEXT & ~filters.COMMAND, PriceBotHandlers.select_shop)],
SELECT_CITY: [MessageHandler(filters.TEXT & ~filters.COMMAND, PriceBotHandlers.select_city)],
SELECT_PROMOTION: [MessageHandler(filters.TEXT & ~filters.COMMAND, PriceBotHandlers.select_promotion)],
SELECT_SHOPS: [MessageHandler(filters.TEXT & ~filters.COMMAND, PriceBotHandlers.select_shops)],
},
fallbacks=[CommandHandler("start", PriceBotHandlers.start)],
)
self.app.add_handler(conv_handler)
def run(self):
"""Start the bot."""
self.app.run_polling()
def main():
"""Main entry point."""
if not PriceBotConfig.TELEGRAM_API_TOKEN:
raise ValueError("TELEGRAM_API_TOKEN environment variable is required")
bot = PriceBot()
bot.run()
if __name__ == "__main__":
main()
Команди select_shop, select_city, select_promotion — це лише логіка формування URL і параметрів. Більш цікавіше має бути запуск спайдера, який ми розглянемо нижче.
Запуск Scrapy через asyncio (Telegram)
Після збору всіх параметрів (наприклад, вибору магазину/міст) — запускається сам спайдер:
await asyncio.wait_for( run_spider_async(shops, file_path, base_url, shop_name), timeout=60 )
⚙️ Функція run_spider_async
Це серце інтеграції Scrapy та asyncio:
from scrapy.crawler import CrawlerRunner
from scrapy.utils.log import configure_logging
from scrapy.utils.defer import deferred_to_future
SPIDERS = {
"АТБ": ATBSpider,
"Сільпо": SilpoSpider,
"Metro": MetroSpider
}
async def run_spider_async(shops, file_path, base_url=None, shop="АТБ"):
configure_logging({"LOG_FORMAT": "%(levelname)s: %(message)s"})
runner = CrawlerRunner(settings={
"USER_AGENT": None,
"DOWNLOAD_HANDLERS": {
"http": "scrapy_impersonate.ImpersonateDownloadHandler",
"https": "scrapy_impersonate.ImpersonateDownloadHandler",
},
"LOG_LEVEL": "INFO",
"FEEDS": {
file_path: {
"format": "csv",
"overwrite": True,
"encoding": "utf8",
}
}
})
crawl_deferred = runner.crawl(SPIDERS[shop], shops=shops, base_url=base_url)
await deferred_to_future(crawl_deferred)
Тут потрібно звернути увагу на deferred_to_future.
Ну і один із spiders
Нічого надлишкового, лише те, що потрібно (і зверніть увагу, що нічого, ну добре майже нічого, не має, що вказує на asyncio):
from random import choice
from scrapy import Spider, Request
class ATBSpider(Spider):
name = "atb_spider"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.shops = kwargs.get("shops")
self.base_url = kwargs.get("base_url") or "https://www.atbmarket.com/catalog/388-aktsiya-7-dniv"
self.total_pages = 5
def start_requests(self):
for shop in self.shops:
cookies = {
"lang": "uk",
"natbdelivery": "0",
"nstore_id": shop["shop_id"],
"ncityid": shop["city_code"]
}
for i in range(1, self.total_pages + 1):
url_page = f"{self.base_url}?page={i}&sort=discount"
yield Request(
url=url_page,
method="GET",
dont_filter=True,
cookies=cookies,
meta={"impersonate": choice(("firefox", "chrome"))},
callback=self.parse
)
def parse(self, response):
items = response.css("article.catalog-item.js-product-container")
for item in items:
yield {
"id": item.css(".catalog-item__title a::attr(href)").get(),
"name": item.css(".catalog-item__title a::text").get(),
"unit": item.css(".product-price__unit::text").get(),
"price": item.css(".product-price__top::attr(value)").get(),
"old_price": item.css(".product-price__bottom::attr(value)").get(),
"image": item.css("img::attr(src)").get(),
"producer": item.css(".catalog-item__counter .b-addToCart::attr(data-brand)").get(),
"currency": item.css(".product-price__currency-abbr::text").get(),
"categories": item.css(".catalog-item__counter .b-addToCart::attr(data-category)").get(),
"shop_address": [shop for shop in self.shops if response.request.cookies.get("nstore_id") == shop["shop_id"]][0]["address"],
}
🧩 Висновок
Scrapy дійсно можна інтегрувати з asyncio, і не обов’язково створювати окремі сервіси чи процеси. Достатньо змінити реактор і правильно використовувати CrawlerRunner.
Завдяки такій інтеграції можна запускати спайдери на запит користувача — швидко, без зайвого оверхеду, і з повним контролем над потоком даних.
Немає коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів