简介:2022年US News、软科、QS与THE四大机构世界大学排名数据,连同配套的Python获取与处理脚本,已整合为一份zip压缩包,面向高校师生、留学规划者以及数据分析初学者,便于直接获取、对比并研究全球高校表现。包内共15个文件,其中8个Python脚本、4个数据压缩包和3个说明文档,整体大小约13.21MB;脚本按照四个排名机构分类,不仅涵盖综合排名与学科排名的爬取,还包含院校基本信息和详细信息的抓取逻辑,网络请求、页面解析、pandas清洗与导出环节均有对应实现;数据压缩包则提供各榜单原始数据快照。已有1185人学习下载。US News聚焦学术声誉与同行评议,软科侧重诺贝尔奖、高被引学者等科研硬指标,QS综合学术与雇主声誉、师生比等维度,THE则评估教学、研究、引用与产业收入——借助这些脚本,可快速生成结构化榜单数据,进行跨排名差异比较、年份趋势或地区分布分析。对Python学习者而言,这份资料同样是一套完整的数据采集与整理实践样例,稍作修改即可迁移到其他排行榜或类似数据集项目。
1. 拿四个榜单当数据源,这件事没有想象中那么现成
每年出国留学或做学术评估时,大家都在 US News、软科(ARWU)、QS、THE 这四个榜单之间来回对比。但真到要动手做分析时,你会发现一个很尴尬的现状:这四个榜单的官网结构、字段定义、排版方式完全不同,想用 Python 把它们整合到一张表里,没有现成的轮子可用。这份资源就是干这件事的——它把 2022 年四大世界大学排名数据全部抓下来,清洗成统一结构化的格式,并附带了完整的 Python 脚本供二次开发使用。适合的对象很明确:想自己做留学选校数据表的学生、做高校科研分析的研究人员,以及需要批量处理排名数据的爬虫初学者。
整份资源的核心价值在于「整合」而非「抓取」。单看任何一个榜单其实都有官方页面,难的是把四个榜单的学校名对齐、把排名指标口径统一、把重复和缺失的数据做归一化。项目直接用脚本把整个流程固化下来了,你拿到手之后不必从零开始逆向四个网站的页面结构,直接就能在清洗好的数据上做自己的分析。
2. 技术选型与目录结构:为什么是 Python 而非现成工具
2.1 用 Python 做这件事的合理性
如果你只是想把 2022 年四榜排名查出来,Excel 手动复制粘贴也算能完成。但要面对的是几百所学校、四个来源、字段名和排名规则都不一致的场景,手动操作大概率会翻车。Python 的 requests 做页面抓取、json 模块解析接口返回、pandas 做表格处理、openpyxl 导出 Excel,这一整套链路的代码量并不大,而且容易调试和复用。
在这个场景下,我用的是 Python 3.8+ 的环境,依赖库集中在 requests、pandas、openpyxl、lxml 这几个。requests 解决 HTTP 请求,lxml 用于解析部分榜单的 HTML 页面,pandas 做 DataFrame 层面的合并去重,openpyxl 负责把结果写入 .xlsx 文件。整个依赖链没有重型框架,轻量、直接、可替换,这个选择本身也是项目保持可维护性的关键。
2.2 目录结构与文件职责
拿到压缩包之后,先别急着跑脚本,花两分钟确认目录结构。项目不是一坨脚本堆在一起,而是按「抓取、解析、整理」三个阶段拆开的:
rankings_project/ ├── data/ │ ├── raw/ # 存放从四个榜单抓取的原始响应或页面 │ └── processed/ # 清洗、合并后的最终结果 ├── scripts/ │ ├── fetch_usnews.py # US News 榜单抓取 │ ├── fetch_shanghai.py # 软科榜单抓取 │ ├── fetch_qs.py # QS 榜单抓取 │ ├── fetch_the.py # THE 榜单抓取 │ ├── merge_datasets.py # 四榜数据合并与排名归一化 │ └── export_excel.py # 导出为 Excel 或 CSV ├── config/ │ └── settings.json # 各榜单的 URL、请求头和输出配置 ├── output/ │ └── 2022_world_university_rankings.xlsx └── requirements.txt这个拆分方式的用意在于:四个榜单的页面结构差异太大,如果写在一个脚本里,后面改动任何一个榜单的解析逻辑,都会牵连到其他三个。单独成文件之后,每个脚本只要负责「抓取某个来源 → 输出标准化的中间 DataFrame」,最后合,并用 merge_datasets.py 统一做字段对齐。这样新增一个榜单源,只需要照着已有格式加一个文件即可。
config/settings.json 是值得留意的部分。请求头、超时时间、重试次数都集中在这里,不用改代码里的硬编码值。首次运行脚本时如果遇到请求被拒,大概率要先检查这里的 User-Agent 是不是太老旧。
3. 核心模块拆解:从请求发送到数据落地的完整链路
3.1 US News 抓取模块:先看接口再看页面
US News 的排名数据其实是通过后端接口动态加载的,直接抓静态 HTML 只能拿到框架壳子。所以 fetch_usnews.py 里我没有用常规的 requests.get 拿首页,而是先从浏览器开发者工具里把真实的数据接口地址找出来,直接请求接口。
import requests import pandas as pd def fetch_usnews(year="2022"): # 注意:实际接口地址和请求头要以浏览器抓到的为准 url = "https://www.usnews.com/education/best-global-universities/api/search" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "application/json", } params = {"year": year, "page": 1, "page_size": 50} names, ranks, scores = [], [], [] for page in range(1, 6): # 2022 年 Top 250,按每页 50 条拉取 params["page"] = page resp = requests.get(url, headers=headers, params=params, timeout=15) data = resp.json()["data"]["items"] for item in data: names.append(item["univ_name"]) ranks.append(item["rank"] if item["rank"] else None) scores.append(item.get("overall_score", None)) # 响应体为空或字段缺失时直接跳出,避免后续空指针异常 if not data: break return pd.DataFrame({"university": names, "usnews_rank": ranks, "usnews_score": scores}) if __name__ == "__main__": df = fetch_usnews() df.to_csv("./data/raw/usnews_2022.csv", index=False)这里有两个关键点要注意。第一,接口的地址和请求参数不能凭经验硬编码,不同年份页面迭代后 API 路径大概率会变,跑之前先用浏览器开发者工具抓一下网络请求确认路径;第二,我把分页循环硬限制为 5 页,而不是用while True,原因是避免接口异常时出现无限循环,请求挂死。
3.2 软科与 QS:HTML 解析的两种不同姿势
软科(ARWU)的榜单页面是传统的服务端渲染,HTML 里直接包含表格数据,用 lxml 解析即可。但 QS 的页面则夹杂了大量无关的推荐内容和脚本片段,直接按表格结构抓取很容易抓出残缺数据。我在处理这两个来源时,用了两种不同的策略。
from lxml import html, etree import requests def fetch_shanghai(year="2022"): url = f"https://www.shanghairanking.cn/rankings/arwu/{year}" resp = requests.get(url, headers={"User-Agent": "Mozilla/5.0"}, timeout=15) resp.encoding = "utf-8" # 软科页面如果乱码,先检查这一行 tree = etree.HTML(resp.text) # XPath 定位到排名表格的每一行 rows = tree.xpath('//table//tr[position() > 1]') result = [] for row in rows: cells = row.xpath('./td/text() | ./td/a/text()') cells = [c.strip() for c in cells if c.strip()] if len(cells) >= 3: result.append({ "university": cells[1], # 学校名称 "arwu_rank": cells[0], # 排名 "arwu_score": cells[2], # 总分 }) return pd.DataFrame(result)这段代码看起来简单,实际使用时最容易出问题的是 XPath 的定位表达式。页面改版之后,//table//tr可能定位不到目标表格,或者表格类名变化导致命中错误的节点。我的排查思路是:先用浏览器复制校名所在单元格的 XPath,再对照页面源码调整。别妄想推导通用表达式,直接看实际 DOM。
QS 的做法不太一样,我会先把全校数据用 json.loads 解析嵌入在<script id="__NEXT_DATA__">或类似标签里的 JSON 字符串,再从中抽取出排名列表。这种从「页面里嵌的 JSON」取数据的方式,比解析 HTML 表格稳定得多,因为前端框架渲染出来的内容通常都会把原始数据源原封不动放在某个 script 标签里。
3.3 THE 与合并归一化:字段名对齐是最大的坑
THE 榜单的字段名最「特立独行」:它对学校的表示方式包含了国家和地区字段,排名数字还带有连字符格式,比如 3-4 这种并列排名。这一步如果不处理,后面合并时 rank 字段会变成字符串,排序逻辑就直接崩了。
merge_datasets.py 的核心在于把四个来源的字段统一,再按学校英文名进行外连接。这里我用了一个较有防御性的做法:先对学校名做标准化,再去重合并。
import pandas as pd def normalize_name(name: str) -> str: """统一学校名称的大小写和空白字符,避免同一学校因写法差异被拆成多行""" return " ".join(name.strip().lower().split()) def merge_rankings(usnews_df, arwu_df, qs_df, the_df): usnews_df["university_norm"] = usnews_df["university"].map(normalize_name) arwu_df["university_norm"] = arwu_df["university"].map(normalize_name) qs_df["university_norm"] = qs_df["university"].map(normalize_name) the_df["university_norm"] = the_df["university"].map(normalize_name) merged = usnews_df.merge(arwu_df[["university_norm", "arwu_rank", "arwu_score"]], on="university_norm", how="outer") merged = merged.merge(qs_df[["university_norm", "qs_rank"]], on="university_norm", how="outer") merged = merged.merge(the_df[["university_norm", "the_rank"]], on="university_norm", how="outer") return merged.drop(columns=["university_norm"])这段合并逻辑里,how="outer"是刻意选的。四个榜单的覆盖范围不同,US News 有 250 所,软科有 300 所,QS 覆盖更多,外连接能保留所有出现过的高校,缺失的排名位留空而不是丢弃。代价就是最终表里会出现不少空的排名位,分析时要么用 fillna 填充标记,要么直接筛选多榜都在前 200 的学校。
关于「并列排名」的处理,我的一般做法是先把3-4拆成3存成整数,这样排序时才能正确反映 3 和 4 之间的并列关系。如果不做这个转换,Excel 打开后按排名排序会得到「10, 11, 2, 3」这种字典序灾难。
4. 运行实战:从零到拿到排行榜 Excel 的完整流程
4.1 环境准备与脚本执行顺序
项目不依赖特殊环境,装好 Python 后直接按 requirements.txt 安装依赖即可。我先给出一条安装命令,这条命令在绝大多数操作系统上都通用。
pip install -r requirements.txtrequirements.txt 里的内容并不复杂,核心就是 requests、pandas、openpyxl、lxml 这四个库。这一步如果安装缓慢,考虑换国内镜像源,不在这个问题上浪费太多时间。
接下来执行顺序是「先抓取、再合并、最后导出」。抓取分四个文件逐个执行,合并脚本依赖前四步的输出文件,因此不能跳过。
python scripts/fetch_usnews.py python scripts/fetch_shanghai.py python scripts/fetch_qs.py python scripts/fetch_the.py python scripts/merge_datasets.py python scripts/export_excel.py执行到 fetch_qs.py 时如果报出JSONDecodeError,不要慌张,大概率是页面结构变动导致脚本里定位 JSON 的正则失效了。这时打开浏览器访问 QS 页面,找到包裹数据的 script 标签,更新一下正则表达式就好。
4.2 输出文件格式说明
全部跑完后,output 目录下会生成一个 2022_world_university_rankings.xlsx 文件。文件里至少包含六个字段:university、usnews_rank、arwu_rank、qs_rank、the_rank、以及一个 overall_rank(四榜排名的综合取平均值)。这里需要明确一点:综合平均的计算逻辑并不复杂,核心是先把缺失值排除掉再算均值,否则一所只出现在两个榜单的学校,综合排名会跟四榜齐全的学校被放进同一维度比较,不公平。
如果你不想要 Excel,只想用 CSV 做后续分析,export_excel.py 里留了一个导出 CSV 的开关段,把df.to_excel改成df.to_csv即可,注意 CSV 默认不带格式和样式。
4.3 新增一个榜单源的接入方式
假设你手头多了一个「泰晤士年轻大学排名」想加进去,不需要改动 merge_datasets.py 的合并逻辑。只需要新建一个脚本,输出 DataFrame 字段中带一个 university 列和一个自定义排名列,然后照葫芦画瓢加一次 merge 调用即可。这正是脚本化拆分的红利:模块之间通过 DataFrame 接口交互,不共享任何全局变量。
5. 避坑指南:这五个问题我实际跑的时候都撞到了
5.1 乱码问题:不是代码问题,是编码声明问题
现象:软科页面抓回来,解析后学校名全部变成乱码。原因:requests 自动猜测的编码是 ISO-8859-1,而页面实际是 UTF-8。解决:在requests.get后手动设置resp.encoding = "utf-8",不要依赖自动检测。此后我再写爬虫,都会主动检查 headers 里的 charset 参数。
5.2 请求被拒:403 与 429 两个状态码的应对
现象:抓 US News 时返回 403 Forbidden,抓 QS 时连续请求后返回 429。原因:前者缺少浏览器特征头或 User-Agent 被识别,后者是触发频控。解决:在 headers 里补全 Accept-Language、Referer 等字段;同时脚本中加入了time.sleep(1)在每页之间,放慢请求频率。如果想完全稳妥,爬虫代码里加重试机制,失败后等待 3 秒再重试一次。
5.3 排名并列导致数据分析出错
现象:用 pandas 的sort_values("arwu_rank")排序后,排名 10、11、2、3 这种顺序出现。原因:soft科页面的3-4这种并列排名被当成字符串存储,字符串排序是字典序。解决:在解析阶段就用正则提取第一个数字并转成 int,或者在合并阶段单独处理。
5.4 学校名不对齐:多榜合并后同一学校出现两行
现象:最终合并表里出现 MIT 和 Massachusetts Institute of Technology 两行。原因:四个榜单对学校的官方写法不一致,有的用缩写,有的用全称。解决:先加载一份常见学校名映射表,把缩写和全称统一后再做 merge。这个映射表的来源是榜单本身的 「previous names」字段,需要自己整理,没有捷径。
5.5 年份参数失效
现象:把 settings.json 里的 year 改成 2023 之后,脚本抓下来的还是往年的数据。原因:某些榜单的年份参数不是放在 URL 里的,而是 POST 请求体里的参数,或者保存在 Cookie 中。解决:调整 fetch 模块里请求参数的传递方式,把 year 从 URL 字符串中移到 params 或 data 字段中。不同榜单的惰性差异化极大,没有一种万能改法。
6. 二次开发:把静态表变成实时更新的专业分析工具
6.1 从「一年一抓」到「持续追踪」的改造思路
跑通了静态脚本之后,下一步是最有价值的改造方向:把这套逻辑从「抓 2022 年数据」扩展成「定时抓取最新年度数据」。这种需求在留学选校、科研评价场景里非常常见——单年的排名表只是快照,跨年份的变化趋势才能反映一所学校的上升或下滑。
改造的第一步是把年份参数完全外置到 settings.json 里,脚本统一读取"year": 2022这个配置项。第二步是引入一个主调度脚本,按季度或每半年自动执行一次完整的抓取-合并-导出流程。这个调度脚本不需要引入 Airflow 这类重型框架,Python 的 schedule 库或者系统 crontab 就足够用了。
import schedule import time import subprocess def job(): scripts = [ "fetch_usnews.py", "fetch_shanghai.py", "fetch_qs.py", "fetch_the.py", "merge_datasets.py", "export_excel.py", ] for s in scripts: subprocess.run(["python", f"scripts/{s}"], check=True) time.sleep(2) # 留出间隔,避免瞬时请求过密 schedule.every().month.do(job) while True: schedule.run_pending() time.sleep(3600)这段代码值得注意的地方在于subprocess.run的check=True参数,一旦某个脚本因为网络或页面结构变化失败,整个任务链会立刻停止并抛出异常,而不是生成一份缺数据的半成品表。宁可任务失败,也不能产出带脏数据的 Excel。
6.2 数据可视化:排名的年度趋势对比
拿到多年度数据后,另一个常见改造方向是做趋势对比。比如用 matplotlib 或 pyecharts 画出一所大学在四年间四个榜单的排名变化折线图。这种图表的工程难度不大,核心是把 merge_datasets.py 的结果按年度纵向堆叠,然后以学校名为筛选条件生成图表。
import pandas as pd import matplotlib.pyplot as plt df = pd.read_excel("output/2022_world_university_rankings.xlsx") # 假设你手动拼接了多年度数据,得到了 year 列 target = df[df["university"].eq("A University")] plt.plot(target["year"], target["usnews_rank"], label="US News") plt.plot(target["year"], target["qs_rank"], label="QS") plt.gca().invert_yaxis() # 排名数字越小越好,Y轴反转更直观 plt.legend() plt.savefig("trend.png", dpi=150)invert_yaxis()这个细节很容易漏掉——如果不反转 Y 轴,排名数字越大在图上位置越高,读起来会误以为这学校排名一路下滑。这类视觉误导在排名分析里很常见,做可视化时要注意坐标轴的语义方向。
6.3 关于扩展性的一句经验
到现在为止,我对这套代码最满意的一点是:新增榜单源不再让我有恐惧感——每个抓取脚本是独立文件,内部怎么解析单榜数据,完全不影响其他模块。唯一需要维护的核心逻辑是 merge 里的字段映射和清洗规则,这在任何数据整合项目里都是真正花时间的部分。
从那次做完这个榜单整合项目的经验之后,我养成了一个习惯:无论接到多小的数据抓取需求,第一件事都是先把输出字段的定义和来源记录成一份 mapping 表,而不是急着写代码。事实证明,几乎所有后续改动的麻烦都出在字段口径不统一上,而不是请求发不出去。先摸清楚数据的形状,再写抓取逻辑,这个顺序能省掉后面一半的返工。希望这个项目的解析和脚本拆解,能帮你在处理下一份多源数据时少走一段弯路。
本文还有配套的精品资源,点击获取