Что такое краулер python
Перейти к содержимому

Что такое краулер python

  • автор:

Пишем свой Google, или асинхронный краулер с rate limits на Python

Меня зовут Александр, я руковожу backend-разработкой в КТS. Сегодня расскажу, как написать асинхронный краулер.

Такая задача часто встречается на практике, когда нужно реализовать периодическую синхронизацию/обкачку между сервисами.

Статья написана по мотивам вебинара, который мы проводили в рамках курса «Асинхронное программирование на Python для джуниор-разработчиков». Если интересно, загляните посмотреть.

Что будет в статье:

  • Цель
  • Исходный код
  • Планировщик
  • Задача для краулера
  • Пробный запуск
  • Промежуточный итог
  • Функции put и join
  • Semaphore
  • Остановка фонового планировщика
  • Работа краулера на примере обкачки нашего блога на Хабре
  • Несколько слов о курсе по асинхронному программированию на Python

Цель

У нас есть краулер, который обкачивает страницы. Это может быть поисковый бот Google, который ходит по сайтам, скачивает данные, кладет в базу и индексирует, или какой-нибудь агрегатор: аптек, маркетплейсов и т.д.

Задача в том, что краулер должен работать и не положить сервис, который он обкачивает.

Код для начала работы:

import asyncio from dataclasses import dataclass from typing import Optional class Pool: def __init__(self, max_rate: int, interval: int = 1, concurrent_level: Optional[int] = None): self.max_rate = max_rate self.interval = interval self.concurrent_level = concurrent_level async def start(pool): await asyncio.sleep(5) def main(): loop = asyncio.get_event_loop() try: loop.run_until_complete(start()) except KeyboardInterrupt: loop.close() if __name__ == '__main__': main()

Краулеру нужно посетить и скачать много страниц, следовательно, много раз обратиться к ресурсу. Мы можем позволить себе отправлять много запросов, но сервис, на который мы приходим, может не выдержать большой нагрузки. Поэтому к источнику данных нужно ходить управляемо — сделать rate-limit.

Если в какой-то момент задача прервалась, или мы сами решили остановить краулер, нужно сделать корректную и аккуратную остановку работы. Для этого начатые задачи должны завершиться, а новые задачи из очереди должны прекратить поступать.

Исходный код

У нас есть сущность Pool. Эта сущность умеет управлять количеством запросов в единицу времени. Pool принимает:

  • max_rate — максимальное количество запросов
  • interval — интервал. Если мы передаем значения max_rate = 5 и interval = 1, в секунду может исполняться 5 запросов
  • concurrent_level — обозначает допустимое количество параллельных запросов

max_rate и concurrent_level могут не совпадать, когда время выполнения запроса больше, чем interval. Например, мы делаем 5 запросов в секунду, как заявлено в переменных, но API все равно отвечает медленнее. Чтобы не положить сервис, мы вводим переменную concurrent_level.

Планировщик

Для начала нужно написать что-то, что позволит делать ровно 5 запросов в секунду, не обращая внимание на время запроса. Для этого мы запустим планировщик, который назовем scheduler. Он будет просыпаться раз в секунду и ставить количество задач, равное max_rate. Планировщик не ждет их исполнения, просто создает 5 задач каждую секунду.

Дополним class Pool и напишем функцию scheduler:

from task import Task class Pool: def __init__(self, max_rate: int, interval: int = 1, concurrent_level: Optional[int] = None): self.max_rate = max_rate self.interval = interval self.concurrent_level = concurrent_level self.is_running = False async def _scheduler(self): while self.is_running: for _ in range(self.max_rate): pass

Обратите внимание на две вещи:

  • функция бесконечная, пока работает наш краулер
  • раз в период функция выполняет max_rate раз какое-то действие

Задача для краулера

Scheduler должен откуда-то взять задачи, которые нужно запланировать. Для этого нам нужно сделать очередь, которую мы возьмем из библиотеки asyncio. Примитив называется asyncio.Queue(). В class Pool дописываем:

class Pool: def __init__(self, max_rate: int, interval: int = 1, concurrent_level: Optional[int] = None): self.max_rate = max_rate self.interval = interval self.concurrent_level = concurrent_level self.is_running = False self._queue = asyncio.Queue()

Теперь мы просыпаемся раз в интервал и получаем количество задач, равное max_rate. Но нужно что-то сделать, чтобы они исполнялись.

Для этого в asyncio есть функция create_task. Она запускает выполнение корутины, но при этом не дожидается ее исполнения, а создает фоновую задачу. В create_task передадим метод perform.

 async def _scheduler(self): while self.is_running: for _ in range(self.max_rate): task = await self._queue.get() asyncio.create_task(task.perform)) await asyncio.sleep(self.interval)

Пробный запуск

Давайте попробуем все это запустить. Сделаем функцию start и таким же образом запустим scheduler. Нам нужно не ждать его, а просто запустить в фоне корутину с помощью create_task:

 async def _scheduler(self): while self.is_running: for _ in range(self.max_rate): task = await self._queue.get() asyncio.create_task(self._worker(task)) await asyncio.sleep(self.interval) def start(self): self.is_running = True asyncio.create_task(self._scheduler())

В будущем для корректного завершения работы краулера нужно завершить работу scheduler. Для этого нужно вызвать cancel у задачи, поэтому возвращаемое значение из create_task мы сохраняем в переменную scheduler_task:

class Pool: def __init__(self, max_rate: int, interval: int = 1, concurrent_level: Optional[int] = None): self.max_rate = max_rate self.interval = interval self.concurrent_level = concurrent_level self.is_running = False self._queue = asyncio.Queue() self._scheduler_task: Optional[asyncio.Task] = None

Выставим rate-limit на 3 и внутри start запустим наш Pool:

def start(self): self.is_running = True self._scheduler_task = asyncio.create_task(self._scheduler()) async def start(pool): pool = Pool(3) pool.start() await asyncio.sleep(5) 

Запускаем и видим, что ничего не произошло:

Это потому, что внутри очереди ничего нет. Мы сделали старт и поспали 5 секунд, а на момент окончания задачи у нас осталась фоновая задача scheduler.

