Пользователь
0,0
рейтинг
21 апреля 2014 в 09:39

Разработка → Основы парсинга с помощью Python+lxml

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


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


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

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


Для начала разберемся с алгоритмом работы парсера. Он будет следующим:
  1. За основу возьмем таблицу со всеми турнирами и их датами, которая находится
    по данному
    адресу
  2. Занесем данные с этой страницы в набор данных, cо следующими столбцами:
  3. турнир
  4. ссылка на описание
  5. дата
  6. По каждой записи набора (по каждому турниру) осуществляем переход по полю
    [ссылка на описание], для получения информации о боях
  7. Записываем информацию по всем боям турнира
  8. К набору данных с информацией о боях добавляем дату проведения турнира из
    набора (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,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()

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. Код указанный в статье не претендует на оптимальность, но корректно выполняет поставленную перед ним задачу.
Как видно из приведенной программы процесс работы с библиотекой довольно прост, что помогает быстро писать нужный код. Кроме того помимо указанных выше функций и методов есть и другие не менее нужные.
@kuznetsovin
карма
55,2
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (8)

  • 0
    Спасибо за статью. Просто любопытно, какие вы сделали выводы из анализа данных, со спортивной точки зрения? Ну, как чаще побеждают? Коленки ломают или удушающим? :)
    • +1
      Я как раз сейчас пишу статью на эту тему. Пока могу сказать, что из сабмишенов чаще всего делают удушение сзади, рычаг руки, гильотну и треугольник. Среди нокаутов большее число от ударов руками. Если будет интересно могу прислать ссылку на статью. Т.к. на хабре, наверное нет смысла ее выкладывать, т.к. она не имеет отношения к IT.
  • 0
    Для питона есть отличная либа — BeautifulSoup. Простой пример — есть магазин, в нем товары находятся в div'ах с классом «product». Внутри div'a таблица с img и td с классами «name» и «id» для названия товара и его ID. Чтобы пройтись по всем div'ам достаточно лишь:
    soup = BeautifulSoup(page) # page - скачиваем страницу и отдаем ее
    for item in soup.findAll("div", {"class": "product"}):
        img = item.find("img")["src"]
        name = soup.find("tr", {"class": "name"}) 
        id = soup.find("tr", {"class": "id"})
    
    • 0
      Тоже самое можно сделать и на lxml. Если страница хорошо сверстана и у элементов есть атрибуты типа, class, id и т.д. BeautifulSoup насколько я знаю используется в pandas для функции read_html(), которая тоже может парсить страницы.
    • 0
      tree = etree.HTML(html)
      for block in tree.xpath("//div[@class='product']"):
          img = block.xpath("//img/@src")[0]
          name = block.xpath("//tr[@class='name']")[0].text
          id = block.xpath("//tr[@class='id']")[0].text
      
  • +1
    Для парсинга сайтов лучше использовать специализированные библиотеки, например Grab. В ней, кроме собственно парсинга (xpath- и css-методы), есть много плюшек, заточенных под быстрый обход сайта и упрощение построение запросов: многопоточность, куки, асинхронность и т.д.
    • 0
      Поработаю некропостером, и скажу, что я пришел на эту статью, чтобы разобраться с lxml, как и советует докуменатция Grab'а. Круг замкнулся.
      • 0
        Спустя два года дам новый совет — попробуйте PySpider — очень мощный инструмент. Для работы с HTML там можно использовать несколько инструментов — по вкусу, по удобству.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.