Немного про парсинг web-страниц. (Python)

Тема не раз обсуждалась на просторах рунета. Соберу в одном топике наиболее важные моменты, применяемые в практике.
Парсить удобно двумя способами: по тегам с классами, по XPath и по микроразметке Schema.org. При этом последний вариант наиболее универсален для тематических сайтов.
Общим моментом для всех способов является загрузка (получение) web-страницы.

Для работы потребуется подключить модуль requests.
import requests
Затем создать сессию и указать юзер-агента:
s = requests.Session()
s.headers.update({
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:45.0) Gecko/20100101 Firefox/45.0'
    })

Для получения содержимого веб-страницы используем метод get, т.е.
requests.get('http://site.com')или
s.get('http://site.com')Для удобства работы создаем функцию:
def get_page(url):
return s.get(url)
Например, нам надо получить страницу сайта объявлений юла, тогда код будет выглядеть следующим образом:
link = 'https://youla.ru/'
page = get_page(link)
Чтобы распечатать текстовое представление (html), используем атрибут text:
print(page.text)
После чего на экране увидим страницу в виде текста html.

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

Приступаем к обработке.
Для парсинга данных по тегам, необходимо подключить модуль bs4, точнее - его компонент BeautifulSoup:
from bs4 import BeautifulSoup
BeautifulSoup - это парсер для синтаксического разбора файлов HTML/XML (http://wiki.python.su/%D0%94%D0%BE%D0%BA%D1%83%D0%BC%D0%B5%D0%BD%D1%82%D0%B0%D1%86%D0%B8%D0%B8/BeautifulSoup). В вики много полезной инфы, рекомендуется к прочтению.
Рассмотрим один из примеров использования. Например, нам надо получить названия (заголовки) объявления, используя классы CSS при помощи lxml. Для этого подготовим страницу к парсингу:
soup = BeautifulSoup(page.text, "lxml")
Примечание: в качестве параметра передается текстовое содержание страницы.
Затем смотрим, как представлены заголовки объявления в исходнике страницы, а представлены они в таком виде:
<div class="product_item__title">ЗАГОЛОВОК</div>
Ищем все элементы с классом product_item__title, делается это следующим образом:
soup.find_all('TAG', {'class': ['CLASS_NAME']})
Тэг у нас - div, а имя класса - product_item__title. и присваиваем все новой переменной.
names = soup.find_all('div', {'class': ['product_item__title']})
Метод find_all возвращает массив объектов, поэтому для просмотра, что мы получили в итоге, необходимо обратиться к каждому из них, а для просмотра текста заголовка - использовать атрибут 'text' для каждого элемента, выглядеть это будет так:
for item in names:
    print(item.text)
В итоге на экране мы увидим заголовки объявлений.
Хорошо, текст заголовка есть, неплохо бы получить и ссылку. Как известно, ссылка указывается в тэге 'a' в атрибуте 'href', чтоб получить содержимое атрибута, используем метод 'get("имя атрибута")'. Хорошо, пробуем.
Ищем все элементы с тэгом 'a':
for item in all_links:
    link = item.get('href')
    print(link)
Вроде получилось, получили много ссылок, но слишком много, больше, чем заголовков, да и ссылки порой к заголовкам не относятся. А все потому, что были спарсены абсолютно все тэги "а". Чтобы все упорядочить, надо проводить парсинг поблочно, поэлементно, то есть, вся страница представлена в виде иерархии элементов, один вложен в другой, в одном элементе может быть куча других, так и ссылка и заголовок находятся внутри какого-то другого элемента, где так же находится и цена. Поэтому надо найти сначала все эти "родительские" элементы, а уже внутри них искать необходимую информацию.
Ок, приступаем. Ищем родительские элементы для объявлений. А родительским элементом является <li class="product_item"......></li>, получаем их список:
parent_blocks = soup.find_all('li', {'class': ['product_item']})
Далее уже внутри каждого элемента ищем заголовки и ссылки точно так же, как делали это раньше, НО уже не в тексте всей страницы, а в тексте только отдельно взятого блока, т.е.:
for parent_block in parent_blocks:
    name = parent_block.find('div', {'class': ['product_item__title']})
    print(name.text)
    link = parent_block.find('a')
    print(link.get('href'))
Здесь уже используем не 'find_all', а просто 'find', т.к. у нас всего один элемент с таким тэгом и классом. Но если будет несколько, то надо использовать 'find_all', а далее с полученными данными работать как с массивом.
Теперь мы получили заголовки и url-ы к каждому объявлению. Так же легко и просто получить и цену для каждого объявления, для этого всего лишь надо получить текст для тэга 'div' с CSS классом 'product_item__description'. Можно заметить, что в случае для выбранного ресурса с текстом цены будут присутствовать символы табуляции и прочие "прелести", но это все легко отсекается простой обработкой строк. Все полученные данные можно сохранять в списки, массивы, объекты классов, базу данных и т.д. - куда вздумается, а далее не проблема переходить уже по полученным ссылкам в зависимости от заголовка объявления и уже внутри текста объявления искать что интересно и делать что потребуется, например, если текст интересен - сохранять объявление куда-либо или отсылать сообщение, или еще что.
Удобно использовать с любыми сайтами, где применяются стили, с некоторыми оговорками, как, например, в случае с нижеизложенным XPath.


Переходим к парсингу с помощью микроразметки.
Многие сайты в угоду поисковым системам, да и просто для нормальной организации структуры сайта, используют микроразметки, одной из самых популярных является Schema.org. Ее и рассмотрим.
Работать с микроразметкой крайне просто. Для этого используется все тот же модуль BeautifulSoup и метод "find". Но... на сайте Юла в обычном ее представлении она отсутствует, возьмем другой сайт, например, сайт комус, и перейдем на страницу товара (любого, ссылки приводить не буду). Отыщем картинки товара, согласно разметке свойство у них - "image", т.е. ищем itemprop="image", ищем все изображения, проверяем, есть ли они вообще, т.е. объект не пустой, и выводим на печать:
images = soup.find_all(itemprop="image")
if(images != None):
    for image in images:
        pict = image.get('src')
        print(pict)
Удобно использовать для работы с каталогами, интернет-магазинами и прочими структурированными сайтами.


Парсинг по тэгам и CSS-классам, и микроразметке рассмотрели. Переходим к парсингу по XPath.



Парсинг с помощью XPath.
Первым делом подключаем html модуль:
from lxml import html
Затем подготавливаем страницу для парсинга с помощью XPath:
tree = html.fromstring(page.content)
Примечание: в отличие от парсинга по классам CSS, используется атрибут не text, а content.
Ищем XPath пусть нужного элемента, для этого в исходном коде страницы выбираем нужный элемент, ПКМ - Copy - Copy XPath. В браузере Chrome удобно использовать расширение Toggle Xpather, запускаем его и вставляем скопированный XPath, например, что-то вроде: //*[@id="app"]/div[1]/div/div/div/main/section[2]/div/ul/li[14]/a/figure/figcaption/div[2]. И видим текст для объявления (его заголовок). Чтобы увидеть все тексты, немножко отредактируем: //*[@id="app"]/div[1]/div/div/div/main/section[2]/div/ul/li/a/figure/figcaption/div[2] (убрали [14]). Получаем список заголовков. Делаем аналогично для цен и ссылок, получаем (текст уже для Python):
xpnames = '//*[@id="app"]/div[1]/div/div/div/main/section[2]/div/ul/li/a/figure/figcaption/div[2]'
xpprices = '//*[@id="app"]/div[1]/div/div/div/main/section[2]/div/ul/li/a/figure/figcaption/div[1]/text()'
xplinks = '//*[@id="app"]/div[1]/div/div/div/main/section[2]/div/ul/li/a/@href'
@href' - добавляется самостоятельно на конце и указывает на атрибут.
Теперь формируем список заголовков в Python:
names = tree.xpath(xpnames)
И выводим на печать:
for name in names:
    print(name.text)
Повторяем для цены и ссылок:
prices = tree.xpath(xpprices)
for price in prices:
    print(price)
links = tree.xpath(xplinks)
for link in links:
    print(link)
Дальнейшая обработка (сопоставление заголовка с ценой и ссылкой и т.д.) - уже обычная работа со строками и списками (массивами). Можно проверять количество объектов, пустой ли объект и т.д., все зависит от конкретного случая, удобства и привычки. Как видно, ничего сложного в этом нет, а для проверки корректности XPath удобно пользоваться расширениями для браузеров. Еще момент: надо быть внимательным и следить, нужно ли использовать атрибут .text или нет, чтоб не заморачиваться и было наверняка - можно написать и запустить 1 раз, тогда станет ясно.
Удобно работать с большинством сайтов, но надо быть предельно внимательным, т.е. может попасться что-то вроде "чай черный", где слово "чай" будет анкорной ссылкой, а "черный" - текстом, который ищется парсером. В таком случае будет найдено только слово "черный", а слово "чай" будет опущено.

Многостраничный проход легко реализовывается по собранным ссылкам: в одном методе формируем список ссылок, в другой - проходим по ним, попутно так же формируя список новых ссылок, и т.д.

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

Комментарии

Популярные сообщения из этого блога

STM32. Часть 4. Задержки, таймеры, системный таймер SysTick

AVR. Assembler. Алгоритмы.

STM32. Создание проекта в Keil.