Промежуточный итог

  1. У нас есть Pool с параметрами:
    ограничение количества запросов max_rate
    — интервал активизации планировщика interval
    — максимальное количество параллельных запросов concurrent_level
  2. Мы написали планировщик scheduler, который работает постоянно, просыпается раз в объявленный интервал, достает из очереди max_rate задач и запускает их исполнение.
  3. Задача task — просто дата-класс с функцией perform. Для описания поведения задачи нужно создать класс-наследник и в нем переопределить perform.
  4. Еще мы написали функцию start, в которой выставили признак работы is_running и в фоне запустили наш планировщик.

Функции put и join

Перед тем, как запустить Pool, попробуем положить туда задачку. Для этого напишем функцию put, которая принимает задачу и кладет ее в нашу внутреннюю очередь.

Дополнительно добавим tid (task_id) и print в код задачи:

import asyncio from dataclasses import dataclass @dataclass class Task: tid: int async def perform(self, pool): print('start perform', self.tid) await asyncio.sleep(3) print('complete perform', self.tid)

И добавим 10 задач перед стартом pool:

async def start(pool): pool = Pool(3) for tid in range(10): await pool.put(Task(tid)) pool.start() await asyncio.sleep(5)

Добавим еще кое-что. У стандартной библиотеки queue есть метод join. Тогда краулер будет ждать не 5 секунд, как мы указали в начале, а до тех пор, пока очередь не опустеет:

async def start(pool): pool = Pool(3) for tid in range(10): await pool.put(Task(tid)) pool.start() await pool.join()

Запустим и посмотрим, что произойдет:

Хотя все зависло, планировщик работал.

Вы можете увидеть, что задача выполняется 3 секунды. И, несмотря на то, что предыдущие задачи еще не завершились, планировщик все равно создает новые. Это плохо, потому что если API отвечает медленнее, чем мы шлем к нему запросы, есть вероятность «положить» сервис. Эту проблему мы решим чуть позже.

Чтобы join отработал, нужно помечать задачи выполненными. Не будем усложнять код scheduler и сделаем отдельную функцию _worker. В нее перенесем perform и ниже добавим self._queue.task_done(). Это означает, что задачу мы выполнили:

async def _worker(self, task: Task): await task.perform(self) self._queue.task_done()

Обратите внимание, что _worker вызывается без await, потому что scheduler не должен ждать его завершения. Иначе он не успеет запланировать задачи.

В scheduler вместо perform нужно передать _worker и task:

async def _scheduler(self): while self.is_running: for _ in range(self.max_rate): task = await self._queue.get() asyncio.create_task(self._worker(task)) await asyncio.sleep(self.interval)

Снова попробуем запустить:

Программа завершилась, но осталось предупреждение о том, что scheduler остался работать в фоне. Функцию stop напишем чуть позже.

Semaphore

На этом этапе видим, что:

  • метод start запускает наш Pool и планировщик scheduler
  • планировщик раз в секунду ставит новые задачи и запускает _worker
  • _worker эти задачи выполняет
  • метод join ждет, пока очередь не станет пустой

Если время выполнения задач больше интервала активизации планировщика (interval), он накидывает дополнительные задачи сверху тех, которые еще не выполнились.

В таком случае количество параллельных запросов к сервису за interval будет больше rate_limit. Поэтому нужно ограничить количество параллельных запросов. Для этого нам потребуется переменная concurrent_level, которая по умолчанию равна rate_limit.

В asyncio есть примитив синхронизации Semaphore. С его помощью можно ограничить количество параллельных исполняемых worker. Если количество запланированных задач больше заданного значения, мы ждем их исполнения. В нашем примере задач 3.

Объявим Semaphore и передадим в него либо concurrent_level, либо max_rate.

Когда worker начинает исполняться, нам нужно занять Semaphore. Для этого используем «асинхронный контекстный менеджер»: async with self._sem. Мы занимаем Semaphore, пока не закончатся операции ниже — await task.perform(self) и self._queue.task_done().

async def _worker(self, task: Task): async with self._sem: await task.perform(self) self._queue.task_done()

Добавим Semaphore внутрь scheduler, чтобы scheduler не запускал новые worker’ы, если количество параллельных worker’ов уже достигло максимума:

async def _scheduler(self): while self.is_running: for _ in range(self.max_rate): async with self._sem: task = await self._queue.get() asyncio.create_task(self._worker(task)) await asyncio.sleep(self.interval)

Мы добавили 3 задачи и ждем, пока они исполнятся. Таким образом мы соблюдаем максимальное параллельное количество запросов.

Остановка фонового планировщика

У нас осталась проблема с корректным завершением планировщика. После завершения остановки краулера появляется предупреждение о незавершенной корутине.

Чтобы этого не было, напишем функцию stop:

async def stop(self): self.is_running = False self._scheduler_task.cancel()

Теперь после того, как внутри пула закончатся задачи, его нужно корректно остановить. Добавим метод stop в конце функции start:

async def start(): pool = Pool(3) for tid in range(10): await pool.put(Task(tid)) pool.start() await pool.join() await pool.stop()

Теперь все работает корректно.

Мы остановили планировщик, когда задачи в очереди закончились. Но если мы остановим краулер в процессе работы, начнут появляться предупреждения о том, что какая-то задача не завершилась:

А чем больше время выполнения perform, тем больше будет таких уведомлений.

Поэтому нам нужно ожидать, когда все worker завершатся. Для этого введем дополнительную переменную, обозначающую количество параллельно работающих worker: concurrent_workers. Изначально она равна 0. При запуске воркера мы увеличиваем concurrent_workers на 1. При выходе, наоборот, уменьшаем на 1:

async def _worker(self, task: FetchTask): async with self._sem: self._cuncurrent_workers += 1 await task.perform(self) self._queue.task_done() self._cuncurrent_workers -= 1

Теперь нужно как-то сказать функции stop, что все параллельные worker завершились. Это произойдет, когда is_running будет false и concurrent_workers станет равной 0.

Для этого есть примитив синхронизации Event. В нашем коде мы добавим его в Pool и назовем stop_event. Это переменная, на которой можно ждать await self._stop_event.wait() до тех пор, пока кто-то не вызовет self._stop_event.set():

class Pool: def __init__(self, max_rate: int, interval: int = 1, concurrent_level: Optional[int] = None): self.max_rate = max_rate self.interval = interval self.concurrent_level = concurrent_level self.is_running = False self._queue = asyncio.Queue() self._scheduler_task: Optional[asyncio.Task] = None self._sem = asyncio.Semaphore(concurrent_level or max_rate) self._cuncurrent_workers = 0 self._stop_event = asyncio.Event()

