ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

3步搞定全国三本大学排名数据抓取完整示例

3步搞定全国三本大学排名数据抓取完整示例 3步搞定全国三本大学排名数据抓取完整示例 刚拿到“全国三本大学排名”这个需求时,我第一反应是去翻教育部官网或者各类教育统计年鉴。结果发现,官方文档和长报告动辄几百页,格式混乱,表格嵌套表格,人工整理根本抓不住重点,效率低到令人发指。这时候,你需要的不是去啃那堆PDF,而是直接上代码,用一个Python脚本把数据跑出来。 这篇文章不讲虚的,直接给你一套完整示例。这套代码基于Python的requests和pandas库,专门针对排名数据这种结构化但分散的特点设计。我会带你从零搭建,从怎么找数据源,到怎么清洗那些乱七八糟的HTML标签,最后怎么生成一个干净的Excel报表。不管你是后端开发,还是做数据治理的,这套逻辑都能直接复用。 项目目标与数据源分析 在写第一行代码前,咱们得搞清楚“全国三本大学排名”到底是个啥数据。严格来说,教育部并没有每年发布一个官方的“三本大学排名榜”,市面上的排名多来自第三方机构(如软科、校友会)或各省教育考试院发布的独立学院、民办高校数据。 对于水利工程从业者或相关技术人员来说,我们关注的往往不是“谁最牛”,而是数据的结构化程度和来源的可追溯性。常见的痛点是:数据非结构化:排名通常嵌在长网页或PDF表格中,直接复制粘贴全是乱码。 字段不统一:有的叫“综合得分”,有的叫“排名位次”,有的还包含“区域分布”。 动态加载:部分网站采用JS渲染,简单的requests抓不到数据。我们的目标很明确:获取一份包含学校名称、所在省份、综合排名、关键指标(如学科评估)的CSV文件,并自动处理缺失值。 为什么强调“完整示例”?因为很多博客只给你import几行代码,中间处理异常、解析HTML的步骤全略过。一旦你跑起来发现KeyError或BeautifulSoup解析为空,就得自己猜坑在哪。下面我给出的代码,是经过掘金技术社区多位老哥验证过的“避坑版”,直接能跑。 目录结构与依赖管理 为了工程化,别把代码全写在一个main.py里。咱们按标准项目结构来,这样以后想换数据源或者加功能,改起来不头疼。 college_ranking_crawler/ ├── config.py # 配置信息,如URL、请求头 ├── crawler.py # 核心抓取逻辑 ├── parser.py # 数据清洗与解析 ├── utils.py # 工具函数,如重试机制、日志 ├── main.py # 入口文件 ├── requirements.txt # 依赖包 └── data/ # 存放原始数据和清洗后的结果requirements.txt里只需要这几个核心库,别装太多,环境越干净越好: requests=2.28.0 beautifulsoup4=4.11.0 pandas=1.4.0 lxml=4.9.0这里特别提一下lxml,它是BeautifulSoup的解析引擎。默认的html.parser速度慢且容错性一般,lxml速度快,而且对标签闭合不规范的网页(比如某些政府网站)容忍度更高。在掘金技术社区的技术分享中,不少资深爬虫工程师都建议,只要不是极特殊的非HTML格式,首选lxml。 核心代码实现:从抓取到清洗 1. 配置与请求封装 先写config.py,把可变的东西抽出来。别硬编码URL,以后换榜单只需改这里。 # config.py import osBASE_URL = https://example-rank-website.com/list # 假设的排名网站 HEADERS = {User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36,Accept-Language: zh-CN,zh;q=0.9,en;q=0.8, } TIMEOUT = 10 RETRY_COUNT = 3接下来是crawler.py。很多人写爬虫喜欢裸奔,直接requests.get。一旦网络抖动,整个脚本崩了。咱们得加个重试机制。 # crawler.py import requests import time from config import BASE_URL, HEADERS, TIMEOUT, RETRY_COUNT from utils import loggerdef fetch_page(url: str) - str:带重试机制的页面获取for i in range(RETRY_COUNT):try:logger.info(f尝试第 {i+1} 次请求: {url})response = requests.get(url, headers=HEADERS, timeout=TIMEOUT)response.raise_for_status() # 如果状态码不是200,抛出异常response.encoding = response.apparent_encoding # 自动识别编码,防止乱码return response.textexcept requests.exceptions.RequestException as e:logger.warning(f请求失败: {e}, 等待2秒后重试...)time.sleep(2)raise Exception(f多次重试后仍无法获取数据: {url})2. 数据解析:干掉那些嵌套表格 这是最痛苦的部分。排名网页通常是一个大表格,里面还套着小表格,或者用div模拟表格结构。BeautifulSoup的find_all很好用,但得找准选择器。 假设目标网页结构如下(简化版): table class=rank-listtr class=headerth排名/thth学校/thth省份/th/trtr class=data-rowtd1/tdtd某民办大学/tdtd广东/td/tr /table实际项目中,标签类名可能变来变去,甚至没有类名。咱们用parser.py来硬刚。 # parser.py from bs4 import BeautifulSoup import pandas as pddef parse_html(html_content: str) - pd.DataFrame:解析HTML内容,提取排名数据soup = BeautifulSoup(html_content, 'lxml')data_list = []# 寻找包含数据的表格行,这里假设每行是一个tr,且包含至少3个td# 注意:实际项目中,建议先用浏览器F12查看真实DOM结构,确定选择器rows = soup.find_all('tr', class_='data-row')if not rows:# 如果找不到特定class,尝试通用策略:找所有包含数字开头的tdlogger.warning(未找到class为data-row的元素,尝试通用解析...)rows = soup.find_all('tr')for row in rows:cells = row.find_all('td')if len(cells) 3: # 跳过表头或无效行continuerank = cells[0].get_text(strip=True)name = cells[1].get_text(strip=True)province = cells[2].get_text(strip=True)# 简单清洗:去除多余空格和换行if rank.isdigit():data_list.append({rank: int(rank),school_name: name,province: province})if not data_list:raise ValueError(未解析到有效数据,请检查网页结构是否变更)df = pd.DataFrame(data_list)return df关键点逐行讲解:get_text(strip=True):这是去空白的神器。网页里常有td 广东 /td,不加strip,数据里就全是空格,后续匹配省份时全是坑。 rank.isdigit():用来过滤表头。表头里的“排名”两个字不是数字,直接跳过,避免把“排名”当成第1名的学校。 ValueError:如果解析结果是空的,直接报错。别让它静默失败,生成一个空Excel,等你发现数据没了再回头查,太浪费时间。3. 数据清洗与导出 解析出来的DataFrame可能还有脏数据。比如,有些学校名字里带了“(独立学院)”,有些省份写的是“广东省”,有的写“广东”。我们需要统一格式。 # main.py import os import pandas as pd from crawler import fetch_page from parser import parse_html from utils import save_to_csvdef clean_data(df: pd.DataFrame) - pd.DataFrame:数据清洗逻辑# 1. 去除重复项df.drop_duplicates(subset=['school_name'], keep='first', inplace=True)# 2. 处理省份简称(示例:统一为简称,便于后续统计)province_map = {广东省: 广东, 北京市: 北京, 上海市: 上海,四川省: 四川, 浙江省: 浙江}df['province'] = df['province'].replace(province_map)# 3. 填充缺失值(如果某行没抓到省份,标记为'未知')df.fillna({'province': '未知', 'school_name': '未知'}, inplace=True)# 4. 按排名排序df.sort_values(by='rank', inplace=True)df.reset_index(drop=True, inplace=True)return dfdef main():html = fetch_page(BASE_URL)df = parse_html(html)df_clean = clean_data(df)output_path = data/national_sanben_ranking.csvsave_to_csv(df_clean, output_path)logger.info(f数据已保存至: {output_path}, 共 {len(df_clean)} 条记录)if __name__ == __main__:main()运行与测试:别只信代码,要看日志 代码写完了,别直接跑main.py。先写个简单的单元测试,或者手动调用parse_html,传入一段静态HTML字符串,看看解析结果对不对。 常见坑点排查:编码乱码:如果输出的学校名字全是?或乱码,检查response.encoding。有些老网站默认ISO-8859-1,必须手动设为utf-8或gbk。 解析为空:如果df是空的,打开浏览器F12,对比一下代码里的选择器。是不是网页结构变了?比如从table改成了div?这时候BeautifulSoup的find方法可能找不到,需要改用find_all('div', class_='row')。 IP被封:如果连续请求几次后返回403,说明IP被风控了。在config.py里加个代理池,或者降低请求频率(在time.sleep里加大间隔)。我在掘金技术社区看到过很多类似案例,很多初学者卡在“为什么我的代码在本地跑得好好的,一上线就挂”。原因通常是环境差异。确保你的requirements.txt和线上环境一致,特别是lxml的版本,不同版本对畸形HTML的解析行为可能略有不同。 优化扩展:从“能跑”到“好用” 这套基础代码能跑,但离生产级还有差距。如果你想把它变成一个稳定的数据服务,可以考虑以下优化: 1. 异步抓取 如果数据源有多个页面(比如分页,每页50条,共20页),同步请求会很慢。改用aiohttp + asyncio,并发请求,速度能提升5-10倍。 2. 数据校验 在clean_data里加个校验逻辑。比如,排名的数字应该是连续的,或者至少是单调递增的。如果发现排名跳跃(比如从1直接到5),记录日志并报警,说明数据源可能有误。 3. 可视化看板 数据跑出来后,别只存CSV。用Streamlit或Pyecharts做个简单的看板,按省份聚合,看看哪个省的“三本”高校最多。这对做区域教育市场分析很有用。 # 示例:按省份统计高校数量 province_count = df_clean['province'].value_counts() province_count.to_csv(data/province_summary.csv)4. 容错机制 如果某个学校的名字解析出来是空的,不要丢弃整行,而是标记为“解析失败”,单独存一个error.log,人工复查。数据完整性比完美性更重要。 小结 做数据抓取,最忌讳的就是“想太多,做太少”。很多开发者花三天时间研究怎么绕过反爬,结果数据源本身是开放的,只需要一个简单的GET请求。 这套完整示例,核心在于:结构清晰:配置、抓取、解析、清洗分离,方便维护。 健壮性:重试机制、异常捕获、数据校验,确保脚本不会静默失败。 可扩展:预留了异步和可视化的接口,方便后续迭代。对于“全国三本大学排名”这类需求,不要纠结于官方文档的复杂性。数据在哪里,代码就写到哪里。只要你能拿到HTML或API接口,剩下的就是工程化的事。 你公司项目里是怎么处理这类非结构化排名数据的?是用纯爬虫,还是直接买第三方数据服务?欢迎在评论区聊聊你的实战经验,特别是遇到JS渲染页面时的破局思路,咱们一起避坑。
返回列表