Импортируем необходимые библиотеки:
from bs4 import BeautifulSoup
import requests
Загрузим страницу с проходными баллами с сайта Вышки:
page = requests.get('https://ma.hse.ru/passingrade')
Создадим объект soup
и сохраним в него html-код страницы:
soup = BeautifulSoup(page.text, 'lxml')
Проходные баллы и названия программ хранятся в таблице, причём не просто в таблице, а в таблице с атрибутом class
, равным data table bordered
(см. исходный код страницы):
table = soup.findAll('table', {'class' : 'bordered'})[0]
Баллы и названия курсов нужно искать в пределах этой таблицы. Обратим внимание, что все баллы заключены в тэги <strong>
:
table.findAll('strong')[0:5] # для примера первые 5 элементов
[<strong> 35</strong>, <strong> 21</strong>, <strong> 33</strong>, <strong> 21</strong>, <strong> 30</strong>]
Сохраним все баллы в список, вытащив из тэгов текст. Воспользуемся списковым включением:
sc = table.findAll('strong')
scores = [score.text for score in sc]
scores[0:5] # первые несколько примеров
['\xa035', '\xa021', '\xa033', '\xa021', '\xa030']
Значения сохранились с пробелами вначале, которые в текущей кодировке страницы записываются как \xa0
. Пока их трогать не будем, лучше преобразуем, когда созраним все значения в датафрейм. Лучше обратим внимание на такую проблему: в списке не хватает одного значения: проходной балл для коммерческих мест на программе «Физика». Соответствующая ячейка не заполнена (таких нет), но это сбивает последующую нумерацию элементов, а нам она пригодится. Добавим пропущенное значение (дефис) сами на нужное место:
print(scores.index('110'))
22
scores.insert(23, '-')
scores[22:25]
['110', '-', '\xa0-']
Похожая история с магистратурой «Церковь, общество, государство». Добавим недостающее «пустое» значение:
scores.insert(153, '-')
Как отделить баллы, соответствующие бюджетным местам, от баллов на коммерческие места? Можно заметить, что все баллы на бюджетные места имеют чётные индексы в списке scores
, а на коммерческие – нечётные. Воспользуемся этим и рассортируем значения:
budget = []
comm = []
for i in range(0, len(scores)):
if i % 2 == 0:
budget.append(scores[i])
else:
comm.append(scores[i])
Списки с баллами сформированы, осталось выгрузить названия образовательных программ. Заметим, что названия сохранены в тех же тэгах, что и ссылки на них (тэг a
). Воспользуемся этим:
links = table.findAll('a')
masters = [link.text for link in links]
masters[0:5] # несколько примеров из списка
['Математика', 'Mathematics', 'Математика и математическая физика', 'Совместная магистратура НИУ ВШЭ и Центра педагогического мастерства', 'Анализ данных в биологии и медицине']
Теперь попробуем объединить полученные списки в датафрейм, предварительно создав словарь, где ключами являются названия столбцов, а значениями – списки значений:
import pandas as pd
df = pd.DataFrame({'programme' : masters,
'budget' : budget,
'commerce' : comm})
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-13-95449f4717d4> in <module>() 1 df = pd.DataFrame({'programme' : masters, 2 'budget' : budget, ----> 3 'commerce' : comm}) /anaconda3/lib/python3.6/site-packages/pandas/core/frame.py in __init__(self, data, index, columns, dtype, copy) 346 dtype=dtype, copy=copy) 347 elif isinstance(data, dict): --> 348 mgr = self._init_dict(data, index, columns, dtype=dtype) 349 elif isinstance(data, ma.MaskedArray): 350 import numpy.ma.mrecords as mrecords /anaconda3/lib/python3.6/site-packages/pandas/core/frame.py in _init_dict(self, data, index, columns, dtype) 457 arrays = [data[k] for k in keys] 458 --> 459 return _arrays_to_mgr(arrays, data_names, index, columns, dtype=dtype) 460 461 def _init_ndarray(self, values, index, columns, dtype=None, copy=False): /anaconda3/lib/python3.6/site-packages/pandas/core/frame.py in _arrays_to_mgr(arrays, arr_names, index, columns, dtype) 7313 # figure out the index, if necessary 7314 if index is None: -> 7315 index = extract_index(arrays) 7316 7317 # don't force copy because getting jammed in an ndarray anyway /anaconda3/lib/python3.6/site-packages/pandas/core/frame.py in extract_index(data) 7359 lengths = list(set(raw_lengths)) 7360 if len(lengths) > 1: -> 7361 raise ValueError('arrays must all be same length') 7362 7363 if have_dicts: ValueError: arrays must all be same length
Не получилось! Почему? Потому что списки имеют разную длину. Проверим!
print(len(budget), len(comm), len(masters))
110 110 144
Действительно, длины разные. Посмотрим, в чём проблема:
masters[0:5]
['Математика', 'Mathematics', 'Математика и математическая физика', 'Совместная магистратура НИУ ВШЭ и Центра педагогического мастерства', 'Анализ данных в биологии и медицине']
Англоязычные названия программ, реализуемых на английском языке, тоже сохраняются в списке! Уберём дубликаты с помощью регулярных выражений: отфильтруем только те названия, которые на кириллице.
import re # импортируем модуль для регулярных выражений
re.findall('[А-Я|а-я]+', masters[0]) # для первого элемента
['Математика']
# поместим всё в цикл
progs = []
for p in masters:
res = re.findall('([А-Я]|[а-я])', p)
if len(res) != 0:
progs.append(p)
progs[0:10] # готово!
['Математика', 'Математика и математическая физика', 'Совместная магистратура НИУ ВШЭ и Центра педагогического мастерства', 'Анализ данных в биологии и медицине', 'Математические методы моделирования и компьютерные технологии', 'Науки о данных', 'Прикладная статистика с методами сетевого анализа', 'Статистическая теория обучения', 'Финансовые технологии и анализ данных', 'Системы управления и обработки информации в инженерии']
Проверим длины списков теперь:
print(len(budget), len(comm), len(progs))
110 110 118
Опять много! Уберём баллы для очно-заочной формы обучения (в списках баллов их нет, если что, сгрузим отдельно):
progs = progs[0:-7]
print(len(budget), len(comm), len(progs)) # и снова неудача!
110 110 111
Из-за того, что в случае совместного конкурса в ячейке с программами по экономике стоят сразу две программы («Прикладная экономика» и «Экономика: исследовательская программа»), у нас образовалось лишнее значение. Давайте одно удалим, а второе честно поправим на более общее:
progs.index("Экономика: исследовательская программа")
32
progs.remove("Экономика: исследовательская программа")
progs[31] = 'Единый конкурс на образовательные программы "Прикладная экономика" и "Экономика: исследовательская программа"'
print(len(budget), len(comm), len(progs)) # ура!
110 110 110
df = pd.DataFrame({'programme' : progs,
'budget' : budget,
'commerce' : comm})
df.head()
programme | budget | commerce | |
---|---|---|---|
0 | Математика | 35 | 21 |
1 | Математика и математическая физика | 33 | 21 |
2 | Совместная магистратура НИУ ВШЭ и Центра педаг... | 30 | 21 |
3 | Анализ данных в биологии и медицине | 53 | 32 |
4 | Математические методы моделирования и компьюте... | 71 | 69 |
Осталось привести в порядок значения в столбцах с баллами – сделать их числовыми (пока они текстовые). Напишем функцию, которая будет принимать на вход строку, убирать пробелы и конвертировать её в целое число. Если это сделать невозможно (например, в графе стоит прочерк, то функция будет возвращать None
).
def to_number(x):
n = x.strip()
try:
res = int(n)
except:
res = None
return res
df['commerce'] = df.commerce.apply(to_number)
df.info() # готово!
<class 'pandas.core.frame.DataFrame'> RangeIndex: 110 entries, 0 to 109 Data columns (total 3 columns): programme 110 non-null object budget 91 non-null float64 commerce 105 non-null float64 dtypes: float64(2), object(1) memory usage: 2.7+ KB
Сохраним таблицу в Excel-файл:
df.to_excel('master-hse.xlsx')