news 2026/9/15 12:04:46

用Python爬虫抓取高考志愿数据:招生计划与分数线采集实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Python爬虫抓取高考志愿数据:招生计划与分数线采集实战

简介:这是一套面向高考志愿填报场景的大学数据爬取脚本,适合有 Python 基础、想批量获取招生计划、专业分数线、历年录取数据及学校基本信息的考生、家长或数据分析爱好者。脚本基于 requests 与 fake_useragent 实现,抓取内容包括专业名称、学科门类、计划招生数、学制、录取批次、招生类型、平均分、最低分/位次、省控线等,并整理学校 ID、名称、类型、科类、级别、位置及招生咨询官网字段。压缩包共 3 个文件,包含 1 个 Python 脚本、1 个说明文档和 1 个数据文件,整体仅 5KB,轻量易用,README 可辅助快速上手。目前已有 1737 人学习下载。借助脚本可获得结构化的志愿数据,便于按院校、年份、分数位次做多维筛选与排序,辅助评估不同学校专业的录取可能,从而节省手动逐校查询与整理的时间。

1. 高考志愿填报季,为什么我选择用爬虫统计大学数据

每年六月,高考出分后到填报截止前那几天,是全国家长最焦虑的时刻。面对几百所院校、上千个专业,官网数据分散在各省考试院和学校招生网里,手动复制到Excel要浪费一整个通宵。这个名叫school_Statistics的Python爬虫项目,就是为了把招生计划、专业分数线、历年录取位次和学校基本信息一次性抓到本地。它只依赖requests和fake_useragent两个库,代码量不大,却覆盖了志愿填报的核心数据维度。如果你也想在最短时间内拿到结构化数据,或者想练习真实场景下的requests爬虫,这个项目值得拆开看看。

2. 拆解school_Statistics:目标数据字段与请求构造

拿到school_Statistics-main.zip后,先看目录结构:一个school_Statistics.py主脚本、一个ID.text文本,以及README。这个项目不是通用的爬虫框架,而是针对特定院校库数据页的定向采集器。在动手改代码前,我们需要先搞清楚它要抓什么、从哪里抓、怎么让对方认为我们是正常访客。

2.1 从ID.text找到学校ID的加载入口

ID.text在项目里承担的是「待抓取学校ID列表」的角色。每行一个学校ID,爬虫启动时依次读取,后续所有请求的URL都会拼上这个ID。之所以把ID单独放在文件里,而不是硬编码在脚本中,是因为高考数据每年更新,只需要替换这个文件就能让爬虫跟踪新的院校集合。

with open('ID.text', 'r', encoding='utf-8') as f: school_ids = [line.strip() for line in f if line.strip()]

这段代码会过滤掉空行,返回字符串列表。注意文件名不是.txt而是.text,在Windows上可能被识别为未知类型,但Python自带open可以正常读取。我用的时候会顺手改成.txt,避免后续编辑器识别混乱。

2.2 请求头伪装与requests会话管理

直接裸用requests抓网页,很容易被服务器拒绝。项目中的解决方案是引入fake_useragent,随机生成一个浏览器UA,同时封住默认的Python-requests签名。

from fake_useragent import UserAgent import requests ua = UserAgent() headers = { 'User-Agent': ua.random, 'Referer': 'https://www.example.com/' } session = requests.Session() session.headers.update(headers)

这里必须用ua.random而不是ua.ieua.chrome,因为随机UA能降低每次请求的指纹一致性。Session对象会复用底层的TCP连接,在连续抓取几百个学校时,比每次新建requests.get快30%左右。如果你要抓的目标站点有更严格的反爬,还需要在headers里补上AcceptAccept-Language,并且设置请求间隔。常见做法是每次请求后time.sleep(0.5),把QPS压到2以下。

2.3 数据字段映射:招生计划、分数线与学校基本信息

参考项目摘要中的四个数据块,可以整理成下面的字段表:

数据块字段JSON Key示例类型
招生计划专业名称majorNamestr
招生计划学科门类subjectCategorystr
招生计划计划招生数planCountint
招生计划学制eduSystemint
专业分数线录取批次batchstr
专业分数线平均分/最低分avgScore/minScorefloat
专业分数线最低位次minRankint
历年分数线年份yearint
历年分数线招生类型recruitTypestr
历年分数线省控线provinceLineint
学校基本信息学校名称namestr
学校基本信息类型/科类/级别type/category/levelstr
学校基本信息位置locationstr
学校基本信息招生咨询官网consultUrlstr

抓取时要注意,不同接口返回的字段名并不统一。常见做法是先打印一版原始JSON,再手动建立映射关系。比如招生计划接口返回的是data.list,而分数线接口返回的是data.rows,稍不留神就会解析到空列表。另外,categorylevel在部分学校接口里可能为空,需要对这些字段做默认值处理,否则后续保存时会报字段缺失。

3. requests + fake_useragent 抓取招生计划与分数线的爬虫实战

当目标URL结构和数据字段都摸清后,核心的爬虫逻辑就清晰了。下面这段代码被我重组过,但保留了原项目的主要流程——先取学校基本信息,再抓招生计划和专业分数线。

3.1 学校基本信息抓取与JSON解析

学校基本信息通常是一个独立的详情接口,参数是schoolId。响应体里的内容比较多,我们需要用json.loadsresponse.json()来解析,使字段可以直接按字典访问。

def fetch_school_info(school_id): url = f"https://api.example.com/school/{school_id}/info" data = requests.get(url, headers=headers, timeout=8).json() info = { 'school_id': school_id, 'name': data.get('name'), 'type': data.get('type'), 'category': data.get('category', '综合'), 'level': data.get('level', '本科'), 'location': data.get('location'), 'consult_url': data.get('consultUrl') } return info

这里用.get(key)而不是data[key],是因为部分学校可能没有设置咨询官网,直接按key取会抛出KeyError,而.get可以安全返回Nonetimeout=8是给请求一个上限,避免某所学校的接口长时间无响应导致主线程卡死。categorylevel的默认值也提前放好,这样后面做统计时不会因为空值中断。

3.2 招生计划字段的解析与空值处理

招生计划列表里,专业名称和学科门类一般都有值,但计划招生数可能为null,学制也可能是字符串。这时候要做一个类型兜底。

def parse_plans(school_id, resp_json): plans = [] for major in resp_json.get('list', []): plans.append({ 'school_id': school_id, 'major_name': major.get('majorName', ''), 'subject_category': major.get('subjectCategory', '未分类'), 'plan_count': int(major.get('planCount') or 0), 'edu_system': int(major.get('eduSystem') or 4) }) return plans

int(major.get('planCount') or 0)的含义是:如果planCountNone、空字符串或0,都统一转换成0。学制同理,默认给4年。这种容错在真实爬虫里几乎是必需项,因为目标站的录入数据并不规范,直接强转会直接炸掉循环。

3.3 历年分数线的循环抓取与参数变化

历年分数线的接口往往比专业分数线多一个year参数。我一般会用当前年份倒推4年,逐个请求。

years = [2024, 2023, 2022, 2021] for year in years: url = f"https://api.example.com/school/{school_id}/score/{year}" resp = requests.get(url, headers=headers, timeout=8).json() for item in resp.get('scoreList', []): score_data = { 'school_id': school_id, 'year': year, 'batch': item.get('batch'), 'recruit_type': item.get('recruitType'), 'min_score': item.get('minScore'), 'min_rank': item.get('minRank'), 'province_line': item.get('provinceLine') } all_scores.append(score_data)

注意,这里的recruitType可能区分「普通类」「中外合作」「民族班」等,同一所学校同一年会返回多条记录。如果只保留最后一条,会丢失报考口径。所以要把每条都追加进列表,后续做数据分析时再按类型过滤。另外,年份参数不要硬编码,最好从当前日期动态生成,否则明年再跑又要改代码。

4. 数据落盘与异常重试:把爬虫跑稳定

