Парсинг сайтов
0,0
рейтинг
2 сентября 2011 в 14:37

Разработка → Grab — python библиотека для парсинга сайтов

Лет пять-шесть назад, когда я ещё программировал преимущественно на PHP, я начал использовать библиотеку curl для парсинга сайтов. Мне нужен был инструмент, который позволял эмулировать сессию пользователя на сайте, отсылать заголовки обычного браузера, давать удобный способ отсылки POST-запросов. Сначала я пытался использовать напрямую curl-расширение, но его интерфейс оказался очень неудобным и я написал обёртку с более простым интерфейсом. Время шло, я пересел на python и столкнулся с таким же дубовым API curl-расширения. Пришлось переписать обёртку на python.

Что такое grab?


Это библиотека для парсинга сайтов. Её основные функции:
  • Подготовка сетевого запроса (cookies, http-заголовки, POST/GET данные)
  • Запрос на сервер (возможно через HTTP/SOCKS прокси)
  • Получение ответа сервера и его первоначальная обработка (парсинг заголовков, парсинг cookies, определение кодировки документа, обработка редиректа (поддерживаются даже редирект в meta refresh тэге))
  • Работа с DOM-деревом ответа (если это HTML-документ)
  • Работа с формами (заполнение, автозаполнение)
  • Отладка: логирование процесса в консоль, сетевых запросов и ответов в файлы


Далее я расскажу о каждом пункте более подробно. Для начала поговорим об инициализации рабочего объекта и подготовке сетевого запроса. Приведу пример кода, который запрашивает страницу с яндекса и сохраняет её в файл:
>>> g = Grab(log_file='out.html')
>>> g.go('http://yandex.ru')

На самом деле параметр `log_file` предназначен для отладки — он указывает куда сохранить тело ответа для дальнейшего изучения. Но можно и для скачивания файла его использовать.

Мы увидели как можно отконфигурировать объкт Grab — прямо в конструкторе. А вот ещё варианты того же кода:
>>> g = grab()
>>> g.setup(url='http://yandex.ru', log_file='out.html')
>>> g.request()

или
>>> g = Grab()
>>> g.go('http://yandex.ru', log_file='out.html')


Самый короткий:
>>> Grab(log_file='out.html').go('http://yandex.ru')


Резюмирую: можно задать конфигурацию Grab через конструктор, через метод `setup` или через методы `go` и `request`. В случае метода `go`, запрашиваемый URL можно передать позиционным аргументом, в других случаях нужно передавать его как именованный аргумент. Отличие методов `go` и `request` в том, что `go` требует обязательным первым параметром URL, в то время как request ничего не требует и использует URL, который мы задали ранее.

Помимо опции `log_file`, есть опция `log_dir`, которая невероятно облегчает отладку многошагового парсера.
>>> import logging
>>> from grab import Grab
>>> logging.basicConfig(level=logging.DEBUG)
>>> g = Grab()
>>> g.setup(log_dir='log/grab')
>>> g.go('http://yandex.ru')
DEBUG:grab:[02] GET http://yandex.ru
>>> g.setup(post={'hi': u'Превед, яндекс!'})
>>> g.request()
DEBUG:grab:[03] POST http://yandex.ru


Видите? Каждый запрос получил свой номер. Ответ на каждый запрос был записан в файл /tmp/[номер].html, также был создан /tmp/[номер].log файл, в котором записаны http-заголовки ответа. А что вообще делает вышеприведённый код? Он идёт на главную страницу яндекса. А затем делает бессмысленный POST-запрос на эту же страницу. Обратите внимание, что во втором запросе мы не указываем URL — по-умолчанию используется url предыдущего запроса.

Давайте рассмотрим ещё одну настройку Grab, предназначенную для отладки.
>>> g = Grab()
>>> g.setup(debug=True)
>>> g.go('http://youporn.com')
>>> g.request_headers
{'Accept-Language': 'en-us;q=0.9,en,ru;q=0.3', 'Accept-Encoding': 'gzip', 'Keep-Alive': '300', 'Accept': 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.3', 'User-Agent': 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en; rv:1.9.0.2) Gecko/2008091620 Firefox/3.0.2', 'Accept-Charset': 'utf-8,windows-1251;q=0.7,*;q=0.7', 'Host': 'www.youporn.com'}


Мы сделали запрос к youporn.com. Опция `debug` включает запоминание заголовков исходящих запросов. Если мы в чём-то не уверены, можно посмотреть, что именно мы отослали на сервер. В аттрибуте `request_headers` сохранён словарь с ключами и значениями http-заголовков запроса.

