1. 为什么需要爬取研招网数据?
研招网作为国内研究生招生的权威平台,包含了全国各大高校的招生计划、专业目录、考试科目等关键信息。这些数据对于考研学生、教育研究机构、培训机构来说都是非常宝贵的资源。但手动收集这些数据不仅效率低下,还容易出错。这时候Python爬虫就能大显身手了。
我去年帮一个考研辅导机构做过类似项目,他们需要定期收集全国计算机专业的招生信息。手动操作的话,一个工作人员每天最多只能收集几十所学校的数据,而用Python爬虫,不到一小时就能完成全国所有高校的数据采集,效率提升了上百倍。
爬虫不仅能帮你快速获取数据,还能实现自动化更新。比如设置每天凌晨自动运行爬虫,就能实时掌握招生计划的变化。这对于考研辅导、志愿填报等场景特别有用。
2. 环境准备与工具选型
2.1 Python环境搭建
首先确保你已经安装了Python 3.6+版本。我强烈建议使用Anaconda来管理Python环境,它能很好地解决依赖冲突问题。安装好Anaconda后,创建一个专属的虚拟环境:
conda create -n yzw_spider python=3.8 conda activate yzw_spider2.2 必备库安装
根据我的实战经验,这些库是研招网爬虫必不可少的:
pip install requests beautifulsoup4 scrapy selenium pandas matplotlib fake-useragent如果你打算用Scrapy框架,还需要安装:
pip install scrapy scrapy-proxies scrapy-redis2.3 工具选型建议
对于新手来说,我建议先从Requests+BeautifulSoup组合开始。这个组合学习曲线平缓,适合小规模数据采集。等熟悉了基本流程后,再过渡到Scrapy框架,它能更好地处理大规模、分布式爬取任务。
我在实际项目中发现,研招网的部分页面使用了JavaScript动态加载,这时候就需要Selenium来模拟浏览器操作。不过Selenium速度较慢,只应在必要时使用。
3. 研招网爬虫实战
3.1 分析目标网页结构
首先打开研招网的专业目录查询页面(https://yz.chsi.com.cn/zsml/queryAction.do),按F12打开开发者工具。通过观察可以发现:
- 查询结果以表格形式展示,class为"ch-table"
- 翻页是通过POST请求实现的
- 专业代码是通过异步请求获取的
这里有个小技巧:在Network面板中过滤XHR请求,可以快速找到数据接口。我发现专业代码实际上是从这个接口获取的:https://yz.chsi.com.cn/zsml/pages/getZy.jsp
3.2 核心爬取代码实现
先来看基础版实现,使用Requests+BeautifulSoup:
import requests from bs4 import BeautifulSoup import pandas as pd def get_major_codes(): url = "https://yz.chsi.com.cn/zsml/pages/getZy.jsp" response = requests.get(url) return {item['mc']: item['dm'] for item in response.json()} def crawl_schools(major_code, province_code="11", page=1): base_url = "https://yz.chsi.com.cn/zsml/queryAction.do" params = { "ssdm": province_code, # 省份代码 "dwmc": "", # 学校名称 "mldm": "zyxw", # 学术学位 "yjxkdm": major_code, # 专业代码 "xxfs": 1, # 全日制 "pageno": page # 页码 } headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } try: response = requests.get(base_url, params=params, headers=headers) 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(strip=True), "院系所": cols[1].get_text(strip=True), "专业": cols[2].get_text(strip=True), "研究方向": cols[3].get_text(strip=True), "招生人数": cols[4].get_text(strip=True), "考试科目": cols[5].get_text(strip=True) }) return pd.DataFrame(data) except Exception as e: print(f"爬取失败: {e}") return pd.DataFrame()3.3 进阶版:Scrapy实现
对于大规模爬取,Scrapy是更好的选择。先创建一个Scrapy项目:
scrapy startproject yzw_spider cd yzw_spider然后定义Item和Spider:
# items.py import scrapy class ResearchItem(scrapy.Item): school = scrapy.Field() department = scrapy.Field() major = scrapy.Field() direction = scrapy.Field() count = scrapy.Field() subjects = scrapy.Field() # spiders/major_spider.py import scrapy from yzw_spider.items import ResearchItem import json class MajorSpider(scrapy.Spider): name = "major_spider" allowed_domains = ["yz.chsi.com.cn"] def start_requests(self): # 先获取专业代码 yield scrapy.Request( "https://yz.chsi.com.cn/zsml/pages/getZy.jsp", callback=self.parse_major_codes ) def parse_major_codes(self, response): majors = json.loads(response.text) for major in majors: if "计算机" in major['mc']: # 只爬计算机相关专业 params = { "ssdm": "11", # 北京 "mldm": "zyxw", "yjxkdm": major['dm'], "xxfs": 1 } url = "https://yz.chsi.com.cn/zsml/queryAction.do" yield scrapy.FormRequest( url, formdata=params, callback=self.parse_list, meta={'major': major['mc']} ) def parse_list(self, response): for tr in response.css('table.ch-table tr')[1:]: item = ResearchItem() item['major'] = response.meta['major'] item['school'] = tr.css('td:nth-child(1)::text').get() item['department'] = tr.css('td:nth-child(2)::text').get() item['direction'] = tr.css('td:nth-child(4)::text').get() item['count'] = tr.css('td:nth-child(5)::text').get() item['subjects'] = tr.css('td:nth-child(6)::text').get() yield item4. 反爬虫策略与应对方案
4.1 常见反爬手段
研招网虽然没有特别严格的反爬措施,但还是有一些限制:
- IP频率限制:短时间内过多请求会被暂时封禁
- User-Agent检查:没有合法User-Agent的请求会被拒绝
- 请求频率限制:连续请求间隔过短会被拦截
4.2 应对策略
这是我总结的有效方案:
- User-Agent池:
from fake_useragent import UserAgent ua = UserAgent() headers = { 'User-Agent': ua.random }- 请求延迟:
import time import random time.sleep(random.uniform(1, 3)) # 1-3秒随机延迟- 代理IP池(如果需要大规模爬取):
# settings.py DOWNLOADER_MIDDLEWARES = { 'scrapy_proxies.RandomProxy': 100, 'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 110, } PROXY_LIST = '/path/to/proxy/list.txt' PROXY_MODE = 0 # 0表示随机选择,1表示按顺序- Cookie处理:
# 先在浏览器登录获取cookie cookies = { 'JSESSIONID': '你的sessionid', 'route': '你的route值' } response = requests.get(url, headers=headers, cookies=cookies)5. 数据存储与处理
5.1 存储方案选择
根据数据量大小,可以选择不同的存储方案:
- 小规模数据(<1万条):CSV或Excel
df.to_csv('research_data.csv', index=False, encoding='utf-8-sig')- 中等规模数据(1万-100万条):MySQL或SQLite
from sqlalchemy import create_engine engine = create_engine('mysql+pymysql://user:password@localhost:3306/research?charset=utf8mb4') df.to_sql('majors', engine, if_exists='append', index=False)- 大规模数据(>100万条):MongoDB
import pymongo client = pymongo.MongoClient("mongodb://localhost:27017/") db = client["research_db"] collection = db["majors"] collection.insert_many(df.to_dict('records'))5.2 数据清洗技巧
研招网的数据通常需要做一些清洗:
# 处理招生人数中的特殊字符 df['招生人数'] = df['招生人数'].str.extract('(\d+)')[0].astype(int) # 拆分考试科目 df['subject1'] = df['考试科目'].str.split('、').str[0] df['subject2'] = df['考试科目'].str.split('、').str[1] # 处理缺失值 df.fillna({'招生人数': 0}, inplace=True)6. 数据可视化分析
6.1 招生规模分析
import matplotlib.pyplot as plt # 按学校统计招生人数 school_counts = df.groupby('学校')['招生人数'].sum().sort_values(ascending=False) plt.figure(figsize=(12, 6)) school_counts.head(10).plot(kind='bar', color='#1f77b4') plt.title('计算机专业招生规模TOP10', fontsize=14) plt.ylabel('招生人数', fontsize=12) plt.xticks(rotation=45, ha='right') plt.tight_layout() plt.savefig('school_counts.png', dpi=300) plt.show()6.2 考试科目词云
from wordcloud import WordCloud import jieba # 合并所有考试科目 text = ' '.join(df['考试科目'].dropna().tolist()) # 中文分词 words = ' '.join(jieba.cut(text)) # 生成词云 wc = WordCloud( font_path='simhei.ttf', background_color='white', width=800, height=600, max_words=100 ).generate(words) plt.imshow(wc, interpolation='bilinear') plt.axis('off') plt.savefig('subjects_wordcloud.png', dpi=300, bbox_inches='tight') plt.show()7. 项目部署与自动化
7.1 定时任务设置
在Linux服务器上,可以使用crontab设置定时任务:
# 每天凌晨2点运行爬虫 0 2 * * * /path/to/python /path/to/spider.py >> /path/to/log.log 2>&1对于Windows系统,可以使用任务计划程序。
7.2 日志记录
良好的日志记录能帮助排查问题:
import logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('spider.log'), logging.StreamHandler() ] ) logger = logging.getLogger(__name__) try: # 爬取代码 logger.info("开始爬取第%d页数据", page) except Exception as e: logger.error("爬取失败: %s", str(e))7.3 异常处理与邮件通知
import smtplib from email.mime.text import MIMEText def send_email(subject, content): msg = MIMEText(content) msg['Subject'] = subject msg['From'] = 'your_email@example.com' msg['To'] = 'receiver@example.com' with smtplib.SMTP('smtp.example.com', 587) as server: server.login('your_email@example.com', 'password') server.send_message(msg) try: # 爬取代码 except Exception as e: error_msg = f"爬虫运行失败: {str(e)}" logger.error(error_msg) send_email("研招网爬虫异常", error_msg)8. 常见问题与解决方案
在实际项目中,我遇到过不少坑,这里分享几个典型问题的解决方法:
页面结构变化:研招网偶尔会改版,导致选择器失效。解决方案是定期检查爬虫运行状态,及时更新选择器。
数据不完整:有时网络问题会导致数据缺失。可以增加重试机制:
from tenacity import retry, stop_after_attempt, wait_exponential @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10)) def fetch_page(url): response = requests.get(url) response.raise_for_status() return response.text验证码问题:虽然研招网大部分页面不需要验证码,但如果遇到,可以考虑使用第三方打码平台或者机器学习模型识别。
数据更新判断:避免重复爬取相同数据,可以在数据库中记录最后爬取时间,只获取新增数据:
last_crawl_time = get_last_crawl_time() new_data = [item for item in data if item['update_time'] > last_crawl_time]- 法律合规性:爬取公开数据一般是合法的,但要注意:
- 控制请求频率,不要对服务器造成负担
- 遵守robots.txt的规定
- 不要爬取个人隐私信息
- 数据使用要符合网站的服务条款