{"id":14268,"url":"\/distributions\/14268\/click?bit=1&hash=1e3309842e8b07895e75261917827295839cd5d4d57d48f0ca524f3f535a7946","title":"\u0420\u0430\u0437\u0440\u0435\u0448\u0430\u0442\u044c \u0441\u043e\u0442\u0440\u0443\u0434\u043d\u0438\u043a\u0430\u043c \u0438\u0433\u0440\u0430\u0442\u044c \u043d\u0430 \u0440\u0430\u0431\u043e\u0447\u0435\u043c \u043c\u0435\u0441\u0442\u0435 \u044d\u0444\u0444\u0435\u043a\u0442\u0438\u0432\u043d\u043e?","buttonText":"\u0423\u0437\u043d\u0430\u0442\u044c","imageUuid":"f71e1caf-7964-5525-98be-104bb436cb54"}

Web Parsing. Основы на Python

Рассмотрим еще один практический кейс парсинга сайтов с помощью библиотеки BeautifulSoup: что делать, если на сайте нет готовой выгрузки с данными и нет API для удобной работы, а страниц для ручного копирования очень много?

Недавно мне понадобилось получить данные с одного сайта. Готовой выгрузки с информацией на сайте нет. Данные я вижу, вот они передо мной, но не могу их выгрузить и обработать. Возник вопрос: как их получить? Немного «погуглив», я понял, что придется засучить рукава и самостоятельно парсить страницу (HTML). Какой тогда инструмент выбрать? На каком языке писать, чтобы с ним не возникло проблем? Языков программирования для данной задачи большой набор, выбор пал на Python за его большое разнообразие готовых библиотек.

Примером для разбора основ возьмем сайт с отзывами banki_ru и получим отзывы по какому-нибудь банку.

Задачу можно разбить на три этапа:

  1. Загружаем страницу в память компьютера или в текстовый файл.
  2. Разбираем содержимое (HTML), получаем необходимые данные (сущности).
  3. Сохраняем в необходимый формат, например, Excel.

Инструменты

Для начала нам необходимо отправлять HTTP-запросы на выбранный сайт. У Python для отправки запросов библиотек большое количество, но самые распространённые urllib/urllib2 и Requests. На мой взгляд, Requests — удобнее, примеры буду показывать на ней.

А теперь сам процесс. Были мысли пойти по тяжелому пути и анализировать страницу на предмет объектов, содержащих нужную информацию, проводить ручной поиск и разбирать каждый объект по частям для получения необходимого результата. Но немного походив по просторам интернета, я получил ответ: BeautifulSoup – одна из наиболее популярных библиотек для парсинга. Библиотеки найдены, приступаем к самому интересному: к получению данных.

Загрузка и обработка данных

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

import requests from bs4 import BeautifulSoup bank_id = 1771062 #ID банка на сайте banki.ru url = 'https://www.banki.ru/services/questions-answers/?id=%d&p=1' % (bank_id) # url страницы r = requests.get(url) with open('test.html', 'w') as output_file: output_file.write(r.text)

После исполнения данного скрипта получится файл text.html.

Открываем данный файл и видим, что необходимые данные получили без проблем.

Теперь очередь для разбора страницы на нужные фрагменты. Обрабатывать будем последние 10 страниц плюс добавим модуль Pandas для сохранения результата в Excel.

Подгружаем необходимые библиотеки и задаем первоначальные настройки.

import requests from bs4 import BeautifulSoup import pandas as pd bank_id = 1771062 #ID банка на сайте banki.ru page=1 max_page=10 url = 'https://www.banki.ru/services/questions-answers/?id=%d&p=%d' % (bank_id, page) # url страницы

На данном этапе необходимо понять, где находятся необходимые фрагменты. Изучаем разметку страницы с отзывами и определяем, какие объекты будем вытаскивать из страницы.

