Python实战研招网数据采集:从反爬策略到数据可视化的完整指南

张开发
2026/4/15 17:56:31 15 分钟阅读

分享文章

Python实战研招网数据采集:从反爬策略到数据可视化的完整指南
1. 项目背景与核心挑战最近在帮朋友分析考研数据时发现研招网的信息虽然全面但查询起来特别麻烦。手动收集不同学校、专业的招生信息简直是个噩梦这让我萌生了用Python自动化采集数据的想法。不过实际操作起来才发现研招网的反爬机制比想象中复杂得多。研招网作为教育部直属平台数据权威性毋庸置疑。但它的页面结构复杂查询参数众多还有动态加载、请求频率限制等各种防护措施。我花了整整两周时间才摸索出一套稳定的采集方案今天就把这些实战经验完整分享给大家。这个方案特别适合需要批量获取招生信息的研究者、教育机构从业者或者像我这样帮朋友分析考研数据的技术外援。2. 技术选型与工具链搭建2.1 基础工具组合经过多次尝试我最终确定了这个工具组合Requests BeautifulSoup处理基础页面请求和HTML解析Selenium应对动态加载内容Scrapy构建完整爬虫框架Pandas数据清洗和预处理Matplotlib/WordCloud数据可视化这里有个小插曲最开始我试图只用Requests搞定所有页面结果发现研招网的专业目录是通过AJAX动态加载的。后来通过浏览器开发者工具抓包才找到真正的数据接口。这个教训告诉我现代网站分析一定要先看Network请求别急着写解析代码。2.2 系统架构设计我的爬虫系统分为四个层次请求层处理HTTP请求、代理轮换和反反爬解析层提取页面中的结构化数据存储层将数据保存到CSV和数据库分析层生成可视化图表和统计报告具体实现时我建议先用RequestsBeautifulSoup快速验证思路等核心逻辑跑通后再迁移到Scrapy框架。这样能避免一开始就陷入框架复杂性的泥潭。3. 关键实现步骤详解3.1 目标页面分析与参数构造研招网的查询接口其实设计得很规范关键是要找到正确的参数组合。以获取北京市计算机专业数据为例base_url https://yz.chsi.com.cn/zsml/queryAction.do params { ssdm: 11, # 北京地区代码 dwmc: , # 学校名称留空查询全部 mldm: zyxw, # 学术学位 yjxkdm: 0812,# 计算机科学与技术代码 xxfs: 1, # 全日制 pageno: 1 # 页码 }这里最关键的专业代码需要从另一个接口获取import requests major_codes requests.get(https://yz.chsi.com.cn/zsml/pages/getZy.jsp).json() # 返回示例[{dm:081200,mc:计算机科学与技术},...]3.2 核心爬取代码实现我推荐两种实现方案各有适用场景方案一快速原型RequestsBS4def fetch_page(params): headers { User-Agent: Mozilla/5.0, Referer: https://yz.chsi.com.cn/zsml/queryAction.do } try: url f{base_url}?{.join(f{k}{v} for k,v in params.items())} response requests.get(url, headersheaders, timeout10) response.raise_for_status() soup BeautifulSoup(response.text, lxml) table soup.find(table, {class: ch-table}) data [] for row in table.find_all(tr)[1:]: cols row.find_all(td) data.append({ 学校: cols[0].get_text(stripTrue), 院系: cols[1].get_text(stripTrue), 专业: cols[2].get_text(stripTrue), 研究方向: cols[3].get_text(stripTrue), 招生人数: int(cols[4].get_text(stripTrue)) if cols[4].get_text(stripTrue) else 0, 考试科目: .join(cols[5].stripped_strings) }) return data except Exception as e: print(f请求失败: {e}) return []方案二生产环境Scrapy框架class MajorSpider(scrapy.Spider): name major_spider custom_settings { DOWNLOAD_DELAY: 2, CONCURRENT_REQUESTS: 1 } def start_requests(self): base_params {...} # 同前文params for page in range(1, self.settings.get(MAX_PAGE, 10)1): params base_params.copy() params[pageno] page url f{base_url}?{.join(f{k}{v} for k,v in params.items())} yield scrapy.Request(url, callbackself.parse_page) def parse_page(self, response): for row in response.css(table.ch-table tr)[1:]: yield { school: row.css(td:nth-child(1)::text).get(), department: row.css(td:nth-child(2)::text).get(), major: row.css(td:nth-child(3)::text).get(), direction: row.css(td:nth-child(4)::text).get(), count: int(row.css(td:nth-child(5)::text).get() or 0), subjects: .join(row.css(td:nth-child(6) *::text).getall()) }4. 反爬策略实战心得研招网的反爬不算最严但有几个坑我踩过要特别注意4.1 IP限制与请求频率我的解决方案是随机延迟每个请求间隔1-3秒import time import random time.sleep(random.uniform(1, 3))代理IP池如果需要大规模采集# settings.py DOWNLOADER_MIDDLEWARES { scrapy.downloadermiddlewares.retry.RetryMiddleware: 90, scrapy_proxies.RandomProxy: 100, scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware: 110, } PROXY_LIST /path/to/proxy/list.txt4.2 请求头优化除了常规的User-Agent我发现Referer和Host头也很关键headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), Referer: https://yz.chsi.com.cn/zsml/queryAction.do, Host: yz.chsi.com.cn }4.3 验证码应对当请求过于频繁时可能会触发验证码。我的经验是控制请求间隔使用Selenium模拟人工操作必要时接入打码平台5. 数据存储方案对比5.1 CSV存储适合小规模数据import pandas as pd df pd.DataFrame(data) df.to_csv(majors.csv, indexFalse, encodingutf-8-sig)5.2 MySQL存储推荐方案from sqlalchemy import create_engine engine create_engine(mysqlpymysql://user:passlocalhost:3306/research) df.to_sql(majors, engine, if_existsreplace, indexFalse)5.3 MongoDB存储非结构化数据from pymongo import MongoClient client MongoClient(mongodb://localhost:27017/) db client[research] db.majors.insert_many(data)存储时要注意字段类型转换特别是招生人数这类字段要转为数值类型。6. 数据分析与可视化实战6.1 招生规模分析import matplotlib.pyplot as plt # 按学校统计招生人数 school_stats df.groupby(学校)[招生人数].sum().sort_values(ascendingFalse) plt.figure(figsize(12,6)) school_stats.head(10).plot(kindbarh, color#1f77b4) plt.title(计算机专业招生规模TOP10, fontsize14) plt.xlabel(招生人数, fontsize12) plt.grid(axisx, linestyle--) plt.tight_layout() plt.savefig(school_stats.png, dpi300)6.2 考试科目词云from wordcloud import WordCloud import jieba # 合并所有考试科目文本 text .join(df[考试科目].dropna().tolist()) # 中文分词处理 words .join(jieba.cut(text)) wc WordCloud( font_pathsimhei.ttf, width800, height600, background_colorwhite, max_words100 ).generate(words) plt.imshow(wc, interpolationbilinear) plt.axis(off) plt.savefig(subjects_wordcloud.png, dpi300, bbox_inchestight)6.3 研究方向分析# 提取研究方向关键词 df[方向关键词] df[研究方向].str.extract(r(人工智能|大数据|机器学习|网络安全)) # 绘制研究方向分布 direction_dist df[方向关键词].value_counts() plt.figure(figsize(10,6)) direction_dist.plot(kindpie, autopct%1.1f%%) plt.title(研究方向分布, fontsize14) plt.ylabel() plt.savefig(direction_dist.png, dpi300)7. 项目优化与扩展在实际运行几个月后我总结出几个优化方向增量采集记录最后更新时间只获取新增数据# 在Scrapy中通过扩展实现 class IncrementalExtension: def __init__(self, stats): self.stats stats classmethod def from_crawler(cls, crawler): return cls(crawler.stats)异常监控设置邮件报警当爬虫异常时通知# settings.py EXTENSIONS { scrapy.extensions.telnet.TelnetConsole: None, scrapy.extensions.corestats.CoreStats: 500, myproject.extensions.EmailAlert: 100, }数据校验检查数据完整性def validate_data(df): # 检查必填字段 required_fields [学校, 专业, 招生人数] if not all(field in df.columns for field in required_fields): raise ValueError(缺少必要字段) # 检查数据范围 if (df[招生人数] 0).any(): raise ValueError(招生人数存在负值)自动化部署使用Scrapyd管理爬虫# 部署爬虫 scrapyd-deploy default -p research_spider这套方案经过半年多的实际运行检验稳定性相当不错。最关键的体会是反爬策略要适度既不能太激进导致被封也不能太保守影响效率。建议先从简单方案开始根据实际情况逐步优化。

更多文章