Быстрый в изучении - мощный в программировании
Скрипт ИНВЕСТОР на Python

Попробуйте себя в качестве инвестора имея в помощники мощный алгоритм советника на Python...

Все уроки по PyQt5

PyQt5 реализован как комплект Python-модулей. Он включает в себя около 620 классов и 6000 функций и методов...

Скрипт отправки SMS через Python

Была задача отправить SMS-ки большому списку номеров телефона с уточнением цены за всю рассылку "До" ее отправки...

python lxml

Здравствуйте, здравствуйте и еще раз здравствуйте! Сегодня хотелось бы посвятить вас в такую тему, как разбор разметки страниц html, используя для этого Python+lxml. На самом деле, это не так сложно, как кажется, поэтому вы с легкостью сможете обуздать довольно простые правила касательно этого процесса. Что такое lxml? Это удобная в работе библиотека, которая позволяет обрабатывать разметку, как html , так и xml, при помощи языка Python. Стоит также отметить ее уникальную способность разбития частей документа в дерево. Итак, приступим.

Какую цель вы будете преследовать, создавая парсинг?

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

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

Последовательность в работе парсера

Первое, с чего следует начать, это определиться, какая будет последовательность действий нашего парсера:

  • Акцентируем внимание на главной странице, которая включает в себя основную информацию по всем проведенным турнирам
  • Перенесем информацию с выбранной страницы в набор данных, создавая такие столбцы, как Турнир, Ссылка на описание, Дата
  • Чтобы у нас был доступ к информации о проведенных боях, создаем переход по столбцу Ссылка на описание для каждого турнира
  • Указываем нужные данные по каждому проведенному бою в рамках турнира
  • Вносим столбец Дата из набора данных в тот, где уже существует информация о боях

Готово! Теперь у нас выстроилась строгая последовательность того, что нужно сделать, и теперь самое время воплотить ее в жизнь.

Первое знакомство с библиотекой lxml

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

import lxml.html as html
from pandas import DataFrame

С целью максимального упрощения работы с парсингом, выделим место для главного домена в новой переменной:

main_domain_stat = 'http://hosteddb.fightmetric.com'

Самое время создать объект для нашего парсинга. Для этого нам понадобится функция parse():

page = html.parse('%s/events/index/date/desc/1/all' % (main_domain_stat))

Следующим шагом будет открытие нашей таблицы при помощи редактора html, чтобы мы смогли во всех подробностях изучить ее. Отдельное внимание следует уделить блоку с классами events_table data_table row_is_link. А все потому, что у него хранится та самая таблица, где содержатся необходимая для нас информация. Отыскать этот блок можно следующим образом:

e = page.getroot().\
        find_class('events_table data_table row_is_link').\
        pop()

Стоит немного уточнить, какой результат следует ожидать от этого кода. Первым делом, используя возможности функции getroot(), мы сможем завладеть корневым элементом документа. Смысл этого действия будет раскрыт чуть позже при работе с документом. После того, как мы воспользуемся функцией find_class(), мы сможем отыскать любые элементы с нужными классами.

Если анализировать html код страницы визуально, то можно заметить, что по заданному критерию будет подходить лишь один элемент и при таком раскладе мы сможем его экспортировать, используя функцию pop(). Следующим нашим шагом будет использование div, благодаря которому мы сможем получить таблицу. Чтобы это стало реальностью, нам нужно будет воспользоваться функцией getchildren(). С ее помощью мы сможем вернуть список подчерненных объектов текущего элемента. Упрощает ситуацию тот факт, что такой элемент у нас будет в единичном варианте.

t = e.getchildren().pop()

Представленная выше переменная t ознаменует собой ту самую таблицу, в которой хранится нужная для нас информация. Самое время обзавестись парочкой второстепенных dataframe'a, после чего, их нужно соединить вместе и тогда можно будет довольствоваться информацией о проведенных турнирах, а также датами и ссылками на результаты.

Названия турниров вместе с ссылками на их страницы будут находиться в первом наборе. Чтобы этого добиться, нам нужно будет воспользоваться возможностями функции iterlinks(), задача которого заключается в том, чтобы вернуть исходный список кортежей, находящегося в определенном элементе. Кортеж будет включать в себя текст и адрес ссылки, что нам и понадобится. Ссылочный текст станет доступным для нас, если мы воспользуемся свойством .text заданного элемента. Код будет выглядеть вот таким образом:

events_tabl = DataFrame([{'EVENT':i[0].text, 'LINK':i[2]} for i in t.iterlinks()][5:])

Если присмотреться, то можно заметить, что в данном цикле мы не используем 5 записей, идущих в самом начале. Они содержат в себе ту информацию, которая для нас совершенно бесполезна. Таким образом, мы смогли получить нужные нам ссылки. Осталось лишь заполучить второй поднабор данных, где указывается дата проведения турниров. Достичь этого можно следующим образом:

event_date = DataFrame([{'EVENT': evt.getchildren()[0].text_content(), 'DATE':evt.getchildren()[1].text_content()} for evt in t][2:])

Вышеуказанный код показывает, что мы задеваем все строки, а именно, теги tr, в таблице t. Лишь после этого, каждая строчка обзаводится целым списком дочерних колонок, которые представляют собой элементы td. Затем мы сможем довольствоваться информацией, которая хранится в первой и второй колонках, используя свойства text_content. С его помощью мы сможем вернуть строку из текста каждого дочернего элемента выбранного столбца. Немного сложно для восприятия, не так ли? Тогда вот вам наглядный пример того, как работает функция text_content. Возьмем следующую структуру документа за основу: <tr><td><span>текст</span><span>текст</span>. В данном случае, функция text_content возвратит строку текст текст в то время, как функция text сможет вернуть лишь текст и то в лучшем случае.

