CoderCastrov logo
CoderCastrov
Поисковые системы

Парсинг веба для поисковой системы

Парсинг веба для поисковой системы
просмотров
7 мин чтение
#Поисковые системы

введение в поиск по вебу с помощью Scrapy

Некоторое время назад я работал в команде из двух человек с Бруно Бахманном над проектом Sleuth, проектом UBC Launch Pad для создания поисковой системы для определенной области. В рамках этого проекта мы создавали все, начиная от веб-сайта и сервера, заканчивая скрапером, который заполнял нашу базу данных веб-сайтами для поиска. Целью было иметь возможность искать подробный контент, связанный с UBC, в частности, тот, который находится на малоизвестных сайтах курсов и подобных, которые трудно найти с помощью поисковых систем, таких как Google. В этой статье мы рассмотрим, как мы реализовали наши краулеры и скраперы для сбора контента для этой поисковой системы.

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

scrapy/scrapy

scrapy - Scrapy, быстрый высокоуровневый фреймворк для парсинга и скрапинга веб-сайтов на языке Python.

github.com

Однако большинство обычных случаев использования Scrapy связаны с работой с конкретными веб-сайтами и предсказуемыми структурами страниц, что не было особенно полезно для данного случая использования. Sleuth был моим первым "настоящим" программным проектом, моим первым опытом использования Python и моим первым опытом парсинга... поэтому мне пришлось искать решение в темноте.

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

Но как насчет поста на Reddit? Лучше всего его описывает содержание поста, заголовок поста, комментарии, название подсайта, боковая панель или их комбинация? Должна ли роль играть карма в приоритете этого поста в результатах? Хотим ли, чтобы поиски по слову "прокрастинация" возвращали эту страницу только потому, что она находится в боковой панели?

Для более специфичных страниц, связанных с UBC, дело становится еще сложнее:

Курс, отображаемый в селекторе курсов UBC

Выше приведенный скриншот из селектора курсов UBC. Когда Sleuth должен отображать эту страницу в результатах поиска? Заголовок и описание работают довольно хорошо, но в данном случае они оба являются довольно нетипичными элементами HTML, поэтому мы не можем просто получить их так же, как получаем содержимое со страницы Википедии. А что насчет данных раздела? Хотим ли мы объединить их в один результат или иметь 100 результатов, где каждый раздел "BIOL 200" будет своим собственным результатом? Также раздражает переход по ссылкам - простое решение заключается в посещении каждой ссылки на каждой странице, но "Сохранить в рабочий список" здесь тоже является ссылкой... которую сканер, вероятно, не захочет посещать.

Богатые результаты 🤔

Сам Google, кажется, немного обходит некоторые из этих проблем, предлагая Google Search Console, где вы можете запросить хиты от краулеров Google и узнать, как сделать так, чтобы "богатые результаты" отображались, когда кто-то выполняет поиск, включающий ваш веб-сайт. Для этого вы используете метаданные теги специальным образом, чтобы краулеры Google могли искать и извлекать их, что, кажется, является способом получить эти красиво оформленные результаты поиска.

Поэтому я построил свое решение по этим принципам, определив фиксированное количество категорий. На верхнем уровне я настроил два сканера: broad_crawler, который будет проходить по всем ссылкам на посещаемых страницах, и custom_crawler, который будет определять подходящий модуль parser для обработки веб-страниц, соответствующих ожидаемому формату. По пути будут отслеживаться также дочерние ссылки каждой страницы, чтобы мы могли установить "связи" между результатами, независимо от их типа. Эти сканеры могут идентифицировать страницы на основе их содержимого и определить лучший парсер для сбора данных. Этот подход имеет преимущество модульности, и новые типы могут быть добавлены в любое время - хотя это довольно ручной процесс.

Поток данных выглядит примерно так:

Грубый контур потока данных наших сканеров.

🕷 Парсеры

Я сделал различие между broad_crawler и custom_crawler, потому что данные о курсах UBC должны были быть спарсены очень специфическим образом с сайта курсов UBC, и нам нужно было получить очень конкретную информацию (такую как строки таблиц на странице для данных о секциях курса). Идея заключалась в том, чтобы custom_crawler был легко расширяемым модулем, который можно использовать для целевых сайтов. Из-за этого сам course_crawler был довольно простым, и чтобы запустить его, я мог просто прикрепить соответствующий парсер и позволить ему работать свободно:

process.crawl(
   'custom_crawler',
   start_urls=CUSTOM_URLS['courseItem'],
   parser=parse_subjects
)

broad_crawler - это место, где дизайн модульного парсера действительно сиял, позволяя мне динамически назначать парсеры после обработки каждого запроса. Я также настроил очень примитивную фильтрацию при получении ссылок на странице:

def process_request(self, req):
   if 'reddit.com' in req.url:
      req = req.replace(priority=100)      if 'comments' in req.url:
         req = req.replace(callback=self.parse_reddit_post)
      else:
         req = req.replace(callback=self.no_parse)   return req

👓 Парсеры

Различные парсеры были созданы для разных типов страниц и размещены вместе в папке /scraper/spiders/parsers (исходный код). Я сильно полагался на xpath для запроса элементов, которые мне нужны.

Мы также настроили несколько "типов данных", которые представляли наши веб-страницы и то, какие данные мы хотели получить:

class ScrapyGenericPage(scrapy.Item):
   url = scrapy.Field()
   title = scrapy.Field()
   # ...

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

# обычный заголовок
response.xpath("//title/text()")[0]# заголовок OpenGraph
response.xpath('//meta[[@property](http://twitter.com/property)="og:site_name"]')[0]# описание OpenGraph
response.xpath('//meta[[@property](http://twitter.com/property)="og:description"]')[0]

Я думаю, что помимо OpenGraph существуют и другие системы метаданных, которые можно использовать для интересных метаданных, хотя я успел реализовать только одну. Опять же, xpath был моим другом здесь - я использовал инспектор Chrome, чтобы быстро выбрать нужные элементы xpath.

Я мог углубиться немного больше с некоторыми более конкретными сайтами. Например, для постов на Reddit я мог сделать разбор постов зависимым от кармы и получать комментарии выше определенного порога кармы.

post_section = response.xpath('//*[[@id](http://twitter.com/id)="siteTable"]')karma = post_section.xpath(
   '//div/div/div[[@class](http://twitter.com/class)="score unvoted"]/text()'
)[0]if karma == "" or int(karma) < ПОРОГ_КАРМЫ_ПОСТА:
   return

Каждый парсер создает элемент Scrapy (в данном случае ScrapyRedditPost) и заполняет его поля данными, полученными из просмотренной страницы. Возможно, использование Reddit API было бы проще, но я считал, что разбор Reddit как стандартных веб-страниц будет наиболее органичным способом собирать "интересные" ссылки, например любые ссылки, которые могут быть в посте или комментарии. Более того, Reddit API, вероятно, ограничивает скорость запросов.

🏇 Производительность

Производительность очень важна при парсинге веб-страниц для поисковой системы. Чем больше данных у вас в хранилище, тем выше вероятность получить хорошие и интересные результаты. Для нас больше данных также означало больше ссылок, которые были основой того, как мы хотели отображать наши данные на веб-интерфейсе Sleuth.

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

Первое, что я хотел изменить, это приоритет глубины. Поскольку мы начинаем с нескольких исходных URL-адресов (прокрутите вверх по диаграмме для напоминания), я не хотел, чтобы Scrapy тратил все наши системные ресурсы на преследование ссылок с первого исходного URL-адреса, поэтому я уменьшил приоритет глубины, чтобы парсеры могли получить большую "ширину" или результаты из более широкого диапазона источников.

# Обрабатывать запросы с меньшей глубиной в первую очередь
DEPTH_PRIORITY = 50

Я также позволил Scrapy использовать ресурсы моего ноутбука:

# Настроить максимальное количество одновременных запросов, выполняемых Scrapy (по умолчанию: 16)
CONCURRENT_REQUESTS = 100# Увеличить максимальный размер пула потоков для DNS-запросов
REACTOR_THREADPOOL_MAXSIZE = 20

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

# Не соблюдать правила robots.txt - извините!
ROBOTSTXT_OBEY = False# Уменьшить время ожидания загрузки, чтобы быстро отбросить застрявшие запросы
DOWNLOAD_TIMEOUT = 15# Уменьшить уровень журналирования ('DEBUG' для получения подробной информации)
LOG_LEVEL = 'INFO'# Отключить использование cookies (включено по умолчанию)
COOKIES_ENABLED = False

Что касается манеры парсинга (да, похоже, есть манеры парсинга! Scrapy включает сообщение "парсить ответственно" в своих настройках по умолчанию), это довольно грубо. Извините, администраторы сайтов. 😛


🔍 Дополнительное чтение

Я также написал об этом более подробно о упомянутых ранее парсерах здесь, и вы можете прочитать краткое описание того, как работает конвейер здесь. Обязательно прочитайте документацию Scapy.

Вы также можете ознакомиться с репозиториями Sleuth, чтобы узнать больше о этом конкретном проекте! Разработка прекращена, но это был довольно интересный опыт обучения, и в конце мы получили довольно хорошее покрытие кода. 🔥 🎉

ubclaunchpad/sleuth

sleuth - Поисковая система для определенной области

github.com

ubclaunchpad/sleuth-frontend

sleuth-frontend - Фронтенд для приложения sleuth

github.com