company_banner

Node.js, Tor, Puppeteer и Cheerio: анонимный веб-скрапинг

Автор оригинала: George Gkasdrogkas
  • Перевод
Веб-скрапинг — это метод сбора данных с веб-сайтов. Этот термин обычно используется в применении к автоматизированному сбору данных. Сегодня мы поговорим о том, как собирать данные с сайтов анонимно. Причина, по которой некто может захотеть анонимности в деле веб-скрапинга, заключается в том, что многие веб-серверы применяют определённые правила к подключениям с IP-адресов, с которых за некий отрезок времени выполнено какое-то количество запросов. Здесь мы будем пользоваться следующими инструментами:

  • Puppeteer — для доступа к веб-страницам.
  • Cheerio — для парсинга HTML-кода.
  • Tor — для выполнения каждого запроса с различного IP-адреса.

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



Установка Tor


Начнём с начала — поэтому первым делом установим клиент Tor, воспользовавшись следующей командой:

sudo apt-get install tor

Настройка Tor


Теперь настроим клиент Tor. В конфигурации Tor, применяемой по умолчанию, используется порт SOCKS, который даёт нам один путь к единственному выходному узлу (то есть — один IP-адрес). Для повседневного использования Tor, вроде обычного просмотра веб-страниц, это вполне подходит. Но нам нужно несколько IP-адресов. Это позволит переключаться между ними в процессе веб-скрапинга.

Для того чтобы настроить Tor так, как нам нужно, мы просто откроем дополнительные порты для прослушивания SOCKS-соединений. Делается это путём добавления нескольких записей SocksPort в главный конфигурационный файл программы, который можно найти по адресу /etc/tor.

Откроем файл /etc/tor/torrc с помощью какого-нибудь текстового редактора и добавим в конец файла следующие записи:

# Откроем 4 SOCKS-порта, каждый из которых даёт нам новый Tor-маршрут.

SocksPort 9050
SocksPort 9052
SocksPort 9053
SocksPort 9054

Тут стоит обратить внимание на следующее:

  • Значение каждого параметра SocksPort является числом. Число — это номер порта, по которому клиент ожидает поступления соединения от приложений, использующих протокол SOCKS, вроде браузеров.
  • Так как значение SocksPort задаёт порт, который будет открыт, порт с соответствующим номером должен быть свободным, а не используемым каким-нибудь другим процессом.
  • Номера портов начинаются с 9050 — номера порта, используемого Tor-клиентом по умолчанию.
  • Мы пропустили номер порта 9051. Этот порт используется Tor для того, чтобы позволить внешним приложениям, подключающимся по данному порту, управлять процессом Tor.
  • Выбирая номера портов после 9051, мы пользуемся простым соглашением, в соответствии с которым номера портов увеличиваются на 1.

Для того чтобы применить изменения, внесённые в конфигурационный файл, перезапустим клиент Tor:

sudo /etc/init.d/tor restart

Создание нового проекта Node.js


Создадим новую директорию для проекта. Назовём её superWebScraping:

mkdir superWebScraping

Перейдём в эту директорию и инициализируем пустой Node-проект:

cd superWebScraping && npm init -y

Установим необходимые зависимости:

npm i --save puppeteer cheerio

Работа с веб-сайтами с помощью Puppeteer


Puppeteer — это браузер без пользовательского интерфейса, который использует протокол DevTools для взаимодействия с Chrome или Chromium. Причина, по которой мы не используем тут библиотеку для работы с запросами, вроде tor-request, заключается в том, что подобная библиотека не сможет обрабатывать сайты, созданные в виде одностраничных веб-приложений, содержимое которых загружается динамически.

Создадим файл index.js и поместим в него следующий код. Основные особенности этого кода описаны в комментариях.

/**
 * Подключим библиотеку puppeteer.
 */
const puppeteer = require('puppeteer');

/**
 * В функции main размещаем код, который
 * будет использован в ходе веб-скрапинга.
 * Причина, по который мы создаём асинхронную функцию,
 * заключаемся в том, что мы хотим воспользоваться асинхронными
 * возможностями puppeteer.
 */
async function main() {
  /**
   * Запускаем Chromium. Установив ключ `headless` в значение false,
   * мы можем видеть интерфейс браузера.
   */
  const browser = await puppeteer.launch({
    headless: false
  });

  /**
   * Создаём новую страницу.
   */
  const page = await browser.newPage();

  /**
   * Используя новую страницу, переходим на https://api.ipify.org.
   */
  await page.goto('https://api.ipify.org');

  /**
   * Ждём 3 секунды и закрываем экземпляр браузера.
   */
  setTimeout(() => {
    browser.close();
  }, 3000);
}

/**
 * Запускаем скрипт, вызвав main().
 */
main();