Имея в наличии пару поднабора данных, логично будет их соединить в один общий набор. Сделать это можно так:

sum_event_link = events_tabl.set_index('EVENT').join(event_date.set_index('EVENT')).reset_index()

Первым делом, наборы получают собственные индексы, а уже после этого происходит процесс сливания наборов и обнуление индексов готового набора. Теперь нам понадобится извлечь готовый dataframe в текстовый документ. Делаем это следующим образом:

sum_event_link.to_csv('..\DataSets\ufc\list_ufc_events.csv',';',index=False)

Процесс обработки одного события в UFC

Теперь, когда у нас есть информация о проведенных турнирах в текстовом файле, можно заняться страницами, где хранятся все данные о результатах данных турниров. В качестве примера используем страницу любого из турниров, код которой мы посмотрим. Не сложно будет увидеть, что вся необходимая для нас информация будет находиться в элементе с классом data_table row_is_link. Стоит сказать, что последовательность действий в парсинге будет примерно такой же, что и выше, но сразу отметим одну неточность: неправильное оформление таблицы результатов.

Если быть точнее, то суть заключается в том, что здесь была создана дополнительная строка для каждого бойца, а это существенно затрудняет работу с таблицей. Чтобы мы смогли от этого избавиться, можно прибегнуть к помощи итератора, который будет функционировать лишь по нечетным строчкам. Что касается четных строчек, то их можно определить по нечетной строчке, выделенной в данный момент. Другими словами, можно будет работать сразу с несколькими строчками, которые в дальнейшем следует перенести в строку. Выглядеть это будет следующим образом:

all_fights = []
for i in sum_event_link.itertuples():
    page_event = html.parse('%s/%s' % (main_domain_stat,active_event_link))
    main_code = page_event.getroot()
    figth_event_tbl = main_code.find_class('data_table row_is_link').pop()[1:]
    for figther_num in xrange(len(figth_event_tbl)): 
        if not figther_num % 2:
            all_fights.append(
                        {'FIGHTER_WIN': figth_event_tbl[figther_num][2].text_content().lstrip().rstrip(), 
                        'FIGHTER_LOSE': figth_event_tbl[figther_num+1][1].text_content().lstrip().rstrip(), 
                        'METHOD': figth_event_tbl[figther_num][8].text_content().lstrip().rstrip(), 
                        'METHOD_DESC': figth_event_tbl[figther_num+1][7].text_content().lstrip().rstrip(), 
                        'ROUND': figth_event_tbl[figther_num][9].text_content().lstrip().rstrip(), 
                        'TIME': figth_event_tbl[figther_num][10].text_content().lstrip().rstrip(),
                        'EVENT_NAME': i[1]} 
                        )
history_stat = DataFrame(all_fights)

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

history_stat.to_csv('..\DataSets\ufc\list_all_fights.csv',';',index=False)

Ну, а теперь, самое время проверить, что же мы смогли сделать:

history_stat.head()
0 UFC Fight Night 38: Shogun vs. Henderson Robbie Lawler Johny Hendricks U. DEC NaN 5 5:00
1 UFC Fight Night 38: Shogun vs. Henderson Carlos Condit Tyron Woodley KO/TKO Knee Injury 2 2:00
2 UFC Fight Night 38: Shogun vs. Henderson Diego Sanchez Myles Jury U. DEC NaN 3 5:00
3 UFC Fight Night 38: Shogun vs. Henderson Jake Shields Hector Lombard U. DEC NaN 3 5:00
4 UFC Fight Night 38: Shogun vs. Henderson Nikita Krylov Ovince Saint Preux SUB Other — Choke 1 1:29

В заключении следует добавить дату к боям, а также экспортировать готовый файл:

all_statistics = history_stat.set_index('EVENT_NAME').join(sum_event_link.set_index('EVENT').DATE)
all_statistics.to_csv('..\DataSets\ufc\statistics_ufc.csv',';', index_label='EVENT')

Данный скрипт можно использовать в собирать и анализировать ставки на чемпионат Мира по хоккею с шайбой и выбрать на кого поставить свои деньги. Такой анализатор можно использовать и в форекс, не только на ставках. Собирая и анализируя таблицы ставок легче чем анализировать графики инвестиции форекс.

Послесловие

Цель данной статьи заключается в том, чтобы разъяснить вам об основных особенностях работы с библиотекой lxml, которая используется для создания парсеров на html и xml. Конечно, вышеуказанный код не займет первого места в номинации «оптимальность года», но зато он отличается стабильной работой. В целом, хочется сказать, что взаимодействие с данной библиотекой находится на чрезвычайно легком уровне, что позволит вам намного быстрее создать требуемый код. Не стоит забывать и про то, что в этой статье были описаны лишь немногие функции, которыми обладает библиотека lxml, поэтому у вас еще будет возможность познакомиться с другими его особенностями.

twitter.com facebook.com vkontakte.ru odnoklassniki.ru mail.ru ya.ru yandex.ru
Комментариев: 3
  1. page.getroot().find_class('events_table data_table row_is_link')

    Возваращает пустой список. Почему?

  2. Привет Виталий.

    Видимо на странице отсутствуют указанные вами классы.

    Отправьте URL посмотрю исходный код.

  3. Виталий | 2016-05-03 в 07:43:56

    Это происходит в коде из вашей статьи: 'http://hosteddb.fightmetric.com'

Оставьте комментарий!

Используйте нормальные имена.

Имя и сайт используются только при регистрации

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

(обязательно)