Python爬虫与数据分析实战:从大学排名榜单挖掘高教资源分布洞察

📅 2026/7/31 16:57:26
Python爬虫与数据分析实战:从大学排名榜单挖掘高教资源分布洞察
1. 项目概述从排名数据到洞察力每年各类大学排名的发布都会在学术界和学生群体中激起不小的波澜。大家热衷于讨论谁升谁降但往往停留在“看个热闹”的层面。作为一个常年和数据打交道的开发者我总觉得只看榜单结论有点“隔靴搔痒”。真正的价值藏在生成这份榜单的原始数据里以及我们如何用自己的方式去解读它。所以我决定动手做一件事用Python把“软科中国大学排名”这类权威榜单的原始数据“抓”下来然后用自己的分析逻辑去“盘”一遍看看除了排名数字我们还能挖出什么有意思的洞见。这个项目的核心目标很明确自动化获取、结构化清洗、多维度分析、可视化呈现。它不仅仅是一个爬虫练习更是一个完整的数据分析小项目闭环。通过它你可以学习到如何从零开始构建一个数据管道——从互联网上的非结构化网页到最终生成一份带有你自己观点的分析报告。无论你是想了解心仪大学的综合实力分布还是研究不同省份的高等教育资源差异甚至是观察某些指标如科研得分、生源质量随时间的变化趋势这个项目都能给你提供一个可复现的技术框架和思考路径。接下来我会带你完整走一遍这个流程重点不是罗列代码而是解释每一步“为什么要这么做”以及我在实际操作中踩过的坑和总结的技巧。我们会用到requests或DrissionPage来应对不同的网页结构用pandas进行数据清洗和运算最后用matplotlib和seaborn让数据自己“说话”。2. 核心思路与工具选型解析2.1 目标数据源分析与爬虫策略制定做爬虫第一步永远不是写代码而是“看”网页。我们需要明确数据在哪、以什么形式存在、如何高效稳定地获取。我选择“软科中国大学排名”作为数据源主要是因为它数据公开、结构相对稳定、指标维度丰富包含总分、办学层次、学科水平、师资规模等多个细分项。打开其官网榜单页面按下F12打开开发者工具切换到“网络”(Network)选项卡然后刷新页面。这里我们主要观察两种数据加载方式静态HTML渲染榜单数据直接嵌入在初次请求返回的HTML页面中。查看“文档”(Doc)类型的请求预览其响应内容如果能直接找到包含排名数据的表格HTML代码那么用requestsBeautifulSoup就是最轻量、最高效的方案。动态Ajax加载页面骨架先加载排名数据通过后续的Ajax请求通常是XHR/Fetch类型获取。这种情况下HTML里没有数据我们需要找到这个真正的数据接口。经过分析软科的榜单页面属于第一种情况数据直接存在于HTML中。但这里有一个关键点翻页。榜单通常有多页我们需要模拟翻页行为。观察翻页时的URL变化或网络请求发现它是通过page参数来控制页码的例如?page2。这就简单了我们可以用一个循环来遍历所有页面。注意在分析页面时务必遵守网站的robots.txt协议并设置合理的请求间隔如time.sleep(2)避免对服务器造成压力这是基本的网络礼仪和规避反爬虫机制的策略。为什么选择DrissionPage作为备选尽管目标页面是静态的但现代网站复杂多变。有些网站虽然数据在HTML中但可能被复杂的JavaScript框架渲染或者有简单的反爬机制如验证数据标签。requests只能获取原始HTML无法执行JS。DrissionPage则像一个可编程的浏览器能完美渲染页面并执行交互对于动态页面或需要模拟点击的场景如下拉加载更多非常有效。在本项目中虽然主方案用requests但掌握DrissionPage能让你应对更多样化的爬取场景。我会在关键步骤提及如何用DrissionPage实现作为对比。2.2 数据分析栈的构建逻辑数据抓下来后是一堆原始的、杂乱的文本和数字。我们需要一套工具链将其转化为整洁的、可供分析的结构化数据并最终实现可视化。pandas数据处理的基石。它的DataFrame结构是二维表格的完美抽象相当于一个内存中的超级Excel。我们用它来读取数据、清洗处理缺失值、去重、格式转换、过滤、排序、分组聚合例如计算各省份的平均得分、以及合并多个数据源。选择pandas是因为其API极其丰富且高效是Python数据分析的事实标准。matplotlibseaborn可视化双雄。matplotlib是底层绘图库功能强大但API稍显繁琐。seaborn基于matplotlib提供了更高级的统计图形接口和美观的默认样式绘制分布图、关系图、分类图等异常方便。两者结合既能快速出图也能进行精细化的定制。numpy高性能计算的幕后英雄。pandas的底层大量依赖numpy的数组进行高速运算。虽然在本项目中我们可能不直接写numpy代码但它是整个生态的支撑。这个工具链的搭配覆盖了从数据获取到洞察呈现的全流程且各组件之间兼容性极佳是经过无数项目验证的“黄金组合”。3. 爬虫实战稳定获取榜单数据3.1 环境准备与请求头伪装首先确保你的Python环境已就绪3.6以上版本均可。使用pip安装必要的库pip install requests beautifulsoup4 pandas matplotlib seaborn # 如果需要使用DrissionPage方案 pip install DrissionPage爬虫的第一步是让自己看起来像一个“普通的浏览器”而不是一个脚本。这主要通过设置请求头Headers来实现。我们从开发者工具中找到任何一个对榜单页面的请求复制它的User-Agent和可能需要的其他头信息如Referer。import requests from bs4 import BeautifulSoup import pandas as pd import time headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 } base_url https://www.shanghairanking.cn/rankings/bcur/202311 # 示例URL请以实际为准实操心得User-Agent非常重要。有些网站会屏蔽没有此头信息或使用Python默认User-Agent的请求。此外如果爬取过程中遇到403错误可以尝试添加Accept-Language、Accept-Encoding等头信息使其更像一个真实的浏览器请求。3.2 解析页面结构与数据提取接下来我们分析页面结构找到数据所在的HTML标签。使用浏览器的“检查元素”功能定位到排名表格。通常这类数据会放在一个table标签内或者由一系列具有相同类名的div或li标签构成。假设我们发现每个大学的排名信息都在一个类名为univ-item的div中其内部又有类名为rank、univ-name、score等的span或div来存放具体数据。def parse_page(html_content): 解析单个页面HTML提取数据 soup BeautifulSoup(html_content, html.parser) data_list [] # 假设每个大学条目在 classuniv-item 的div中 for item in soup.find_all(div, class_univ-item): try: rank item.find(div, class_rank).text.strip() name item.find(a, class_univ-name).text.strip() # 总分和细分项可能在不同标签里需要具体分析 total_score item.find_all(td)[2].text.strip() # 示例根据实际结构调整 # ... 提取其他字段如省份、办学层次、科研得分等 # 注意有些数据可能是属性如data-score需要用 item[data-score] 获取 data_list.append({ 排名: rank, 学校名称: name, 总分: total_score, # ... 其他字段 }) except AttributeError as e: # 某个字段可能缺失记录日志或跳过 print(f解析条目时出错: {e}, 跳过该条目) continue return data_list关键点网页结构可能变动所以解析代码要健壮。使用try...except捕获异常避免因单个条目解析失败导致整个程序崩溃。提取数据时优先使用具有唯一性的类名或选择器避免使用不稳定的索引位置。3.3 实现多页爬取与数据存储知道了单页怎么解析多页就简单了——构造一个页码循环。同时为了友好和稳定必须在每次请求间添加延迟。def crawl_all_pages(start_page1, end_page10): 爬取指定页码范围内的数据 all_data [] for page in range(start_page, end_page 1): print(f正在爬取第 {page} 页...) # 构造带页码的URL url f{base_url}?page{page} try: response requests.get(url, headersheaders, timeout10) response.raise_for_status() # 检查请求是否成功 # 可能涉及编码问题如果出现乱码可以尝试 response.encoding utf-8 page_data parse_page(response.text) all_data.extend(page_data) except requests.exceptions.RequestException as e: print(f爬取第 {page} 页时发生错误: {e}) break # 或者 continue根据需求决定 # 重要间隔一段时间避免请求过快 time.sleep(2) # 休眠2秒 # 将数据转换为DataFrame df pd.DataFrame(all_data) # 保存到CSV文件 df.to_csv(university_ranking_2023.csv, indexFalse, encodingutf-8-sig) # utf-8-sig解决Excel打开中文乱码 print(f爬取完成共获取 {len(df)} 条数据已保存至CSV文件。) return df # 执行爬取 df_raw crawl_all_pages(1, 5) # 假设爬取前5页避坑技巧超时设置timeout10非常重要。网络不稳定时没有超时设置的请求可能会永远挂起。异常处理一定要用try...except包裹网络请求和解析逻辑并记录错误信息便于排查。编码问题保存CSV时使用utf-8-sig编码这是为了兼容Windows系统下的Excel否则用Excel直接打开可能会出现中文乱码。数据去重由于网络或页面问题有时可能重复爬取到相同数据。可以在保存前使用df.drop_duplicates()进行去重。使用DrissionPage的替代方案 如果页面是动态加载或者元素需要交互才能显示requests就力不从心了。这时可以用DrissionPagefrom DrissionPage import ChromiumPage def crawl_with_drissionpage(): page ChromiumPage() page.get(base_url) all_data [] while True: # 模拟点击“下一页”直到没有 # 等待数据加载 page.wait.ele_loaded(css selector, .univ-item) # 解析当前页数据 (解析逻辑与BeautifulSoup类似但使用page的语法) items page.eles(.univ-item) for item in items: # ... 使用 item.ele() 等方法来查找子元素并提取数据 pass # 尝试找到并点击“下一页”按钮 next_btn page.ele(text:下一页, timeout2) if next_btn: next_btn.click() page.wait.load_start() # 等待新页面开始加载 time.sleep(3) # 等待数据渲染 else: break # 没有下一页退出循环 # ... 后续保存数据DrissionPage的优点是能处理任何动态内容缺点是需要启动浏览器速度相对较慢资源占用更高。应根据目标网站特性灵活选择工具。4. 数据清洗与预处理打造高质量数据集爬取到的原始数据往往存在各种问题直接用于分析会产生偏差。数据清洗是数据分析中至关重要且耗时的一步。4.1 常见数据问题及处理查看我们爬取到的DataFrameprint(df_raw.head()) # 查看前几行 print(df_raw.info()) # 查看数据概览发现缺失值和数据类型 print(df_raw.describe()) # 查看数值型字段的统计分布通常会遇到以下问题缺失值某些学校的某些指标可能为空例如新成立的大学可能没有“科研得分”。处理方式df.dropna()直接删除含有缺失值的行。如果缺失数据很少可以用。df.fillna(value)用特定值填充例如用该指标的平均值df[总分].fillna(df[总分].mean(), inplaceTrue)或用中位数、众数。对于本项目如果“总分”缺失这行数据价值不大可以考虑删除。如果是细分项缺失可以暂时用0或平均值填充并在分析时注明。数据类型错误爬取的数字可能是字符串类型如85.3需要转换为数值型float。df[总分] pd.to_numeric(df[总分], errorscoerce) # errorscoerce将无法转换的变为NaN df[排名] pd.to_numeric(df[排名], errorscoerce)字符串杂音学校名称可能包含多余空格、换行符\n或特殊字符。df[学校名称] df[学校名称].str.strip() # 去除首尾空格 df[学校名称] df[学校名称].str.replace(\n, ) # 去除换行符重复数据翻页机制可能导致首尾重复。df_cleaned df_raw.drop_duplicates(subset[学校名称], keepfirst) # 根据学校名去重保留第一条异常值例如排名为0或负值总分超过100等。需要根据业务逻辑判断和处理。# 假设总分应在0-100之间 df_cleaned df_cleaned[(df_cleaned[总分] 0) (df_cleaned[总分] 100)]4.2 数据增强与特征工程为了后续分析更丰富我们可以从现有数据中衍生出新特征提取省份信息很多大学名称中包含省份如“北京大学”、“浙江大学”。我们可以用简单的规则或字典映射来提取省份便于进行地域分析。# 一个简单的省份提取函数不完善实际需更复杂的规则或外部映射表 province_list [北京, 上海, 江苏, 浙江, 广东, ...] def extract_province(name): for province in province_list: if province in name: return province return 其他 # 或进一步分析 df_cleaned[省份] df_cleaned[学校名称].apply(extract_province)划分梯队/层级根据排名或总分将大学划分为不同梯队如“Top 10”, “11-50”, “51-100”, “100”等。def categorize_rank(rank): if rank 10: return 顶尖梯队 elif rank 50: return 优秀梯队 elif rank 100: return 重点梯队 else: return 其他梯队 df_cleaned[梯队] df_cleaned[排名].apply(categorize_rank)经过清洗和增强我们得到了一个干净、结构化的DataFrame可以放心地进行下一步分析了。5. 多维度数据分析与可视化探索数据准备好了现在进入最有趣的部分——探索数据背后的故事。我们围绕几个核心问题展开。5.1 整体分布与头部格局分析首先看看排名的整体分布和顶尖大学的格局。import matplotlib.pyplot as plt import seaborn as sns # 设置中文字体和图表样式 plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] # 用来正常显示中文标签 plt.rcParams[axes.unicode_minus] False # 用来正常显示负号 sns.set_style(whitegrid) # 1. 排名分布直方图 plt.figure(figsize(12, 6)) plt.subplot(1, 2, 1) sns.histplot(datadf_cleaned, x排名, bins50, kdeTrue) plt.title(大学排名分布直方图) plt.xlabel(排名) plt.ylabel(频数) # 2. 总分分布箱线图 plt.subplot(1, 2, 2) sns.boxplot(datadf_cleaned, y总分) plt.title(总分分布箱线图) plt.ylabel(总分) plt.tight_layout() plt.show() # 3. 展示Top 20大学 top20 df_cleaned.nsmallest(20, 排名)[[排名, 学校名称, 总分]] print(2023年排名前20的大学) print(top20.to_string(indexFalse))分析要点直方图可以观察排名是否均匀分布还是集中在某个区间。通常头部排名靠前的大学数量少尾部数量多呈长尾分布。箱线图可以直观看到总分的中位数、上下四分位数以及异常值。它能快速揭示数据的集中趋势和离散程度。Top 20列表直接观察顶尖大学的名单和分数差距。你会发现第1名和第20名的总分差距可能远小于第100名和第120名的差距这说明头部竞争异常激烈分数区分度很高。5.2 地域省份教育资源对比高等教育资源分布是否均衡我们可以从省份维度进行聚合分析。# 按省份分组统计 province_stats df_cleaned.groupby(省份).agg( 大学数量(学校名称, count), 平均排名(排名, mean), 平均总分(总分, mean), 最高排名(排名, min), # 排名数字越小越好所以用min 进入前100数量(排名, lambda x: (x 100).sum()) ).round(2).sort_values(by大学数量, ascendingFalse) print(各省份高等教育资源统计) print(province_stats.head(15)) # 展示前15个省份 # 可视化各省份大学数量条形图 plt.figure(figsize(14, 8)) province_stats_top15 province_stats.head(15) sns.barplot(dataprovince_stats_top15.reset_index(), x大学数量, y省份, paletteviridis) plt.title(各省份上榜大学数量 Top 15) plt.xlabel(大学数量) plt.ylabel(省份) plt.tight_layout() plt.show() # 可视化平均总分 vs 大学数量 散点图观察关系 plt.figure(figsize(10, 8)) sns.scatterplot(dataprovince_stats.reset_index(), x大学数量, y平均总分, size进入前100数量, hue进入前100数量, sizes(20, 200), palettecoolwarm) # 为每个点添加省份标签为避免重叠可只标注部分点 for i, row in province_stats.reset_index().iterrows(): if row[大学数量] 10 or row[平均总分] 70: # 标注条件示例 plt.text(row[大学数量]0.3, row[平均总分]0.1, row[省份], fontsize9) plt.title(各省份大学数量与平均总分关系气泡大小表示进入前100数量) plt.xlabel(大学数量) plt.ylabel(平均总分) plt.grid(True, linestyle--, alpha0.7) plt.tight_layout() plt.show()分析洞察大学数量图可以清晰看出哪些省份是“高教大省”。通常北京、上海、江苏、湖北、陕西等省份会名列前茅。散点图更深入地揭示了“数量”与“质量”的关系。理想情况是右上角数量多、平均分高但有些省份可能数量多但平均分不高“大而不强”有些则数量少但平均分高“少而精”。气泡大小前100数量进一步展示了顶尖资源的集中度。5.3 排名与分数关系深入探究排名和总分理论上应该是强相关的但实际关系如何我们可以用相关性分析和散点图来观察。# 计算排名与总分的相关系数负相关因为排名数字越小越好 correlation df_cleaned[排名].corr(df_cleaned[总分]) print(f排名与总分的相关系数: {correlation:.4f}) # 由于排名是顺序总分是数值相关系数应为负值且绝对值越接近1说明排名越能由总分线性解释。 # 绘制排名-总分散点图与趋势线 plt.figure(figsize(12, 8)) sns.regplot(datadf_cleaned, x排名, y总分, scatter_kws{s: 20, alpha: 0.6}, line_kws{color: red}) plt.title(大学排名与总分关系散点图含线性回归趋势线) plt.xlabel(排名数值越小越好) plt.ylabel(总分) plt.grid(True, linestyle--, alpha0.5) # 可以重点标注一些偏离趋势线的点异常点它们可能是有趣的案例 # 例如计算残差找出实际总分与预测总分差异较大的学校 from sklearn.linear_model import LinearRegression X df_cleaned[[排名]].values y df_cleaned[总分].values model LinearRegression().fit(X, y) df_cleaned[预测总分] model.predict(X) df_cleaned[残差] df_cleaned[总分] - df_cleaned[预测总分] # 找出残差绝对值较大的例如前5和后5 outliers_high df_cleaned.nlargest(5, 残差) # 实际分数远高于预测排名被低估 outliers_low df_cleaned.nsmallest(5, 残差) # 实际分数远低于预测排名被高估 print(\n残差为正可能被低估的Top 5) print(outliers_high[[排名, 学校名称, 总分, 预测总分, 残差]]) print(\n残差为负可能被高估的Top 5) print(outliers_low[[排名, 学校名称, 总分, 预测总分, 残差]]) # 在图上标注这些异常点 for _, row in outliers_high.iterrows(): plt.annotate(row[学校名称], (row[排名], row[总分]), textcoordsoffset points, xytext(0,10), hacenter, fontsize9, colorgreen) for _, row in outliers_low.iterrows(): plt.annotate(row[学校名称], (row[排名], row[总分]), textcoordsoffset points, xytext(0,-15), hacenter, fontsize9, colorred) plt.tight_layout() plt.show()深度解读相关系数一个接近-1的值例如-0.98表明排名几乎完全由总分决定榜单规则透明。如果相关系数绝对值较小如-0.7说明有其他因素或随机性影响了排名或者总分计算方式复杂。异常点分析这是最有价值的部分。那些远在趋势线上方的学校绿点意味着用同样的排名模型预测它们的分数应该更低但实际上很高。这可能是因为它们在某个未体现在“总分”中的细分指标上表现极端优异或劣势值得进一步研究其细分得分。这些学校可能是“特色鲜明”或“性价比高”的代表。5.4 梯队间差异与内部比较我们将大学分梯队后可以比较不同梯队之间的差异以及同一梯队内部的分布。# 确保有‘梯队’列 if 梯队 in df_cleaned.columns: # 1. 各梯队总分分布小提琴图结合箱线图 plt.figure(figsize(14, 6)) order_list [顶尖梯队, 优秀梯队, 重点梯队, 其他梯队] sns.violinplot(datadf_cleaned, x梯队, y总分, orderorder_list, innerbox, paletteSet2) plt.title(不同梯队大学总分分布小提琴图) plt.xlabel(梯队) plt.ylabel(总分) plt.tight_layout() plt.show() # 2. 各梯队统计指标 tier_stats df_cleaned.groupby(梯队).agg( 学校数量(学校名称, count), 平均总分(总分, mean), 总分标准差(总分, std), 最低总分(总分, min), 最高总分(总分, max) ).round(2).reindex(order_list) # 按指定顺序排列 print(\n各梯队统计摘要) print(tier_stats) # 3. 梯队内排名与总分关系分面散点图 g sns.FacetGrid(df_cleaned, col梯队, col_orderorder_list, col_wrap2, height5, aspect1.5) g.map_dataframe(sns.scatterplot, x排名, y总分, alpha0.7) g.set_titles({col_name}) g.set_axis_labels(排名, 总分) g.tight_layout() plt.show()分析结论小提琴图比箱线图更直观地展示了数据的实际分布形状概率密度。可以看到“顶尖梯队”总分分布非常集中且处于高位而“其他梯队”分布则非常分散且偏低。梯队统计量化了各梯队的差距。例如“顶尖梯队”的平均分可能比“优秀梯队”高出20分而“优秀梯队”与“重点梯队”的差距可能只有10分。标准差则反映了梯队内部的同质性“顶尖梯队”的标准差通常最小。分面散点图让我们能在每个梯队内部观察排名与分数的关系。有时在某个梯队内部这种线性关系可能并不明显这提示我们在不同水平区间影响排名的因素权重可能不同。6. 高级分析与扩展思路基础分析完成后我们可以尝试一些更深入的视角。6.1 多年趋势分析如果爬取了多年数据如果你爬取了2021、2022、2023等多年的数据就可以进行趋势分析这是单个年份数据无法提供的价值。合并多年数据将多个年份的DataFrame合并并添加“年份”列。学校排名变化追踪针对特定学校如你的母校或目标学校绘制其排名和总分随时间变化的折线图。省份实力变化计算各省份上榜大学平均排名或总分的年均变化率找出进步最快或退步最明显的省份。相关性趋势计算每年排名与总分的相关系数观察榜单评价体系的稳定性。6.2 细分指标关联性分析如果数据中包含“科研得分”、“师资得分”、“生源质量得分”等细分指标我们可以进行多变量分析。相关系数矩阵热力图计算所有数值型指标两两之间的相关系数并用sns.heatmap()绘制热力图。这能一眼看出哪些指标间高度相关例如“科研得分”和“总分”可能相关性极高哪些相对独立。主成分分析PCA如果指标很多例如10个以上可以用PCA降维看少数几个主成分能否解释大部分差异并分析每个主成分主要由哪些原始指标贡献从而理解榜单背后的核心评价维度。6.3 构建交互式可视化仪表盘静态图表适合报告交互式仪表盘则适合探索。你可以使用Plotly、Dash或Pyecharts库创建一个Web应用允许用户选择不同的省份查看该省份的大学列表和分布。鼠标悬停查看每个大学的具体指标。通过滑块筛选排名或总分范围。对比两所或多所大学的各项指标雷达图。这能将你的数据分析项目提升到一个新的水平也是数据工程师/科学家技能的很好体现。7. 常见问题、避坑指南与优化建议在整个项目过程中你可能会遇到以下问题这里是我的解决方案和经验。7.1 爬虫阶段常见问题Q1: 请求被拒绝返回403或429状态码A1: 这是最常见的反爬措施。首先检查并完善你的请求头特别是User-Agent和Referer。其次显著降低请求频率增加time.sleep()的随机间隔如time.sleep(random.uniform(2, 5))。如果还不行考虑使用IP代理池。对于DrissionPage可以启用浏览器隐身模式或加载特定插件来模拟真人行为。Q2: 解析不到数据find返回空列表A2: 首先确认你的选择器如class_univ-item是否正确。网页结构可能已更新。使用print(soup.prettify()[:5000])打印部分HTML重新定位数据所在标签。其次注意动态加载的内容可能需要改用DrissionPage。最后检查是否有iframe数据可能在嵌套的iframe页面里。Q3: 数据编码乱码A3: 明确指定响应编码。尝试response.encoding utf-8或response.encoding gbk。保存文件时使用encodingutf-8-sig。7.2 数据分析与可视化阶段常见问题Q1: 绘图时中文显示为方框A1: 这是Matplotlib的经典问题。确保已执行以下代码plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, DejaVu Sans] # 添加备选字体 plt.rcParams[axes.unicode_minus] False如果系统没有这些字体需要安装中文字体并配置Matplotlib的字体路径。Q2:DataFrame分组或运算速度慢A2: 对于大数据集几十万行以上优先使用Pandas的向量化操作避免使用apply函数进行逐行循环。例如上面的省份提取函数如果用apply处理1000行数据没问题但处理10万行就会慢。可以考虑用更高效的字符串方法或映射。Q3: 图表过于拥挤看不清A3: 调整图形大小figsize减少不必要的数据点例如只画前200名或者使用分面FacetGrid、交互式图表。对于条形图如果类别太多可以考虑只显示Top N或者将类别按大小排序。7.3 项目优化与进阶建议模块化与配置化不要把所有代码写在一个脚本里。将爬虫、清洗、分析、绘图分别写成函数或类并放在不同的模块中。将URL、请求头、数据库连接信息等写入配置文件如config.yaml或config.py提高代码可维护性。增加数据持久化中间层除了保存最终CSV可以将爬取的原始HTML或JSON响应也保存下来并记录爬取日志。这样当解析逻辑需要调整时无需重新爬取直接从本地原始数据解析即可。添加简单的数据校验在数据清洗后添加一些断言或检查例如“排名应该是1到1000之间的整数”、“总分应在0到100之间”、“学校名称不能为空”等确保数据质量。编写单元测试为关键函数如解析函数、清洗函数编写单元测试确保它们在不同输入下能正确工作。这在网页结构变化时能快速定位问题。考虑使用数据库如果数据量很大或需要频繁更新可以将数据存入SQLite或MySQL数据库便于查询和管理历史版本。这个项目从爬取到分析贯穿了数据科学的几个核心环节。它最大的价值不在于得出“XX大学第一”的结论而在于提供了一套方法论和可复现的代码框架。你可以轻松地将它迁移到爬取其他榜单如世界大学排名、专业排名、其他行业数据如企业财报、电影评分上用数据驱动的方式去发现你自己的洞见。记住工具和代码是死的但你的分析思路和问题意识才是让数据产生价值的核心。