Рассмотрим базовые возможности по составлению запросов.

Методы http-запроса


POST-запрос. Всё довольно просто. Укажите в опции `post` словарь с ключами и значениями. Grab автоматически изменит типа запроса на POST.
>>> g = Grab()
>>> g.setup(post={'act': 'login', 'redirec_url': '', 'captcha': '', 'login': 'root', 'password': '123'})
>>> g.go('http://habrahabr.ru/ajax/auth/')
>>> print g.xpath_text('//error')
Неверный код защиты


GET-запрос. Если явно не были заданы POST-данные или метод запроса, то Grab сгенерирует GET-запрос.

PUT, DELETE, HEAD методы. Теоритически всё будет работать, если вы зададите опцию method='delete', method='put' или method='head'. Практически же я мало работал с этими методами и не уверен в их работоспособности.

Важное замечание о POST-запросах. Grab устроен так, что сохраняет все заданные опции и использует их в следующих запросах. Единственная опция, которую он не сохраняет — это `post` опция. Если бы он сохранял её, то в следущем примере вы бы отправили POST-запрос на второй URL, а это вряд ли то, что вы хотели:
>>> g.setup(post={'login': 'root', 'password': '123'})
>>> g.go('http://example.com/login')
>>> g.go('http://example.com/news/recent')


Настройка http-заголовков


Теперь рассмотрим, как можно настраивать отправляемые http-заголовки. Просто задайте словарик заголовков опцией `headers`. По-умолчанию, Grab генерирует некоторые заголовки, чтобы больше быть похожим на браузер: Accept, Accept-Language, Accept-Charset, Keep-Alive. Их вы также можете менять опцией `headers`:
>>> g = Grab()
>>> g.setup(headers={'Accept-Encoding': ''})
>>> g.go('http://digg.com')
>>> print g.response.headers.get('Content-Encoding')
None
>>> g.setup(headers={'Accept-Encoding': 'gzip'})
>>> g.go('http://digg.com')
>>> print g.response.headers['Content-Encoding']
gzip


Работа с cookies


По-умолчанию, Grab сохраняет полученные cookies и отсылает их в следующем запросе. Вы получаете эмуляцию пользовательских сессий из коробки. Если вам это не нужно, отключите опцию `reuse_cookies`. Вы можете задать cookies вручную опцией `cookies`, она должна содержать словарик, обработка которого аналогична обработке данных, переданных в `post` опции.
>>> g.setup(cookies={'secureid': '234287a68s7df8asd6f'})


Вы можете указать файл, который следует использовать как хранилище cookies, опцией `cookiefile`. Это позволит вам сохранять cookies между запусками программы.

В любой момент вы можете записать cookies Grab объекта в файл методом `dump_cookies` или загрузить из файла методом `load_cookies`. Чтобы очистить cookies Grab объекта используйте метод `clear_cookies`.

User-Agent


По-умолчанию, Grab претворяется настоящим браузером. У него есть список различных User-Agent строк, одна из которых выбирается случайным образом при создании Grab объекта. Конечно, вы можете задать свой User-Agent опцией `user_agent`.
>>> from grab import Grab
>>> g = Grab()
>>> g.go('http://whatsmyuseragent.com/')
>>> g.xpath('//td[contains(./h3/text(), "Your User Agent")]').text_content()
'The Elements of Your User Agent String Are:\nMozilla/5.0\r\nWindows\r\nU\r\nWindows\r\nNT\r\n5.1\r\nen\r\nrv\r\n1.9.0.1\r\nGecko/2008070208\r\nFirefox/3.0.1'
>>> g.setup(user_agent='Porn-Parser')
>>> g.go('http://whatsmyuseragent.com/')
>>> g.xpath('//td[contains(./h3/text(), "Your User Agent")]').text_content()
'The Elements of Your User Agent String Are:\nPorn-Parser'


Работа с прокси-сервером


Всё банально. В опции `proxy` нужно передать адрес прокси в виде «server:port», в опции `proxy_type` передаём её тип: «http», «socks4» или «socks5» Если ваши прокси требуют авторизации, используйте опцию `proxy_userpwd`, значение которой имеет вид «user:password».
Простейший поисковик прокси-серверов на базе Google поиска:
>>> from grab import Grab, GrabError
>>> from urllib import quote
>>> import re
>>> g = Grab()
>>> g.go('http://www.google.ru/search?num=100&q=' + quote('free proxy +":8080"'))
>>> rex = re.compile(r'(?:(?:[-a-z0-9]+\.)+)[a-z0-9]+:\d{2,4}')
>>> for proxy in rex.findall(g.drop_space(g.css_text('body'))):
... g.setup(proxy=proxy, proxy_type='http', connect_timeout=5, timeout=5)
... try:
... g.go('http://google.com')
... except GrabError:
... print proxy, 'FAIL'
... else:
... print proxy, 'OK'
... 
210.158.6.201:8080 FAIL
...
proxy2.com:80 OK
….
210.107.100.251:8080 OK
….


