Pull to refresh

Comments 55

Вообще ни одной ссылки нет. Хоть бы на сам фреймворк ссылку дали.
Вам приз за внимательность. Поправил.
Мне приходилось сталкиваться с сайтами где нужна «хитрая» авторизация, когда после входа, средствами js дергается third party сайт, который собственно и финализирует авторизацию.

Стояла задача автоматического забора информации с подобного ресурса — доходило до абсурда в виде сборки webkit'а, по концовке все делалось в .net winforms с их IE based webbrowser'ом который такое умеет.

Так вот там в .net 4.5 асинхронность очень забавно делается — в самом простом случае просто дописывается async к определению метода и все.
UFO just landed and posted this here
UFO just landed and posted this here
Там я немного про другую вещь писал, про Grab — это синхронный API к pycurl. А это статья про асинхронный spider, который как раз и является аналогом scrapy — решает абсолютно те же задачи.
Потому что я всегда любил велосипеды. Было интересно попробовать свой собственный асинхронный велосипед. Кроме всего прочего скрапи меня немного пугает свою монструозностью. В моём же модуле spider всего 700 строчек, которые я сам писал. Месяц назад сознательно попытался написать один парсер на скрапи, но его багафича отбила всякое желание делать что-то дальше: groups.google.com/group/scrapy-users/browse_thread/thread/5092eb5cc8695f2a/aa36618c58c539ec Но это всё мелочи жизни. Просто у меня есть время и желание писать свои решения, — я их пишу. А ну и ещё — синхронный интерфейс Grab я уже пишу/использую много лет, поэтому хотелось как-то заюзать эти наработки. Очень здорово получилось, что получилось натянуть старый интерфейс Grab на асинхронность.

А ещё scrapy не поддерживает socks-прокси. Впрочем grab:spider — тоже, в debian stable устаревшая версия curl-библиотеки, в которой есть баг — multicurl не работает с socks. В более новых версиях curl этот баг пофиксили.
но его багафича отбила всякое желание делать что-то дальше: groups.google.com/group/scrapy-users/browse_thread/thread/5092eb5cc8695f2a/aa36618c58c539ec

Хохохо… Ругаться что
hxs.select('//div[@class="thumb"]').extract()[0]
медленнее чем
hxs.select('//div[@class="thumb" and position() = 1]').extract()[0]
все равно что ругаться на то, что
db.query("SELECT * FROM my_huge_table")[0]
медленнее чем
db.query("SELECT * FROM my_huge_table LIMIT 1")[0]
Я объясню. Мне нужно было пробежаться по всем этим элементам, не просто достать первый такой элемент, а перебрать их все и у каждого взять несколько свойств. Если вы попробуете это сделать через lxml/xpath, то никаких тормозов не будет, а scrapy делает что-то больно хитрое, такое хитрое, что оно в 200 раз медленнее работает. Судя по всему он каждую ноду из результата заворачивает в какой-то свой класс — это и даёт феерическое замедление.
Если вам не сложно, приведите, пожалуйста, минимальные примеры кода для lxml и для Scrapy HtmlXPathSelector, которые решают вашу задачу и которые сильно различаются по скорости… Тот кусок, который вы запостили в тикет, как выяснилось, не до конца отражает вашу задачу (вам нужна итерация, а в тикете забирается только первый элемент).

Хоть я со Scrapy сам использую lxml вместо ихнего HtmlXPathSelector, но стало очень интересно что-же там такое медленное.
Попробую завтра, башка не варит уже сегодня.
Так, с примером проблема. Я не знаю, как в scrapy без создания целого парсера просто получить HtmlXPathSelector от какого-либо содерижмого, он хочет какой-то response-объект, если вы мне подскажите, как его сделать, я напишу пример.
pyquery предоставляет возможность писать селекторы в виде jquery-селекторов, не путайте их с css-селекторами.Это разные вещи, хоть и похожи. Кроме того, я писал выше, что lxml не поддерживает сложные css-селекторы. Мне сейчас проверять лень.
Да, чё-то глючит меня. Я тест написал.
# -*- coding: utf-8 -*- 
import time 
from scrapy.selector import HtmlXPathSelector 
import urllib
from lxml.html import fromstring