Запустим скрипт следующей командой:

node index.js

После этого на экране должно появиться окно браузера Chromium, в котором открыт адрес https://api.ipify.org.


Окно браузера, Tor-подключение не используется

Я открыл в окне браузера именно https://api.ipify.org из-за того, что эта страница может показать общедоступный IP-адрес, с которого к ней обращаются. Это — тот адрес, который виден посещаемым мной сайтам в том случае, если я захожу на них без использования Tor.

Изменим вышеописанный код, добавив следующий ключ в объект с параметрами, который передаётся puppeteer.launch:

  /**
   * Запускаем Chromium. Установив ключ `headless` значение false,
   * мы можем видеть интерфейс браузера.
   */
  const browser = await puppeteer.launch({
  headless: false,
  
  // Добавим следующую строку.
  args: ['--proxy-server=socks5://127.0.0.1:9050']
});

Мы передали браузеру аргумент --proxy-server. Значение этого аргумента сообщает браузеру о том, что он должен использовать socks5-прокси сервер, работающий на нашем компьютере и доступный на порте 9050. Номер порта является одним из тех номеров, которые мы до этого внесли в файл torrc.

Снова запустим скрипт:

node index.js

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


Окно браузера, Tor-подключение используется

В моём случае в данном окне появился адрес 144.217.7.33. У вас это может быть какой-нибудь другой адрес. Обратите внимание на то, что если вы снова запустите скрипт и используете тот же номер порта (9050), то вы получите тот же IP-адрес, что получали до этого.


Окно повторно запущенного браузера, Tor-подключение используется

Именно поэтому в настройках Tor мы открыли несколько портов. Попробуйте подключить браузер к другому порту. Это приведёт к изменению IP-адреса.

Сбор данных с помощью Cheerio


Теперь, когда в нашем распоряжении имеется удобный механизм загрузки страниц, пришло время заняться веб-скрапингом. Для этого мы собираемся воспользоваться библиотекой cheerio. Это — HTML-парсер, API которого устроено так же, как API jQuery. Наша задача заключается в том, чтобы взять со страницы Hacker News 5 заголовков самых свежих постов.

Перейдём на сайт Hacker News.


Сайт Hacker News

Мы хотим взять с открытой страницы 5 свежих заголовков (сейчас это «HAKMEM (1972)», «Larry Roberts has died» и другие). Исследуя заголовок статьи с помощью инструментов разработчика браузера, я заметил, что каждый заголовок помещён в HTML-элемент <a> с классом storylink.


Исследование структуры документа

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

  • Запуск нового экземпляра браузера без пользовательского интерфейса, подключённого к Tor-прокси.
  • Создание новой страницы.
  • Переход по адресу https://news.ycombinator.com/.
  • Получение HTML-содержимого страницы.
  • Загрузка HTML-содержимого страницы в cheerio.
  • Создание массива для сохранения заголовков статей.
  • Получение доступа к элементам с классом storylink.
  • Получение первых 5 элементов с помощью метода cheerio slice().
  • Обход полученных элементов с использованием метода cheerio each().
  • Запись каждого найденного заголовка в массив.

Вот код, реализующий эти действия:

const puppeteer = require('puppeteer');

/**
 * Подключим библиотеку cheerio.
 */
const cheerio = require('cheerio');

async function main() {
  const browser = await puppeteer.launch({
    /**
     * Применим стандартный режим без пользовательского интерфейса (окно браузера не видно).
     */
    headless: true,
    args: ['--proxy-server=socks5://127.0.0.1:9050']
  });

  const page = await browser.newPage();

  await page.goto('https://news.ycombinator.com/');

  /**
   * Получим содержимое страницы в виде HTML-кода.
   */
  const content = await page.content();

  /**
   * Загрузим код в cheerio.
   */
  const $ = cheerio.load(content);


  /**
   * Создадим массив для хранения заголовков статей.
   */
  const titles = [];

  /**
   * Работа с элементами, имеющими класс `storylink`.
   * Метод slice() используется для доступа только к 5 первым таким элементам.
   * Перебираем их с помощью метода each().
   */
  $('.storylink').slice(0, 5).each((idx, elem) => {
    /**
     * Получаем внутренний HTML-код, соответствующий тексту заголовка.
     */
    const title = $(elem).text();
  
    /**
     * Помещаем заголовок в массив.
     */
    titles.push(title);
  })

  browser.close();
  
  /**
   * Выводим массив заголовков в консоль.
   */
  console.log(titles);
}

main();

Вот что произойдёт после запуска этого скрипта.


Первые 5 заголовков с Hacker News успешно извлечены из кода страницы

Непрерывный скрапинг с использованием разных IP-адресов