Подробно покопавшись во внутренностях страницы, мы увидим, что необходимые данные «вопросы-ответы» находятся в блоках <table class:qaBlock >, соответственно, сколько этих блоков на странице, столько и вопросов.

result = pd.DataFrame() r = requests.get(url) #отправляем HTTP запрос и получаем результат soup = BeautifulSoup(r.text) #Отправляем полученную страницу в библиотеку для парсинга tables=soup.find_all('table', {'class': 'qaBlock'}) #Получаем все таблицы с вопросами for item in tables: res=parse_table(item)

Приступая к получению самих данных, я кратко расскажу о двух функциях, которые будут использованы:

  • Find(‘table’) – проводит поиск по странице и возвращает первый найденный объект типа ‘table’. Вы можете искать и ссылки find(‘table’) и рисунки find(‘img’). В общем, все элементы, которые изображены на странице;
  • find_all(‘table’) – проводит поиск по странице и возвращает все найденные объекты в виде списка

У каждой из этих функций есть методы. Я расскажу от тех, которые использовал:

  • find(‘table’).text – этот метод вернет текст, находящийся в объекте;
  • find(‘a’).get(‘href’) – этот метод вернет значение ссылки

Теперь, уже обладая этими знаниями и навыками программирования на Python, написал функцию, которая разбирает таблицу с отзывом на нужные данные. Каждые стадии кода дополнил комментариями.

def parse_table(table):#Функция разбора таблицы с вопросом res = pd.DataFrame() id_question=0 link_question='' date_question='' question='' who_asked='' who_asked_id='' who_asked_link='' who_asked_city='' answer='' question_tr=table.find('tr',{'class': 'question'}) #Получаем сам вопрос question=question_tr.find_all('td')[1].find('div').text.replace('<br />','\n').strip() widget_info=question_tr.find_all('div', {'class':'widget__info'}) #Получаем ссылку на сам вопрос link_question='https://www.banki.ru'+widget_info[0].find('a').get('href').strip() #Получаем уникальным номер вопроса id_question=link_question.split('=')[1] #Получаем того кто задал вопрос who_asked=widget_info[1].find('a').text.strip() #Получаем ссылку на профиль who_asked_link='https://www.banki.ru'+widget_info[1].find('a').get('href').strip() #Получаем уникальный номер профиля who_asked_id=widget_info[1].find('a').get('href').strip().split('=')[1] #Получаем из какого города вопрос who_asked_city=widget_info[1].text.split('(')[1].split(')')[0].strip() #Получаем дату вопроса date_question=widget_info[1].text.split('(')[1].split(')')[1].strip() #Получаем ответ если он есть сохраняем answer_tr=table.find('tr',{'class': 'answer'}) if(answer_tr!=None): answer=answer_tr.find_all('td')[1].find('div').text.replace('<br />','\n').strip() #Пишем в таблицу и возвращаем res=res.append(pd.DataFrame([[id_question,link_question,question,date_question,who_asked,who_asked_id,who_asked_link,who_asked_city,answer]], columns = ['id_question','link_question','question','date_question','who_asked','who_asked_id','who_asked_city','who_asked_link','answer']), ignore_index=True) #print(res) return(res)

Функция возвращает DataFrame, который можно накапливать и экспортировать в EXCEL.

result = pd.DataFrame() r = requests.get(url) #отправляем HTTP запрос и получаем результат soup = BeautifulSoup(r.text) #Отправляем полученную страницу в библиотеку для парсинга tables=soup.find_all('table', {'class': 'qaBlock'}) #Получаем все таблицы с вопросами for item in tables: res=parse_table(item) result=result.append(res, ignore_index=True) result.to_excel('result.xlsx')

Резюме

В результате мы научились парсить web-сайты, познакомились с библиотеками Requests, BeautifulSoup, а также получили пригодные для дальнейшего анализа данные об отзывах с сайта banki.ru. А вот и сама результирующая таблица.

