Добрый день, уважаемые читатели.

В сегодняшней статье я покажу основы разбора HTML разметки страниц с помощью библиотеки lxml для Python.

Если вкратце, то lxml это быстрая и гибкая библиотека для обработки разметки XML и HTML на Python. Кроме того, в ней присутствует возможность разложения элементов документа в дерево. В статье я постараюсь показать, насколько просто ее применение на практике.

Выбор цели для парсинга

Т.к. я активно занимаюсь спортом, в частности БЖЖ мне захотелось посмотреть статисту по болевым приемам во все проведенных турнирах мировых турнирах по MMA.

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

Чтобы объеденить всю информацию по турнирам в одну таблицу, пригодную для анализа, было принято решение написать парсер описанный ниже.

Алгоритм работы парсера

Для начала разберемся с алгоримом работы парсера. Он будет следующим:

  1. За основу возьмем таблицу со всеми турнирами и их датами, которая находится по данному адресу
  2. Занесем данные с этой страници в набор данных, cо следующими столбцами:
    • турнир
    • ссылка на описание
    • дата
  3. По каждой записи набора (по каждому турниру) осуществляем переход по полю [ссылка на описание], для получения информации о боях
  4. Записываем информацию по всем боям турнира
  5. К набору данных с информацие о боях добавляем дату проведения турнира из набора (2)

Алгоритм готов и можно перейти к его реализации

Начало работы с lxml

Для работы нам понадобятся модули lxml и pandas. Подгрузим их в нашу программу:

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‘a, полученного ранее. Для этого воспользуемся методом getchildren(), который возвращает список подчененных объектов текущего элемента. И потому, что у нас только один такой объект, ты мы извлекаем этот его из списка.

t = e.getchildren().pop()

Теперь переменная t содержит таблицу с необходимой для нас информацией. Теперь, я получу 2 вспомогательных dataframe’a, объеденив которые, мы получим данные о турнирах с датами их проведения и ссылками на результаты.

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

Тест ссылки можно получить обративший к свойству .text соответсвующего элемента. Код будет следующим:

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

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

Итак, ссылки мы получили. Теперь получим 2 поднабор данных с датами проведения турниров. Это можно сделать так:

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 не вернет ничего, или же просто текст.

Теперь, когда у нас есть 2 поднабора данных, объеденим их в итоговый набор:

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

Страницу с перечнем турниров мы выгрузили в удобном формате. Пришло время разобраться со страницами с результатами по соревнований. Для примера возьмем последний турнир и посмотрим HTML код страницы.

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

Некорретность ее в том, что для каждого бойца в ней заведена отдельная строка, что никак не удобно при анализе. Чтобы избавиться от этого неубодства при разборе результатов я принял решение использовать итератор, только по нечетным строкам. Номер же четной вычислять из текущей нечетной строки.

Таким образом я буду обрабатывать сразу пару строк и переносить их в строку. Код будет следующий:

all_fights = []
for i in sum_event_link.itertuples():
    page_event = html.parse('%s/%s' % (main_domain_stat,i[2]))
    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()
EVENT_NAME FIGHTER_LOSE FIGHTER_WIN METHOD METHOD_DESC ROUND TIME
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, пердназначенной для парсинга разметки XML и HTML. Код указанный в статье не претендует на оптимальность, но корректно выполняет поставленную перед ним задачу.

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