Добрый день, дорогие друзья.

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

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

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

Собственно, к делу. Недавно вышел Headless Chrome. Это означает, что теперь мы аж использовать хром, как кравлер (но это неточно). Однако, я не нашел нормальных утилит для использования его в качестве кравлера. Нашел только chrome-remote-interface из списка сторонних клиентов (остальные были крайне скучными и совсем непонятными на первый взгляд).

Пробежавшись по документации и готовому проекту, и убедившись что никто толком не реализовал клиент под 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.

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

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

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

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

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

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


  1. mizhgun
    16.05.2017 15:41
    +4

    Поздравляю, вам удалось достаточно близко подобраться к изобретению Splash.


    1. mizhgun
      16.05.2017 15:48
      +4

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


      1. mitinsvyat
        16.05.2017 16:00
        +3

        Суть в том, что я ничего особо не изобрел.
        За меня все сделал Гугл, добавив headless режим в хром.
        Я взял последнюю апишку хрома: https://chromedevtools.github.io/devtools-protocol/
        Рано или поздно найдется умелец, который это нормально сделает все нативно через Mojo, и я почти уверен что все на это пересядут.
        Ну а про то, что мне не нравится в селениуме с фантомом я же написал.


        1. mizhgun
          16.05.2017 16:24
          +2

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


          1. mitinsvyat
            16.05.2017 16:51
            +1

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


            1. joann
              16.05.2017 22:10

              С каких пор вебкит «разработка Гугла»? вроде всегда была Apple, тока Blink Гугалавская замена вебкита.


              1. mitinsvyat
                16.05.2017 22:44
                +1

                Мне кажется, что не только Apple разрабатывало WebKit.
                https://trac.webkit.org/wiki/Companies%20and%20Organizations%20that%20have%20contributed%20to%20WebKit


  1. medvoodoo
    16.05.2017 16:55
    +3

    обидели мой любимый силениум и питончик:

    import selenium.webdriver 
    
    driver = selenium.webdriver.Firefox()
    driver.get("http://www.google.com")
    driver.get_cookies() # получаем куки
    

    WebDriverWait — используется для гибкого задание времени ожидания загрузки страниц


    1. mitinsvyat
      16.05.2017 16:56

      Я точно не помню, но там разве не для текущего домена (и урла соответственно)?


      1. mitinsvyat
        16.05.2017 16:57

        Суть в том, что в этом же примере тебе пришлось перейти на гугл, чтобы получить куки.
        А через апи и не надо как бы.


        1. mitinsvyat
          16.05.2017 16:59

          О, еще нельзя HTTPonly куки тырить


  1. LenionNoinel
    16.05.2017 19:03
    +1

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


    1. mitinsvyat
      16.05.2017 19:22

      упс, вам коммент ниже


  1. mitinsvyat
    16.05.2017 19:21

    Такс.
    Как понял:

    • вы использовали какой-то фетчер, который что-то прогружал
    • вы использовали IE как движок для джаваскрипта (результат его выполнения вы отправляли себе куда-то)

    Так?

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

    Вот


    1. LenionNoinel
      16.05.2017 19:27

      Да, но там тоже прогружался полноценный IE, а потом прога просто забирала DOM в переменную. Другое дело, что медленновато было.


  1. DaneSoul
    16.05.2017 19:52

    А Scrapy не рассматривали?
    Как раз web crawler на Python, более того, весьма известный и востребованный на рынке парсинга сайтов.


    1. mitinsvyat
      16.05.2017 20:01

      Конечно, смотрел.
      Собственно Scrapy используется для того, чтобы собирать однотипные данные (это не точно, но похоже на истину).
      Ну и там джаваскрипта таки нету. В Scrapnghub (это их продукт) используют Splash, чтобы рендерить Javascript.
      Т.е. Scrapy это немного другое.
      Я думаю, что возможно внедрение хрома в тот же Scrapy по аналогии с Splash.
      Мне же желательно сделать программу, которая будет бегать по сайтику, тыкать некоторые кнопки и еще что-то там.
      Т.е. ближайшими аналогами для меня были: PhantomJS, Splash, Selenium все же.


  1. inwady
    16.05.2017 20:12

    Кстати, если нужно делать js на странице, то TamperMonkey может помочь чем-нибудь. Тут речь идет об исполнении js-кода на странице по фильтру.

    Проект открытый, поэтому может написать расширение для перехвата fetch или подобного функционала? WebWorkers с этим вполне могут работать. Уверен, что Chrome на это способен.


    1. mitinsvyat
      16.05.2017 20:37

      Как понял, вы хотите расширение использующее Service Workers для перехвата трафика (и подмены там).
      Вообще, идея имеет место для жизни, но я все же бы это делал через Mojo, наверное (но это не точно).


  1. veveve
    16.05.2017 21:30
    +1

    PyQt, а именно QtWebEngine, наверное, и есть тот самый headless Chrome с готовым интерфейсом для Питона, нет?


    1. mitinsvyat
      16.05.2017 21:43

      Ну я его не использовал. Можешь расписать?


      1. veveve
        16.05.2017 23:05

        Взяли хромиум и сделали к нему апи для плюсов (чтобы загружать урлы, работать с html, управлять куками и т.п.). PyQt, соответственно, предоставляет возможность делать все это на питоне. Phantomjs, кстати, написан как раз поверх qt (только там управление js-ом и менее гибко чем при ручной работе с webengine).

        Легче примеры погуглить. Вот, в частности, один старенький для получения скриншота страницы https://webscraping.com/blog/Webpage-screenshots-with-webkit/


        1. mitinsvyat
          16.05.2017 23:31

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

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


    1. gearbox
      16.05.2017 22:00

      нет. embedded chromium и headless chrome — это перпендикулярные вещи.


      1. veveve
        16.05.2017 23:08

        headless — это что? Браузер, только без окна и отрисовки страниц и, соответственно, накладных расходов с этим связанных. Webengine, на сколько помню, позволяет это.


        1. gearbox
          16.05.2017 23:30

          Да, но тем не менее хром и хромиум — две разные вещи.
          Phantomjs прекращает разработку как раз ввиду выхода хрома с headless mode.
          headless chrome

          В хромиум тоже впилили, так что для этих целей лишняя прослойка в виде QT вряд ли нужна:
          headless chromium
          Хотя не уверен, может там интерфейс поинтереснее, не знаю. Я из мира ноды, у нас все сразу по людски делается.


          1. alekciy
            17.05.2017 09:08

            Phantomjs прекращает разработку как раз ввиду выхода хрома с headless mode.

            А webdriver впили и на сколько адекватно?


            1. gearbox
              17.05.2017 15:21

              так webdriver для хрома был (chromedriver), headless — всего лишь режим работы, с чего бы webdriver-у пропадать?
              running selenium with headless chrome


  1. alek0585
    17.05.2017 13:25

    Особенно порадовала подкачка

    Автоматическая подкачка последней версии протокола


    1. mitinsvyat
      17.05.2017 13:46

      Ну сейчас я автоматически забираю протокол с localhost:9222/json/protocol


  1. marni
    17.05.2017 13:47

    Интересная статья. Но есть вопрос к автору: ваше решение сможет обойти Distil? И ко всем: как вы решаете проблему с Distil? Спасибо


    1. mitinsvyat
      17.05.2017 13:54

      Такс. Distil я не использовал, и не знаю тамошние методы защиты. Мне кажется, это должно обходить примерно на том же уровне, что и селениум. Думаю, если напишешь поведение в браузере похоже на человеческое, то мож и не запалят.
      Однако, кто-то должен это протестировать.


      1. marni
        17.05.2017 14:22

        В зависимости от сайта, Distil палит селениум в лутшем случее через 30-50 запросов, в худшем через 1. Надо будет протестировать ваш вариант на досуге.


        1. mitinsvyat
          17.05.2017 15:57

          Ну надо повтыкать, что за трюки используются им, чтобы детектить

          P.S. Замажь дефолтный User-Agent. Он там палевный :)


        1. mitinsvyat
          17.05.2017 19:01

          P.S. Если что-то не так у меня или непонятно — пиши, потому что я очень часто меняю что-то в проекте сейчас и он не сильно стабилен.


  1. bfcmyxa
    17.05.2017 13:55

    Ребята, подскажите пожалуйста, на чем сейчас проще всего написать бота, который будет парсить странчику и кликать когда спарсил то что ему надо? Естественно с поддерживанием хттп-запросов и чтобы подгружал javascript. Я посмотрел сплэш, но к моему сожалению я совершенно не знаю пайтон. Есть ли что-то подобное на js или php?


    1. mitinsvyat
      17.05.2017 14:08

      Но Splash же lua использует, вроде.
      Ну используй PhantomJS или это (если хочешь попробовать использовать решение, которое в статье).
      Если честно, я бы на твоем месте использовал PhantomJS, если что-то несложное или какой-нибудь кравлер на ноде.


      1. bfcmyxa
        17.05.2017 17:07

        Спасибо за ответ, думаю самое подходящее для меня будет разобраться в PhantomJS.


  1. selenite
    17.05.2017 15:36

    У меня есть таск дёргать из dev tools хрома время и график загрузки страницы… я понимаю, что к этому окну можно получить доступ через эмуляцию f12, либо через иньекцию js в само окно браузера, но вот что-то ни один найденнй метод не работает. Идеи?


    1. mitinsvyat
      17.05.2017 16:07

      Стопе. В том, что я написал, не эмуляция F12, там апишки, с помощью которых, ты можешь проделать все тоже самое, чтобы ты мог делать через F12.
      Ну и ты там можешь все это проследить.
      Говорю, запусти хром с ключом --remote-debugging-port=9222, и подключись к хрому по адресу localhost:9222, потом открой вкладку и открой инспектор.

      Получится такая веселая чепуха:
      image


  1. admmelit
    19.05.2017 01:10

    Всем добрым людям привет!
    Подскажите пжл тулзу, которая позволила бы парсить вэб страницу, но есть небольшое условие, но эти тулзы не должны требовать программирование?


    1. mitinsvyat
      19.05.2017 01:11

      Может Portia?


  1. admmelit
    19.05.2017 12:10

    ОК. Попробую.