只把数据打印出来没有意义,关键是要落盘。项目里选择用JSON文件保存,而不是CSV,是因为招生计划、分数线这些数据天然嵌套,JSON能原样保留层级关系。这一步做不好,之前爬回来的数据就是一堆废弃变量。

4.1 用json.dump保存每次请求结果

最简单的保存方式,是把所有抓取结果汇总在一个大字典里,最后一次性写文件。

import json result = { 'school_id': school_id, 'school_info': info, 'plans': plans, 'scores': scores } with open(f'output/{school_id}.json', 'w', encoding='utf-8') as f: json.dump(result, f, ensure_ascii=False, indent=2)

ensure_ascii=False是必须的,否则中文会变成\uXXXX转义序列,可读性极差。indent=2仅用于调试,强制缩进会让文件体积变大,对后续分析没有帮助。生产环境我一般改用jsonl格式,一行一个学校,方便增量写入。如果你打算把数据导入数据库,jsonl也更容易逐行解析。

4.2 异常处理:超时、连接失败、JSONDecodeError

爬虫跑半小时后,最怕遇到某次请求超时导致整个进程退出。项目在循环里加入重试机制,对常见的异常做区分处理。

def safe_request(url, max_retries=3): for attempt in range(max_retries): try: resp = requests.get(url, headers=headers, timeout=8) resp.raise_for_status() return resp.json() except (requests.exceptions.Timeout, requests.exceptions.ConnectionError): time.sleep(2 * (attempt + 1)) except requests.exceptions.HTTPError as e: if resp.status_code == 404: return None # 学校不存在,跳过 time.sleep(3) return None

time.sleep(2 * (attempt + 1))是线性退避,第一次失败等2秒,第二次4秒,第三次6秒。这个策略比固定重试要温和,能明显减少触发对方WAF的概率。对于404错误直接返回None,因为学校ID失效后继续重试只会白白占用线程。如果你发现某次请求返回了200但JSON解析失败,多半是响应被压缩或反爬页拦截,这时可以检查resp.text前200个字符来判断。

4.3 增量抓取:借助ID.text实现断点续跑

项目直接从头跑到尾,一旦中途断掉,前面抓的学校还得重新来。我给这个项目加过一个断点逻辑:每成功抓完一个学校,就把它的ID从ID.text中移除,或者把已完成ID追加到一个done.text里。

def is_done(school_id): with open('done.text', 'r') as f: return school_id in f.read() def mark_done(school_id): with open('done.text', 'a') as f: f.write(school_id + '\n')

启动时先检查is_done,若已处理过就跳过。这样即使进程崩溃,只要done.text没有损坏,就能继续从上次的位置往下跑。这个技巧在长任务里非常实用,特别是在志愿填报高峰期,数据更新频繁,需要隔几个小时就增量刷新一次。

5. 从爬虫数据到志愿推荐:位次换算与多表关联的技巧

数据落地后,下一步就是让数据产生真正的价值。高考志愿填报不能只看绝对值,尤其不能只看分数。同样的600分,在试卷难度极低的年份排位可能在一万名开外,在难度极高的年份可能进入前五千。所以,爬虫输出的minRank字段比minScore更值得关注。

5.1 用位次波动判断院校热度

将同一所学校近四年的专业最低位次做成一个序列,如果位次逐年上升(即数值变大),说明录取门槛在降低,可能是该校专业遇冷;反之则说明竞争加剧。下面这个简单的波动率计算,可以直接套用你抓下来的数据。

def rank_trend(school_scores): ranks = sorted([ item['min_rank'] for item in school_scores if item.get('min_rank') ]) if len(ranks) < 2: return 0.0 return (ranks[-1] - ranks[0]) / ranks[0] * 100

返回值的正负代表位次上升/下降的百分比。注意,这里必须过滤掉min_rankNone的数据,否则排序会报错或产生虚假结果。如果波动率超过15%,说明该专业录取状况极不稳定,填报时要格外谨慎。

5.2 将招生计划与历年分数线按专业名称合并