data = urllib.urlopen('http://tubesexclips.com/').read()

start = time.time() 
hxs = HtmlXPathSelector(text=data)
scrapy_results = set()
for elem in hxs.select('//div[@class="added-download"]/a'):
    href, text = elem.select('@href').extract()[0], elem.select('text()').extract()[0]
    scrapy_results.add((href, text))
print 'HtmlXpathSelector: %.2f' % (time.time() - start) 

start = time.time() 
tree = fromstring(data)
lxml_results = set()
for elem in tree.xpath('//div[@class="added-download"]/a'):
    href, text = elem.xpath('@href')[0], elem.xpath('text()')[0]
    lxml_results.add((href, text))
print 'lxml: %.2f' % (time.time() - start) 

print 'Equal: %s' % (scrapy_results == lxml_results)


Показана реальная ситуация, я очень часто итерируюсь по xpath выборке и применяю дополнительные xpath-выражения к каждому элементу выборки.

У меня такие результаты:
lorien@athlon:/web/barn$ python speed3.py
HtmlXpathSelector: 0.75
lxml: 0.02
Equal: True
Да, подтверждаю. У меня такого же порядка получились результаты. Даже если в качестве бэкенда к HtmlXPathSelector использовать lxml

from scrapy.conf import settings
settings['SELECTORS_BACKEND'] == 'lxml'


Спасибо! Появится время — поищу в чем причина.
Собственно, пофиксил github.com/scrapy/scrapy/pull/79
Не знаю как скоро рассмотрят, но после фикса скорость стала одинаковой примерно.
о, уже приняли pull-request. Благодарю за содействие в улучшении вашего конкурента))
А как тут с обработкой ошибок? Например, сетевых.

Я в моем случае с использованием Queue и потоков при неудаче (даже если hammer mode не отработал) кладу задание обратно в очередь и делаю Н попыток скачки/парсинга.

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

А тут как предлагается обрабатывать подобные ситуации?
Задания с сетевые ошибками (в том числе c кодами > 400 and != 404) по-умолчанию засылаются обратно в очередь:

* bitbucket.org/lorien/grab/src/7000b67ff7ad/grab/spider/base.py#cl-520

Количество попыток можно настраивать. Когда количество попыток для задания истекает, то просто логируется ошибка:

* bitbucket.org/lorien/grab/src/7000b67ff7ad/grab/spider/base.py#cl-395

Можно в принципе добависть фичку, чтобы вызывался callback какой-нить настраиваемый в таком случае. Можно будет в этом callback взять и опять запихать задание в очередь, чтобы ещё раз повторить цикл попыток.

> Потом в случае чего выдаю ошибку. Промежуточные структуры данных сохраняю в сериализованном виде на диск, чтобы потом если что можно было перезапуститься с ключиком --continue
Я обычно настраиваю парсер так, чтобы он работал без ошибок и потом запускаю главный парсинг.

Я сейчас эксперементирую с кэшем. Записываю в монго скачанные документы. Это поведение можно так включить: bot = SomeSpider(use_cache=True, cache_db='some-db-name') Кэшируются только GET-запросы. Столкнулся с проблемой, что не влезает всё на винчестер, ну нету у меня винчестера на 2 терабайта. Тогда я придумал на лету применять gzip-компрессию. Включается опцией use_cache_compression=True. Документы сжимаются в 10 раз — это очень круто. Но нужно время CPU на компрессию-декомпрессию — это не круто :( Я пытался через multiprocessing организовать обработку на множестве ядер, вроде как есть ускорение в два раза на 4-ядерном атлоне. Но это пока нихрена не оттестировано даже на эмпирическом уровне. Короче пока работа с кэшем довольно медленная, я вообще пока плохо понимаю что делаю — все эти асинхронности и mongodb — новые для меня области :) Как минимум, работа с кешем очень упрощает отладку парсера на начальном этапе.

