Python爬虫实战:整合四大世界大学排名(US News/QS/THE/软科)
简介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(year2022): # 注意实际接口地址和请求头要以浏览器抓到的为准 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, headersheaders, paramsparams, timeout15) 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, indexFalse)这里有两个关键点要注意。第一接口的地址和请求参数不能凭经验硬编码不同年份页面迭代后 API 路径大概率会变跑之前先用浏览器开发者工具抓一下网络请求确认路径第二我把分页循环硬限制为 5 页而不是用while True原因是避免接口异常时出现无限循环请求挂死。3.2 软科与 QSHTML 解析的两种不同姿势软科ARWU的榜单页面是传统的服务端渲染HTML 里直接包含表格数据用 lxml 解析即可。但 QS 的页面则夹杂了大量无关的推荐内容和脚本片段直接按表格结构抓取很容易抓出残缺数据。我在处理这两个来源时用了两种不同的策略。from lxml import html, etree import requests def fetch_shanghai(year2022): url fhttps://www.shanghairanking.cn/rankings/arwu/{year} resp requests.get(url, headers{User-Agent: Mozilla/5.0}, timeout15) 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]], onuniversity_norm, howouter) merged merged.merge(qs_df[[university_norm, qs_rank]], onuniversity_norm, howouter) merged merged.merge(the_df[[university_norm, the_rank]], onuniversity_norm, howouter) return merged.drop(columns[university_norm])这段合并逻辑里howouter是刻意选的。四个榜单的覆盖范围不同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, fscripts/{s}], checkTrue) time.sleep(2) # 留出间隔避免瞬时请求过密 schedule.every().month.do(job) while True: schedule.run_pending() time.sleep(3600)这段代码值得注意的地方在于subprocess.run的checkTrue参数一旦某个脚本因为网络或页面结构变化失败整个任务链会立刻停止并抛出异常而不是生成一份缺数据的半成品表。宁可任务失败也不能产出带脏数据的 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], labelUS News) plt.plot(target[year], target[qs_rank], labelQS) plt.gca().invert_yaxis() # 排名数字越小越好Y轴反转更直观 plt.legend() plt.savefig(trend.png, dpi150)invert_yaxis()这个细节很容易漏掉——如果不反转 Y 轴排名数字越大在图上位置越高读起来会误以为这学校排名一路下滑。这类视觉误导在排名分析里很常见做可视化时要注意坐标轴的语义方向。6.3 关于扩展性的一句经验到现在为止我对这套代码最满意的一点是新增榜单源不再让我有恐惧感——每个抓取脚本是独立文件内部怎么解析单榜数据完全不影响其他模块。唯一需要维护的核心逻辑是 merge 里的字段映射和清洗规则这在任何数据整合项目里都是真正花时间的部分。从那次做完这个榜单整合项目的经验之后我养成了一个习惯无论接到多小的数据抓取需求第一件事都是先把输出字段的定义和来源记录成一份 mapping 表而不是急着写代码。事实证明几乎所有后续改动的麻烦都出在字段口径不统一上而不是请求发不出去。先摸清楚数据的形状再写抓取逻辑这个顺序能省掉后面一半的返工。希望这个项目的解析和脚本拆解能帮你在处理下一份多源数据时少走一段弯路。本文还有配套的精品资源点击获取