Работа с ответом


Допустим, вы сделали сетевой запрос с помощью Grab. Что дальше? Методы `go` и `request` вернут вам объект Response, который также доступен через аттрибут `response` объекта Grab. Вас могут заинтересовать следующие аттрибуты и методы объекта Response: code, body, headers, url, cookies, charset.
  • code — HTTP-код ответа. Если ответ отличяется от 200-го, никаких ислючений не будет сгенерировано, имейте это в виду.
  • body — это собственно тело ответа, исключая http-заголовки
  • headers — а это заголовки в словарике
  • url — может отличаться от исходного, если был редирект
  • cookies — куки в словарике
  • charset — кодировка документа, ищется в META тэге документа, также в Content-Type http-заголовке ответа и xml-декларации XML-документов.


Grab объект имеет метод `response_unicode_body`, который возвращает тело ответа, преобразованное в unicode, учтите, что HTML entities типа "&" не преобразовывается в уникодовые аналоги.

Response объект последнего запроса всегда хранится в аттрибуте `response` Grab объекта.
>>> g = Grab()
>>> g.go('http://aport.ru')
>>> g.response.code
200
>>> g.response.cookies
{'aportuid': 'AAAAGU5gdfAAABRJAwMFAg=='}
>>> g.response.headers['Set-Cookie']
'aportuid=AAAAGU5gdfAAABRJAwMFAg==; path=/; domain=.aport.ru; expires=Wed, 01-Sep-21 18:21:36 GMT'
>>> g.response.charset
'windows-1251'


Работа с текстом ответа (grab.ext.text расширение)


