February 4, 2021

Пример парсинга всей страницы на Python

На  пути к реализации полнофункционального парсера/скрапера каждый раз  приходится сталкиваться с задачей разбора отдельных тегов на странице  для извлечения требуемой информации. Так и в нашем случае, чтобы  заработала программа автоматического закачивания сведений с заданного  сайта, требуется реализовать не только модули общей функциональности  (обход страниц и ссылок на них), но и методы прохождения по тегам и извлечения их контента.

Напомню, что мы занимаемся поэтапной реализацией парсера, согласно приведенной схеме (подробнее здесь):

Мы  столкнулись с необходимостью заполнения специфических для сайта полей  (инструментарий "Специфического модуля"). Задачу будем рассматривать на  примере разбора страницы о результатах поединка, организованного  спортивной организацией UFC (подробнее здесь):

Ранее мы рассмотрели применение методов объекта BeautifulSoup  find и findAll,  позволяющих получать первый и все результаты поиска тегов,  свойств BeautifulSoup parent и children для навигации по родительским и дочерним тегам (подробнее здесь), а также написали функцию, извлекающую набор тегов из контейнера get_items_list_from2tags (подробнее здесь).  В результате скачана информация из верхней половины страницы с именами  бойцов, деталями завершения схватки (метод выигрыша, раунд, время  завершения, судья поединка).

Оставшаяся часть информации организована в виде секций:

Для их извлечения необходимо набрать следующие команды:

sections = bsObj.findAll('section', {'class':'b-fight-details__section js-fight-section'})
sections = [sec for i, sec in enumerate(sections) if not i in [0,3] ]

Первая  и четвертая по порядку секции нас не интересуют, так как содержат  только заголовки "TOTALS" и "SIGNIFICANT STRIKES". Кроме того, по  непонятным причинам секция с таблицей общих результатов по областям  нанесенных повреждений находится не в секции, а в теге "table" (но в  окружении тегов section).

Так как соответствующий тег не имеет значимых атрибутов (только стиль), простой поиск с использованием findAll  дает много результатов. Поэтому  я сначала нашел  родителя искомого  элемента (тег div с 'class'='b-fight-details, он в единственном  экземпляре на странице) а затем в цикле по дочерним тегам выбрал  необходимый по имени и наличию поля стиля (остальные - это секции, у  которых этого поля нет):

sign_st_parent = bsObj.find('div',{'class':'b-fight-details'})
for child in sign_st_parent.children:
 try:  
   if  'style' in child.attrs:
       sign_st_table = child
 except:
     pass

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

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

sec_head_t = ItemsParser.get_items_list_from2tags(sections[0],'thead,class,b-fight-details__table-head',\
'th,class,b-fight-details__table-col', delay)
[item.get_text().strip() for item in sec_head_t]

Этот  процесс можно облечь в функцию. И соответственно, так можно получить  нужные списки альтернативным образом:

def get_head_details(section, tag_container, tag_els, delay):
       sec_head_t = ItemsParser.get_items_list_from2tags(section,tag_container,tag_els, delay)
       sec_head = [item.get_text().strip() for item in sec_head_t]    
       return sec_head

sign_act_head = get_head_details(sections[0],'thead,class,b-fight-details__table-head', 'th,class,b-fight-details__table-col', delay)
sign_st_head = get_head_details(sign_st_table,'thead,class,b-fight-details__table-head', 'th,class,b-fight-details__table-col', delay)

Чтобы  получить значения столбцов для общей статистики потребуется сначала  извлечь теги, в которых хранятся пары результатов (по одному для каждого  из бойцов), а затем посредством вызова функции get_fight_det_l  с этими тегами на входе получить список пар. В данной функции для  получения каждой очищенной пары, извлекается содержимое тега их родителя  (get_text), а потом, так как результат  содержит много пробельных и символов перевода на новую строку,  происходит разбиение содержимого по '\n' с взятием только начала и конца  последовательности (где и будут искомые значения):

def get_fight_det_l(tags_l):
   res_l = []
   for item in tags_l:
       l = item.get_text().strip().split('\n')
       res_l.append([item.strip() for i, item in enumerate(l) if i in[0,len(l)-1]])
   return res_l

sign_act_body_t = ItemsParser.get_items_list_from2tags(sections[0],'tbody,class,b-fight-details__table-body', 'td,class,b-fight-details__table-col', delay)
sign_act_body = get_fight_det_l(sign_act_body_t)

sign_st_body_t  = ItemsParser.get_items_list_from2tags(sign_st_table,'tbody,class,b-fight-details__table-body','td,class,b-fight-details__table-col', delay)
sign_st_body_res = get_fight_det_l(sign_st_body_t)

И  напоследок извлечение пораундовой статистики, которое в обоих случаях  (для таблиц статистики активности и распределения значимых ударов )  происходит схожим образом. Сначала получаем теги с содержимым статистики  по раундам (первый тег ненужный), а затем для каждого собираем искомые  значения  посредством вышеуказанной функции get_fight_det_l:

sign_act_rounds = []
rounds_body = sections[1].findAll('tr',{'class','b-fight-details__table-row'})
for i,sec in enumerate(rounds_body):
   if (i!=0):
       sign_act_rounds.append(get_fight_det_l(sec.findAll('td',{'class':'b-fight-details__table-col'})))

sign_st_rounds=[]
rounds_body = sections[2].findAll('tr',{'class','b-fight-details__table-row'})

for i,sec in enumerate(rounds_body):
   if (i!=0):
       sign_st_rounds.append(get_fight_det_l(sec.findAll('td',{'class':'b-fight-details__table-col'})))