Если равна, то все worker завершили свою работу, планировщик отменен и не создает новые задачи. В таком случае все компоненты Pool остановлены или завершили свою работу — программу можно завершать.

Но если concurrent_workers не равна 0, нам нужно внутри метода stop подождать событие stop_event:

async def stop(self): self.is_running = False self._scheduler_task.cancel() if self._cuncurrent_workers != 0: await self._stop_event.wait()

Когда Pool остановлен, последний работающий worker должен отправить уведомление:

 async def _worker(self, task: FetchTask): async with self._sem: self._cuncurrent_workers += 1 await task.perform(self) self._queue.task_done() self._cuncurrent_workers -= 1 if not self.is_running and self._cuncurrent_workers == 0: self._stop_event.set()

Обновим функцию main, чтобы все корректно работало:

def main(): loop = asyncio.get_event_loop() pool = Pool(3) try: loop.run_until_complete(start(pool)) except KeyboardInterrupt: loop.run_until_complete(pool.stop()) loop.close()

Теперь все работает. После нажатия Ctrl + C выполняются оставшиеся задачи, и программа завершается:

Работа краулера на примере обкачки нашего блога на Хабре

Мы реализовали механику пула на нашей абстрактной задачке task.

Для следующего этапа я подготовил задачу FetchTask.

MAX_DEPTH = 2 PARSED_URLS = set() @dataclass class FetchTask(Task): url: URL depth: int def parser(self, data: str) -> List['FetchTask']: if self.depth + 1 > MAX_DEPTH: return [] soup = BeautifulSoup(data, 'lxml') res = [] for link in soup.find_all('a', href=True): new_url = URL(link['href']) if new_url.host is None and new_url.path.startswith('/'): new_url = URL.build( scheme=self.url.scheme, host=self.url.host, path=new_url.path, query_string=new_url.query_string ) if new_url in PARSED_URLS: continue PARSED_URLS.add(new_url) res.append(FetchTask( tid=self.tid, url=new_url, depth=self.depth + 1 )) return res async def perform(self, pool): async with aiohttp.ClientSession() as session: async with session.get(self.url) as resp: print(self.url, resp.status) data = await resp.text() res: List[FetchTask] = await asyncio.get_running_loop().run_in_executor( None, self.parser, data ) for task in res: await pool.put(task)

Внутри функции parcer есть переменная soup, которая объявлена как soup = BeautifulSoup(data, ’lxml’). Дам небольшие пояснения.

BeautifulSoup — парсер для анализа HTML/XML.

lxml — реализация HTML/XML парсера. Из-за GIL мы специально запускаем res внутри функции perform через executor:

async def perform(self, pool): async with aiohttp.ClientSession() as session: async with session.get(self.url) as resp: print(self.url, resp.status) data = await resp.text() res: List[FetchTask] = await asyncio.get_running_loop().run_in_executor( None, self.parser, data ) for task in res: await pool.put(task)

GIL — блокировка, которая запрещает параллельные потоки в Python. Но если вы пишите расширение на С, есть возможность «отпустить» GIL.

Парсер lxml написан на С. У себя под капотом он умеет отпускать GIL и выполняться в отдельном потоке. Это относится и к некоторым другим расширениям: https://lxml.de/2.0/FAQ.html#id1

В fetch_task также переопределяем функцию perform, в которой нужно сходить в сеть. Для этого я взял aiohttp client.

В задаче FetchTask мы идем на указанный URL, оттуда получаем данные и запускаем executor для их обработки. Нужно взять все ссылки в документе, перейти на них и тоже обкачать:

 def parser(self, data: str) -> List['FetchTask']: if self.depth + 1 > MAX_DEPTH: return [] soup = BeautifulSoup(data, 'lxml') res = [] for link in soup.find_all('a', href=True): new_url = URL(link['href']) if new_url.host is None and new_url.path.startswith('/'): new_url = URL.build( scheme=self.url.scheme, host=self.url.host, path=new_url.path, query_string=new_url.query_string ) if new_url in PARSED_URLS: continue PARSED_URLS.add(new_url) res.append(FetchTask( tid=self.tid, url=new_url, depth=self.depth + 1 )) return res

В конце мы добавляем в результат новую задачу и увеличиваем на 1 глубину depth.

Например, когда мы поставили задачку habr.com, глубина была равна 1. Мы скачали этот документ, в котором есть и другие ссылки: блоги Mail.ru, Yandex или KTS. Когда мы стали обкачивать следующие страницы, глубина увеличилась до 2. Этот параметр нужен для ограничения количества обкачиваемых ресурсов, фактически — глубины.

Обратите внимание, что у нас есть список посещенных страничек PARSED_URLS. Так мы не будем дважды посещать одни и те же страницы.

Теперь импортируем задачи в краулер из fetch_task и изменяем start:

async def start(pool): await pool.put( FetchTask(URL('https://habr.com/ru/company/kts/blog/'), 1) ) pool.start() await pool.join() await pool.stop()

Выставляем 3 запроса в секунду и смотрим, как наш краулер потихоньку обкачивает Хабр:

Спасибо за внимание

На этом все! Спасибо всем, кто дочитал статью.

Если сталкивались с подобными задачами, пожалуйста, поделитесь своим опытом в комментариях.

Другие наши статьи по бэкенду и асинхронному программированию для начинающих:

  • Цикл статей «Первые шаги в aiohttp»: пишем первое hello-world-приложение, подключаем базу данных, выкладываем проект в Интернет
  • Визуализация 5 алгоритмов сортировки на Python
  • Разбираемся в асинхронности: где полезно, а где — нет?

Другие наши статьи по бэкенду и асинхронному программированию для продвинутого уровня:

  • Пишем асинхронного Телеграм-бота
  • Пишем Websocket-сервер для геолокации на asyncio

Несколько слов о курсе по асинхронному программированию на Python ��

Этот курс — маст хев для тех, кто хочет прокачать харды и стать специалистом, который не боится сложных проектов.

  • Вы разберётесь, как работает асинхронное программи­рование и где его лучше применять.
  • Научитесь мыслить нелинейно и сможете продумывать более сложные архитектуры приложений.
  • Получите опыт работы с микросервисами и узнаете best practices написания асинхронных приложений на Python.

