避开这些坑!用Python爬研招网数据时,我遇到的3个反爬难题与解决方案

张开发
2026/4/21 19:58:49 15 分钟阅读

分享文章

避开这些坑!用Python爬研招网数据时,我遇到的3个反爬难题与解决方案
避开这些坑用Python爬研招网数据时我遇到的3个反爬难题与解决方案去年帮实验室抓取研招网招生数据时我本以为用RequestsBeautifulSoup就能轻松搞定结果连续三天被各种反爬机制按在地上摩擦。最崩溃的是每次以为问题解决了换个时间段运行脚本又会冒出新的异常。本文将复盘那段血泪史重点分享三个最棘手的反爬问题及其解决方案这些经验同样适用于其他教育类网站的数据采集。1. 动态令牌那个会隐身的__jsluid参数第一次请求研招网首页时控制台突然报错403。打开开发者工具仔细比对发现响应头里有个诡异的Set-Cookie字段Set-Cookie: __jsluidabc123; path/; HttpOnly这个看似普通的cookie其实是个动态令牌研招网用它来验证请求的合法性。如果首次访问不带这个cookie服务端会返回一段JavaScript代码document.cookie__jsluidxyz456;path/; location.reload();解决方案分三步走首次请求捕获JS代码import re first_resp requests.get(https://yz.chsi.com.cn, headersheaders) js_code first_resp.text正则提取cookie值jsluid re.search(r__jsluid([^;]), js_code).group(1)后续请求携带cookiesession requests.Session() session.cookies.set(__jsluid, jsluid)实际测试发现这个cookie有效期约30分钟超时需要重新获取。建议在爬虫中增加cookie过期检测逻辑。2. 参数加密当查询条件变成乱码研招网的搜索接口queryAction.do要求POST请求但直接发送参数会返回空数据。用Chrome开发者工具的Network面板抓包发现参数被加密成类似这样wQxV12%2FzKp%3D%3D%7Cabcdef逆向分析发现加密流程是原始参数JSON序列化AES加密密钥动态生成Base64编码拼接动态salt应对方案有两种选择方案A模拟浏览器执行加密JSfrom selenium import webdriver driver webdriver.Chrome() driver.get(https://yz.chsi.com.cn/zsml/query.do) encrypted_params driver.execute_script(return window.encryptParams(originalParams))方案BPython还原加密逻辑需逆向分析JSfrom Crypto.Cipher import AES import base64 def encrypt_params(params): key get_dynamic_key() # 从首页JS提取 cipher AES.new(key, AES.MODE_CBC, iv) padded pad(json.dumps(params).encode()) encrypted cipher.encrypt(padded) return base64.b64encode(encrypted).decode()实测发现方案B性能更好但需要定期维护加密逻辑。这里有个小技巧研招网每周五凌晨更新加密密钥可以在周四晚上跑一次密钥获取脚本。3. 请求指纹你的User-Agent出卖了你即使用上了随机UA和代理IP爬虫运行一段时间后仍然会被封。通过Wireshark抓包对比发现这些细节差异特征项浏览器请求爬虫请求TCP窗口大小6553516384TLS指纹Chrome 102Python/3.9HTTP2帧顺序HEADERSDATA只有HEADERS终极解决方案是使用undetected-chromedriverimport undetected_chromedriver as uc options uc.ChromeOptions() options.add_argument(--disable-blink-featuresAutomationControlled) driver uc.Chrome(optionsoptions) driver.get(https://yz.chsi.com.cn) html driver.page_source关键配置参数--disable-blink-featuresAutomationControlled隐藏自动化标志--window-size1920,1080模拟常见分辨率--langzh-CN设置中文环境4. 容灾设计让爬虫具备断点续传能力研招网在招生季经常进行服务维护最稳妥的方案是实现以下容灾机制分页缓存每成功抓取一页就立即本地存储import pickle def save_page(page_num, data): with open(fpage_{page_num}.pkl, wb) as f: pickle.dump(data, f)异常重试使用tenacity库实现智能重试from tenacity import retry, stop_after_attempt, wait_exponential retry(stopstop_after_attempt(3), waitwait_exponential(multiplier1)) def fetch_page(url): response requests.get(url) response.raise_for_status() return response.json()增量爬取记录最后成功的时间戳CREATE TABLE crawl_log ( id INT AUTO_INCREMENT, crawl_time TIMESTAMP, page_count INT, PRIMARY KEY (id) );5. 效率优化从单线程到异步集群当需要抓取全国数据时单机运行需要近20小时。通过以下改造将时间压缩到2小时内架构升级路径单线程 → 多线程ThreadPoolExecutor多线程 → 异步IOaiohttp单机 → 分布式Scrapy-Redis性能对比测试方案1000页耗时CPU占用内存占用单线程82分钟15%200MB多线程(10)9分钟85%800MB异步(100并发)3分钟60%500MB推荐配置# async_worker.py async def fetch(session, url): async with session.get(url) as response: return await response.text() async def main(urls): connector TCPConnector(limit100) async with ClientSession(connectorconnector) as session: tasks [fetch(session, url) for url in urls] return await asyncio.gather(*tasks)记得在爬取间隔中加入随机延时避免对服务器造成压力import random await asyncio.sleep(random.uniform(0.5, 1.5))

更多文章