Конец brain dump'а :)
Уже заюзал кеширование, мне нравится :) Еще раз спасибо
Кстати, что дает distributed_mode? Я попробовал запускаться с ним, так вся память выжралась и все зависло.

Еще фичреквест — хорошо бы было добавить handler для SIGINT, чтобы можно было правильно прервать цикл парсинга нажав CTRL+C. Я так в своих скриптах обычно делаю.
Ага, интересная идея. У меня там кстати щас есть хэндлер на SIGUSR1 — пишется статистика счётчиков в /tmp/spider.log или куда-то туда… Топорно конечно, но вот как бы думаю в этом направлении.
Так, возник такой вопрос. Допустим, у меня есть task_initial, который скачивает страницу. На этой странице есть куча картинок, которые я тоже должен выкачать. Парсю страницу, генерю новые Task'и, в обработчике task_image сохраняю картинку куда нужно, генерю миниатюру и т.д. Внимание вопрос — как мне вернуться к обработке страницы после того как обработались все картинки и сгенерить например HTML'ину которая включает в себя результат работы Task'ов картинок?
Гыгы, без понятия :) Можно сделать два спайдера и запускать второй после того как первый скачал все картинки. Ну или можно как-то извращаться и в task_images и определять что это последняя картинка и запускать нужный обработчик. Да, не помешала бы какая-то встроенная фича для такой задачи (генерация сигнала после обработки помеченных тасков), но пока ничего такого нет.
Начали познавать «прелести» асинхронного программирования?))
def task_initial(...):
    #... parse page ...
    self.pages[url] = common_data
    self.counters[url] = len(images_list)
    for img in images_list:
        yield Task('save_img', grab=grab, shared_data={'page_url': url})

def task_save_img(...):
    # ... store image, generate thumb ...
    self.counters[page_url] -= 1
    if self.counters[page_url] == 0:
        self.generate_my_page(self.pages[page_url])

Ну или если какой-либо аналог Scrapy 'meta' (shared_data) не поддерживается в Grab, то дописать свой костыльчик
Я уже ничо не соображаю. Сейчас спать пойду. Но могу сказать, чего есть в Task объектах: можно передавать сколько угодно именованных параметров и они потом будут доступны как аттрибуты task объекта, который передаётся в обработчик третьим параметром. Наверное это аналог meta из scrapy.
distributed mode — это мои эксперименты с выполнением task-обработчиков на нескольких ядрах процессора с помощью multiprocessing, но это всё нереально сырое, лучше эту опцию пока не трогать.
А вообще круто, попробую в одном проекте. Как раз и mongo там тоже планируется.
Автор, если Вы рассматривали BeautifulSoup, можете написать, чем «парсящая» часть вашей библиотеки лучше его? Может ли Ваша библиотека обрабатывать «не совсем корректный» html?
О, любитель ковычек, вы забыли слово лучше в кавычки заключить :)

Я использовал BeatifulSoup до тех пор, пока не созрел до lxml. Это библиотека написанная на C. Она полностью поддерживает поиск через xpath-выражения, а также понимает битый html. Grab использует lxml для парсинга, но вообще ничего не мешает вам в том же Grab:Spider работать с BeautifulSoup.

Плюсы BeautifulSoup:
* pure python
* умещается в одном файле
* допускаю, что новичку проще вникнуть в работу с BS, чем с lxml

Минусы BeautifulSoup
* отвратительно медленный
* возможно жрёт память, мне щас лень писать тесты для памяти
* меня лично напрягает писать два длинных camel case имени в строке импорта: «from BeautifulSoup import BeautifulSoup»

Для скорости мне тесты писать не лень. Запустите у себя на компьютере и ужаснитесь:

from lxml.html import fromstring
from BeautifulSoup import BeautifulSoup
import time
import urllib

data = urllib.urlopen('http://habrahabr.ru').read()

start = time.time()
for x in xrange(10):
    tree = BeautifulSoup(data)
    print tree.find('title').text
print 'BeautifulSoup: %.2f' % (time.time() - start)

start = time.time()
for x in xrange(10):
    tree = fromstring(data)
    print tree.xpath('//title')[0].text