АЛГОРИТМ РАБОТЫ ВЕБ- КРАУЛЕРА ДЛЯ РЕШЕНИЯ ЗАДАЧИ СБОРА ДАННЫХ ИЗ ОТКРЫТЫХ ИНТЕРНЕТ ИСТОЧНИКОВ Текст научной статьи по специальности «Компьютерные и информационные науки»

Аннотация научной статьи по компьютерным и информационным наукам, автор научной работы — Вьет Нгуен Тхань, Кравец Алла Григорьевна

Сбор данных является первым этапом реализации аналитической задачи или проекта. В его основе лежит процесс извлечения и организации хранения данных в виде, наиболее пригодном с точки зрения их обработки на конкретной аналитической платформе или решения конкретной аналитической задачи. Веб-краулеры — также известные как пауки или поисковые роботы — это программы, которые реализуют процесс перемещения по страницам и/или документам в Интернете для сбора определенной информации, статистики или сохранения ресурсов сайта. В статье рассматриваются описание работы и алгоритмы для осуществления автоматического сбора информации . В качестве апробации приведена задача извлечения данных всех статей, представленных в электронном виде журнала «Молодой ученый».

i Надоели баннеры? Вы всегда можете отключить рекламу.

Похожие темы научных работ по компьютерным и информационным наукам , автор научной работы — Вьет Нгуен Тхань, Кравец Алла Григорьевна

Автоматизация сбора информации из открытых интернет-источников
Адаптивный краулер для поиска и сбора внешних гиперссылок
Разработка программы сбора данных о структуре веб-сайтов

Оценка состояния транспортных магистралей Северо-Западного федерального округа с использованием анализа тональности отзывов пользователей сети Интернет

Разработка приложения веб-скрапинга с возможностями обхода блокировок
i Не можете найти то, что вам нужно? Попробуйте сервис подбора литературы.
i Надоели баннеры? Вы всегда можете отключить рекламу.

THE ALGORITHM OF WEB CRAWLER TO SOLVE THE PROBLEM OF COLLECTING DATA FROM OPEN INTERNET SOURCES

Data collection is the first step in the implementation of analytical task or project. It is based on the process of extracting and organizing data storage in the form that is most suitable for processing on a specific analytical platform or solving a specific analytical task. Web crawlers — also known as spiders or search robots — are programs that implement the process of navigating the pages and/or documents of the Web in order to collect specific information, statistics or save site resources. The article discusses the job description and algorithms for the automatic collection of information. As testing, the task of extracting data of all articles submitted in the electronic form of the Young Scientist magazine was carried out.

Текст научной работы на тему «АЛГОРИТМ РАБОТЫ ВЕБ- КРАУЛЕРА ДЛЯ РЕШЕНИЯ ЗАДАЧИ СБОРА ДАННЫХ ИЗ ОТКРЫТЫХ ИНТЕРНЕТ ИСТОЧНИКОВ»

Nguyen Thanh Viet1, Alla G. Kravets1

THE ALGORITHM OF WEB CRAWLER TO SOLVE THE PROBLEM OF COLLECTING DATA FROM OPEN INTERNET SOURCES

Volgograd state technical university, Lenin Pr., 28, Volgograd, 400005, Russia. e-mail: vietqn1987@gmail.com

Data collection is the first step in the implementation of analytical task or project. It is based on the process of extracting and organizing data storage in the form that is most suitable for processing on a specific analytical platform or solving a specific analytical task. Web crawlers -also known as spiders or search robots — are programs that implement the process of navigating the pages and/or documents of the Web in order to colect specific information, statistics or save site resources. The article discusses the job description and algorithms for the automatic collection of information. As testing, the task of extracting data of all articles submitted in the electronic form of the Young Scientist magazine was carried out.

Keywords: web crawler, information gathering, depth first search, breadth first search, Young Scientist magazine.

Нгуен Тхань Вьет1, Кравец Алла Григорьевна1

АЛГОРИТМ РАБОТЫ ВЕБ-КРАУЛЕРА ДЛЯ РЕШЕНИЯ ЗАДАЧИ СБОРА ДАННЫХ ИЗ ОТКРЫТЫХ ИНТЕРНЕТ ИСТОЧНИКОВ

Волгоградский государственный технический университет, пр. им. Ленина, 28, Волгоград, 400005, Россия. е-таИ: vietqn1987@gmail.com

Сбор данных является первым этапом реализации аналитической задачи или проекта. В его основе лежит процесс извлечения и организации хранения данных в виде, наиболее пригодном с точки зрения их обработки на конкретной аналитической платформе или решения конкретной аналитической задачи. Веб-краулеры -также известные как пауки или поисковые роботы — это программы, которые реализуют процесс перемещения по страницам и/или документам в Интернете для сбора определенной информации, статистики или сохранения ресурсов сайта. В статье рассматриваются описание работы и алгоритмы для осуществления автоматического сбора информации. В качестве апробации приведена задача извлечения данных всех статей, представленных в электронном виде журнала «Молодой ученый».

Ключевые слова: веб-краулер, сбор информации, поиск в глубину, поиск в ширину, журнал «Молодой ученый»..

Сбор данных является первым этапом реализации аналитической задачи или проекта [4]. В ее основе содержится процесс извлечения и организации хранения данных в виде, наиболее пригодном с точки зрения их обработки на конкретной аналитической платформе или решения конкретной аналитической задачи [7]. Для этого используются Веб-краулеры, также известные как пауки или поисковые роботы. Это программы, которые реализуют процесс перемещения по страницам и/или документам Веба с целью сбора определенной информации, статистики или сохранения ресурсов сайта [1].

Информация в Интернете разбросана среди миллиардов страниц, пользователи могут следить за гиперссылками для доступа к информации, практически переходя от одной страницы к другой. Поисковый робот может посещать многие сайты для сбора информации, которая может быть проанализирована и добыта в центральном месте, либо онлайн (как он загружен), либо оффлайн (после его сохранения).

Тем не менее, Web — динамичный субъект, развивающийся быстрыми темпами. Следовательно, существует постоянная потребность в краулерах, чтобы помочь приложениям оставаться в режиме постоянной активности, поскольку страницы и ссылки добавляются, удаляются, перемещаются или изменяются [5].

