Парсинг сайтов python 3: подробный видеокурс и программный код
подробный видеокурс и программный код
В видеокурсе из семи уроков описывается парсинг сайтов с различной структурой при помощи Python третьей версии, библиотек requests и BeautifulSoup.
В этом видеокурсе Олег Молчанов подробно, не торопясь, рассказывает про парсинг сайтов при помощи Python 3. Раскрываются особенности парсинга многостраничных ресурсов, использования прокси с различными User-Agent, сохранения изображений и распознавания простого текста, а также быстрый мультипроцессорный парсинг сайтов.
В ряде случаев предлагаемые автором программные решения несколько устарели из-за изменения структуры страниц, подвергаемых парсингу. Автор курса не преследует цели создать идеальный парсер, а лишь излагает определенные концепции и иллюстрирует их примерами. Для облегчения вашей работы, мы привели исходные коды программ, набранные нами во время прохождения курса, с некоторыми поправками.
В уроке рассматривается мультипроцессорный парсинг на примере сайта CoinMarketCap.com. Сначала приводится пример однопоточного парсинга. Затем рассматривается, как модифицировать программу для реализации мультипроцессорного подхода при помощи библиотеки multiprocessing. Сравниваются временные интервалы, необходимые для парсинга в один и несколько потоков. Попутно рассказывается об экспорте данных в csv-файл.
Обратите внимание: для установки BeautifulSoup для Python 3 в видео указана неправильная команда. Правильный вариант: pip install beautifulsoup4 (либо для систем с двумя версиями Python: pip3 install beautifulsoup4).
Программный код при однопоточном парсинге:
import csv from datetime import datetime import requests from bs4 import BeautifulSoup def get_html(url): response = requests.get(url) return response.text def get_all_links(html): soup = BeautifulSoup(html, 'lxml') tds = soup.find('table',).find_all('td', class_='currency-name') links = [] for td in tds: a = td.find('a', class_='currency-name-container').get('href') link = 'https://coinmarketcap.com' + a links.append(link) return links def text_before_word(text, word): line = text.split(word)[0].strip() return line def get_page_data(html): soup = BeautifulSoup(html, 'lxml') try: name = text_before_word(soup.find('title').text, 'price') except: name = '' try: price = text_before_word(soup.find('div', class_='col-xs-6 col-sm-8 col-md-4 text-left').text, 'USD') except: price = '' data = {'name': name, 'price': price} return data def write_csv(i, data): with open('coinmarketcap.csv', 'a') as f: writer = csv.writer(f) writer.writerow((data['name'], data['price'])) print(i, data['name'], 'parsed') def main(): start = datetime.now() url = 'https://coinmarketcap.com/all/views/all' all_links = get_all_links(get_html(url)) for i, link in enumerate(all_links): html = get_html(link) data = get_page_data(html) write_csv(i, data) end = datetime.now() total = end - start print(str(total)) a = input() if __name__ == '__main__': main()
Программный код при использовании мультипроцессорного парсинга с 40 процессами:
from multiprocessing import Pool import csv from datetime import datetime import requests from bs4 import BeautifulSoup def get_html(url): response = requests.get(url) return response.text def get_all_links(html): soup = BeautifulSoup(html, 'lxml') tds = soup.find('table',).find_all('td', class_='currency-name') links = [] for td in tds: a = td.find('a', class_='currency-name-container').get('href') link = 'https://coinmarketcap.com' + a links.append(link) return links def text_before_word(text, word): line = text.split(word)[0].strip() return line def get_page_data(html): soup = BeautifulSoup(html, 'lxml') try: name = text_before_word(soup.find('title').text, 'price') except: name = '' try: price = text_before_word(soup.find('div', class_='col-xs-6 col-sm-8 col-md-4 text-left').text, 'USD') except: price = '' data = {'name': name, 'price': price} return data def write_csv(data): with open('coinmarketcap.csv', 'a') as f: writer = csv.writer(f) writer.writerow((data['name'], data['price'])) print(data['name'], 'parsed') def make_all(link): html = get_html(link) data = get_page_data(html) write_csv(data) def main(): start = datetime.now() url = 'https://coinmarketcap.com/all/views/all' all_links = get_all_links(get_html(url)) with Pool(40) as p: p.map(make_all, all_links) end = datetime.now() total = end - start print(str(total)) a = input() if __name__ == '__main__': main()
Характерные времена могут разниться в зависимости от оборудования и текущего состояния сайта.
Во втором уроке рассматривается извлечение информации с многостраничного сайта по типу Avito.ru. В качестве примера берется задача поиска телефона по названию фирмы изготовителя с выводом названия товара, цены, ближайшей станции метро и ссылки объявления. Показывается простейший способ фильтрации нерелевантных блоков.
Обратите внимание: при многостраничном парсинге таких сайтов, как Avito.ru, ваш IP может быть временно забанен на совершение парсинга.
import requests from bs4 import BeautifulSoup import csv def get_html(url): r = requests.get(url) return r.text def get_total_pages(html): soup = BeautifulSoup(html, 'lxml') divs = soup.find('div', class_='pagination-pages clearfix') pages = divs.find_all('a', class_='pagination-page')[-1].get('href') total_pages = pages.split('=')[1].split('&')[0] return int(total_pages) def write_csv(data): with open('avito.csv', 'a') as f: writer = csv.writer(f) writer.writerow((data['title'], data['price'], data['metro'], data['url'])) def get_page_data(html): soup = BeautifulSoup(html, 'lxml') divs = soup.find('div', class_='catalog-list') ads = divs.find_all('div', class_='item_table') for ad in ads: try: div = ad.find('div', class_='description').find('h4') if 'htc' not in div.text.lower(): continue else: title = div.text.strip() except: title = '' try: div = ad.find('div', class_='description').find('h4') url = "https://avito.ru" + div.find('a').get('href') except: url = '' try: price = ad.find('div', class_='about').text.strip() except: price = '' try: div = ad.find('div', class_='data') metro = div.find_all('p')[-1].text.strip() except: metro = '' data = {'title':title, 'price':price, 'metro':metro, 'url':url} write_csv(data) def main(): url = "https://avito.ru/moskva/telefony?p=1&q=htc" base_url = "https://avito.ru/moskva/telefony?" page_part = "p=" query_par = "&q=htc" # total_pages = get_total_pages(get_html(url)) for i in range(1, 3): url_gen = base_url + page_part + str(i) + query_par html = get_html(url_gen) get_page_data(html) if __name__ == '__main__': main()
Во второй части видео о парсинге Avito внимание фокусируется на совмещении парсинга с извлечением информации из изображений. Для этой задачи вместо библиотеки BeautifulSoup используется библиотека Selenium, работающая непосредственно с браузером на вашем компьютере. Для совместной работы может потребоваться файл драйвера браузера. Захват изображений и преобразование в текстовые строки осуществляется при помощи библиотек pillow и pytesseract.
from selenium import webdriver from time import sleep from PIL import Image from pytesseract import image_to_string class Bot: def __init__(self): self.driver = webdriver.Chrome() # or Firefox() or smth else self.navigate() def take_screenshot(self): self.driver.save_screenshot('avito_screenshot.png') def tel_recon(self): image = Image.open('tel.gif') print(image_to_string(image)) def crop(self, location, size): image = Image.open('avito_screenshot.png') x = location['x'] y = location['y'] width = size['width'] height = size['height'] image.crop((x, y, x+width, y+height)).save('tel.gif') self.tel_recon() def navigate(self): self.driver.get('https://www.avito.ru/velikie_luki/telefony/lenovo_a319_1161088336') button = self.driver.find_element_by_xpath( '//button[@class="button item-phone-button js-item-phone-button button-origin button-origin-blue button-origin_full-width button-origin_large-extra item-phone-button_hide-phone item-phone-button_card js-item-phone-button_card"]') button.click() sleep(3) self.take_screenshot() image = self.driver.find_element_by_xpath( '//div[@class="item-phone-big-number js-item-phone-big-number"]//*') location = image.location size = image.size self.crop(location, size) def main(): b = Bot() if __name__ == '__main__': main()
В этом видео на более абстрактном примере локальной страницы даны дополнительные примеры работы методов find и find_all библиотеки BeautifulSoup. Показывается, как находить включающие информацию блоки при помощи методов parent, find_parent и find_parents. Описаны особенности работы методов next_element и next_sibling, упрощающих поиск данных в пределах одного раздела. В конце видео рассматривается, как сочетать поиск в BeautifulSoup с регулярными выражениями.
В пятом видео анализируется основной прием, помогающий избежать при парсинге бана или появления капчи посредством использования прокси-сервера с меняющимся адресом и изменения параметра User-Agent. Демонстрируется пример использования метода find_next_sibling. Рассмотрена передача IP и User-Agent при запросе библиотеки requests. Для использования актуального списка доступных прокси-серверов мы рекомендуем дополнить код парсером соответствующего сайта (в видео используются заранее подготовленные файлы со списками прокси и браузерных агентов).
import requests from bs4 import BeautifulSoup from random import choice, uniform from time import sleep def get_html(url, useragent=None, proxy=None): print('get_html') r = requests.get(url, headers=useragent, proxies=proxy) return r.text def get_ip(html): print('get_ip') print('New Proxy & User-Agent:') soup = BeautifulSoup(html, 'lxml') ip = soup.find('span', class_='ip').text.strip() ua = soup.find('span', class_='ip').find_next_sibling('span').text.strip() print(ip) print(ua) print('---------------------------') def main(): url = 'https://sitespy.ru/my-ip' useragents = open('useragents.txt').read().split('\n') proxies = open('proxies.txt').read().split('\n') for i in range(10): # sleep(uniform(3, 6)) proxy = {'http': 'http://' + choice(proxies)} useragent = {'User-Agent': choice(useragents)} try: html = get_html(url, useragent, proxy) except: continue get_ip(html) if __name__ == '__main__': main()
В этом видеоуроке на примере двух сайтов объясняется, как обрабатывать сайты с ошибками или сайты, сделанные непрофессионалами. Первый ресурс https://us-proxy.org/ представляет пример сайта, который выглядит как многостраничный, но это лишь видимость – все данные загружаются на главную страницу при первом обращении к сайту. Во втором примере в структуре блога http://startapy.ru/ используется три вертикальных списка с необычным горизонтальным смещением размещаемых блоков при добавлении новых публикаций. В видеоуроке показано, как средства анализа браузеров в связи с некорректной версткой могут вводить нас в заблуждение, при том что BeautifulSoup отображает и использует исправленный вариант.
На примере сайта со свободными изображениями для фона рабочего стола https://www.hdwallpapers.in/ показывается процедура загрузки изображений с сайта – каждого в отдельную папку. Используются только библиотеки os (для работы с заданием пути сохранения файла) и requests. Показывается работа с блочной загрузкой файлов посредством метода iter_content.
import os import requests # url = 'https://www.hdwallpapers.in/thumbs/2018/spiral_silver_metal-t1.jpg' # r = requests.get(url, stream=True) # stream for partial download # with open('1.jpg', 'bw') as f: # for chunk in r.iter_content(8192): # f.write(chunk) urls = [ 'https://www.hdwallpapers.in/walls/messier_106_spiral_galaxy_5k-wide.jpg', 'https://www.hdwallpapers.in/walls/helix_nebula_5k-wide.jpg', 'https://www.hdwallpapers.in/walls/orion_nebula_hubble_space_telescope_5k-wide.jpg', 'https://www.hdwallpapers.in/walls/solar_space-wide.jpg' ] def get_file(url): r = requests.get(url, stream=True) return r def get_name(url): name = url.split('/')[-1] folder = name.split('.')[0] if not os.path.exists(folder): os.makedirs(folder) path = os.path.abspath(folder) return path + '/' + name def save_image(name, file_object): with open(name, 'bw') as f: for chunk in file_object.iter_content(8192): f.write(chunk) def main(): for url in urls: save_image(get_name(url), get_file(url)) if __name__ == '__main__': main()
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(./h4/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(./h4/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('//h3/a[@class="topic"]').get('href')
'http://habrahabr.ru/blogs/qt_software/127555/'
>>> print g.xpath_text('//h3/a[@class="topic"]')
Релиз Qt Creator 2.3.0
>>> print g.css_text('h3 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: Пофиксил примеры исходного кода в статье. После очередного апргрейда хабрахабр по малопонятным для меня причинам не стал исправлять форматирование кода в старых статьях и оно везде поехало. Спасибо Алексею Мазанову за исправления статьи. Ещё он хочет попасть на хабр, если у вас есть инвайт, его майл: [email protected]
Парсим товары на сайте конкурентов при помощи Python
#подключаем бибилиотеку request
import requests
#подключаем бибилиотеку BeautifulSoup
from bs4 import BeautifulSoup
#создаем список для хранения данных о товарах
d=[]
# получаем страницы при помощи цикла
for j in range(50):
#указываем url и get параметры запроса
url = ‘https://www.dns-shop.ru/catalog/17a8a01d16404e77/smartfony/’
# указываем get параметр с помощью которого определяется номер страницы
par = {‘p’: j}
# записываем ответ сервера в переменную r
r = requests.get(url, params=par)
# получаем объект BeautifulSoup и записываем в переменную soup
soup = BeautifulSoup(r.text, ‘html.parser’)
# с помощью циклам перебераем товары на странице и получаем из них нужные параметры
for i in range(20):
# получаем название товара
product = soup.find_all(‘h4’)[i].get_text()
# получаем цену товара
price = soup.find_all(class_=’price_g’)[i].get_text()
# удаляем пробел из цены
price = price.replace(» «, «»)
#получаем ссылку на товар
link = soup.find_all(class_=’show-popover ec-price-item-link’, attrs={‘data-role’ : ‘product-cart-link’})[i][‘href’]
#добавляем домен к ссылке
link = ‘www.dns-shop.ru’ + link
# добавляем данные о товаре в список
d.append([product, price, link])
#открываем файл на запись
with open(‘C:/Users/admin1/Desktop/dns.csv’, ‘w’) as ouf:
#перебираем элементы списка d
for i in d:
#преобразуем элемент списка в строку
i=str(i)
#очищаем строку от ненужных символов
i=i.replace(«\'», «»)
i=i.replace(«[«, «»)
i=i.replace(«]», «»)
#записываем строку в файл
ouf.write(i + ‘\n’)
Шпаргалки по парсингу HTML документа в Python используя Requests
Недавно я работал с парсером для клиента, которому нужно было получить около миллиона записей с сайта недвижимости. После определенной отметки, парсер перестал работать по той причине, что я забыл внедрить определенные проверки, так как я думал, что клиент не пойдет в этом направлении, но он пошел!
Через несколько дней я задумался над написанием парсера в Python при помощи Beautifulsoup. В этой статье я хочу обсудить, как сделать ваш парсер более удобным для людей, не особо знакомых с технической частью.
Есть вопросы по Python?
На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!
Telegram Чат & Канал
Вступите в наш дружный чат по Python и начните общение с единомышленниками! Станьте частью большого сообщества!
Паблик VK
Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!
1. Проверьте код состояния 200
Всегда хорошо проверять код состояния HTTP заранее и делать это периодически. Вот хороший пример:
import requests
r = requests.get(‘https://google.com’)
if r.status_code == 200:
print(‘Все в норме!’)
if r.status_code == 404:
print(‘Страница не существует!’)
| import requests
r = requests.get(‘https://google.com’)
if r.status_code == 200: print(‘Все в норме!’)
if r.status_code == 404: print(‘Страница не существует!’) |
2. Никогда не верьте HTML
Да, особенно если вы не можете его контролировать. Веб скрепинг зависит от HTML DOM, простое изменение в элементе или классе может сломать весь скрипт. Лучший способ справится с этим – узнать, возвращает ли None или нет.
page_count = soup.select(‘.pager-pages > li > a’)
if page_count:
# Все в норме, работаем дальше…
else:
# Ошибка! Отправляем уведомление админу.
| page_count = soup.select(‘.pager-pages > li > a’) if page_count: # Все в норме, работаем дальше… else: # Ошибка! Отправляем уведомление админу. |
Здесь я проверяю, вернул ли CSS селектор что-нибудь законное, если да – то продолжаем дальше.
3. Настройте заголовки
Python Requests не заставляет вас использовать заголовки при отправке запросов, однако есть несколько умных сайтов, которые не дадут вам прочитать ничего важного, если определенные заголовки не присутствуют.
Однажды я столкнулся с ситуацией: HTML, который я видел в браузере отличался от того, который был в моем скрипте. Так что делать запросы настолько правильными, насколько вы можете – очень хорошая практика. Меньшее, что вы должны сделать – это установить User-Agent.
headers = {
‘user-agent’: ‘Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36’
}
r = requests.get(url, headers=headers, timeout=5)
| headers = { ‘user-agent’: ‘Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36’ }
r = requests.get(url, headers=headers, timeout=5) |
4. Настройка таймаута
Одна из проблем Python Requests – это то, что вы не указываете таймаут, так что он будет ждать ответа от сайта до последнего. Это может быть хорошо при определенных условиях, но не в большинстве случаев. Тем не менее, всегда хорошо настроить значение таймаута для каждого запроса. Здесь я установлю таймаут на 5 секунд.
r = requests.get(url, headers=headers, timeout=5)
| r = requests.get(url, headers=headers, timeout=5) |
5. Обработка ошибок
Всегда хорошо реализовать обработку ошибок. Это не только поможет избежать неожиданного выхода скрипта, но также помоет вести журнал ошибок и уведомлений. Используя запросы Python, я предпочитаю ловить ошибки следующим образом:
Попробуйте:
try:
# Логика нашего парсера.
r = requests.get(‘https://python-scripts.com’)
except requests.ConnectionError as e:
print(«OOPS!! Connection Error. Make sure you are connected to Internet. Technical Details given below.\n»)
print(str(e))
except requests.Timeout as e:
print(«OOPS!! Timeout Error»)
print(str(e))
except requests.RequestException as e:
print(«OOPS!! General Error»)
print(str(e))
except KeyboardInterrupt:
print(«Someone closed the program»)
| try: # Логика нашего парсера. r = requests.get(‘https://python-scripts.com’)
except requests.ConnectionError as e: print(«OOPS!! Connection Error. Make sure you are connected to Internet. Technical Details given below.\n») print(str(e)) except requests.Timeout as e: print(«OOPS!! Timeout Error») print(str(e)) except requests.RequestException as e: print(«OOPS!! General Error») print(str(e)) except KeyboardInterrupt: print(«Someone closed the program») |
Проверьте последнюю часть кода. В ней программе указывается, что если кто-нибудь хочет завершить программу через Ctrl+C, то содержимое сначала оборачивается, после чего выполняется. Эта ситуация хороша, если вы храните информацию в файле и хотите сбросить все в момент выхода.
6. Эффективная обработка файлов
Одна из функций парсера – это хранение данных как в базе данных, так и в обычных файлах, таких как CSV/Text. Если собираете большой объем данных, это не означает, что операция ввода-вывода будет в цикле. Давайте рассмотрим, как это делается.
Пробуем:
try:
a_list_variable = []
a_list_variable.extend(a_func_return_record())
except requests.ConnectionError as e:
print(«Упс!! Ошибка подключения к интернету.»)
print(str(e))
except requests.Timeout as e:
print(«Упс!! Время ожидания истекло.»)
print(str(e))
except requests.RequestException as e:
print(«Упс!! Возникла непредвиденная ошибка!»)
print(str(e))
except KeyboardInterrupt:
print(«Кто-то закрыл принудительно программу.»)
finally:
print(«Total Records = » + str(len(property_urls)))
try:
# файл для хранения URL
record_file = open(‘records_file.txt’, ‘a+’)
record_file.write(«\n».join(property_urls))
record_file.close()
except Exception as ex:
print(«Возникла ошибка при сохранении данных, текст ошибки:»)
print(str(e))
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| try: a_list_variable = [] a_list_variable.extend(a_func_return_record()) except requests.ConnectionError as e: print(«Упс!! Ошибка подключения к интернету.») print(str(e)) except requests.Timeout as e: print(«Упс!! Время ожидания истекло.») print(str(e)) except requests.RequestException as e: print(«Упс!! Возникла непредвиденная ошибка!») print(str(e)) except KeyboardInterrupt: print(«Кто-то закрыл принудительно программу.») finally: print(«Total Records = » + str(len(property_urls))) try: # файл для хранения URL record_file = open(‘records_file.txt’, ‘a+’) record_file.write(«\n».join(property_urls)) record_file.close() except Exception as ex: print(«Возникла ошибка при сохранении данных, текст ошибки:») print(str(e)) |
Здесь я вызываю функцию (хотя вы не обязаны делать то же самое), которая добавляет записи в список. Как только это будет сделано, или программа будет остановлена, перед завершением она просто сохранит весь список в файл за раз. Намного лучше, чем несколько операций ввода-вывода
Надеюсь, эта статья была для вас полезной. Пожалуйста, Поделитесь своим опытом о том, как сделать парсер более эффективным!
Есть ли собственный модуль синтаксического анализа HTML для Python 3.2?
Переполнение стека
- Около
Продукты
- Для команд
Переполнение стека
Общественные вопросы и ответыПереполнение стека для команд
Где разработчики и технологи делятся частными знаниями с коллегамиВакансии
Программирование и связанные с ним технические возможности карьерного ростаТалант
Нанимайте технических специалистов и создавайте свой бренд работодателяРеклама
Обратитесь к разработчикам и технологам со всего мира- О компании
.
Страница не найдена · GitHub Pages
Страница не найдена · GitHub Pages
Файл не найден
Сайт, настроенный по этому адресу, не
содержать запрошенный файл.
Если это ваш сайт, убедитесь, что регистр имени файла соответствует URL-адресу.
Для корневых URL (например, http://example.com/
) вы должны предоставить
index.html
файл.
Прочтите полную документацию
для получения дополнительной информации об использовании GitHub Pages .
.