CoderCastrov logo
CoderCastrov
Парсер

Парсинг таблиц без текста

Парсинг таблиц без текста
просмотров
7 мин чтение
#Парсер

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

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

Если вы просто хотите спарсить полную таблицу с Википедии, вы можете легко воспользоваться этим сайтом, чтобы сгенерировать файл csv. Конечно, вы также можете сделать это на Python с помощью Requests и BeautifulSoup, но, честно говоря, нет необходимости изобретать велосипед, когда у вас уже есть отличная альтернатива. Однако на этой неделе мы рассмотрим таблицу с результатами сезона 1 шоу "The Great British Bake Off".

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

Baker,1,2,3,4,5,6
Edd,,,,,,WINNER
Ruth,,,,,,Runner-up
Miranda,,,,,,Runner-up
Jasminder,,,,,OUT,
David,,,,OUT,,
Jonathan,,,OUT,,,
Annetha,,OUT,,,,
Louise,,OUT,,,,
Lea,OUT,,,,,
Mark,OUT,,,,,

А вот как выглядит таблица на самой странице:

Очевидно, что csv-файл не учитывает цвета в этой таблице, и, как мы видим в ключе цветов, эти цвета несут потенциально ценную информацию:

Поэтому нам нужно зайти в код, чтобы получить всю информацию. Как всегда, мы сначала передаем URL в BeautifulSoup, чтобы пройти через HTML:

import requests
from bs4 import BeautifulSoup
import pandas as pd
import re

url = 'https://en.wikipedia.org/wiki/The_Great_British_Bake_Off_(series_1)'
page = requests.get(url)
soup = BeautifulSoup(page.content)

Теперь мы собираемся выделить таблицу с результатами:

elim_chart = soup.find('th', text=re.compile('Elimination Chart')).find_parent('table')

Эти строки фактически говорят: "посмотрите все таблицы, если текст 'Elimination Chart' есть в тексте заголовка, то это то, что мы определяем как 'elim_chart'".

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

['Baker', '1', '2', '3', '4', '5', '6']

Чтобы сгенерировать этот список, вот код:

header_list = [item.text.rstrip() for item in elim_chart.find_all('th') if not item.text.startswith('Elim')]

Эти строки фактически говорят: "для элементов в таблице, которые имеют тег 'th', добавьте текст каждого элемента в список заголовков, если текст элемента не начинается с 'Elim' (поскольку мы не хотим парсить большой заголовок 'Elimination Chart')". .rstrip() добавляется в конце, потому что в исходном списке после цифры 6 был символ новой строки, и это может усложнить задачу.

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

meaning_list = soup.find('b', text=re.compile('Colour key:')).find_all_next('dd', limit=6)

Эта строка фактически парсит всю информацию о цветах и тексте, назначенном им в ключе цветов. Используя re.compile при поиске текста, вам не нужно иметь точное совпадение, что может сильно упростить вашу жизнь, если учесть потенциальные невидимые символы, такие как пробелы и символы новой строки. Затем мы погружаемся в этот список, делая следующее:

Сначала мы создаем два списка: color_list, который является списком всех цветов в ключе цветов, и meaning_text_list, который является списком текста, назначенного каждому цвету. Мы получаем доступ к цветам, выполнив re.split на части стиля каждого элемента в списке meaning_list, сделав фразу перед цветом, background-color:, незахватывающей группой, а затем захватывая слово непосредственно после него. Затем мы преобразуем все слова в списке цветов в нижний регистр, чтобы избежать возможных несоответствий из-за несогласованного написания. Чтобы получить текст каждого значения цвета, мы разбиваем текст элемента и захватываем слова после тире, затем берем последний элемент в полученном списке. Затем мы объединяем эти два списка в один словарь full_text_meaning_dict:

{'lightblue': 'Baker got through to the next round',
 'orangered': 'Baker was eliminated',
 'plum': "Baker was one of the judges' least favourite bakers that week, but was not eliminated",
 'cornflowerblue': "Baker was one of the judges' favourite bakers that week",
 'limegreen': 'Baker was a series runner-up',
 'yellow': 'Baker was the series winner'}

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

shortened_text_list = ['next_round', 'eliminated', 'least_fav', 'fav', 'runner_up', 'winner']

Затем я объединил этот список и список цветов, чтобы получить этот словарь:

{'lightblue': 'next_round',
 'orangered': 'eliminated',
 'plum': 'least_fav',
 'cornflowerblue': 'fav',
 'limegreen': 'runner_up',
 'yellow': 'winner'}

Затем я добавил следующую запись, silver определенную как not_in_comp (не участвует в соревновании), поскольку она не была включена в ключ цветов, но будет необходима при просмотре таблицы:

{'silver': 'not_in_comp'}

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

Мы будем парсить эту таблицу, выполнив следующее:

row_list = []
for row in elim_chart.find_all('tr')[1:]:
    name = row.find('th').text
    row = [name] + [item.text for item in row.find_all('td')]
    for i, item in enumerate(row):
        col = 1
        color = item.split(' ')[0]
        if len(item.split(' ')) > 1:
            col = int(item.split(' ')[1])
        row[i] = (color.rstrip(), shortened_text_list[meaning_list.index(full_text_meaning_dict[color])]) * col
    row_list.append(dict(row))

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

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

Отсюда мы смотрим на каждый элемент в каждой строке (строка 7). Для каждого элемента мы получаем цвет этого элемента, и у нас есть переменная col, которая устанавливается равной единице. У нас есть эта переменная col, потому что иногда, если участник получает то же самое значение для двух последовательных эпизодов (т.е. он является фаворитом судей в течение двух недель подряд), код просто скажет "создать блок цвета фаворита судей, который занимает два столбца". Однако это усложняет задачу, когда мы пытаемся воссоздать таблицу, и нам нужно шесть элементов для каждой строки. Поэтому строки 12 и 13 говорят: "Если код указывает, что блок цвета шире одного столбца, сделайте переменную col равной количеству столбцов, которые занимает этот блок". Строки 15-17 затем добавляют этот цвет в список строк, обрезая переменную color, созданную в строке 9, чтобы получить только имя цвета, затем запуская этот цвет через словарь, который мы создали, определяющий значение каждого цвета, а затем умножая этот цвет на переменную col. Это гарантирует, что каждый список строк, который мы создаем, имеет одинаковую длину и представляет повторяющиеся результаты. В конце мы получаем список строк, который выглядит так:

[{'Baker': 'Edd',
  '1': 'next_round',
  '2': 'fav',
  '3': 'fav',
  '4': 'least_fav',
  '5': 'fav',
  '6': 'winner'},
 {'Baker': 'Ruth',
  '1': 'fav',
  '2': 'next_round',
  '3': 'fav',
  '4': 'fav',
  '5': 'fav',
  '6': 'runner_up'},
 {'Baker': 'Miranda',
  '1': 'fav',
  '2': 'fav',
  '3': 'next_round',
  '4': 'fav',
  '5': 'least_fav',
  '6': 'runner_up'},
 {'Baker': 'Jasminder',
  '1': 'next_round',
  '2': 'next_round',
  '3': 'least_fav',
  '4': 'fav',
  '5': 'eliminated',
  '6': 'not_in_comp'},
 {'Baker': 'David',
  '1': 'least_fav',
  '2': 'least_fav',
  '3': 'least_fav',
  '4': 'eliminated',
  '5': 'not_in_comp',
  '6': 'not_in_comp'},
 {'Baker': 'Jonathan',
  '1': 'next_round',
  '2': 'fav',
  '3': 'eliminated',
  '4': 'not_in_comp',
  '5': 'not_in_comp',
  '6': 'not_in_comp'},
 {'Baker': 'Annetha',
  '1': 'fav',
  '2': 'eliminated',
  '3': 'not_in_comp',
  '4': 'not_in_comp',
  '5': 'not_in_comp',
  '6': 'not_in_comp'},
 {'Baker': 'Louise',
  '1': 'least_fav',
  '2': 'eliminated',
  '3': 'not_in_comp',
  '4': 'not_in_comp',
  '5': 'not_in_comp',
  '6': 'not_in_comp'},
 {'Baker': 'Lea',
  '1': 'eliminated',
  '2': 'not_in_comp',
  '3': 'not_in_comp',
  '4': 'not_in_comp',
  '5': 'not_in_comp',
  '6': 'not_in_comp'},
 {'Baker': 'Mark',
  '1': 'eliminated',
  '2': 'not_in_comp',
  '3': 'not_in_comp',
  '4': 'not_in_comp',
  '5': 'not_in_comp',
  '6': 'not_in_comp'}]

Отсюда все, что нам нужно сделать, чтобы превратить это в настоящую таблицу pandas, это следующее:

df = pd.DataFrame.from_dict(row_list)
df.set_index('Baker', inplace=True)

Обратите внимание, что я установил столбец с именем участника в качестве индекса, потому что это имеет больше смысла для меня. Этот код создает следующую таблицу:

Теперь это просто фактический парсинг таблицы со страницы. Однако, если вы хотите выполнить какой-либо анализ этих данных, вам придется создать фиктивные переменные. Если вы не знаете, что такое фиктивная переменная, это в основном числовое представление категориальных данных (в данном случае, как участник прошел в эпизоде). В pandas есть функция под названием get_dummies. Если мы сделаем это:

pd.get_dummies(df)

Мы получим это:

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

t_df = df.transpose()

Этот фрейм данных выглядит так:

Затем создание фиктивных переменных выглядит так:

pd.get_dummies(t_df)

И это результат:

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