Дата поступления — 23 октября 2019 года

Существует множество приложений для веб-краулера. Одно из них — бизнес-аналитика, на основе которого организации собирают информацию о конкурентах и о потенциальных сотрудниках. Другое использование — мониторинг веб-сайтов и страниц, чтобы пользователи или сообщество могли быть уведомлены о новой информации, которая появляется в определенных местах. Существуют также вредоносные приложения краулеров, например, которые спамерами собирают адреса электронной почты или личную информацию при фишинге и другой краже личных данных [6]. Однако наиболее распространенное использование краулеров — это поддержка поисковых систем. Фактически, краулеры являются основными потребителями пропускной способности Интернета. Они собирают страницы для поисковых систем при создании индексов. Известные поисковые системы Google, Yahoo. запускают очень эффективные универсальные краулеры, предназначенные для сбора всех страниц, независимо от их содержимого. Другие краулеры, иногда называемые предпочтительными, становятся более целенаправленными. Они загружают только страницы определенных типов или тем.

Для анализа и описания работы веб-краулера необходимо решить следующие задачи:

1. Описание основной стратегии Веб-краулера.

2. Описание алгоритма поиска в глубину и поиска в ширину.

3. Автоматизации сбора информаций статей, представленных в электронном виде журнала «Молодой ученый», в качестве апробации вышерассмотрен-ных алгоритмов.

Основная стратегия Веб-краулера

В простейшей форме краулер запускается из набора исходных страниц (URL-адресов) и затем использует ссылки внутри них для извлечения других. Ссылки в этих страницах, в свою очередь, извлекаются и соответственно страницы посещаются. Процесс повторяется до тех пор, пока будет проанализировано требуемое число страниц или достигнута заданная цель. Это простое описание опускает ряд аспектов, связанных с сетевыми подключениями, ловушками пауков, канонизацией URL, парсингами и этикой сбора [1].

Выделить ссылку из фронта

Рисунок 1. Блок-схема типичного последовательного краулера

Краулер по сути является алгоритмом поиска графа. Можно считать интернет большим графом со страницами, как его узлы и гиперссылки как его ребра. Краулер запускается из нескольких начальных узлов, а затем следует за ребрами для прибытия других узлов. Процесс извлечения страниц и ссылок внутри него аналогичен расширению узла в графическом поиске. Существует два классических алгоритма обхода графов: поиск в глубину и поиск в ширину.

Поиск в глубину

В его основе содержится рекурсия, и он представляет следующую последовательность действий:

1. Пойти в смежную вершину, не посещенную

2. Запустить из этой вершины алгоритм обхода в глубину.

3. Вернуться в начальную вершину.

На рис. 1 показан поток типичного последовательного краулера. Такой краулер загружает одну страницу каждый раз. Краулер поддерживает список непосещаемых URL-адресов, называемых фронт (frontier). Такой список инициализируется URL-адресами источниками (seed), которые могут быть предоставлены пользователем или другой программой. На каждой итерации основного цикла краулер выбирает следующий URL-адрес с фронта, скачивает страницу соответствующего URL-адреса через HTTP, анализирует ее для извлечения URL-адресов, добавляет обнаруженные URL-адреса на фронт и сохраняет страницу (или другую извлеченную информацию такую, как термины индекса, файлы. ) в локальном хранилище. Процесс сканирования может быть прекращен, когда определенное количество страниц было достигнуто. Краулер может также быть остановлен, если фронт становится пустым, хотя это редко случается на практике из-за высокого среднего количества ссылок.

4. Повторить пункты 1-3 для всех не посещенных ранее смежных вершин.

Такой алгоритм представлен на рис. 2.

Помечать текущую вершину обработанной

1ершина поллечена^-^ Нет .. посещенной?

Перейти на следующую вершину

Рисунок 2. Блок-схема обхода в глубину

Метод поиска в глубину целесообразно применять для того, чтобы обойти веб-страницы на небольшом сайте, однако для обхода Интернета он не очень удобен [2].

В целом обе эти ситуации можно решить, для этого воспользуемся другим классическим алгоритмом — поиском в ширину.

Поиск в ширину (breadth-first search, BFS) работает аналогично поиску в глубину, однако он обходит вершины графа в порядке удаленности от началь-

ной страницы. Для этого алгоритм использует структуру данных «очередь» — в очереди можно добавлять элементы в конец и извлекать из начала.

Алгоритм, который показан на рис. 3, можно описать следующим образом:

1. Добавляем в очередь начальную вершину и помечаем посещенной.

2. Если очередь не пуста, то извлекаем из нее следующую вершину для обработки.

3. Обрабатываем вершину.

4. Для всех ребер, исходящих из обрабатываемой вершины, не входящих во множество посещенных:

a) Добавить в очередь

b) Добавить во множество посещенных

5. Перейти к пункту 2 [2].

Таким образом, в очереди вначале окажутся вершины, находящиеся на расстоянии одной ссылки от начальной, потом двух ссылок, потом трех и т.д., то есть алгоритм поиска в ширину доходит до вершины кратчайшим путем.

Следует отметить один важный момент: очередь и множество посещенных вершин в нашем случае используют только простые интерфейсы (добавить, взять, проверить на вхождение) и легко могут быть вынесены в отдельный сервер, связанный с клиентом через эти интерфейсы. Эта особенность дает нам возможность реализовать многопоточный обход графа: можно запустить несколько одновременных обработчиков, использующих одну очередь.

Апробация алгоритма обхода

В качестве апробации вышерассмотренных алгоритмов проведена задача автоматизации сбора ин-формаций статей, представленных в электронном виде журнала «Молодой ученый». Научный журнал «Молодой ученый» — ежемесячное издание, предоставляющее аспирантам, докторантам, соискателям, специалистам и студентам возможность опубликовать результаты научных исследований.

В отдельный список реферируемых изданий («Список ВАК») журнал не входит. При этом, статьи, опубликованные в журнале «Молодой ученый», учитываются ВАК как печатный труд. Полное библиографическое описание статей журнала представлено в Научной электронной библиотеке elibrary.ru для формирования Российского индекса научного цитирования (РИНЦ). Также журнал включен в международный каталог периодических изданий «Ulrich’s Periodicals Directory». [3]

