October 29, 2021

Маневры между тегами при парсинге с Python

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

Освоим нюансы такой навигации на конкретном примере. Пусть перед нами стоит задача извлечь сведения о проведенном поединке между бойцами UFC (сайт - www.ufcstats.com).

Чтобы понять, в каких тегах содержатся искомые элементы следует навести курсор на элемент и нажать исследовать (в Firefox):

И первая строка описаний поединка и вторая содержатся в тегах <p> с атрибутом class="b-fight-details__text", а конкретные элементы а тегах <i>:

Чтобы извлечь нужные значения, следует обратиться к содержимому тегов <i> с классом b-fight-details__label:

Для парсинга элемента можно извлечь его имя с помощью метода get_text, а чтобы получить свойство - обратиться к родителю, извлечь содержимое с get_text и заменить имя на пустую строку. Загрузим страницу и проделаем описанную работу (часть шагов рассматривалась ранее):

Найдем все теги с <i> с классом b-fight-details__label и исследуем первый:

а теперь посмотрим на родителя:

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

def get_dict_tags_text(parent_tag):
    first_child = [it for it in parent_tag if isinstance(it, bs4.element.Tag)][0]
    key = first_child.get_text().strip()
    value =  parent_tag.get_text().strip().replace(key,'').strip()
    return (key, value)

Вторая строка (где поле Details:) сложнее, так как она может иметь несколько видов:

такой, когда дело дошло до решения судей:

в противном случае - такой:

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

Загрузим новую ссылку (используйте другой url, который был выше закомментирован), извлечем все теги с <i> с классом b-fight-details__label, рассмотрим элемент прародителя нашего свойства Details:

Теперь для этого элемента осталось пройтись по списку дочерних тегов:

Все это можно объединить:

f_det_res_t = bsObj.findAll('i',{'class':'b-fight-details__label'})

det_res = {}
for i,f_det_res_tag in enumerate(f_det_res_t):
    key = f_det_res_tag.get_text().strip()
    if not key=='Details:':
        k, v =  get_dict_tags_text(f_det_res_tag.parent)
        det_res[k] = v
    else:
        det_tag = f_det_res_tag.parent.parent
        childs = list(det_tag.children)
        childs = [it for it in childs if isinstance(it, bs4.element.Tag)]
        if len(childs)==1:
            k, v =  get_dict_tags_text(det_tag)
            det_res[k] = v
        else:
            s = ''
            for child_tag in childs[1:]:
                s = s + ':'.join(get_dict_tags_text(child_tag))+';'
            det_res[key] = s
det_res