print 'lxml: %.2f' % (time.time() - start)


Если вы ещё не используете lxml для парсинга, то мне вас жаль :)
Опять-же, в lxml поддерживается православный XPath (или CSS-селекторы для тех кому лень XPath изучить), в то время как в BeautifulSoup для выборки какой-то свой уникальный костыль)
А лично я предпочитаю pyquery, голый lxml не осилил почему-то :)
А там можно задать условия, например, на текст внутри элемента?

//div/strong[contains(text(), «Google»)]
Хм, вроде как нет. У меня такой необходимости, по крайней мере, не возникало :)
Бывают сайты с табличной вёрсткой, где множество вложенных тэгов table, tr, td. Там особо не к чему привязаться кроме поясняющего текста внутри нужной ячейки
Из документации:

>>> d('p').filter(lambda i: PyQuery(this).text() == 'Hi')
[]
Ясно. Ну такой подход ожидаемо тормозит по сравнению с lxml. Тормозит чуть ли не в 10 раз: dumpz.org/122917/ Думаю тормоза из-за того, что каждый раз вызывается функция и каждый раз в ней стрится дополнительный PyQuery-объект.

А вообще, раз мы про pyquery заговорили, я подумал — самое время — и замержился с pyquery-форком. В общем, теперь в грабе через аттрибут `pyquery` доступно PyQuery-дерево.
Эти все новшества на bitbucket искать, или на pypi тож вже есть?
не, pyquery пока тока в репозитории
в lxml же есть csselect (http://lxml.de/cssselect.html) — зачем pyquery нужен, так и не понял)
А там, по-моему, обрезанный какой-то css, я как-то пробовал :nth-child(x) — у меня не заработало. А pyquery, наверное, всё корректно транслирует в xpath.
Так я-ж написал что lxml CSS селекторы тоже умеет lxml.de/cssselect.html )))
Или у pyquery они какие-то особые?

Кстати, по-секрету, lxml транслирует CSS селекторы в XPath и потом просто выполняет полученный XPath запрос

>>> from lxml.cssselect import CSSSelector
>>> sel = CSSSelector('div.content')
>>> sel.path
"descendant-or-self::div[contains(concat(' ', normalize-space(@class), ' '), ' content ')]"
Хм, проблемка у меня одна возникла.

Пишу спайдер. В методе обработчике генерю еще пачку методов, которые делают POST на другой сайт. Они попадают в очередь, я вижу в консоли сообщение POST… и… всё. Дальше выполнение приостанавливается. log_dir никакой информации не дает. Примерный код: dumpz.org/120523/
Ой, не на тот уровень ответил
Да и нашел где косяк — в update'е словаря
больше спать надо :)
Ага. Но у меня почему-то без явного задания method не работало.

Тут еще на одни грабли наступил. У меня шлются POST'ы в форму Google Translate. В однопоточном режиме всегда (99.9%) отрабатывалось корректно. В ассинхронном же регулярно вылазят страницы, которые не содержат перевод. Такое ощущение, что при post'е передаются не все данные и где-то по сети что-то обрезается. Я думаю, что собака порыта в pycurl, потому что с Н-ной попытки как правило такие запросы отрабатывают нормально (я просто проверяю при парсинге, при нобходимости снова делаю yield.

Сейчас я под Windows 7 32 bit вынужден работать, не знаю как в Linux. Но довольно интересная картина.
Хм, даже не знаю, у меня POST-запросы без проблем работают в linux. Напишу попозже тесты на множественные POST-запросы, посмотрим.

Весь трафик кстати можно логировать как с обычным Grab.

bot = SomeSpider(...)
bot.setup_grab(log_dir='/path/to/dir')
Кстати, мы можем делать yield из не-task функции? У меня почему-то не заработало.
А почему оно должно было заработать? Yield же должен кто-то ловить и обрабатывать, ну вот его и ловит код, который task-функцию вызывает. Если хотите добавить из другого места task, то можете использовать просто self.add_task(Task(..)). А yield это просто для удобства и красоты.
Sign up to leave a comment.

Articles