Приобретенные навыки можно использовать для получения данных с других сайтов и получать обобщенную и структурированную информацию с бескрайних просторов Интернета.

Я надеюсь, моя статья была полезна. Спасибо за внимание.

0
7 комментариев
Написать комментарий...
Олег Нечаев

Парсер можно писать на чем угодно. Да и библиотеки не всегда нужны, использовал раньше только ssl для https. В некоторых случаях проще писать свой код. Единственное, что хорошо бы иметь - параллельные потоки и прокси. Писал давно универсальный парсер, столкнулся с неприятным моментом ошибок в коде доноров, отсутствии закрывающих тегов или генерации страниц скриптами. Решил гибкой системой настроек паттернов, в приложении добавил редактор данных в таблице, куда заносятся паттерны для поочередной обработки данных. Для увеличения скорости добавил стоп-паттерн (та уникальная часть кода страницы, после которой нет нужных данных), после которого не нужно ждать окончания загрузки, а можно сразу работать с кодом страницы. И для ускорения именно разбора добавил старт-позицию до которой не нужно анализировать данные. Ну ещё счетчик страниц, автосохранение данных и настроек и т.п.

Ответить
Развернуть ветку
Мик Ололо

Мои приветствия. А как изменится код, если необходимо пройти по куче однотипных ссылок вида http://ololo.ru/forum/forum_id, где forum_id - цифры по нарастающей и выдрать какую-то информацию?
Т.е. тут добавляется цикл хождения по ссылкам for forum_id in ()
И где-то здесь конкретно у меня появляется проблема, что в результат либо пишется  информация по последней пройденной ссылке, либо выдается информация по всем ссылкам, но с дублированием заголовков, т.е. что-то такое:
        заголовок1      заголовок2
0      инф1                  инф2
        заголовок1      заголовок2
0      инф1                  инф2
Как сделать вывод одной нормальной таблицей? Целый, видимо, квест =(

Ответить
Развернуть ветку
NTA
Автор

Добрый день.
Необходимо понимать структуру формирования ссылки по которой формируется необходимая страница.
На некоторый сайтах добавляется такой параметр как offset(на разных сайтах может писаться по разному, но смысл должен быть один), т.е. сколько необходимо пропустить страниц или записей. Поперемещайтесь по страницам и посмотрите какой параметр меняется еще.

Ответить
Развернуть ветку
Denis Kubarskiy

Очень круто написано. Понятно и доступно. Спасибо огромное. 
Не сможет ли кто-то подсказать, как исправить такую ошибку ?    
 File "I:/PYTHON/Обучение/Study/...../Parsing_01.py", line 43, in parse_table

 date_question = widget_info[1].text.split('(')[1].split(')')[1].strip()

IndexError: list index out of range

Возникает, когда место проживания пишут типа (Балашиха (Московская область)), то есть две пары скобок :-(  (Или хотя бы как прописать, чтобы игнорил эту ошибку и оставлял переменную пустой)

Ответить
Развернуть ветку
Denis Kubarskiy

   У самого получилось:
# Получаем дату вопроса

 try:

    date_question = widget_info[1].text.split('(')[1].split(')')[1].strip()

 except IndexError:

    date_question = widget_info[1].text.split('(')[2].split(')')[2].strip()

Ответить
Развернуть ветку
NTA
Автор

Добрый день.

Можно обойтись без исключения, добавив в коде проверку на двойные скобки.

Пример написан на основе вашего:

if widget_info[1].text.find('))')!=-1:

      date_question=widget_info[1].text.split('(')[2].split(')')[2].strip()

else:

      date_question=widget_info[1].text.split('(')[1].split(')')[1].strip()

Ответить
Развернуть ветку
NTA
Автор

Да, вы правы, что для более быстрой работы необходима многопоточность, а также более глубокое понимание содержимого ресурса, который парсим. Частные проблемы тоже надо учитывать (автогенерация страниц)

Ответить
Развернуть ветку
4 комментария
Раскрывать всегда