招生计划抓到的是今年新计划,历年分数线是往年的实际录取结果。要预测某个专业的录取位次,需要将两表通过school_id + major_name关联起来观察,但专业名称在不同年份可能微调,如「计算机类」时而细分、时而大类招生。

merged = {} for plan in plans: key = (plan['school_id'], plan['major_name']) merged.setdefault(key, []) merged[key].append(plan) for score in scores: key = (score['school_id'], score['major_name']) if key in merged: merged[key].append(score)

这种以元组作为字典key的做法,比单纯用字符串拼接更严谨,可以避免专业名包含特殊字符时产生的拼接冲突。合并后的数据就能用于做差值分析,比如招生计划扩招了多少,位次可能下降多少。但要注意,每年专业目录可能会有调整,如果匹配不上,就单独保留原始记录。

5.3 在填报期间定时刷新数据

高考出分到填报截止往往只有一周,而院校可能在期间更新招生计划。给school_Statistics.py增加一个定时运行的任务,不用复杂框架,就用time.sleep(1800)半小时跑一次,或者用系统cron调度。

在Linux服务器上,编辑crontab加入下面的任务即可每天凌晨两点更新一次:

0 2 * * * cd /path/to/school_Statistics && /usr/bin/python3 school_Statistics.py >> run.log 2>&1

将输出重定向到run.log后,第二天可以检查日志末尾的异常,再配合前面说的断点续跑机制,一个能持续运行整个填报季的爬虫服务就完成了。记住,任何爬虫都有遇到接口改版的风险,在上线前最好先打印一次原始响应体,确认字段没有变化。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 12:04:42

2026年Docker镜像加速器可用清单与配置排查全攻略

每隔两三个月&#xff0c;就会有人拿着网上流传的 Docker 国内镜像源列表来问我&#xff1a;“怎么按着配了还是拉不动&#xff1f;”说实话不怪他们&#xff0c;这类列表的更新速度永远追不上失效速度。9 月 13 日我把平时收集的加速地址重新过了一遍&#xff0c;逐个做了连通…

作者头像 李华
网站建设 2026/9/15 12:04:29

JavaScript内存泄漏实战指南:定位、修复与自动化防控

1. 这不是“理论课”&#xff0c;是浏览器里正在发生的内存战争你有没有遇到过这样的场景&#xff1a;一个页面刚打开时流畅如丝&#xff0c;滚动几十次后开始卡顿&#xff0c;再点几个按钮&#xff0c;Chrome 的任务管理器里那个标签页的内存占用从 80MB 暴涨到 420MB&#xf…

作者头像 李华
网站建设 2026/9/15 12:04:23

Flutter WebView唤起微信支付宝实战指南

1. 项目概述&#xff1a;为什么H5在Flutter WebView里“唤不起”微信和支付宝&#xff1f;最近两周&#xff0c;我连续帮三个团队处理了同一个问题&#xff1a;用flutter_webview_plugin&#xff08;注意&#xff0c;不是webview_flutter&#xff09;加载一个H5页面&#xff0c…

作者头像 李华
网站建设 2026/9/15 12:04:19

UE4角色移动系统搭建:奔跑、冲刺、蹲下与蹲走的动画状态机实践

UE4 里让角色同时具备奔跑、冲刺、蹲下、蹲走这一整套动作切换&#xff0c;是很多动作类项目起步时绕不开的第一道蓝图骨架。哪怕你只是做一个小型独立游戏&#xff0c;角色移动手感往往直接决定了玩家对这个游戏的第一印象&#xff1b;状态切得顺不顺、动画跟不跟手、蹲伏会不…

作者头像 李华
网站建设 2026/9/15 12:04:05

RHCSA认证核心技能:文件权限与用户管理实战

1. RHCSA认证与作业体系解析作为红帽认证系统管理员&#xff08;RHCSA&#xff09;的备考者&#xff0c;我深刻理解这套认证体系对Linux系统管理能力的严苛要求。RHCSA考试采用实操评估方式&#xff0c;要求考生在限定时间内完成一系列真实的系统管理任务。而作业环节作为备考过…

作者头像 李华