cdut-admission-auto

🎯 项目背景

这篇文章分享的是成都理工大学招生信息网(🔗学院专业页面的自动化爬取实战。它集成了瑞数五代动态验证、自动化行为检测、动态 Cookie/请求头校验等一系列重磅反爬屏障,直接使用 requests/urllib 裸连根本拿不到真实数据,必须借助带有完整浏览器渲染能力的方案才能突破。

最终我们实现了一条龙流程:绕过所有防护 → 模拟人类操作 → 提取学院专业数据 → 导出 Excel


🕵️ 网页快速分析

先快速看一眼目标页结构:

  • 表面上看是一个学院专业列表的静态布局,但刷新页面会发现有短暂的瑞数安全跳转
  • 数据通过 ul.xy-list 下的 li.li1 包裹每个学院,每个学院下用 dd > a 存放专业名称和链接

通过开发者工具的 Network + Console 观察防护层的细节:

  • 首次请求会返回瑞数混淆脚本,生成类似 sMLAeTqisZbFP 这样的动态 Cookie
  • 脚本会检测 navigator.webdriver、浏览器特征变量、鼠标/键盘交互行为
  • 412 错误是请求头缺失的典型表现,而 SSL 证书偶尔会在本地测试时触发验证失败

🚩 核心问题清单

  1. 动态 Cookie 更新:瑞数生成的 Cookie 有效期极短,纯静态维护会立刻失效
  2. 瑞数五代绕过:混淆 JS 动态生成验证数据,静态逆向难度极大
  3. SSL 证书验证:部分测试环境会拦截 HTTPS 请求,导致连接失败
  4. 完整请求头构建:Referer、Accept-Language、Sec-Fetch-* 这些字段缺一个就可能被拦截
  5. 反自动化检测:必须隐藏浏览器自动化特征,并模拟真实用户操作

🏗️ 技术架构

我们采用了 「DrissionPage 浏览器自动化 + 反检测 JS 注入 + urllib 安全请求」 的混合方案:

  • DrissionPage:比 Selenium 轻量得多,自带智能等待机制,特别适合处理复杂渲染页面
  • 反检测 JS:在页面加载初期直接注入,覆盖掉浏览器暴露的自动化特征
  • urllib:在拿到有效 Cookie 后,用来做数据抓取的轻量请求,降低浏览器资源的持续消耗

这样搭配的核心思路是:让浏览器去冲过瑞数验证,后续数据提取用轻量请求完成,既安全又高效。


💻 核心功能实现

1. 浏览器初始化配置

from DrissionPage import ChromiumPage

class AntiAntiSpider:
    def __init__(self):
        self.browser = ChromiumPage(timeout=15)
        self.browser.set.window.max()  # 最大化窗口降低分辨率/视口特征
        self.target_url = "https://www.zs.cdut.edu.cn/xyzy.htm"

⚠️ 关键参数说明:

  • timeout=15:给足瑞数脚本执行的时间,避免过早操作导致验证失败
  • set.window.max():最大化窗口可避免小窗口、固定分辨率这类典型的自动化特征

2. 反自动化 JS 注入

瑞数会通过检查 navigator.webdriver、CDP 注入留下的特征变量、甚至 debugger 断点来识别爬虫。我们在页面加载前注入 JS,直接针对这些检测点进行覆盖:

// 核心注入代码
// 1. 禁用所有类型的debugger
window.debugger = function(){};
Object.defineProperty(window, 'debugger', {
    get: function(){ return null; },
    set: function(){},
    configurable: false
});

// 2. 覆盖Selenium/CDP的webdriver标识
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
Object.defineProperty(window, 'navigator', {value: {webdriver: undefined}});

// 3. 删除CDP特征变量(常见于Selenium/Playwright)
const propsToDelete = [
    'cdc_adoQpoasnfa76pfcZLmcfl_Array',
    'cdc_adoQpoasnfa76pfcZLmcfl_Object',
    'cdc_adoQpoasnfa76pfcZLmcfl_Promise',
    'cdc_adoQpoasnfa76pfcZLmcfl_Proxy',
    'cdc_adoQpoasnfa76pfcZLmcfl_Symbol'
];
propsToDelete.forEach(prop => delete window[prop]);

// 4. 拦截含debugger的setInterval/setTimeout
const originalSetInterval = window.setInterval;
window.setInterval = function(callback, delay) {
    if (callback.toString().includes('debugger')) return 0;
    return originalSetInterval(callback, delay);
};
const originalSetTimeout = window.setTimeout;
window.setTimeout = function(callback, delay) {
    if (callback.toString().includes('debugger')) return 0;
    return originalSetTimeout(callback, delay);
};

这段 JS 会在浏览器加载目标页面后第一时间运行,确保在瑞数脚本获取特征之前就把漏洞堵上。


3. 人类行为模拟

单靠隐藏特征还不够,瑞数还会监测鼠标、键盘、滚动等交互行为。加一段简单的随机操作就能大幅提高通过率:

import time
import random

def simulate_human_behavior(self):
    # 非匀速随机滚动3-5次
    for _ in range(random.randint(3, 5)):
        scroll_px = random.randint(200, 900)
        self.browser.scroll.down(scroll_px)
        time.sleep(random.uniform(0.4, 1.8))  # 0.4-1.8秒的随机间隔

    # 滚动回顶部附近
    self.browser.scroll.to_top()
    time.sleep(random.uniform(0.6, 1.2))

    # 点击页面空白处(避免页面完全无交互特征)
    self.browser.ele("tag:body").click()
    time.sleep(random.uniform(0.8, 1.5))

这里的所有时间间隔都故意加了随机抖动,模仿人类不精确的操作节奏。


4. 瑞数安全核心绕过

整个方案的关键逻辑:先让浏览器正面硬刚瑞数验证,验证通过后把有效 Cookie 传给 urllib 做后续轻量请求

def bypass_ruishi(self):
    try:
        # 首次访问触发瑞数验证
        self.browser.get(self.target_url)
        # 注入反检测JS(要在页面刚加载,瑞数还没完全执行时注入)
        self.browser.run_js(self.anti_detection_js)

        # 核心等待:先等页面开始加载真实内容,再额外等3-6秒
        self.browser.wait.load_start()
        wait_time = random.uniform(4, 6)
        time.sleep(wait_time)
        print(f"瑞数验证等待时间:{wait_time:.1f}s")

        # 检查验证是否通过
        if "验证" in self.browser.title or "瑞数" in self.browser.html:
            raise Exception("瑞数验证触发,可能需要更新反检测JS或手动确认")

        return True

    except Exception as e:
        print(f"❌ 瑞数验证处理失败: {str(e)}")
        return False

⚠️ 如果验证失败,别慌,先检查反检测 JS 是否还适配当前瑞数版本,必要时抓包更新。


📡 请求构造模块

拿到有效 Cookie 后,我们用 urllib 封装安全请求,避免频繁打开/关闭浏览器页面,也降低了被反爬系统持续监控的风险。

def get_cookies_dict(self):
    cookies = self.browser.cookies()
    return {cookie['name']: cookie['value'] for cookie in cookies}

这里提取的关键 Cookie 有两个:

  • JSESSIONID:常规会话标识
  • sMLAeTqisZbFP:瑞数五代动态令牌(名称可能动态变化,但模式类似)

2. 完整请求头构建

瑞数对请求头极度敏感,Referer、User-Agent、Accept-Language、Sec-Fetch-* 系列字段一个都不能少

def create_request(self, url, cookies=None, headers=None):
    if headers is None:
        headers = {
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
            "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "Pragma": "no-cache",
            "Referer": self.target_url,  # 必须和目标域名一致
            "Sec-Fetch-Dest": "document",
            "Sec-Fetch-Mode": "navigate",
            "Sec-Fetch-Site": "same-origin",
            "Upgrade-Insecure-Requests": "1",
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
        }

    req = urllib.request.Request(url, headers=headers)

    if cookies:
        cookie_str = "; ".join([f"{k}={v}" for k, v in cookies.items()])
        req.add_header("Cookie", cookie_str)

    return req

这套请求头严格模仿了真实 Chrome 浏览器的请求特征,可以避免 412 这类由请求头缺失导致的拦截。

3. 安全请求封装

加入随机延迟、重试机制、禁用 SSL 验证(仅限测试环境):

import ssl
import urllib.request
from urllib.error import URLError, HTTPError

def safe_urlopen(self, url, max_retries=3, timeout=30):
    cookies = self.get_cookies_dict()

    for i in range(max_retries):
        try:
            req = self.create_request(url, cookies=cookies)
            # 禁用SSL验证(仅用于测试环境,生产环境建议安装目标证书)
            context = ssl._create_unverified_context()
            # 随机延迟1-3秒
            time.sleep(random.uniform(1, 3))

            with urllib.request.urlopen(req, timeout=timeout, context=context) as response:
                return response.read().decode('utf-8')

        except (URLError, HTTPError) as e:
            print(f"⚠️ 尝试 {i + 1}/{max_retries} 失败: {str(e)}")
            if i < max_retries - 1:
                time.sleep(random.uniform(2, 5))
                continue
            raise Exception(f"❌ 所有 {max_retries} 次请求均失败")

重试间隔和请求间隔都加了随机数,避免过于规律的节奏被反爬系统识别。


📊 数据提取与导出

拿到真实的 HTML 后,用 BeautifulSoup 解析结构,结合 pandas 导出成 Excel,一步到位:

import pandas as pd
from bs4 import BeautifulSoup

# 解析并提取数据
with open("result.html", "r", encoding="utf-8") as f:
    html_content = f.read()

soup = BeautifulSoup(html_content, 'html.parser')
data = []

# 遍历每个学院的li标签
for li in soup.find_all('li', class_='li1'):
    h6 = li.find('h6')
    if h6:
        college_name = h6.find('i').text.strip()
        # 遍历该学院下的所有专业
        for dd in li.find_all('dd'):
            a_tag = dd.find('a')
            if a_tag:
                major_name = a_tag.text.strip()
                # 拼接专业链接的绝对路径(处理相对链接)
                major_url = urllib.parse.urljoin("https://www.zs.cdut.edu.cn", a_tag['href'])
                data.append({
                    '学院名称': college_name,
                    '专业名称': major_name,
                    '专业链接': major_url
                })

# 导出Excel
excel_file = '成都理工大学学院专业信息.xlsx'
with pd.ExcelWriter(excel_file, engine='openpyxl') as writer:
    df = pd.DataFrame(data)
    df.to_excel(writer, index=False, sheet_name='学院专业列表')

print(f"✅ 数据已成功保存到 {excel_file}")

注意:这里使用了 urllib.parse.urljoin 来处理相对链接,确保导出的专业链接都是完整可访问的。


🛡️ exception-handling方案

错误类型触发条件解决方案
瑞数验证失败检测到自动化特征1. 更新反检测 JS
2. 增加随机等待时间
3. 检查是否有最新的 CDP 特征变量
412 Precondition Failed请求头不完整1. 补全 Referer 和 Sec-Fetch-* 系列字段
2. 更新 User-Agent 到最新 Chrome 版本
连接重置/503 错误IP 被临时封禁1. 降低请求频率到 2 - 5 秒 / 次
2. 启用 DrissionPage 的代理功能
SSL 证书验证失败本地 HTTPS 拦截1. 测试环境用 ssl._create_unverified_context()
2. 生产环境安装目标网站的根证书

📝 环境与执行

环境要求

  • Python 3.8+
  • Chrome/Chromium 100+(也可以由 DrissionPage 自动下载内置 Chromium)
  • 依赖库一键安装:
pip install DrissionPage pandas beautifulsoup4 openpyxl

执行命令

python cdut_spider.py

输出示例

✅ 瑞数验证等待时间:4.7s
开始模拟人类操作...
获取页面数据...
✅ 成功获取受保护数据!
关闭浏览器...
✅ 数据已成功保存到 成都理工大学学院专业信息.xlsx

📌 注意事项

  1. 仅供学习交流使用:请勿用于商业用途或大规模爬取,尊重学校的服务器资源。
  2. 瑞数版本可能更新:如果反检测 JS 失效,需要通过开发者工具观察新的检测点并及时更新。
  3. 代理 IP 慎用:该网站反爬更侧重特征检测,IP 封禁较少,频繁更换代理反而容易增加可疑特征。
  4. 数据结构变化:建议定期检查学校招生网的页面结构,及时调整 BeautifulSoup 的解析逻辑,保证数据准确抓取。