Для большей наглядности, постановка реальной практической задачи описывается следующим образом:

1. Извлечь статьи из вебсайта журнала «Молодой ученый» (moluch.ru) в свою базу данных;

2. Каждая статья (пост) должна содержать такие атрибуты, как: ссылка на первоисточник статьи; заголовок (наименование) статьи; авторы; рубрика, к которой принадлежит статья; абстракт или краткое описание о содержании статьи; ключевые слова, термины, год издания статьи.

Для решения нашей задачи, принято решение создать веб-краулер, один цикл работы которого описывается следующим алгоритмом:

1. Переход на страницу сайта источника moluch.ru.

2. Получение ссылок со страницы, фильтрация элементов.

3. Извлечение нужной нам информации.

4. Обход полученных ссылок.

Так как краулер извлекает требуемые атрибуты из HTML — разметки сайта-источника, то ему должны быть заранее известны элементы веб-страниц для ориентации. Таким образом, необходимо определить ряд атрибутов сайта, на основе которых поисковой робот извлекает требуемую информацию. В нашем случае такими атрибутами являются:

1. Атрибут (регулярное выражение: ^archiveV\d+V\d+V$), определяющий наличие ссылки статьи на странице.

2. Атрибут (селектор div.page_header hi), определяющий заголовок статьи.

3. Атрибут (селектор div.j_des span [itemprop =’author’]), определяющий авторов статьи.

4. Атрибут (селектор div.j_des span [itemprop =’articleSection’]), определяющий рубрику статьи.

5. Атрибут (селектор div[itemprop =’articleBody’]>p>em), определяющий абстракт статьи.

6. Атрибут (селектор span[itemprop=’keywords’]), определяющий ключевые слова.

7. Атрибут (селектор span[itemprop=’issueNumber’]), определяющий год издания.

Рисунок 3. Блок-схема обхода в ширину

ОЩжМи. ВЩиМи ВЩиМи ВЩиМи ВЩиМи ВЩиМи ВЩиМи ВЩиМи ВЩиМи ВЩиМи ВЩиМи ВЩиМи ВЩиМи ВЩиМи ВЩиМи ВЩиМи ВЩиМи ВЩиМи В 2016_aiïhive. В 2016_aiïhive.

В 2016_aiïhive. В 2016_aiïhive. В 2016_aiïhive.

.1 ими JÜ_372Si_,M JÜJJ414.M .133.371 iSM .130.3533Ш .130_36098_,М J!1_36504_M .134_376В_,М J!4J7f1ï_,M

.132.3690Ш JÜJJ14S.M .ИЗЩЫ .132.369Í9M .1¡1_36607_,ht УЛШАЫ J 33.373 DI ..M JÎIJWIJ.ht .133.3717? M .133.3722Ш J30.361S4.M J34.376Í9.U JÜ.3731Z.M J31_36309_,M 132 36717 M

24/04/2019 12:29,. TeíDocument

24/04/2019 12:32,. TeíDocument

24/04/2019 12:27. TatDocumeiit

File Edit Format View Help

«abstract»: «Возможности для финансирования предприятий зависят от макро и микроэкономическ ости используется чаще, особенно в финансовых анализах для оценки финансовой стабильности] п «keywords»: «финансирование, оптимизация, промышленное предприятие, финансовый менеджмент», «year»: «2D16″>

¿ЧЩШМ IÍ.JU. iHiuoiumem ¿ ‘г-

24/04/2019 12:11 „, Tat Document 2KB

24/04/2019 12:27. Tat Document 2KB

i Не можете найти то, что вам нужно? Попробуйте сервис подбора литературы.

24/04/2019 12:28. Tat Document 2KB

24/04/2019 12:06. Tat Document 2KB

24/04/2019 12:36. Tat Document 2KB

24/04/2019 12:30. Tat Document 2KB

24/04/2019 12:09. Tat Document 2KB

24/04/2019 12:11. Tat Document 2KB

Рисунок 4. Извлеченные данные статей в БД

Алгоритмом поиска в ширину собрано 65835 статей (из всех 67268 ссылок, в момент написания этой статьи) в рамках одного веб-сайта журнала «Молодой ученый» с целенаправленными данными. На рис. 4 показаны извлеченные данные из сайта-источника и сохраненные в текстовых файлах .БОМ).

На рис. 5 показаны графики изменения количества найденных ссылок (зеленая линия), ссылок, содержащих статьи (красная линия), ссылок в очереди (голубая линия) при каждой итерации.

Рисунок 5. Графики изменения количества ссылок при каждой итерации

Программа апробации написана на языке Python с пакетами Requests и Beautiful Soup для использования данных с веб-страниц. Модуль Requests позволяет интегрировать программы Python с веб-сервисами, а модуль Beautiful Soup предназначен для ускорения анализа экранных данных. С использованием интерактивной консоли Python и этих библиотек можно проанализировать веб-страницу и работать с имеющейся текстовой информацией.

В работе описан алгоритм для осуществления обхода краулера. В качестве апробации алгоритма поиска в ширину решена задача извлечения данных статей, представленных в электронном виде в журнале «Молодой ученый». В рамках решения поставленной задачи написана программа для автоматического сбора информации, соответствующей критериям, описанным в постановке задачи. В результате собрано 65835 статей для дальнейшей аналитической задачи.

В дальнейшем планируется расширить эксперимент, сравнив тематический краулер на основе алгоритма Клеинберга, анализирующий гиперссылочную структуру Веб-пространства, и сфокусированный крау-лер на основе TF-IDF алгоритма, анализирующий содержание каждой Веб-страницы.

Исследование выполнено при финансовой поддержке РФФИ в рамках научного проекта № 19-0701200.

1. Pant G, Srinivasan P., Menczer F Crawling the Web In: Web Dynamics. Berlin, Heidelberg: Springer, 2004. Р. 153-177.

2. Петров А. Поиск под капотом Глава 1. Сетевой паук. URL: https://habr.com/ru/post/345672/ (дата обращения: 25.01.2019).

3. Молодой ученый. URL: http://lib.usfeu.ru/index.php/elektronnye-versii-zhurnalov#e9 (дата обращения: 30.11.2018).