Метод `search` позволяет установить присутствует ли заданная строка в теле ответа, метод `search_rex` принимает в качестве параметра объект регулярного выражения. Методы `assert_substring` и `assert_rex` генерируют DataNotFound исключение, если аргумент не был найден. Также в этом расширении находятся такие удобные функции как `find_number — ищет первое числовое вхождение, `drop_space` — удаляет любые пробельные символы и `normalize_space` — заменяет последовательности пробелов одним пробелом.
>>> g = Grab()
>>> g.go('http://habrahabr.ru')
>>> g.search(u'Google')
True
>>> g.search(u'яндекс')
False
>>> g.search(u'Яндекс')
False
>>> g.search(u'гугл')
False
>>> g.search(u'Медведев')
True
>>> g.search('Медведев')
Traceback (most recent call last):
File "", line 1, in 
File "grab/ext/text.py", line 37, in search
raise GrabMisuseError('The anchor should be byte string in non-byte mode')
grab.grab.GrabMisuseError: The anchor should be byte string in non-byte mode
>>> g.search('Медведев', byte=True)
True
>>> import re
>>> g.search_rex(re.compile('Google'))
<_sre.SRE_Match object at 0xb6b0a6b0>
>>> g.search_rex(re.compile('Google\s+\w+', re.U))
<_sre.SRE_Match object at 0xb6b0a6e8>
>>> g.search_rex(re.compile('Google\s+\w+', re.U)).group(0‌)
u'Google Chrome'
>>> g.assert_substring('скачать торрент бесплатно')
Traceback (most recent call last):
File "", line 1, in 
File "grab/ext/text.py", line 62, in assert_substring
if not self.search(anchor, byte=byte): 
File "grab/ext/text.py", line 37, in search
raise GrabMisuseError('The anchor should be byte string in non-byte mode')
grab.grab.GrabMisuseError: The anchor should be byte string in non-byte mode
>>> g.assert_substring(u'скачать торрент бесплатно')
Traceback (most recent call last):
File "", line 1, in 
File "grab/ext/text.py", line 63, in assert_substring
raise DataNotFound('Substring not found: %s' % anchor)
grab.grab.DataNotFound
>>> g.drop_spaces('foo bar')
Traceback (most recent call last):
File "", line 1, in 
AttributeError: 'Grab' object has no attribute 'drop_spaces'
>>> g.drop_space('foo bar')
'foobar'
>>> g.normalize_space(' foo \n \t bar')
'foo bar'
>>> g.find_number('12 человек на сундук мертвеца')
'12'


Работа с DOM-деревом (grab.ext.lxml расширение)


Подходим к самому интересному. Благодаря замечательной библиотеке lxml Grab предоставляет вам возможность работать с xpath-выражениями для поиска данных. Если очень кратко: через аттрибут `tree` вам доступно DOM-дерево с ElementTree интерфейсом. Дерево строится с помощью парсера библиотеки lxml. Работать с DOM-деревом можно используя два языка запросов: xpath и css.

Методы работы с xpath:
  • xpath — вернуть первый элемент удовлетворяющий запросу
  • xpath_list — вернуть все элементы xpath_text — вернуть текстовое содержимое элемента (и всех вложенных элементов)
  • xpath_number — вернуть первое числовое вхождение из текста элемента (и всех вложенных элементов)

Если элемент не был найден, то функции `xpath`, `xpath_text` и `xpath_number` сгенеририруют DataNotFound исключение.

Функции `css`, `css_list`, `css_text` и `css_number` работают аналогично, за одним исключением, аргументом должен быть не xpath-путь, а css-селектор.
>>> g = Grab()
>>> g.go('http://habrahabr.ru')
>>> g.xpath('//h2/a[@class="topic"]').get('href')
'http://habrahabr.ru/blogs/qt_software/127555/'
>>> print g.xpath_text('//h2/a[@class="topic"]')
Релиз Qt Creator 2.3.0‌
>>> print g.css_text('h2 a.topic')
Релиз Qt Creator 2.3.0‌
>>> print 'Comments:', g.css_number('.comments .all')
Comments: 5
>>> from urlparse import urlsplit
>>> print ', '.join(urlsplit(x.get('href')).netloc for x in g.css_list('.hentry a') if not 'habrahabr.ru' in x.get('href') and x.get('href').startswith('http:'))
labs.qt.nokia.com, labs.qt.nokia.com, thisismynext.com, www.htc.com, www.htc.com, droider.ru, radikal.ru, www.gosuslugi.ru, bit.ly


Формы (grab.ext.lxml_form расширение)


Когда я реализовал функциональность по автоматическому заполнению форм я был очень рад. Порадуйтесь и вы! Итак, есть методы `set_input` — заполняет поле с указанным именем, `set_input_by_id` — по значению аттрибута id, и `set_input_by_number` — просто по номеру. Эти методы работают с формой, которую можно задать руками, но обычно Grab сам угадывает правильно, с какой формой нужно работать. Если форма одна — всё понятно, а если несколько? Grab возьмёт ту форму, в которой больше всего полей. Чтобы задать форму вручную используйте метод `choose_form`. Методом `submit` можно отправить заполненную форму. Grab сам построит POST/GET запрос для полей, которые мы не заполнили явно (например hidden поля), вычислит action формы и метод запроса. Есть также метод `form_fields` который вернёт в словарике все поля и значения формы.
>>> g.go('http://ya.ru/')
>>> g.set_input('text', u'бесплатное порно')
>>> g.submit()
>>> print ', '.join(x.get('href') for x in g.css_list('.b-serp-url__link'))
http://gigporno.ru/, http://drochinehochu.ru/, http://porno.bllogs.ru/, http://www.pornoflv.net/, http://www.plombir.ru/, http://vuku.ru/, http://www.carol.ru/, http://www.Porno-Mama.ru/, http://kashtanka.com/, http://www.xvidon.ru/


Транспорты


По-умолчанию, Grab использует pycurl для всех сетевых операций. Эта фунциональность реализована тоже в виде расшерения и можно подключить другое транспорт-расширение, например, для запросов через urllib2 библиотеку. Есть только одна проблема, это расширение нужно предварительно написать :) Работы по urllib2 расширению ведутся, но весьма неспешно — меня на 100% устраивает pycurl. Я думаю, pycurl и urllib2 расширения по-возможностям будут аналогичны, за исключением того, что urllib2 не умеет работать с SOCKS-проксями. Все примеры, приведённые в данной статье используют pycurl-транспорт, который включен по-умолчанию.
>>> g = Grab()
>>> g.curl
<pycurl.Curl object at 0x9d4ba04>
>>> g.extensions
[<grab.ext.pycurl.Extension object at 0xb749056c>, <grab.ext.lxml.Extension object at 0xb749046c>, <grab.ext.lxml_form.Extension object at 0xb6de136c>, <grab.ext.django.Extension object at 0xb6a7e0ac>]


Режим молотка (hammer-mode)


Этот режим включен по-умолчанию. Для каждого запроса у Grab есть таймаут. В режиме молотка в случае таймаута Grab не генерирует сразу исключение, а пытается ещё несколько раз сделать запрос с возростающими таймаутами. Этот режим позволяет значительно увеличить стабильность программы т.к. микро-паузы в работе сайтов или разрывы в канале встречаются сплошь и рядом. Для включения режима испльзуйте опцию `hammer_mode`, для настройки количества и длины таймаутов используйте опцию `hammer_timeouts`, в которую должен быть передан список числовых пар: первое число это таймаут на соединение с сокетом сервера, второе число — таймаут на всё время операции, включая получение ответа.
>>> import logging
>>> logging.basicConfig(level=logging.DEBUG)
>>> g = Grab()
>>> g.setup(hammer_mode=True, hammer_timeouts=((1, 1), (2, 2), (30, 30)))
>>> URL = 'http://download.wikimedia.org/enwiki/20110803/enwiki-20110803-stub-articles5.xml.gz'
>>> g.go(URL, method='head')
DEBUG:grab:[01] HEAD http://download.wikimedia.org/enwiki/20110803/enwiki-20110803-stub-articles5.xml.gz
>>> print 'File size: %d Mb' % (int(g.response.headers['Content-Length']) / (1024 * 1024))
File size: 3 Mb
>>> g.go(URL, method='get')
DEBUG:grab:[02] GET http://download.wikimedia.org/enwiki/20110803/enwiki-20110803-stub-articles5.xml.gz
DEBUG:grab:Trying another timeouts. Connect: 2 sec., total: 2 sec.
DEBUG:grab:[03] GET http://download.wikimedia.org/enwiki/20110803/enwiki-20110803-stub-articles5.xml.gz
DEBUG:grab:Trying another timeouts. Connect: 30 sec., total: 30 sec.
DEBUG:grab:[04] GET http://download.wikimedia.org/enwiki/20110803/enwiki-20110803-stub-articles5.xml.gz
>>> print 'Downloaded: %d Mb' % (len(g.response.body) / (1024 * 1024))
Downloaded: 3 Mb


Django-расширение (grab.ext.django)


Да-да. Есть и такое :-) Допустим, у вас есть модель Movie с ImageField-полем `picture`. Вот как можно скачать картинку и сохранить её в объект Movie.
>>> obj = Movie.objects.get(pk=797)
>>> g = Grab()
>>> g.go('http://img.yandex.net/i/www/logo.png')
>>> obj.picture = g.django_file()
>>> obj.save()


Что есть ещё в Grab?


Есть и другие фишки, но я боюсь, что статья слишком большая получится. Главное правило пользователя библиотеки Grab — если что-то непонятно, нужно смотреть в код. Документация пока слабая

Планы развития


Я использую Grab уже много лет, в том числе и в production сайтах, например в агрегаторе, где можно купить купоны на скидку в Москве и других городах. В 2011 году я начал писать тесты и документацию. Возможно напишу функционал для асинхронных запросов на базе multicurl. Также было бы неплохо допилить urllib-транспорт.

Как можно помочь проекту? Просто используйте его, шлите багрепорты и патчи. Также можете заказывать у меня написание парсеров, граберов, скриптов обработки информации. Регулярно пишу подобные вещи с использованием grab.

Официальный репозиторий проекта: bitbucket.org/lorien/grab Библиотеку можно также поставить с pypi.python.org, но в репозитории обычно код свежее.

UPD: В комментариях озвучивают всяческие альтернативы грабу. Решил резюмировать их списочком + кое-что из головы. На самом деле альтернатив этих вагон и маленькая тележка. Думаю, каждый N-ый программист в один прекрасный день решает навелосипедить себе утилитку для сетевых запросов:


UPD2: Пожалуйста, пишите ваше вопросы по библиотеке в google-группу: groups.google.com/group/python-grab/ Другим пользователям grab будет полезно ознакомиться с вопросами и ответами.

UPD3: Актуальная документация содержится по адресу: docs.grablib.org/

UPD4: Актуальный проект сайта: grablib.org

UPD5: Пофиксил примеры исходного кода в статье. После очередного апргрейда хабрахабр по малопонятным для меня причинам не стал исправлять форматирование кода в старых статьях и оно везде поехало. Спасибо Алексею Мазанову за исправления статьи. Ещё он хочет попасть на хабр, если у вас есть инвайт, его майл: egocentrist@me.com
@itforge
карма
71,2
рейтинг 0,0
Парсинг сайтов
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

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

  • +1
    отличная библиотека, уже достаточно давно работаю с ней. сильно упрощает жизнь =)

    раньше был свой велосипед, но после знакомства с grab необходимость в нём отпала.
    есть еще один очень удобный инструмент (фрэймворк) для парсинга — scrapy, где-то на хабре была статья про него. эти 2а инструмента полностью перекрывают все мои рабочие потребности.
  • +13
    >>> g.go('youporn.com')

    Мы сделали запрос к гуглу

    Куда куда, простите? Точно к гуглу? :)
    • +2
      Ага, поправил. Много раз переписывал примеры, запарился :) Сначала написал синтетические примеры, потом решил везде код работы с живыми сайтами показать.
      • +6
        Ох уж мне эти «живые сайты» :)
  • –6
    > Также можете заказывать у меня написание парсеров, граберов, скриптов обработки информации.

    Это вам лучше бы на PHP так как он доступнее (хостинги) и обычно парсеры требуют именно на нем.
    • +5
      Есть множество заказчиков, которые не против python. Говорю по собственному опыту. Даже на одеске регулярно публикуются заказы на парсеры, где люди сами пишут, что нужна система на python или указывают этот язык одним из вариантов. PHP я забыл как страшный сон. Множество версий PHP, несовместимых между собой и сами с собой (на ризличных хостинг-конфигурациях) не доставляли особой радости :) Типичный пример парсера на PHP — веб страница с простым интерфейсом, запускающая большой процесс внутри mod_php и благополучно умирающая по таймату с пустым экраном т.к. логинг ошибок отключен скриптописателем или админом веб-хостинга. А если скрипт всё же смог отработать он вываливает стопицот мегабайт данных прямо в окно браузера :) На питон такие волности допустить трудно т.к. как прикруить к нему веб-интерфейс — ещё знать надо. Поэтому даже у говнокодеров шансы сделать что-то работающее выше. Да и вообще для работы нужен хотя бы ssh-доступ к VPS или шаред-хостингу, этим отсекаются совсем дубовые заказчики, которые сэкономили на хостинг со школьных завтраков.
      • –2
        А как, если не секрет, ушли с PHP? У меня был не очень большой опыт разработки на Python — но пришлось вернутся на PHP. Рынком более востребовано.
        • +12
          Очень просто. Я реально устал от причуд PHP и стал искать язык, куда бы свалить. Был выбор между ruby и python. А Perl я уже знал до этого и он мне не нравился. Я почитал про ruby и увидел в нём всякие фишки, напоминающие perl, меня это не сильно обрадовало и так я выбрал python. Его фича с выделением блока табулированием — я в неё влюбился сразу — это гениально :) Так что выбор был эмоциональным. По поводу рынка я никогда не парился. Python-программисты нужны, например, часто вакансию публикуются о django-вакансиях. В общем веб-кодинг востребован. Человек с реальными знаниями нигде не пропадёт. Даже в таких компаниях как яндекс, рамблер, mail.ru есть вакансии для python-разработчиков.
      • 0
        вы сейчас какую-то чушь написали про php, и про таймаут, и про лог ошибок, и про вывод в окно броузера.
        • +3
          Я написал впечатления, полученные эмпирическим путём. Я сам писал такую чушь, а потом мне в руки попадала такая чушь и это чуши очень много в интернетах каждый день пишется. Вообще, я считаю, что любой программист в первые годы становления себя как профессионала пишет преимущественно чушь. На php взращивается гораздо больше программеров, чем на питоне, отсюда и количество чуши на нём больше. А ещё я выражаю следующую мысль: лёгкость написания веб-интерфейса на php порой оказывает отрицательное воздействие на качество некоторых программ, в частности, парсеров и граберов, которые по природе своей процессы фоновые. Когда их пытаются оформить как часть веб-интерфейса, то получаются различного рода неприятности.
          • –1
            Вы убедили меня лишь в одном. Запятые — наше все!
      • 0
        Заступлюсь за php, хотя тоже собираюсь сваливать с него на python как проект закончу, но щас вот этот проект на php как раз занимается парсингом десятков тысяч страниц ежедневно, и скажу что при определенных совершенно не больших знаниях, php прекрасно работает по 14 часов(конечно может и больше, это у меня столько он работает) в консоли, добавить надо 3-4 строчки.
                ob_implicit_flush(true);
                ob_end_flush();
                set_time_limit(0);
                ini_set('memory_limit', '2048M');
        

        Не уверен в том что первые две грамотные, но они работают, нужны для того чтоб выводить в режиме реального времени информацию.
        Потом задаюм бесконечный таймаут.
        И если надо увеличиваем количество допустимой памяти.
    • 0
      Парсеры на PHP? Мсье знает толк в извращениях
      • 0
        Эх, и старый же пост вы нашли :D

        А вообще это не извращение, а вполне себе рабочий вариант, который к тому востребован. Я могу конечно и на Nodejs написать или на Go но кому это надо кроме меня самого? Красоту кода и его изящность к сожалению большинство заказчиков не оценят.
  • 0
    Как дела обстоят с basic auth и всяческими кастомными хедерами?
    • 0
      Кастомные хедеры шлются опцией `headers` — она описана в статье. Для базик авторизации настройки нету, ни разу не нужна оба была. С любым недостающим функционалом можно работать через `curl` аттрибут — это объект pycurl, который умеет всё на свете :)
    • 0
      Теперь basic auth доступен и через интерфейс Grab: bitbucket.org/lorien/grab/changeset/042fbe0f5797
  • +1
    а чем mechanize не устроил?
    • 0
      Он какой-то нечеловеческий, пару раз пробовал его использовать и бросал. Документации не нашёл толковой. Я сейчас уже плохо помню, давно это было.
      • НЛО прилетело и опубликовало эту надпись здесь
        • +2
          Немного разные акценты у библиотек — скрапи — это реально паук такой, бегает по сети, тянет в тыщу потоков информацию. А grab — это скорее швейцарский нож, вы его берёте и начинает вдумчиво колупать сайт. Асинхронной многопоточности в grab нет, всё что вы можете — это создать несколько tread-объектов и в каждом работать с grab. Но лучше только скачивать, у меня были проблемы с использованием lxml-модуля в нескольких потоках. Т.е. скачиваем в несколько потоков, парсим HTML в одном потоке. В curl есть некий multicurl, дающий эту самую асинхронность, но за несколько лет у меня так и не возникло острой надобности разобраться с ним. Это у меня в планах.
          • +1
            Всё-таки в таком деле, как разработка грабберов, асинхронная модель рулит. Я бы посоветовал приглядеться к scrapy повнимательнее — в мою недолгую бытность фрилансером он меня здорово выручал. А швейцарский нож тут скорее сам питон — всё что можно легко делать в grab, так же просто реализуется в scrapy.
  • +1
    Для работы с cURL могу под питоне еще порекомендовать human_curl (pip install human_curl) или вот репозиторий: github.com/lispython/human_curl

    Использовать можно как:
    >>> import human_curl as requests
    >>> r = requests.post('http://h.wrttn.me/post', files=(('file_1', '/tmp/testfile1.txt'),
    ... ('file2', open('/tmp/testfile2.txt'))), data={'var_name': 'var_value'})
    ...
    >>> r.status_code
    201


    или так, используя базовую авторизацию

    >>> import human_curl as hurl
    >>> # unfortunately hulr.it keep this name :-)
    >>> r = hurl.get('http://h.wrttn.me/basic-auth/test_username/test_password', auth=('test_username', 'test_password'))
    >>> r.status_code
    200
    >>> r.content
    '{"username": "test_username", "password": "test_password", "authenticated": true}'
    


    В свое время видел grap, но именно по той причине, что он занимается процессингом контента страницы не стал его использовать.
    • 0
      На самом деле, по-умолчанию он почти что ничего и не делает со страницей, функции преобразования в уникод и построения DOM-дерева включаютс только когда идёт обращение к методам, отвественным за работу с DOM-деревом. Первоначально grab разрабатывался как обёртка над curl, но затем я невольно наблюдал как акцент смещается на обработку полученной информации. И это круто — очень удобно :) Мне.
    • 0
      Супер! А куки можно словарем передавать?
      • +1
        Можно словарем, можно CookieJar объект.
  • –1
    Расскажите пожалуйста, какое отличие этой библиотеки от Beautiful Soup? Поддержка сетевых функций?
    • +2
      Процитирую сам себя:
      Это библиотека для парсинга сайтов. Её основные функции:
      1) Подготовка сетевого запроса (cookies, http-заголовки, POST/GET данные)
      2) Запрос на сервер (возможно через HTTP/SOCKS прокси)
      3) Получение ответа сервера и его первоначальная обработка (парсинг заголовков, парсинг cookies, определение кодировки документа, обработка редиректа (поддерживаются даже редирект в meta refresh тэге))
      4) Работа с DOM-деревом ответа (если это HTML-документ)
      5) Работа с формами (заполнение, автозаполнение)
      6) Отладка: логирование процесса в консоль, сетевых запросов и ответов в файлы


      BeautifulSoup — это только четвёртый пункт из представленного выше списка. Я давно отказался от BeautifulSoup — он тормозной, менее стабильный чем lxml и не поддерживает xpath и множества других вещей из модуля lxml.html, которые так скрашивают жизнь.
    • 0
      lxml + xpath + firebug (copy xpath) в сто тысяч раз лучше и проще Beautiful soup.
      • +1
        Не юзайте copy xpath файрбага… Он ооочень неоптимальный и зачастую нерабочий XPath запрос выдает.
  • +3
    Есть также замечательная библиотека requests с более приятным API, на мой взгляд.
    • +1
      Grab и requests разные библиотеки для разных целей. Библиотека requests относится к первому и второму пунктам в вышеприведённом списке и частично к третьему пункту.
  • 0
    Боже, как раз уже месяц занимаюсь этим BeautifulSoup, lxml, mechanize, scrapy… будем пробовать теперь и это
  • 0
    на редкость удачно библиотека получилась, реализация логики программы с помощью граба по большей части чисто описательный процесс: зайди на этот сайт, заполни форму, нажми сабмит, посмотри есть ли на странице вот эта строка, дай-ка теперь мне вон то значение из таблицы и т.д.

    меньше действий (и кода) — меньше вероятность ошибки, + встроенная система ведения логов позволяет контролировать результат каждого запроса.
  • +1
    • –1
      >>> g = Grab()
      >>> g.setup(post={'act': 'login', 'redirec_url': '', 'captcha': '', 'login': 'root', 'password': '123'})
      >>> g.go('habrahabr.ru/ajax/auth/')

      хотел сказать, что такой интерфейс для отправки POST запроса мне кажется бесчеловечным. в мире TDD есть лучшие примеры, реализующие ту же функциональность. прямо мм… как некоторые шоткаты на хабре)
      • +1
        Простите, не понял ничего, что вы написали. Приведите явные примеры человечного интерфейса для задания POST-данных.
  • +1
    А почему в примере User-Agent не изменился? Так и должно быть?
    • +1
      Вы правильно подметили. Спасибо. Поправил, пример. User-Agent меняется, конечно.
  • +1
    Никак не могу найти про треды?

    Скачать 1 раз 1 сайт — это как говорится много ума не нужно.

    У меня по 600к запросов в день идет, Ваша либка потянет?

    Использую свой велик, но он уже стал феррари ) ( использует pycurl + треды)
    • +1
      600k запросов это 6 запросов в секунду или например 6 тредов параллельных по 1 запросу в секунду. Не вижу ничего необычного. Потянет или нет, зависит от вашего железа и настроек ОС. Вам есть смысл переходить на grab только если вам больше нравится его API, скорости он вряд ли вам прибавит. Чем больше одновременных соединений вам нужно, тем больше следует думать о том, чтобы перейти на асинхронную модель работы с сетью. Я лично с асинхронной нагрузкой практически не работал т.к. не было задач таких, поэтому в граби и отсутсвует интерфейс к multicurl.
  • –1
    Сори за оффтопик:
    есть постоянные вакансии для Pyhton(Django) программистов в офисе в Минске, на хороших з-п (до 3к). Если интересно пишите — skype tankgen
    • 0
      Тут попробуйте: pyjob.ru pep8.ru/job
      • 0
        Спасибо, попробую. Уже везде искали, но что-то пока тихо, вот на Хабре может кто заметит, в сообветствующий раздел вакансию уже так же закинули.
  • 0
    Клиентские https сертификаты SSL/TLS умеет? Было бы очень полезно.
  • 0
    Объясните, пожалуйста, как можно отключить опцию request_cookies.
    • 0
      Что за опция такая? Вы имеете в виду, как запретить грабу запоминать куки?
      Вот так:

      g.setup(reuse_cookies=False)
      • 0
        Да, именно. Спасибо!
        Спросил про request_cookies, потому что в тексте сказано: "Вы получаете эмуляцию пользовательских сессий из коробки. Если вам это не нужно, отключите опцию `request_cookies`".
  • 0
    Я использую Grab и Tor. В тот момент, когда начинается бан, я меняю identity в Tor. Cookie отключены. Тем не менее, бан не снимается. Однако, если перезапустить скрипт, то бан снимается. С чем это может быть связано?
    • 0
      Проверяйте, что ip действительно меняется. Провярйте, что cookie-действительно не передаются (смотрите grab.request_headers) Вопросы по грабу лучше в майл-лист писать, ссылка в конце статьи дана.
  • 0
    Кто-нибудь grabил сайты на GWT?

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