Python 实战:构建高考录取数据爬虫与本地数据库
1. 项目背景与数据需求分析高考志愿填报是每个考生家庭的重要决策时刻而历年录取分数线数据则是决策的核心依据。传统的手工查询方式效率低下难以进行跨年度、跨学校的横向对比。这正是Python爬虫技术可以大显身手的地方——通过自动化采集公开数据构建本地数据库为后续的分析和决策提供数据支撑。我去年帮亲戚家孩子填报志愿时就深刻体会到了数据的重要性。当时花了整整三天时间在不同网站间反复切换查询不仅效率低还容易出错。这次经历让我萌生了用技术手段解决这个痛点的想法。从技术角度看这类数据采集项目涉及三个关键环节数据源确认掌上高考等平台提供了结构化的API接口数据清洗原始JSON数据需要提取关键字段并规范化数据存储选择适合的数据库存储方案2. 环境准备与工具选择2.1 基础开发环境建议使用Python 3.8版本进行开发主要依赖库包括requests用于HTTP请求pandas数据清洗和处理sqlalchemy数据库ORM工具安装命令如下pip install requests pandas sqlalchemy对于IDE选择我实测下来VSCode和PyCharm都很适合。个人更推荐PyCharm的专业版它的数据库工具可以直接预览SQLite数据调试爬虫时特别方便。2.2 数据库选型方案根据数据量和使用场景有两种主流选择数据库类型优点缺点适用场景SQLite零配置、单文件、无需服务并发性能差个人使用、小型项目MySQL性能好、支持复杂查询需要安装服务端团队协作、中大型项目对于大多数个人开发者我建议先用SQLite快速验证想法。等需要更复杂查询时可以轻松迁移到MySQL。下面这段代码展示了如何创建SQLite连接from sqlalchemy import create_engine # SQLite连接 engine create_engine(sqlite:///college_admission.db) # MySQL连接示例 # engine create_engine(mysqlpymysql://user:passwordlocalhost/dbname)3. 数据采集实战3.1 API接口分析通过浏览器开发者工具分析掌上高考的数据接口主要有两类学校列表接口分页获取学校基本信息专业分数线接口根据学校ID获取具体专业录取数据关键发现接口需要添加合法的User-Agent头分页参数通过page字段控制专业数据接口需要拼接学校ID和年份这里有个小技巧先用Postman测试接口确认参数有效性后再写爬虫代码。我最初直接写代码时就因为漏了一个参数调试了半天。3.2 爬虫核心代码实现完整的数据采集流程分为三个步骤获取学校列表def fetch_schools(page): url fhttps://api.eol.cn/web/api/?page{page}size20 headers {User-Agent: Mozilla/5.0} response requests.post(url, headersheaders) return response.json()[data][item]获取专业分数线def fetch_majors(school_id): url fhttps://static-data.gaokao.cn/www/2.0/schoolspecialindex/{school_id}/2022/33/3/16/1.json response requests.get(url) return response.json()[data][item]数据存储def save_to_db(data, engine): df pd.DataFrame(data) df.to_sql(admission_data, engine, if_existsappend, indexFalse)实际运行时要注意两点添加适当的延时如time.sleep(1)避免被封做好异常处理特别是网络请求部分4. 数据清洗与结构化4.1 常见数据问题原始数据通常存在以下问题字段缺失部分学校可能没有某些年份数据格式不一致分数线可能是字符串580或数字580冗余字段包含不需要的分析指标这是我遇到过的一个真实案例某高校的艺术类专业分数线显示为文化课/专业课需要特殊处理def parse_special_score(score_str): if / in score_str: cultural, professional score_str.split(/) return float(cultural.strip()) return float(score_str)4.2 数据规范化处理建议建立统一的数据标准字段名类型说明provincestr省份名称schoolstr学校名称majorstr专业名称min_scorefloat最低录取分min_rankint最低录取位次yearint年份使用pandas进行清洗的示例def clean_data(raw_df): df raw_df.copy() # 统一空值处理 df.fillna({min_score: 0, min_rank: 0}, inplaceTrue) # 类型转换 df[min_score] df[min_score].astype(float) # 去重 df.drop_duplicates(inplaceTrue) return df5. 数据库设计与优化5.1 表结构设计合理的数据库设计能大幅提升查询效率。建议采用星型模型学校表(schools)id (主键)nameprovinceis_985is_211专业表(majors)id (主键)namecategory (文科/理科/综合)录取分数表(admissions)id (主键)school_id (外键)major_id (外键)yearmin_scoremin_rank创建表的SQL语句CREATE TABLE schools ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, province TEXT, is_985 BOOLEAN, is_211 BOOLEAN ); CREATE TABLE admissions ( id INTEGER PRIMARY KEY, school_id INTEGER, major_id INTEGER, year INTEGER, min_score REAL, min_rank INTEGER, FOREIGN KEY(school_id) REFERENCES schools(id), FOREIGN KEY(major_id) REFERENCES majors(id) );5.2 查询优化技巧当数据量超过10万条时需要注意添加索引CREATE INDEX idx_admissions_school ON admissions(school_id); CREATE INDEX idx_admissions_score ON admissions(min_score);分页查询def query_by_page(page, size): offset (page - 1) * size query SELECT * FROM admissions LIMIT ? OFFSET ? return pd.read_sql(query, engine, params(size, offset))使用物化视图预计算常用指标CREATE VIEW school_avg_score AS SELECT school_id, AVG(min_score) as avg_score FROM admissions GROUP BY school_id;6. 应用场景扩展有了基础数据库后可以开发很多实用功能志愿推荐系统def recommend_schools(score, rank, province): query SELECT schools.name, admissions.min_score, admissions.min_rank FROM admissions JOIN schools ON admissions.school_id schools.id WHERE schools.province ? AND admissions.min_score ? ORDER BY admissions.min_score DESC LIMIT 10 return pd.read_sql(query, engine, params(province, score))趋势分析def analyze_trends(school_name): query SELECT year, AVG(min_score) as avg_score FROM admissions JOIN schools ON admissions.school_id schools.id WHERE schools.name ? GROUP BY year ORDER BY year df pd.read_sql(query, engine, params(school_name,)) return df.plot(xyear, yavg_score)专业热度分析def popular_majors(year): query SELECT majors.name, COUNT(*) as count FROM admissions JOIN majors ON admissions.major_id majors.id WHERE admissions.year ? GROUP BY majors.name ORDER BY count DESC LIMIT 5 return pd.read_sql(query, engine, params(year,))7. 项目经验与避坑指南在完成这个项目的过程中我积累了一些宝贵经验反爬策略应对使用随机User-Agent可以从fake_useragent库获取设置合理的请求间隔建议2-5秒使用代理IP池当频繁遇到403错误时考虑数据质量保证建立数据校验机制比如检查每年数据量是否合理实现断点续传记录已采集的页码避免重新开始定期备份数据库特别是使用SQLite时性能优化技巧批量插入数据不要逐条insert# 不好的做法 for item in data: df pd.DataFrame([item]) df.to_sql(table, engine, if_existsappend) # 推荐做法 pd.DataFrame(data).to_sql(table, engine, if_existsappend)关闭索引大数据量导入时先禁用索引导入后再重建使用内存数据库先存到SQLite内存数据库再导出到文件这个项目最让我意外的是数据清洗部分花费的时间比爬取还多。特别是处理特殊专业名称时需要大量人工干预。建议大家在设计数据库时就给专业名称等字段预留足够的扩展性。