Теперь поговорим о том, как воспользоваться различными SOCKS-портами, которые мы задали в файле torrc. Это довольно просто. Мы объявим массив, в каждом из элементов которого будет содержаться номер порта. Затем переименуем функцию main() в функцию scrape() и объявим новую функцию main(), которая будет вызывать функцию scrape(), передавая ей при каждом вызове новый номер порта.

Вот готовый код:

const puppeteer = require('puppeteer');
const cheerio = require('cheerio');

async function scrape(port) {
  const browser = await puppeteer.launch({
    args: ['--proxy-server=socks5://127.0.0.1:' + port]
  });

  const page = await browser.newPage();
  await page.goto('https://news.ycombinator.com/');
  const content = await page.content();

  const $ = cheerio.load(content);

  const titles = [];

  $('.storylink').slice(0, 5).each((idx, elem) => {
    const title = $(elem).text();
    titles.push(title);
  });

  browser.close();
  return titles;
}

async function main() {
  /**
   * Номера SOCKS-портов Tor, заданные в файле torrc. 
   */
  const ports = [
    '9050',
    '9052',
    '9053',
    '9054'
  ];
  
  /**
   * Вечный веб-скрапинг...
   */
  while (true) {
    for (const port of ports) {
      /**
       * ...каждый раз - с новым номером порта.
       */
      console.log(await scrape(port));
    }
  }
}

main();

Итоги


Теперь в вашем распоряжении есть инструменты, которые позволяют заниматься анонимным веб-скрапингом.

Уважаемые читатели! Приходилось ли вам заниматься веб-скрапингом? Если да — просим рассказать о том, какие инструменты вы для этого используете.

RUVDS.com
VDS/VPS-хостинг. Скидка 10% по коду HABR

Комментарии 16

    +1

    Почему бы вместо cheerio не инжектить на страницу внутри puppeteer jQuery и не тащить сразу финальные данные оттуда?

      0
      Можно ещё проще: после
      await page.goto('https://news.ycombinator.com/');
      пишем
      const titles = await page.$$eval('.storylink', links => links.slice(0, 5).map(link => link.textContent));

      и получаем то, что нужно без jQuery и Cheerio.
      0
      Мне кажется, чтобы взять достаточно данных там, где этого не хотят и фильтруютд по адресу, 4 порта никак не хватит
        0
        В чем разница между прямым запросом на страницу, и получение её HTML в response, и открытии этой страницы в браузере, с последующим копированием HTML?
          0
          Я как бы не спец в этом, но ваша страница может быть динамической и подгружаться на лету. Вы можете её этим способом всю получить. А так, пришлось бы получать её из каких-нибудь веб-сервисов или по частям.
            +1
            Тут дело в том, что при попытке получить HTML-код страницы, которая является SPA (Single Page Application), вы, скорее всего, в получите в ответ страницу, в теле которой будет только что-то вроде такого:
            <script src="app.js"></script>
            


            А если брать содержимое страницы из браузера – то получим уже нормальный HTML.
            В статье, кстати, про это говорится
              0
              Так это означает что это приложение общается по API и весь вопрос в том расковыривать ли вам API, или оно так сложно что легче парсить html
            0
            Здравствуйте!

            Спасибо за статью. Я по скрапингу не очень, поэтому появился вопрос. Если описываемым способом заходить на такой сайт на JS: 1xstavka.ru/en/live/Basketball, произойдет рендеринг html, чтобы, например, сохранить его как файл на диске?
              +1
                const content = await page.content();
              И в этой переменной как раз и будет уже готовый HTML из браузера.
            +1
            Сталкивался с похожей проблемой, но Tor оказался слишком медленным — нужно было скрейпить миллионы запросов.

            Возможная альтернатива Тору — покупка VPN (есть позволяющие до 8 одновременных коннектов) и запуск массива Docker контейнеров, подключающихся по OpenVPN и заворачивающих трафик в SOCKS.

            Также, в большинстве случаев headless browser может быть избыточен, сессии в библиотеке requests на питоне (уверен, есть похожая библиотека для JS) + подмена User-Agent и Referrer почти всегда решают задачу. Конечно, часто на настройку этого требуется больше времени, однако и производительность существенно выше.
              +1

              Tor не только очень медленный, но ещё и IP адреса выходных нодов часто бывают забанены на сайте или закрыты какой-нибудь гугл капчой.

                0
                А у вас не возникало ситуации, когда надо получить выборку релевантную местоположению? Если не использовать headless browser, есть какой-то способ тот же гугл убедить, что ты находишься в конкретном городе?
                +1

                Хмм, раз у сайта есть API, то почему бы его не юзать напрямую, минуя при этом headless браузер?

                  0

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

                    0

                    Бывает ключик генерится на клиенте обфусцированным алгоритмом слегка

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

                  Самое читаемое