4. Кравец А.Д., Петрова ИЮ, Кравец А.Г. Агрегация информации о перспективных технологиях на основе автоматической генерации интеллектуальных агентов мультиагентных систем // Прикаспийский журнал: управление и высокие технологии. 2015. № 4. С. 141-147.

5. Перепелицын В.А., Кравец А.Г. The method of random walks for the analysis of social networks // Ин-

формационные технологии в науке, образовании и управлении: матер. XLIV междунар. конф. и XIV меж-дунар. конф. молодых учёных IT+S&E»16. Весенняя сессия Гурзуф, 22 мая — 1 июня 2015 г. / под ред. Е.Л. Глориозова. М.: ИНИТ, 2015. C. 168-173.

6. Гнеушев В.А., Кравец А.Г., Козунова С.С., Бабенко А.А. Моделирование сетевых атак злоумышленников в корпоративной информационной системе // Промышленные АСУ и контроллеры. 2017. № 6. С. 5160.

7. Ананченко И.В., Гайков А.В., Мусаев А.А. Технологии слияния гетерогенной информации из разнородных источников (data fusion) // Известия СПбГТИ(ТУ). 2013. № 19(45). С. 98-105.

1. Pant G, Srinivasan P., Menczer F Crawling the Web In: Web Dynamics. Berlin, Heidelberg: Springer, 2004. Р. 153-177.

2. Petrov A. Poisk pod kapotom Glava 1. Setevoj pauk. URL: https://habr.com/ru/post/345672/ (data obrashcheniya: 25.01.2019).

3. Molodoj uchenyj. URL: http://lib.usfeu.ru/index.php/elektronnye-versii-zhurnalov#e9 (data obrashcheniya: 30.11.2018).

4. Kravec A.D., Petrova I.YU, Kravec A.G. Agregaciya informacii o perspektivnyh tekhnologiyah na osnove avtomaticheskoj generacii intellektual’nyh agentov mul’tiagentnyh sistem // Prikaspijskij zhurnal: upravlenie i vysokie tekhnologii. 2015. № 4. S. 141-147.

5. Perepelicyn V.A., Kravec A.G. The method of random walks for the analysis of social networks // Infor-macionnye tekhnologii v nauke, obrazovanii i upravlenii: mater. XLIV mezhdunar. konf. i XIV mezhdunar. konf. molodyh uchyonyh IT+S&E» 16. Vesennyaya sessiya Gur-zuf, 22 maya — 1 iyunya 2015 g. / pod red. E.L. Glori-ozova. M.: INIT, 2015. C. 168-173.

6. Gneushev V.A., Kravec A.G,, Kozunova S.S., Babenko A.A. Modelirovanie setevyh atak zloumyshlenni-kov v korporativnoj informacionnoj sisteme // Promysh-lennye ASU i kontrollery. 2017. № 6. S. 51-60.

7. Ananchenko I. V,, Gajkov A. V,, Musaev A.A. Tekhnologii sliyaniya geterogennoj informacii iz raznorod-nyh istochnikov (data fusion) // Izvestiya SPbGTI(TU). 2013. № 19(45). S. 98-105.

Парсеры (сrawlers) сайтов на Python

Базовый пример использования запросов и lxml для очистки некоторых данных

# For Python 2 compatibility. from __future__ import print_function import lxml.html import requests def main(): r = requests.get("https://httpbin.org") html_source = r.text root_element = lxml.html.fromstring(html_source) # Note root_element.xpath() gives a *list* of results. # XPath specifies a path to the element we want. page_title = root_element.xpath('/html/head/title/text()')[0] print(page_title) if __name__ == '__main__': main() 

Ведение веб-сессии с запросами

Это хорошая идея , чтобы поддерживать веб-соскоб сессии упорствовать печенье и другие параметры. Кроме того, это может привести в повышению производительности , так как requests.Session повторно использует ТСР — соединение с узлом:

 import requests with requests.Session() as session: # all requests through session now have User-Agent header set session.headers = # set cookies session.get('http://httpbin.org/cookies/set?key=value') # get cookies response = session.get('http://httpbin.org/cookies') print(response.text) 

Соскоб с использованием основы Scrapy

Сначала вы должны создать новый проект Scrapy. Введите каталог, в котором вы хотите хранить свой код, и запустите:

 scrapy startproject projectName 

Чтобы очистить нам нужен паук. Пауки определяют, как будет очищен определенный сайт. Вот код для паука , который следует ссылки на верхнюю проголосовали вопросы по StackOverflow и соскребают некоторые данные из каждой страницы ( источник ):

import scrapy class StackOverflowSpider(scrapy.Spider): name = 'stackoverflow' # each spider has a unique name start_urls = ['https://codecamp.ru/questions?sort=votes'] # the parsing starts from a specific set of urls def parse(self, response): # for each request this generator yields, its response is sent to parse_question for href in response.css('.question-summary h3 a::attr(href)'): # do some scraping stuff using css selectors to find question urls full_url = response.urljoin(href.extract()) yield scrapy.Request(full_url, callback=self.parse_question) def parse_question(self, response): yield

Сохраните классы паукообразных в projectName\spiders каталога. В данном случае — projectName\spiders\stackoverflow_spider.py .

Теперь вы можете использовать свой паук. Например, попробуйте запустить (в каталоге проекта):

 scrapy crawl stackoverflow 

Изменить пользовательский агент Scrapy

