Скрапинг бесконечно прокручивающейся страницы


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



В эру одностраничных приложений и тонн AJAX-запросов на одной странице множество веб-сайтов заменили кнопку навигации "вперёд/назад" на причудливый механизм бесконечной прокрутки страницы. Веб-сайты использующие этот механизм загружают новую сущность каждый раз, когда пользователь достигает конца страницы при вертикальной прокрутке(вспомните Twitter, Facebook, Google Images). Даже несмотря на то, что UX-эксперты утверждают что механизм бесконечной прокрутки предоставляет чрезмерное количество данных для пользователей, мы видим увеличивающееся количество веб-страниц прибегающих к предоставлению бесконечного списка результатов.


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


Хотя такой сценарий и может показаться классическим случаем для таких JavaScript-фреймворков как Splash или Selenium, это, действительно, легко внедрить. Всё что вам нужно сделать вместо имитации взаимодействия с пользователем через один из тех фреймворков — это исследовать AJAX-запросы вашего браузера при прокрутке страницы и затем воссоздать эти запросы в вашем пауке в Scrapy.


Давайте используем Spidy Quotes в качестве примера и построим паука, который получит все элементы перечисленные на странице.


Инспектирование страницы


Первым делом нам надо понять как работает бесконечная прокрутка на странице. И мы можем сделать это используя панель Network в инструментах разработчика в браузере. Откроем панель и прокрутим страницу, чтобы увидеть запросы отправляемые браузером:

Нажмём на запрос, чтобы рассмотреть его подробнее. Как мы можем видеть, браузер посылает запрос на /api/quotes?page=x и получает в ответе JSON-объект подобный этому:


{
   "has_next":true,
   "page":8,
   "quotes":[
      {
         "author":{
            "goodreads_link":"/author/show/1244.Mark_Twain",
            "name":"Mark Twain"
         },
         "tags":["individuality", "majority", "minority", "wisdom"],
         "text":"Whenever you find yourself on the side of the ..."
      },
      {
         "author":{
            "goodreads_link":"/author/show/1244.Mark_Twain",
            "name":"Mark Twain"
         },
         "tags":["books", "contentment", "friends"],
         "text":"Good friends, good books, and a sleepy ..."
      }
   ],
   "tag":null,
   "top_ten_tags":[["love", 49], ["inspirational", 43], ...]
}

Это информация которая нужна нам для нашего паука. Всё что нужно сделать в нём — это генерировать запросы на /api/quotes?page=x увеличивая x пока значение поля has_next не станет false. Самое прекрасное в этом — что нам даже не надо скрапить содержимое HTML, чтобы получить данные которые нам нужны. Это всё содержится в красивом машиночитаемом формате JSON.


Построение паука


Вот наш паук. Он извлекает целевые данные из содержимого в JSON-формате, которое мы получили в ответе от сервера. Такой подход проще и более надёжен, чем копаться в HTML дереве страницы, надеясь что изменение разметки не сломает наших пауков.


import json
import scrapy

class SpidyQuotesSpider(scrapy.Spider):
    name = 'spidyquotes'
    quotes_base_url = 'http://spidyquotes.herokuapp.com/api/quotes?page=%s'
    start_urls = [quotes_base_url % 1]
    download_delay = 1.5

    def parse(self, response):
        data = json.loads(response.body)
        for item in data.get('quotes', []):
            yield {
                'text': item.get('text'),
                'author': item.get('author', {}).get('name'),
                'tags': item.get('tags'),
            }
        if data['has_next']:
            next_page = data['page'] + 1
            yield scrapy.Request(self.quotes_base_url % next_page)

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


Заключение


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

Поделиться с друзьями
-->

Комментарии (2)


  1. ivaaaan
    16.10.2016 22:52

    del


  1. hudson
    17.10.2016 14:31

    Я так и делаю — некоторый «реверс-инжиниринг» страниц, которые надо парсить (порой логика бывает посложнее, чем описано в статье, но ничего невозможного пока не встречал) — после воссоздание запросов в пауке и краулинг по ним.

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

    Планирую внедрять scrapy-splash — не поделитесь кто чем пользуется в продакшене?