Асинхронный HTTP клиент (crawler)

Discussion in 'С/С++, C#, Rust, Swift, Go, Java, Perl, Ruby' started by Gar|k, 28 Oct 2015.

  1. Gar|k

    Gar|k Moderator

    Joined:
    20 Mar 2009
    Messages:
    1,166
    Likes Received:
    266
    Reputations:
    82
    На этом форуме ОЧЕНЬ часто задают вопросы по поводу разработки HTTP или TCP клиентов и БОЛЬШИНСТВО новичков считает, что работа с сокетами так же проста, как чтение и запись в файл, но это далеко не так.

    Естественно я не собираюсь описывать все нюансы сетевого программирования (я и сам их не все знаю), все описано в умных книгах, так что RTFM:
    • "Эффективное программирование TCP/IP" Йон Снейдер
    • "UNIX. Разработка сетевых приложений" У.Ричард Стивенс
    Это по сетевому, а это по системному:
    • "UNIX. Профессиональное программирование" У.Ричард Стивенс
    • "Windows. Создание эффективных Win32-приложений с учётом специфики 64-разрядной версии Windows" Рихтер Дж.
    Поведаю я вам ребятки о блокирующих операциях четния/записи (I/O) и неблокирующих.

    Да будет вам известно, что операция чтения read/recv/WSARecv, записи write/send/WSASend, connect,
    accept и т.д. являются блокирующими (синхронными), то есть после вызова этой функции работа программы приостанавливается до ее завершения. А сеть это далеко не идеальная среда, например пока ты скачиваешь гигабайты порнографии с компьютера соседа, твой кот может опрокинуть вазу на роутер, техник перерезать провода в подвале, а сосед выдернуть коннектор из компьютера и твоя гениальная какарская программа зависнет ожидая завершения функции recv.
    Естественно придумали способ опроса соединения, например опция сокета SO_KEEPALIVE, НО все равно это дает задержку, а насколько я помню в UNIX timeout этого keepalive задается для всей системы.

    Все эти тормоза при опросе в принципе не так критичны ЕСЛИ ты пишешь клиента, который поддерживает одно соединение, например ты можешь создать 2 потока - 1 на чтение, 2 на отправку и синхронизировать их между собой (читай Рихтера или Стивенса, там все разжевано). Но часто требуется чтобы клиент поддерживал сразу несколько соединений и вот тут начинается самое интересное.

    Что ВЫ делаете, ВЫ пишете чекеры-*уекеры в 100500 потоков и у кого потоков заявлено больше, тот значит и круче, но это наборот ТОРМОЗИТ выполнение программы, читайте как устроена ОС, читайте что такое "переключение контекста", теоретически ПАРРАЛЕЛЬНО может выполняться N потоков, где N - это количество ядер в процессоре, да и то Windows и Linux не операционные системы реального времени (гуглите что это значит). Кроме того что вы убиваете производительность кучей потоков, никто не учитывает вышеописанные тормоза блокирующих операций в итоге ваш поток висит, а вы ожидаете его завершения.

    Ладно, отошел я от темы. В общем давным давно и за нас умные ребята придумали не блокирующий ввод-ввыод, это значит, что вызов вышеперечисленных функций не будет блокировать выполнение основной программы, а по завершении операции система вас об этом уведомит. Я не буду описывать, как это сделать, всё давно описано RTFM (список выше). Суть в том что существуют различные API асинхронного ввода-вывода select, poll, epoll, kqueue, IOCP (гуглите), можете выбирать, тот который вам нравится и писать, но опять таки умные ребята собрали эти API за нас в удобные библиотеки libevent, libev, libuv...

    На мой взгляд простой для понимания, но не идельной по бенчмаркам является библиотека libevent, это такой комбайн в котором есть и HTTP сервер и DNS килент и прочие ништяки вроде кроссплатформенности и поддержки разных механизмов асинхронного ввода-вывода. libev переработанная libevent, в ней увеличена производительность и убраны "лишние" фишки, в общем что хотите то и используйте.

    https://github.com/Garik-/crawler - собственно проект HTTP клиента с использованием библиотеки libevent, файл easy.c специально для вас.
    Написание бенчмарка в процессе, ни какой оптимизации у меня в мыслях не было, результаты такие

    Code:
    Asynchronous requests: 10000; timeout connect: 3;
    Checked: 20914; errors: 12587 (60%); found: 2; time: 246200 milliseconds
    
    Asynchronous requests: 1000; timeout connect: 3;
    Checked: 10522; errors: 2697 (25%); found: 3; time: 201726 milliseconds
    При этом нагрузка на CPU в пике работы 7% (AMD Athlon(tm) 64 X2 Dual Core Processor 4200+), а памяти жрет ~150 мегабайт (на вход подается 300 мегабайтный csv).

    Оптимизация работы программы это вообще отдельная тема, кто-то для всех переменных программы использует структуру указателей и выделяет 1 блок памяти под свои переменные и данные программы, кто-то пишет свои менеджеры памяти, кто-то пишет свои препроцессоры, лично я стараюсь по максимуму использовать возможности системы, юзаю MMF, а для буфферов неизвестной длинны под Windows временные файлы (читай Рихетра узнаешь почему).

    НЕ ПАРСИТЕ HTML РЕГУЛЯРНЫМИ ВЫРАЖЕНИЯМИ!!!
    HTML это не просто текст, это "язык" гипертекстовой разметки, используйте DOM парсеры (гуглите gumbo-parser).

    Эх, хотел просто ссылку выложить на github, а получился крик души.
     
    _________________________
    #1 Gar|k, 28 Oct 2015
    Last edited: 28 Oct 2015
    Chrome~, GRRRL Power and makag like this.
  2. Kaimi

    Kaimi Well-Known Member

    Joined:
    23 Aug 2007
    Messages:
    1,761
    Likes Received:
    818
    Reputations:
    230
    Пишу "чекеры-*уекеры в 100500 потоков" и парсю "HTML РЕГУЛЯРНЫМИ ВЫРАЖЕНИЯМИ". Спроси меня почему.
     
    _________________________
  3. Fepsis

    Fepsis Elder - Старейшина

    Joined:
    17 Sep 2008
    Messages:
    803
    Likes Received:
    391
    Reputations:
    72
    Ну рассказывай уже )
     
  4. Kaimi

    Kaimi Well-Known Member

    Joined:
    23 Aug 2007
    Messages:
    1,761
    Likes Received:
    818
    Reputations:
    230
    Потеря актуальности продукта через месяц, а то и меньше (независимо от того, чем обрабатывается хтмл), простота и скорость разработки, короче софт, который пишется, например, за 1-2 часа за сумму в 10-30$ не стоит того, чтобы заводить под него репозиторий, писать автотесты, придерживаться какой-то конкретной конвенции именования переменных и прочее, прочее.
    Короче, плохо переносить и использовать плохие практики в хороших продуктах с длительным процессом разработки и поддержки, а использовать хорошие практики в плохих продуктах - обычно не выгодно.
     
    _________________________
    Chrome~ and waik like this.
  5. waik

    waik Elder - Старейшина

    Joined:
    2 Nov 2008
    Messages:
    413
    Likes Received:
    163
    Reputations:
    12
    Надо эту мысль себе в блокнотик записать)
     
  6. Gar|k

    Gar|k Moderator

    Joined:
    20 Mar 2009
    Messages:
    1,166
    Likes Received:
    266
    Reputations:
    82
    Естественно все зависит от задачи, мне была поставлена задача просканировать рунет, а это где-то 3.000.000 доменов.
    Но надо вбивать правильные мысли в подрастающие поколение.

    Запомни НУБ! (это я не Kaimi, а массе) Если ты знаешь суть, как и почему, ты сможешь быстрее и эффективнее выбрать инструмент для решения поставленной задачи. Читай книги, пиши велосипеды и активней участвуй в жизни класса, девочки не любят стеснительных, расширяй кругозор, прочти Рихтера и поясни этой шкуре, для чего нужен аргумент lpOverlapped в функции WSASend!
     
    _________________________
    #6 Gar|k, 29 Oct 2015
    Last edited: 29 Oct 2015
  7. Fepsis

    Fepsis Elder - Старейшина

    Joined:
    17 Sep 2008
    Messages:
    803
    Likes Received:
    391
    Reputations:
    72
    Gar|k, все-таки твоя позиция относительно парсинга html регулярками не совсем ясна. Зачем тащить в проект либу для работы с DOM, если регулярки, как правило, на уровне языка поддерживаются? А к смене структуры html оба варианта не устойчивы.
     
  8. Gar|k

    Gar|k Moderator

    Joined:
    20 Mar 2009
    Messages:
    1,166
    Likes Received:
    266
    Reputations:
    82
    2 Fepsis http://stackoverflow.com/questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags

    А вообще допустим ты новичок и тебе поступила задача пройтись по ссылкам меню.
    HTML:
    <div class="nav">
    
    <a href="#" class="nav__item">123</a>
    </div>
    Code:
    <div class="nav">\n<a href="(.*?)" class="nav__item">.*?<\/a>
    Потом они взяли и поменяли код вот так, а ты поумнел и сделал так
    HTML:
    <div class="nav">
    <a class="nav__item" href="#">123</a>
    </div>
    Code:
    <div class="nav">\n<a.*?href="(.*?)".*?>.*?<\/a>
    Потом они взяли и вставили баннер в блок меню и узнали о HTML5
    HTML:
    <aside class="nav">
    <img src="" class="banner__img" alt="">
    <a class="nav__item" href="#">123</a>
    </aside>
    
    Code:
    <.*?class="nav".*?>.*?<a.*?href="(.*?)".*?>.*?<\/a>
    А потом они вдруг узнали о атрибуте data и сделали вот так
    HTML:
    <aside class="nav">
    <img src="" class="banner__img" alt="">
    <a class="nav__item" data-href="lol" href="#">123</a>
    </aside>
    
    И теперь предыдущая верная регулярка возвращает - lol. Они так же могут использовать одинарные ковычки ' вместо "...

    Ладно так можно продолжать пока мне не надоест придумывать примеры или тебе регулярки.
    Допустим ты знаешь JavaScript используешь высокоскоростной движок Vanilla.js. Как бы ты решил поставленную задачу:

    Code:
    var links = document.querySelectorAll(".nav .nav__item");
    for(var i = 0; i < links.length; i++) {
        console.log(links[i].getAttribute("href"));
    }
    
    ^ и все, и тебе не надо бороться с front-end ером до тех пор пока они не поменяют DOM дерево (вложенности самих элементов).
     
    _________________________
  9. GRRRL Power

    GRRRL Power Moderator

    Joined:
    13 Jul 2010
    Messages:
    823
    Likes Received:
    185
    Reputations:
    84
    Под все четыре варианта, кстати, пишется одна регулярка :)
     
    _________________________
  10. Gar|k

    Gar|k Moderator

    Joined:
    20 Mar 2009
    Messages:
    1,166
    Likes Received:
    266
    Reputations:
    82
    И что же ты ее не написал? ) Ладно, это выдуманный пример.

    Но вот что странно если бы ты программируя на PHP/Python/Ruby/C++/C#/Java/Objective-C и т.д. парсил бы XML регулярками, senior developer бегал бы за тобой по офису с молотком, что бы пальцы тебе отбить :)
     
    _________________________
  11. int

    int Member

    Joined:
    18 May 2011
    Messages:
    80
    Likes Received:
    10
    Reputations:
    6
    Сколько доменов удается обработать в ~ секунду?
    Парсю иногда большинство доменных зон (python curl multi). В среднем удается за неделю всё распарсить на дешевеньком vps. Но вот у меня упирается всё в CPU.
    По поводу DOM - как он будет себя вести на невалидном html?
     
  12. Gar|k

    Gar|k Moderator

    Joined:
    20 Mar 2009
    Messages:
    1,166
    Likes Received:
    266
    Reputations:
    82
    2 int, все зависит от железа на котором запускать и скорости исходящих соединений. До бенчмарка руки пока не дошли, времени нет. Изначально при разработке я тестировал на валидных доменах, забвивал 5-7 штук и он успевал их обработать за секунду. Выше вон я публиковал результаты за 4 минуты при одновременных 10 000 запросов с таймаутом 3 секунды было обработано 20 914 доменов. 20914/246 ~ 85 доменов в секунду, но и ошибок было 60% получается 20914-12587/246 ~ 34 домена в секунду.

    нормальный DOM парасер на не валидном HTML вернет ошибку, я так думаю. Под невалидным я понимаю нечто типа "<table><p>123</br>"
     
    _________________________
    #12 Gar|k, 30 Oct 2015
    Last edited: 30 Oct 2015
  13. Chrome~

    Chrome~ Elder - Старейшина

    Joined:
    13 Dec 2008
    Messages:
    939
    Likes Received:
    162
    Reputations:
    27
    К счастью, все наоборот. Нормальный DOM парсер не боится невалидного HTML. Иначе все эти DOM парсеры были бы неэффективны.

    Но немного странные результаты в первом посте:
    10000 запросов за раз и большое количество ошибок. Сеть не вытягивает такой нагрузки?

    Также нагрузка на CPU может серьезно расти, когда что то делать с результатом (передавать в тот же DOM парсер, например). И здесь уже не играет особой роли, асинхронный ли парсер или многопоточный.
     
    #13 Chrome~, 30 Oct 2015
    Last edited: 30 Oct 2015
  14. GRRRL Power

    GRRRL Power Moderator

    Joined:
    13 Jul 2010
    Messages:
    823
    Likes Received:
    185
    Reputations:
    84
    У XML заранее известная схема, которая может быть сразу десериализована в структуры данных. А здесь речь идет о парсинге чужого HTML'а, который может меняться как угодно и когда угодно без предупреждения.
     
    _________________________
  15. Gar|k

    Gar|k Moderator

    Joined:
    20 Mar 2009
    Messages:
    1,166
    Likes Received:
    266
    Reputations:
    82
    2 GRRRL Power, эээм http://htmlbook.ru/html/!doctype, как бы... У HTML есть спецификации DTD (document type definition), при желании можно и свои теги придумывать, шаг влево шаг в право расстрел. В моем понимании DOM парсер это лексический анализатор, который на выходе тебе дает уже готовое дерево с сущностями и ему как бы по барабану какая структура у документа. <[ТЭГ][атрибут="значение"]+> разобрал сложил в структуру и если тэг по DTD парный, то создал ветку.

    В общем надо заканчивать флуд, мне удобнее работать с DOM деревом, чем с текстом, позицию свою я объяснил, а написать аналог querySelector и querySelectorAll используя уже готовую библиотеку от Google gumbo, не трудно.
     
    _________________________
Loading...