Иногда по умолчанию Scrapy агент пользователя ( «Scrapy/VERSION (+http://scrapy.org)» ) блокируется хостом. Чтобы изменить пользователя по умолчанию агента открытым settings.py в, раскомментируйте и отредактируйте следующую строку к тому , что вы хотите.

 #USER_AGENT = 'projectName (+http://www.yourdomain.com)' 
 USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36' 

Выскабливание с использованием BeautifulSoup4

 from bs4 import BeautifulSoup import requests # Use the requests module to obtain a page res = requests.get('https://www.codechef.com/problems/easy') # Create a BeautifulSoup object page = BeautifulSoup(res.text, 'lxml') # the text field contains the source of the page # Now use a CSS selector in order to get the table containing the list of problems datatable_tags = page.select('table.dataTable') # The problems are in the tag, # with class "dataTable" # We extract the first tag from the list, since that's what we desire datatable = datatable_tags[0] # Now since we want problem names, they are contained in tags, which are # directly nested under tags prob_tags = datatable.select('a > b') prob_names = [tag.getText().strip() for tag in prob_tags] print prob_names 

Соскоб с использованием Selenium WebDriver

Некоторые сайты не любят, когда их вычищают. В этих случаях вам может понадобиться симулировать реального пользователя, работающего с браузером. Selenium запускает и контролирует веб-браузер.

 from selenium import webdriver browser = webdriver.Firefox() # launch firefox browser browser.get('https://codecamp.ru/questions?sort=votes') # load url title = browser.find_element_by_css_selector('h1').text # page title (first h1 element) questions = browser.find_elements_by_css_selector('.question-summary') # question list for question in questions: # iterate over questions question_title = question.find_element_by_css_selector('.summary h3 a').text question_excerpt = question.find_element_by_css_selector('.summary .excerpt').text question_vote = question.find_element_by_css_selector('.stats .vote .votes .vote-count-post').text print "%s\n%s\n%s votes\n-----------\n" % (question_title, question_excerpt, question_vote) 

Селен может сделать гораздо больше. Он может изменять файлы cookie браузера, заполнять формы, имитировать щелчки мышью, делать скриншоты веб-страниц и запускать пользовательский JavaScript.

Простая загрузка веб-контента с помощью urllib.request

Стандартный модуль библиотеки urllib.request может быть использована для загрузки веб — контента:

 from urllib.request import urlopen response = urlopen('https://codecamp.ru/questions?sort=votes') data = response.read() # The received bytes should usually be decoded according the response's character set encoding = response.info().get_content_charset() html = data.decode(encoding) 

Аналогичный модуль также доступен в Python 2 .

Соскоб с завитком

 from subprocess import Popen, PIPE from lxml import etree from io import StringIO 
 user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36' url = 'https://codecamp.ru' get = Popen(['curl', '-s', '-A', user_agent, url], stdout=PIPE) result = get.stdout.read().decode('utf8') 

-s : бесшумный скачать

-A : флаг пользовательского агента

tree = etree.parse(StringIO(result), etree.HTMLParser()) divs = tree.xpath('//div')

Web crawler с использованием Python и Chrome

Недавно, сидя на диване, я задумался о том, что хочется мне сделать своего паука, который что-то бы смог качать с веб сайтов. Но качать он должен был бы не простой загрузкой, а как настоящий милый добрый браузер (т.е. JavaScript чтобы исполнялся).

В моей голове всплыли такие интересные штуки, как Selenium, PhantomJS, Splash и всякое подобное. Все эти штуки были мне немного втягость. Вот какие причины я выявил:

  • Дело в том, что я хотел бы писать на своем любимом питоне, потому что очень не люблю JavaScript, а это уже означает, что большая часть уже не работала бы (или пришлось их как-то склеивать, что тоже отстой).
  • Еще эти безголовые браузеры обновляются как когда.
  • Но вот Selenium очень милая штука, но я не нашел, как там отслеживать загрузку страниц, или хотя бы адекватного способа выдрать куку или задать её. Слышал, что многие любители селениума инжектят в страничку JavaScript, что для меня дико, потому что где-то полгода назад я делал сайтик, который отрывал любые JavaScript вызовы с сайта и потенциально мог определять моего паука. Мне бы очень не хотелось таких казусов. Хочется чтобы мой паук выглядел как браузер максимально точно.

Пробежавшись по документации и готовому проекту, и убедившись что никто толком не реализовал клиент под Python, я решил сделать свой клиент.

Протокол у Chrome Remote Debug достаточно простой. Для начала нам надо запустить Chrome вот с такими параметрами:

chrome --incognito --remote-debugging-port=9222 --headless

image

Теперь у нас есть API, доступное, по адресу http://127.0.0.1:9222/json/, в котором я обнаружил такие методы как list, new, activate, version, которые используются для управления вкладками.

Также, если мы просто перейдем на http://127.0.0.1:9222/, то сможем перейти на прекрасный веб отладчик, который полностью имитирует стандартный. В нем очень удобно отслеживать как работают апишные методы хрома (окно отладки справа эмулируется внутри окна, а окно браузера — отрисовано на канвасе).

image

Собственно, перейдя на вкладку list, мы можем узнать, адрес вебсокета, с помощью которого мы сможем общаться с вкладкой.

Дальше мы подсоединяемся через вебсокет к желаемой вкладке, и общаемся с нею. Мы можем:

  • Выполнить запрос
  • Подписаться на события в вкладке

image

  • Автоматическая подкачка последней версии протокола
  • Обертка протокола в питонистический вид
  • Синхронный и асинхронный клиент (синхронный только для отладки)
  • Надеюсь, удобная абстракция вкладок

Вот так выглядит прога, которая подгружает страничку и выдает длину каждого ответа на запрос:

import asyncio import chrome_remote_interface if __name__ == '__main__': class callbacks: async def start(tabs): await tabs.add() async def tab_start(tabs, tab): await tab.Page.enable() await tab.Network.enable() await tab.Page.navigate(url='http://github.com') async def network__loading_finished(tabs, tab, requestId, **kwargs): try: body = tabs.helpers.unpack_response_body(await tab.Network.get_response_body(requestId=requestId)) print('body length:', len(body)) except tabs.FailReponse as e: print('fail:', e) async def page__frame_stopped_loading(tabs, tab, **kwargs): print('finish') tabs.terminate() async def any(tabs, tab, callback_name, parameters): pass # print('Unknown event fired', callback_name) asyncio.get_event_loop().run_until_complete(chrome_remote_interface.Tabs.run('localhost', 9222, callbacks))

Тут мы используем систему колбеков. Самые интересные: start и any:

  • start(tabs, tab) — вызывается при старте.
  • any(tabs, tab, callback_name, parameters) — вызывается, в случае если событие не нашлось в списке колбеков.
  • network__response_received(tabs, tab, **kwargs) — пример библиотечного события Network.responseReceived.

Однако, было одно но, из-за которого я все таки плачу. С помощью remote API нельзя производить перехват и модификацию запросов и ответов. Насколько я понял — это возможно через mojo, который позволяет использовать хром в качестве библиотеки.

Однако, я подумал, что компиляция нестабильного хрома и отсутствие Python прослойки для меня будет большим горем (сейчас есть C++ и JavaScript в процессе разработки).

Надеюсь, статья была полезной. Спасибо.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *