Перейти к основному содержимому

Парсинг рейтингов университетов

Одна из самых популярных «пвузомерок» — международные рейтинги университетов. Есть 🇷🇺 российские, есть зарубежные. В данном материале собраны 👨‍🍳 «рецепты» парсинга следующих рейтингов:

  1. Московский рейтинг университетов — три миссии университета
  2. Рейтинг университетов Интерфакса
  3. RAEX ТОП 100
  4. Шанхайский рейтинг университетов, в т.ч. по предметам
  5. Рейтинг Best Global Universities, в т.ч. по предметам
  6. Рейтинг университетов от Times Higher Education
  7. QS-рейтинг университетов, в т.ч. по предметам

Рейтинги выходят (обновляются) в разное время года. На момент написания данного материала (июль 2023 г.), многие рейтинги включали 2023 год (внимание на переменную years, если она используется в коде).

Московский рейтинг университетов

Необходимые библиотеки
import time
import requests
import pandas as pd
from bs4 import BeautifulSoup
Парсим данные рейтинга
years = list(range(2020, 2022 + 1))
msk_data = []

for y in years:
# Обратите внимание на вариативность в url (последний год отличается от всех остальных)
msk_url = f"https://mosiur.org/{'ranking' if y == 2022 else 'ranking' + str(y)}/"
msk_resp = requests.get(msk_url)

if msk_resp.status_code == 200:
msk_soup = BeautifulSoup(msk_resp.text)

for tr in msk_soup.select('#top_table tbody tr'):
tds = tr.find_all('td')
msk_data.append({
'year': y,
'rank': tds[0].text,
'university': tds[1].text,
'country': tds[2].text
})
else:
print(f"{msk_url} return status code {msk_resp.status_code}")

# Бережём источник данных
time.sleep(1)

msk_df = pd.DataFrame(msk_data)
msk_df.to_csv(f"data/msk_{min(years)}-{max(years)}.csv", index=False)
msk_df.to_excel(f"data/msk_{min(years)}-{max(years)}.xlsx", index=False)
msk_df.head(5)
yearrankuniversitycountry
020201Harvard UniversityСША
120202Massachusetts Institute of TechnologyСША
220203University of CambridgeВеликобритания
320204University of OxfordВеликобритания
420205University of PennsylvaniaСША

Данные «Московского рейтинга» получены. Давайте посмотрим представительство стран по годам (ТОП 10).

Строим сводную таблицу и выводим ТОП 10
msk_pivot = pd.pivot_table(
msk_df,
index='country',
columns='year',
values='university',
aggfunc='count'
)
msk_pivot['sum'] = msk_pivot.sum(axis=1)
msk_pivot.sort_values(by='sum', ascending=False).head(10)
year202020212022sum
country
США220.0239.0253.0712.0
Китай122.0144.0173.0439.0
Россия101.0112.0146.0359.0
Великобритания98.0106.0108.0312.0
Япония93.0102.0101.0296.0
Германия69.073.074.0216.0
Италия50.054.054.0158.0
Индия45.051.058.0154.0
Испания42.055.056.0153.0
Франция40.045.054.0139.0

Рейтинг Интерфакса

Необходимые библиотеки
import time
import requests
import pandas as pd
Вспомогательная функция для парсинга данных
def get_interfax_list_by_year(year, page=1):
url = f"https://academia.interfax.ru/data/rating/?rating=1&year={year}&page={page}"
result = []
resp = requests.get(url)

# Бережём источник данных
time.sleep(1)

if resp.status_code == 200:
json_data = resp.json()
result += json_data['universities']
page_count = json_data['page_count']

if page != page_count:
result += get_interfax_list_by_year(year, page + 1)
else:
print(f"{url} return status code {resp.status_code}")

return result
Парсим данные рейтинга
years = list(range(2020, 2023 + 1))
interfax_df = pd.DataFrame()

for y in years:
interfax_df_by_year = pd.DataFrame(get_interfax_list_by_year(y))
interfax_df_by_year['year'] = y
interfax_df = pd.concat([interfax_df, interfax_df_by_year], ignore_index=True)

interfax_df.to_csv(f"data/interfax_{min(years)}-{max(years)}.csv", index=False)
interfax_df.to_excel(f"data/interfax_{min(years)}-{max(years)}.xlsx", index=False)
interfax_df.head()
changedescriptionis_closepointranknameidurlyear
00NoneFalse10001Московский государственный университет имени М...1https://www.msu.ru2020
10NoneFalse9632Национальный исследовательский ядерный универс...2https://mephi.ru2020
20NoneFalse9613Московский физико-технический институт (национ...4https://mipt.ru2020
30NoneFalse8574Национальный исследовательский университет «Вы...6https://www.hse.ru2020
41NoneFalse8485Новосибирский национальный исследовательский г...3http://www.nsu.ru/?lang=ru2020

В данных рейтинга Интерфакса уже есть значения про движение университета в рамках рейтинга. Давайте посмотрим на ТОП 10 университетов, которые поднялись в рейтинге за последние года больше всех.

Строим сводную таблицу и выводим ТОП 10
interfax_pivot = pd.pivot_table(
interfax_df,
index='name',
columns='year',
values='change',
aggfunc='sum'
)
interfax_pivot['sum'] = interfax_pivot.sum(axis=1)
interfax_pivot.sort_values(by='sum', ascending=False).head(10)
year2020202120222023sum
name
Университет Синергия-1.078.043.044.0164.0
Московский институт психоанализа-8.03.0128.025.0148.0
Чеченский государственный университет имени А.А.Кадырова-22.083.058.014.0133.0
Московский государственный психолого-педагогический университет15.078.018.012.0123.0
Уральский государственный аграрный университет-21.040.0104.0-4.0119.0
Красноярский государственный аграрный университет-1.0-12.0122.03.0112.0
Уральский государственный горный университет25.017.021.048.0111.0
Кировский государственный медицинский университет25.012.031.040.0108.0
Кубанский государственный технологический университет24.018.061.0-9.094.0
Государственный университет просвещения (МГОУ)5.028.051.01.085.0

RAEX ТОП 100

Необходимые библиотеки
import re
import time
import requests
import numpy as np
import pandas as pd
from bs4 import BeautifulSoup
Парсим данные рейтинга
years = list(range(2020, 2023 + 1))
raex_list = []

for y in years:
raex_url = f"https://raex-rr.com/pro/education/russian_universities/top-100_universities/{y}/"
raex_resp = requests.get(raex_url)

if raex_resp.status_code == 200:
raex_soup = BeautifulSoup(raex_resp.text)
raex_trs = raex_soup.select('#rrp_table_wrapper > table > tbody.list > tr')

for i, tr in enumerate(raex_trs):
th = tr.findChildren('th', recursive=False)
td = tr.findChildren('td', recursive=False)

raex_list.append({
'year': y,
'rank': int(th[0].text),
'title': re.sub(r'[\s\n]+', ' ', th[1].text).strip(),
'previous': float(td[0].text) if td[0].text != '-' else np.NaN,
'points': float(td[1].text),
'quality': int(td[2].text),
'graduates': int(td[3].text),
'science': int(td[4].text)
})
else:
print(f"{raex_url} return status code {raex_resp.status_code}")

# Бережём источник данных
time.sleep(1)

raex_df = pd.DataFrame(raex_list)
raex_df.to_csv(f"data/raex_{min(years)}-{max(years)}.csv", index=False)
raex_df.to_excel(f"data/raex_{min(years)}-{max(years)}.xlsx", index=False)
raex_df.head()
yearranktitlepreviouspointsqualitygraduatesscience
020201Московский государственный университет имени М...1.04.8419111
120202Московский физико-технический институт (национ...2.04.7734272
220203Национальный исследовательский ядерный универс...3.04.5535554
320204Санкт-Петербургский государственный университет4.04.53943127
420205Национальный исследовательский университет "Вы...5.04.49336211

В отличие от предыдущих рейтингов, в данном рейтинге каждому университету присваивается конкретная позиция в рейтинге. Давайте посмотрим ТОП 10 лидеров по средней позиции в рейтинге за скачанные года.

Вычисляем ТОП 10 по средней позиции
raex_df[['title', 'rank']].groupby(by='title').mean().sort_values(by='rank').head(10)
rank
title
Московский государственный университет имени М.В. Ломоносова1.000000
Московский физико-технический институт (национальный исследовательский университет)2.000000
Национальный исследовательский ядерный университет «МИФИ»3.500000
Санкт-Петербургский государственный университет3.500000
Национальный исследовательский университет "Высшая школа экономики"5.250000
Московский государственный технический университет имени Н.Э. Баумана (национальный исследовательский университет)6.000000
МГИМО МИД России6.750000
Санкт-Петербургский политехнический университет Петра Великого8.250000
Национальный исследовательский Томский политехнический университет8.666667
Томский политехнический университет9.000000

Обратите внимание на то, что рейтинговое агентство RAEX «богато» на рейтинги (полный список тут). Для парсинга других рейтингов, механики будут +/- аналогичные.

Шанхайский рейтинг университетов

Необходимые библиотеки
import os
import re
import json
import time
import js2py
import requests
import pandas as pd

Общий рейтинг

Забираем данные рейтинга
url_ranking = 'https://www.shanghairanking.com/api/pub/v1/inst'
resp_ranking = requests.get(url_ranking)
ranking = pd.DataFrame()

if resp_ranking.status_code == 200:
json_data = resp_ranking.json()
ranking = pd.json_normalize(json_data['data'])
else:
print(f"{url_ranking} return status code {resp_ranking.status_code}")

ranking.to_csv('data/shanghai.csv', index=False)
ranking.to_excel('data/shanghai.xlsx', index=False)
ranking.head()
nameEnunivLogounivUpregionrankingInforanking
0Harvard Universitylogo/032bd1b77.pngharvard-universityUnited StatesARWU 20221
1Stanford Universitylogo/13de8913b.pngstanford-universityUnited StatesARWU 20222
2Massachusetts Institute of Technology (MIT)logo/79165fd8b.pngmassachusetts-institute-of-technology-mitUnited StatesARWU 20223
3University of Cambridgelogo/8d9861b69.pnguniversity-of-cambridgeUnited KingdomARWU 20224
4University of California, Berkeleylogo/0ff179fb8.pnguniversity-of-california-berkeleyUnited StatesARWU 20225

Пока данных не много и можно, для примера, посмотреть ТОП 10 🇷🇺 российских университетов в рейтинге.

Выводим ТОП 10 российских университетов
ranking[ranking['region'] == 'Russia'][['nameEn', 'ranking']].head(10)
nameEnranking
114Moscow State University101-150
347Saint Petersburg State University301-400
529Moscow Institute of Physics and Technology501-600
626HSE University601-700
654Sechenov University601-700
742Novosibirsk State University701-800
753Skolkovo Institute of Science and Technology701-800
761Tomsk State University701-800
797Ural Federal University701-800
847National Research Nuclear University MEPhI (Mo...801-900

Предметные и ретроспектива

Для того чтобы получить данные по предметным рейтингам, а также получить ретроспективные данные, потребуется обойти все страницы университетов на сайте. Страниц много (более 3 тыс.), поэтому запасаемся терпением и ждём, переодически проверяем содержимое папки data/downloads/shanghai.

Сохраняем данные по университетам
data_folder = 'data/downloads/shanghai'

for i, r in ranking.iterrows():
file_path = f"{data_folder}/{i+1}.json"

if not os.path.isfile(file_path):
university_path = r['univUp']
university_url = f'https://www.shanghairanking.com/institution/{university_path}'
html_resp = requests.get(university_url)

if html_resp.status_code == 200:
html = html_resp.text
reg = f"\/_nuxt\/static\/\d+\/institution\/{university_path}\/payload\.js"
payload = re.search(f"/_nuxt/static/\d+/institution/{university_path}/payload\.js", html)[0]
university_id = payload.replace('/_nuxt/static/', '').replace(f"/{university_path}/institution/payload.js", '')
js_url = f"https://www.shanghairanking.com/{payload}"
js_resp = requests.get(js_url)

if js_resp.status_code == 200:
js_data = js_resp.text
js_data = js2py.eval_js(js_data.replace(f'__NUXT_JSONP__("/institution/{university_path}", ', '')[:-2])
university_data = js_data.data[0].univData.to_dict()

with open(file_path, 'w') as f:
f.write(json.dumps(university_data, ensure_ascii=False, indent=4))
else:
print(f"{university_url} return status code {html_resp.status_code}")
else:
print(f"{university_url} return status code {html_resp.status_code}")

# Бережём источник данных
time.sleep(0.5)

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

Выводим данные МГУ
example = json.load(open(f"{data_folder}/493.json", 'r'))
example
Данные МГУ
{
"address": "Moskovskij Gosudarstvennyj Universitet im. M.V. Lomonosova, Leninskie Gory, Moscow 119992, Russian Federation",
"detail": {
"arwu": {
"datasetId": 1,
"intro": "The Academic Ranking of World Universities (ARWU) was first published in June 2003 by the Center for World-Class Universities (CWCU), Graduate School of Education (formerly the Institute of Higher Education) of Shanghai Jiao Tong University, China, and updated on an annual basis. Since 2009 the Academic Ranking of World Universities (ARWU) has been published and copyrighted by ShanghaiRanking Consultancy. ShanghaiRanking Consultancy is a fully independent organization on higher education intelligence and not legally subordinated to any universities or government agencies. ARWU uses six objective indicators to rank world universities, including the number of alumni and staff winning Nobel Prizes and Fields Medals, number of highly cited researchers selected by Clarivate, number of articles published in journals of Nature and Science, number of articles indexed in Science Citation Index Expanded™ and Social Sciences Citation Index™ in the Web of Science™, and per capita performance of a university. More than 2500 universities are actually ranked by ARWU every year and the best 1000 are published.",
"latestVerNo": 2022,
"nameId": "ARWU",
"rkHistory": [
{"ranking": "93", "yr": 2020},
{"ranking": "97", "yr": 2021},
{"ranking": "101-150", "yr": 2022}
],
"rkLatest": {"name": "Academic Ranking of World Universities", "ranking": "101-150"}
},
"bcur": null,
"gras": {
"latestVerNo": 2022,
"nameId": "GRAS",
"subjAdva": [
{"categoryCode": "RS01", "categoryNameShort": "SCI", "code": "RS0102", "name": "Physics", "ranking": "76-100"},
{"categoryCode": "RS02", "categoryNameShort": "ENG", "code": "RS0207", "name": "Instruments Science & Technology", "ranking": "201-300"},
{"categoryCode": "RS01", "categoryNameShort": "SCI", "code": "RS0101", "name": "Mathematics", "ranking": "301-400"},
{"categoryCode": "RS01", "categoryNameShort": "SCI", "code": "RS0103", "name": "Chemistry", "ranking": "301-400"},
{"categoryCode": "RS01", "categoryNameShort": "SCI", "code": "RS0106", "name": "Ecology", "ranking": "301-400"},
{"categoryCode": "RS02", "categoryNameShort": "ENG", "code": "RS0214", "name": "Nanoscience & Nanotechnology", "ranking": "301-400"},
{"categoryCode": "RS02", "categoryNameShort": "ENG", "code": "RS0220", "name": "Biotechnology", "ranking": "401-500"},
{"categoryCode": "RS04", "categoryNameShort": "MED", "code": "RS0401", "name": "Clinical Medicine", "ranking": "401-500"}
],
"subjCategory": [
{
"code": "RS01",
"name": "Natural Sciences",
"nameShort": "SCI",
"subj": [
{"code": "RS0101", "name": "Mathematics", "ranking": "301-400"},
{"code": "RS0102", "name": "Physics", "ranking": "76-100"},
{"code": "RS0103", "name": "Chemistry", "ranking": "301-400"},
{"code": "RS0106", "name": "Ecology", "ranking": "301-400"}
]
},
{
"code": "RS02",
"name": "Engineering\n",
"nameShort": "ENG",
"subj": [
{"code": "RS0207", "name": "Instruments Science & Technology", "ranking": "201-300"},
{"code": "RS0214", "name": "Nanoscience & Nanotechnology", "ranking": "301-400"},
{"code": "RS0220", "name": "Biotechnology", "ranking": "401-500"}
]
},
{
"code": "RS04",
"name": "Medical Sciences",
"nameShort": "MED",
"subj": [
{"code": "RS0401", "name": "Clinical Medicine", "ranking": "401-500"
}
]
}
]
}
},
"foundYear": 1755,
"introEn": "",
"nameEn": "Moscow State University",
"programs": [],
"ranking": "101-150",
"rankingInfo": "ARWU 2022",
"region": "Russia",
"regionDetail": "Eastern Europe",
"studentsStatis": [
[
{"nameEn": "Total Enrollment", "ratio": "", "value": "29235"},
{"nameEn": "International Students", "ratio": "29.0%","value": ""}
],
[
{"nameEn": "Undergraduate Enrollment", "ratio": "", "value": ""},
{"nameEn": "International Students", "ratio": "", "value": ""}
],
[
{"nameEn": "Graduate Enrollment", "ratio": "", "value": ""},
{"nameEn": "International Students", "ratio": "", "value": ""}
]
],
"univLogo": "logo/98123a275.png",
"univUp": "moscow-state-university",
"website": "http://www.msu.ru"
}

Для упрощения возьмём только данные связанные с ретроспективой по основному рейтингу и предметные рейтинги.

Собираем данные по ретроспективе и предметным рейтингам
file_numbers = sorted(list([int(x.replace('.json', '')) for x in os.listdir(data_folder)]))
rating_retro = pd.DataFrame()
rating_subject = pd.DataFrame()

for fn in file_numbers:
file_path = f"{data_folder}/{fn}.json"
json_data = json.load(open(file_path, 'r'))

if json_data['detail']['arwu'] and 'rkHistory' in json_data['detail']['arwu'].keys():
chunk_retro = pd.json_normalize(json_data['detail']['arwu']['rkHistory'])
chunk_retro['nameEn'], chunk_retro['region'] = json_data['nameEn'], json_data['region']
rating_retro = pd.concat([rating_retro, chunk_retro], ignore_index=True)

if json_data['detail']['gras']['subjAdva']:
chunk_subject = pd.json_normalize(json_data['detail']['gras']['subjAdva'])
chunk_subject['nameEn'], chunk_subject['region'] = json_data['nameEn'], json_data['region']
rating_subject = pd.concat([rating_subject, chunk_subject], ignore_index=True)

retro_years = list(rating_retro['yr'].unique())
rating_retro.to_csv(f"data/shanghai_retro_{min(retro_years)}-{max(retro_years)}.csv", index=False)
rating_retro.to_excel(f"data/shanghai_retro_{min(retro_years)}-{max(retro_years)}.xlsx", index=False)
rating_subject.to_csv('data/shanghai_subject.csv', index=False)
rating_retro.to_excel('data/shanghai_subject.xlsx', index=False)

В качестве результатов по ретроспективе, давайте посмотрим количество российских университетов по годам.

Выводим статистику по годам
rating_retro[rating_retro['region'] == 'Russia'][['yr', 'nameEn']].\
groupby(by='yr').count().rename(columns={'nameEn': 'count'})
count
yr
202013
202113
202213

В предметных рейтингах выделим самые представленные области университетами 🇷🇺 России.

Выводим статистику по предметам
rating_subject[rating_subject['region'] == 'Russia'][['name', 'nameEn']].\
groupby(by='name').count().rename(columns={'nameEn': 'count'}).\
sort_values(by='count', ascending=False).head(10)
count
name
Physics6
Metallurgical Engineering6
Biological Sciences4
Nanoscience & Nanotechnology4
Mathematics4
Materials Science & Engineering3
Agricultural Sciences2
Economics2
Pharmacy & Pharmaceutical Sciences2
Mechanical Engineering2

Best Global Universities

Необходимые библиотеки
import os
import json
import time
import requests
import numpy as np
import pandas as pd
from bs4 import BeautifulSoup

Особенности данного рейтинга, что на сайте не сохраняется ретроспектива. Парсить будем что есть в сейчас на сайте рейтинга (на момент написания рейтинг 2022-2023).

Для начала станем немного больше похожими на браузер.

Создаём сессию для работы с сайтом
session = requests.session()
session.headers.update({
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'ru',
'cache-control': 'no-cache',
'pragma': 'no-cache',
'sec-ch-ua': '".Not/A)Brand";v="99", "Google Chrome";v="103", "Chromium";v="103"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Linux"',
'sec-fetch-dest': 'document',
'sec-fetch-mode': 'navigate',
'sec-fetch-site': 'none',
'sec-fetch-user': '?1',
'upgrade-insecure-requests': '1',
'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36'
})

Общий рейтинг

Сборка данных общего рейтинга
data_list = []
url_tpl = 'https://www.usnews.com/education/best-global-universities/search?format=json&page='
url_first = url_tpl + '1'
resp_first = session.get(url_first)

if resp_first.status_code == 200:
data_first = resp_first.json()
total_pages = data_first['total_pages']
data_list += data_first['items']

for page_number in range(2, total_pages + 1):
url = url_tpl + str(page_number)
resp = session.get(url)

if resp.status_code == 200:
json_data = resp.json()
data_list += json_data['items']
else:
print(f"{url} return status code {resp.status_code}")

# Бережём источник данных
time.sleep(1)
else:
print(f"{url_first} return status code {resp_first.status_code}")

ranking = pd.DataFrame(data_list)
ranking.head()
urlidnamecitycountry_namethree_digit_country_coderanksstatsimage_urlblurb
0https://www.usnews.com/education/best-global-u...166027Harvard UniversityCambridge (U.S.)United StatesUSA[{'value': '1', 'is_tied': False, 'is_ranked':...[{'value': '100.0', 'label': 'Global Score'}, ...{'src': 'https://www.usnews.com/object/image/0...Founded in 1636, Harvard University is the old...
1https://www.usnews.com/education/best-global-u...166683Massachusetts Institute of Technology (MIT)Cambridge (U.S.)United StatesUSA[{'value': '2', 'is_tied': False, 'is_ranked':...[{'value': '97.7', 'label': 'Global Score'}, {...{'src': 'https://www.usnews.com/object/image/0...Massachusetts Institute of Technology, founded...
2https://www.usnews.com/education/best-global-u...243744Stanford UniversityStanfordUnited StatesUSA[{'value': '3', 'is_tied': False, 'is_ranked':...[{'value': '95.2', 'label': 'Global Score'}, {...{'src': 'https://www.usnews.com/object/image/0...Stanford University was founded in 1885 and is...
3https://www.usnews.com/education/best-global-u...110635University of California BerkeleyBerkeleyUnited StatesUSA[{'value': '4', 'is_tied': False, 'is_ranked':...[{'value': '88.7', 'label': 'Global Score'}, {...{'src': 'https://www.usnews.com/object/image/0...The University of California—Berkeley is situa...
4https://www.usnews.com/education/best-global-u...503637University of OxfordOxfordUnited KingdomGBR[{'value': '5', 'is_tied': False, 'is_ranked':...[{'value': '86.8', 'label': 'Global Score'}, {...{'src': 'https://www.usnews.com/object/image/0...The exact date of the University of Oxford’s f...

Вытащим данные про рейтинг из столбца ranks и сохраним результаты, а далее выстроим, для примера, производный рейтинг стран по медианной позиции университетов в рейтинге (посмотрим ТОП 10).

Приводим данные в порядок и выводим статистику по странам
for i, r in ranking.iterrows():
rank_value = r['ranks'][0]['value']

ranking.loc[i, 'ranks.value'] = np.NaN if rank_value == 'Unranked' else int(rank_value.replace(',', ''))
ranking.loc[i, 'ranks.is_tied'] = r['ranks'][0]['is_tied']
ranking.loc[i, 'ranks.is_ranked'] = r['ranks'][0]['is_ranked']
ranking.loc[i, 'ranks.label'] = r['ranks'][0]['label']

# Сохраняем результаты
ranking.to_csv('data/best_global_universities.csv', index=False)
ranking.to_excel('data/best_global_universities.xlsx', index=False)

# ТОП 10 стран по медиане
ranking[['country_name', 'id', 'ranks.value']].groupby(by='country_name').agg({
'id': 'count',
'ranks.value': 'median'
}).rename(columns={
'id': 'count',
'ranks.value': 'rank_median'
}).sort_values(by='rank_median').head(10)
countrank_median
country_name
Netherlands1597.5
Hong Kong7100.0
Switzerland12150.0
Singapore4231.0
Denmark7261.0
Australia39273.0
Belgium11292.0
Sweden19346.5
Finland11447.0
Iceland1452.0

Предметные

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

Парсим список предметов
subjects = []
subject_first_url = 'https://www.usnews.com/education/best-global-universities/rankings'
subject_first_resp = session.get(subject_first_url)

if subject_first_resp.status_code == 200:
subject_html = subject_first_resp.text
subject_soup = BeautifulSoup(subject_html)
subject_select = subject_soup.find('select', {'name': 'subject'})

for o in subject_select.find_all('option'):
if o['value']:
subjects.append({
'title': o.text,
'value': o['value']
})
else:
print(f"{subject_first_url} return status code {subject_first_resp.status_code}")

pd.DataFrame(subjects).head()
titlevalue
0Agricultural Sciencesagricultural-sciences
1Artificial Intelligenceartificial-intelligence
2Arts and Humanitiesarts-and-humanities
3Biology and Biochemistrybiology-biochemistry
4Biotechnology and Applied Microbiologybiotechnology-applied-microbiology

Теперь скачаем данные по тематикам и сохраним их на диске. Это займёт какое-то время, наберитесь терпения. Процесс можно отслеживать в папке, куда сохраняются данные: data/downloads/bgu_by_subjects.

Если что-то «залипнет», останавливайте и перезапускайте процесс, в том числе, на всякий случай, блок выше, где создаётся объект session. Код каждый запуск будет скачивать только то, что не успел скачать в прошлый раз.

Сохраняем данные по предметным рейтигам
subject_data_folder = 'data/downloads/bgu_by_subjects'
subject_url_prefix = 'https://www.usnews.com/education/best-global-universities'

for i, s in enumerate(subjects):
subject_value = s['value']
subject_dir = f"{subject_data_folder}/{subject_value.replace('-', '_')}"

# Создаём папки для данных, если их не существует
if not os.path.exists(subject_dir):
os.makedirs(subject_dir)

url_subject_tpl = f"{subject_url_prefix}/{subject_value}?format=json&page="
first_url_subject = url_subject_tpl + '1'
first_resp_subject = session.get(first_url_subject)

if first_resp_subject.status_code == 200:
first_json_subject = first_resp_subject.json()
total_pages_subject = first_json_subject['total_pages']

with open(f"{subject_dir}/1.json", 'w') as f:
f.write(json.dumps(first_resp_subject.json()['items'], ensure_ascii=False, indent=4))

for page_number in range(2, total_pages_subject + 1):
subject_page_file_data = f"{subject_dir}/{page_number}.json"

if os.path.isfile(subject_page_file_data):
continue

url_subject = url_subject_tpl + str(page_number)
resp_subject = session.get(url_subject)

if resp_subject.status_code == 200:
with open(subject_page_file_data, 'w') as f:
f.write(json.dumps(resp_subject.json()['items'], ensure_ascii=False, indent=4))
else:
print(f"{url_subject} return status code {resp_subject.status_code}")

# Бережём источник данных
time.sleep(1)
else:
print(f"{first_url_subject} return status code {first_resp_subject.status_code}")

Соберём всё в кучку из скачанных файлов.

Формируем окончательные результы
df_by_subject = pd.DataFrame()

for s in subjects:
subject_value = s['value']
subject_dir = f"{subject_data_folder}/{subject_value.replace('-', '_')}"
bgu_files_numbers_in_subject = list(sorted([int(x.replace('.json', '')) for x in os.listdir(subject_dir)]))

for n in bgu_files_numbers_in_subject:
file_path = f"{subject_dir}/{n}.json"
json_data = json.load(open(file_path, 'r'))
chunk = pd.json_normalize(json_data)

for i, r in chunk.iterrows():
for rank_idx, rank in enumerate(r['ranks']):
rank_value = rank['value']
chunk.loc[i, f'rank-{rank_idx + 1}.value'] = np.NaN if rank_value == 'Unranked' else int(rank_value.replace(',', ''))
chunk.loc[i, f'rank-{rank_idx + 1}.is_tied'] = rank['is_tied']
chunk.loc[i, f'rank-{rank_idx + 1}.is_ranked'] = rank['is_ranked']
chunk.loc[i, f'rank-{rank_idx + 1}.label'] = rank['label']

df_by_subject = pd.concat([df_by_subject, chunk], ignore_index=True)

df_by_subject.to_csv('data/best_global_universities_subject.csv', index=False)
df_by_subject.to_excel('data/best_global_universities_subject.xlsx', index=False)
df_by_subject.head()
urlidnamecitycountry_namethree_digit_country_coderanksstatsblurbimage_url.srcimage_url.mediumimage_url.largerank-1.valuerank-1.is_tiedrank-1.is_rankedrank-1.labelrank-2.valuerank-2.is_tiedrank-2.is_rankedrank-2.label
0https://www.usnews.com/education/best-global-u...501112Wageningen University & ResearchWageningenNetherlandsNLD[{'value': '1', 'is_tied': False, 'is_ranked':...[{'value': '100.0', 'label': 'Subject Score'},...NaNNaNNaN1.0FalseTrueBest Universities for Agricultural Sciences89.0TrueTrueBest Global Universities
1https://www.usnews.com/education/best-global-u...500688China Agricultural UniversityBeijingChinaCHN[{'value': '2', 'is_tied': False, 'is_ranked':...[{'value': '96.3', 'label': 'Subject Score'}, ...NaNNaNNaN2.0FalseTrueBest Universities for Agricultural Sciences332.0FalseTrueBest Global Universities
2https://www.usnews.com/education/best-global-u...504800Jiangnan UniversityWuxiChinaCHN[{'value': '3', 'is_tied': False, 'is_ranked':...[{'value': '93.9', 'label': 'Subject Score'}, ...NaNNaNNaN3.0FalseTrueBest Universities for Agricultural Sciences598.0TrueTrueBest Global Universities
3https://www.usnews.com/education/best-global-u...505115South China University of TechnologyGuangzhouChinaCHN[{'value': '4', 'is_tied': False, 'is_ranked':...[{'value': '91.7', 'label': 'Subject Score'}, ...NaNNaNNaN4.0FalseTrueBest Universities for Agricultural Sciences219.0TrueTrueBest Global Universities
4https://www.usnews.com/education/best-global-u...166629University of Massachusetts AmherstAmherstUnited StatesUSA[{'value': '5', 'is_tied': False, 'is_ranked':...[{'value': '88.3', 'label': 'Subject Score'}, ...NaNNaNNaN5.0FalseTrueBest Universities for Agricultural Sciences160.0TrueTrueBest Global Universities

Давайте посмотрим ТОП 10 предметных рейтингов по количеству университетов, в которых представлена 🇷🇺 Россия.

Выводим статистику по предметным рейтигам
df_by_subject[df_by_subject['three_digit_country_code'] == 'RUS'][['rank-1.label', 'id']].\
groupby(by='rank-1.label').count().rename(columns={'id': 'count'}).\
sort_values(by='count', ascending=False).head(10)
count
rank-1.label
Best Universities for Physics20
Best Universities for Chemistry17
Best Universities for Materials Science14
Best Universities for Engineering9
Best Universities for Physical Chemistry9
Best Universities for Optics8
Best Universities for Mathematics7
Best Universities for Condensed Matter Physics6
Best Universities for Geosciences5
Best Universities for Computer Science4

Times Higher Education

Общий рейтинг

Необходимые библиотеки
import re
import time
import requests
import pandas as pd
from bs4 import BeautifulSoup

Для парсинга этого рейтинга тоже создаём объект session.

Создаём сессию для работы с сайтом
session = requests.session()
session.headers.update({
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'ru',
'cache-control': 'no-cache',
'pragma': 'no-cache',
'sec-ch-ua': '".Not/A)Brand";v="99", "Google Chrome";v="103", "Chromium";v="103"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Linux"',
'sec-fetch-dest': 'document',
'sec-fetch-mode': 'navigate',
'sec-fetch-site': 'none',
'sec-fetch-user': '?1',
'upgrade-insecure-requests': '1',
'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36'
})
years = range(2020, 2024 + 1)

Дальше собираем данные по годам.

Собираем данные по годам
url_tpl = 'https://www.timeshighereducation.com/world-university-rankings/{year}/world-ranking'
ranking = pd.DataFrame(columns=['year'])

for year in years:
html_url = url_tpl.format(year=year)
html_resp = session.get(html_url)

if html_resp.status_code == 200:
html = html_resp.text
soup = BeautifulSoup(html)
scripts = soup.find_all('script')
search_words = 'init_drupal_core_settings'
drupal_settings_str = None

for script in scripts:
script_text = script.text

if script_text.find(search_words) > -1:
drupal_settings_str = script_text

if drupal_settings_str:
found = re.search(f"world_university_rankings_{year}_0__[\w\d]+\.json", drupal_settings_str)

if found:
data_url = f'https://www.timeshighereducation.com/sites/default/files/the_data_rankings/{found[0]}'
data_resp = session.get(data_url)

if data_resp.status_code == 200:
data = data_resp.json()
chunk = pd.json_normalize(data['data'])
chunk['year'] = year
ranking = pd.concat([ranking, chunk], ignore_index=True)
else:
print(f"{data_url} return status code {data_resp.status_code}")
else:
print(f"{html_url} return status code {html_resp.status_code}")

# Бережём источник данных
time.sleep(0.5)

ranking.to_csv(f"data/the_{min(years)}-{max(years)}.csv", index=False)
ranking.to_excel(f"data/the_{min(years)}-{max(years)}.xlsx", index=False)
ranking.head()
yearrank_orderranknamescores_overallscores_overall_rankscores_teachingscores_teaching_rankscores_researchscores_research_rank...stats_pc_intl_studentsstats_female_male_ratioaliasessubjects_offeredclosedunaccrediteddisabledapply_linkcta_button.linkcta_button.text
02020101University of Oxford95.41090.5699.61...41%46 : 54University of OxfordAccounting & Finance,General Engineering,Commu...FalseFalseFalsehttps://www.timeshighereducation.com/student/r...https://www.timeshighereducation.com/student/r...Admissions Support
12020202California Institute of Technology94.52092.1297.24...30%34 : 66California Institute of Technology caltechLanguages, Literature & Linguistics,Economics ...FalseFalseFalsehttps://www.timeshighereducation.com/student/r...https://www.timeshighereducation.com/student/r...Admissions Support
22020303University of Cambridge94.43091.4498.72...37%47 : 53University of CambridgeBusiness & Management,General Engineering,Art,...FalseFalseFalsehttps://www.timeshighereducation.com/student/r...https://www.timeshighereducation.com/student/r...Admissions Support
32020404Stanford University94.34092.8196.45...23%43 : 57Stanford UniversityPhysics & Astronomy,Computer Science,Politics ...FalseFalseFalseNaNhttps://www.timeshighereducation.com/student/r...Admissions Support
42020505Massachusetts Institute of Technology93.65090.5592.410...34%39 : 61Massachusetts Institute of TechnologyMathematics & Statistics,Languages, Literature...FalseFalseFalsehttps://www.timeshighereducation.com/student/r...https://www.timeshighereducation.com/student/r...Admissions Support

5 rows × 33 columns

Из полученных данных соберём количество 🇷🇺 российских университетов в рейтинге по годам.

Выводим статистику по количеству университетов
ranking[ranking['location'] == 'Russian Federation'][['year', 'location']].\
groupby(by='year').count().rename(columns={'location': 'count'})
count
year
202039
202148
2022100
2023103
2024108

Предметные

Код практически идентичен предыдущему, только появляется обход по каждому предмету рейтингу.

Предметный рейтинг
url_subject_tpl = 'https://www.timeshighereducation.com/world-university-rankings/{year}/subject-ranking/{subject}'
ranking_subject = pd.DataFrame(columns=['year', 'subject'])
subject_map = [
{'title': 'Arts & humanities', 'url_suffix': 'arts-and-humanities', 'file_prefix': 'arts_humanities_rankings'},
{'title': 'Business & economics', 'url_suffix': 'business-and-economics', 'file_prefix': 'business_economics_rankings'},
{'title': 'Clinical & health', 'url_suffix': 'clinical-pre-clinical-health', 'file_prefix': 'clinical_pre_clinical_health_ran'},
{'title': 'Computer science', 'url_suffix': 'computer-science', 'file_prefix': 'computer_science_rankings'},
{'title': 'Education', 'url_suffix': 'education', 'file_prefix': 'education_rankings'},
{'title': 'Engineering', 'url_suffix': 'engineering-and-it', 'file_prefix': 'engineering_technology_rankings'},
{'title': 'Law', 'url_suffix': 'law', 'file_prefix': 'law_rankings'},
{'title': 'Life sciences', 'url_suffix': 'life-sciences', 'file_prefix': 'life_sciences_rankings'},
{'title': 'Physical sciences', 'url_suffix': 'physical-sciences', 'file_prefix': 'physical_sciences_rankings'},
{'title': 'Psychology', 'url_suffix': 'psychology', 'file_prefix': 'psychology_rankings'},
{'title': 'Social sciences', 'url_suffix': 'social-sciences', 'file_prefix': 'social_sciences_rankings'}
]

for year in years:
for subject in subject_map:
html_url = url_subject_tpl.format(year=year, subject=subject['url_suffix'])
html_resp = session.get(html_url)

if html_resp.status_code == 200:
html = html_resp.text
soup = BeautifulSoup(html)
scripts = soup.find_all('script')
search_words = 'init_drupal_core_settings'
drupal_settings_str = None

for script in scripts:
script_text = script.text

if script_text.find(search_words) > -1:
drupal_settings_str = script_text

if drupal_settings_str:
found = re.search(f"{subject['file_prefix']}_{year}_0__[\w\d]+\.json", drupal_settings_str)

if found:
data_url = f'https://www.timeshighereducation.com/sites/default/files/the_data_rankings/{found[0]}'
data_resp = session.get(data_url)

if data_resp.status_code == 200:
data = data_resp.json()
chunk = pd.json_normalize(data['data'])
chunk[['year', 'subject']] = year, subject['title']
ranking_subject = pd.concat([ranking_subject, chunk], ignore_index=True)
else:
print(f"{data_url} return status code {data_resp.status_code}")
else:
print(f"{html_url} return status code {html_resp.status_code}")

# Бережём источник данных
time.sleep(0.5)

ranking_subject.to_csv(f"data/the_{min(years)}-{max(years)}_subject.csv", index=False)
ranking_subject.to_excel(f"data/the_{min(years)}-{max(years)}_subject.xlsx", index=False)
ranking_subject.head()
yearsubjectrank_orderranknamescores_overallscores_overall_rankscores_citationsscores_citations_rankscores_industry_income...stats_pc_intl_studentsstats_female_male_ratioaliasessubjects_offeredclosedunaccrediteddisabledcta_button.linkcta_button.textapply_link
02020Arts & humanities101Stanford University89.91080.9672.4...23%43 : 57Stanford UniversityHistory, Philosophy & Theology,Art, Performing...FalseFalseFalsehttps://www.timeshighereducation.com/student/r...Admissions SupportNaN
12020Arts & humanities202University of Cambridge86.72062.814458.8...37%47 : 53University of CambridgeHistory, Philosophy & Theology,Architecture,Ar...FalseFalseFalsehttps://www.timeshighereducation.com/student/r...Admissions Supporthttps://www.timeshighereducation.com/student/r...
22020Arts & humanities303University of Oxford86.43068.18535.2...41%46 : 54University of OxfordArt, Performing Arts & Design,History, Philoso...FalseFalseFalsehttps://www.timeshighereducation.com/student/r...Admissions Supporthttps://www.timeshighereducation.com/student/r...
32020Arts & humanities404Massachusetts Institute of Technology85.84076.31856.5...34%39 : 61Massachusetts Institute of TechnologyArchitecture,Archaeology,Languages, Literature...FalseFalseFalsehttps://www.timeshighereducation.com/student/r...Admissions Supporthttps://www.timeshighereducation.com/student/r...
42020Arts & humanities505Harvard University84.35068.38436.0...24%49 : 51Harvard UniversityArchitecture,History, Philosophy & Theology,La...FalseFalseFalsehttps://www.timeshighereducation.com/student/r...Admissions Supporthttps://www.timeshighereducation.com/student/r...

Для пример давайте посмотрим динамику университетов некоторых стран в предметных рейтингах.

Количество университетов в предметных рейтингах по странам
pd.pivot_table(
ranking_subject[ranking_subject['location'].isin([
'Russian Federation', 'China',
'India', 'Germany', 'Turkey',
'United Kingdom', 'United States',
'France', 'Brazil', 'Iran',
'Indonesia', 'Pakistan', 'Japan',
'Thailand', 'Italy'
])],
index='location',
columns='year',
values='name',
aggfunc='count'
).sort_values(by=[2024], ascending=False)
year20202021202220232024
location
United States13851470149914951447
United Kingdom695737757785793
China396468513528499
Japan322331351347360
Italy271288319336352
Brazil243287307341332
Germany284286310320320
Turkey122145181216271
India153172195221270
France219235231228243
Russian Federation130166196230241
Iran107128146167185
Pakistan36506589120
Indonesia23406279106
Thailand5865748597

QS-рейтинг университетов

Сайт заблокирован

Доступ к сайту данного рейтинга заблокирован по решению Пучежского районного суда (Ивановской области) № 2а-62/2023 от 19.01.2023. С учётом этого, а также политизированной позиции в отношении 🇷🇺 Российских университетов руководителей рейтинга, ниже представленный код публикуется без проверки.

По каким-то причинам на сайте суда <данные изъяты> и не видно доменного имени, может потребоваться эта ссылка на реестр Роскомнадзора. В поле «Искомый ресурс» укажите — www.topuniversities.com.

Общий рейтинг
import json
import requests
import pandas as pd
from bs4 import BeautifulSoup

year = 2022
page_url = f'https://www.topuniversities.com/university-rankings/world-university-rankings/{year}'
page_resp = requests.get(page_url)

if page_resp.status_code == 200:
page_html = page_resp.text
page_soup = BeautifulSoup(page_html)
drupal_settings = page_soup.find('script', attrs={'data-drupal-selector': 'drupal-settings-json'}).text
drupal_settings_json = json.loads(drupal_settings)
nid = drupal_settings_json['statistics']['data']['nid']
rank_resp = requests.get(f"https://www.topuniversities.com/rankings/endpoint?nid={nid}&page=0&items_per_page=2000")

if rank_resp.status_code == 200:
rank_json = rank_resp.json()
df = pd.json_normalize(rank_json['score_nodes'])
Предметный рейтинг
import json
import time
import requests
import pandas as pd
from bs4 import BeautifulSoup

# Собираем список предметов

year = 2022
first_url = f"https://www.topuniversities.com/university-rankings/university-subject-rankings/{year}/natural-sciences"
first_resp = requests.get(first_url)
subjects = []

if first_resp.status_code == 200:
first_html = first_resp.text
first_soup = BeautifulSoup(first_html)
drupal_settings = first_soup.find('script', attrs={'data-drupal-selector': 'drupal-settings-json'}).text
drupal_settings_json = json.loads(drupal_settings)
first_nid = drupal_settings_json['qs_rankings_rest_api']['nid']
subject_url = f"https://www.topuniversities.com/rankings/filter/endpoint?nid={first_nid}"
subject_resp = requests.get(subject_url)

if subject_resp.status_code == 200:
subject_json = subject_resp.json()

for c, subjects_list in subject_json['subjects'].items():
for s in subjects_list:
subjects.append({'category': c, 'name': s['name'], 'url': s['url']})
else:
print(f"{subject_resp} return status code {subject_resp.status_code}")
else:
print(f"{first_url} return status code {first_resp.status_code}")

# Забираем данные по категориям

ranking = pd.DataFrame(columns=['subject_category', 'subject'])

for i, s in enumerate(subjects):
url = f"https://www.topuniversities.com/{s['url']}"
resp = requests.get(url)

if resp.status_code == 200:
html = resp.text
soup = BeautifulSoup(html)
drupal_settings = soup.find('script', attrs={'data-drupal-selector': 'drupal-settings-json'}).text
drupal_settings_json = json.loads(drupal_settings)
nid = drupal_settings_json['qs_rankings_rest_api']['nid']
url_subject = f"https://www.topuniversities.com/rankings/endpoint?nid={nid}&items_per_page=10000&tab=?&page=0"
resp_subject = requests.get(url_subject)

if resp_subject.status_code == 200:
json_subject = resp_subject.json()
chunk_subject = pd.json_normalize(json_subject['score_nodes'])
chunk_subject['subject_category'] = s['category']
chunk_subject['subject'] = s['name']
ranking = pd.concat([ranking, chunk_subject], ignore_index=True)
else:
print(f"{url_subject} return status code {resp_subject.status_code}")
else:
print(f"{url} return status code {resp.status_code}")

print(f"{i}/{len(subjects)}: {s['name']} is done!", end='\r')
time.sleep(1)

Исходники и данные

Исходные Jupiter Notebook-и для данной страницы находится в папке по ссылке в Github.

Получившиеся наборы данных, на момент написания, вы можете скачать по ссылкам ниже:

  1. Московский рейтинг университетов — три миссии университета: ⬇️ csv, ⬇️ xlsx
  2. Рейтинг университетов Интерфакса: ⬇️ csv, ⬇️ xlsx
  3. RAEX ТОП 100: ⬇️ csv, ⬇️ xlsx
  4. Шанхайский рейтинг университетов:
    • Общий: ⬇️ csv, ⬇️ xlsx
    • Общий с ретроспективой: ⬇️ csv, ⬇️ xlsx
    • Предметный ректинг: ⬇️ csv, ⬇️ xlsx
  5. Best Global Universities:
    • Общий: ⬇️ csv, ⬇️ xlsx
    • Предметный: ⬇️ csv, ⬇️ xlsx
  6. Times Higher Education:
    • Общий: ⬇️ csv, ⬇️ xlsx
    • Предметный: ⬇️ csv, ⬇️ xlsx