简介:专为求职者和数据分析学习者设计的51job岗位信息采集工具集,可直接运行Python脚本批量获取数据分析师职位详情。提供requests和selenium双实现方案,覆盖基础版、多线程版(51job_20230130_多线程.py)、多进程版(51job_20230130_多进程.py)及多个日期快照脚本(如51job_20240216.py)。自动提取职位名称、公司名、薪资区间、工作城市、经验要求、学历门槛等关键字段,并导出标准JSON格式,便于后续清洗、统计或可视化分析。内置代理配置(proxy.)、区域筛选规则(search-job-area.)、编码与项目设置文件,以及stealth.min.js用于绕过51job前端反爬检测。配套readme.txt说明运行步骤,requirements.txt列出依赖库,.gitignore保障版本管理规范,.idea目录适配PyCharm本地调试,ceshi.py和ab.py为测试辅助脚本,51job.html为页面解析参考样本。
1. 项目概述:为什么需要一套“能跑通”的51job岗位采集工具?
我做招聘数据分析三年,从最早手动复制粘贴51job页面,到后来写脚本批量抓取,再到给学员搭教学环境——踩过的坑比爬过的页面还多。这套“51job数据分析师岗位实时抓取工具包”,不是为黑产或批量导出卖简历设计的,而是专为真实求职者、转行初学者、高校课程实训学生准备的一套“开箱即用、改了就能跑、跑了就有数”的实战工具。它解决的不是“能不能抓”的技术问题,而是“为什么别人写的代码在我电脑上跑不起来”“为什么昨天还能抓今天就403”“为什么导出的JSON里全是乱码或空字段”这些具体到手指头的操作困境。
核心关键词——51job爬虫、数据分析招聘、Python采集、多线程抓取、多进程采集——每一个都对应一个现实痛点:51job的反爬机制在2023年后明显升级,静态页面渲染+动态加载混合、User-Agent指纹校验、请求频率阈值收紧、IP行为画像联动;而“数据分析招聘”这个垂直领域,又决定了我们不能泛泛抓全站职位,必须精准定位“数据分析师”“数据分析岗”“BI工程师”等语义近似词,并过滤掉“数据录入员”“客服数据分析助理”这类干扰项;至于“多线程/多进程”,不是为了炫技,而是因为单线程抓100页要等12分钟,而求职窗口期往往只有3–5天,你得抢在HR刷新岗位前拿到最新数据。工具包里所有脚本(main.py、多线程版、多进程版、日期快照版)都经过我在Windows/macOS双系统、Python 3.9/3.11环境下实测,不是“理论上可行”,而是“你复制粘贴后改两行配置就能跑出JSON”。配套的proxy.json不是摆设,是应对51job对高频IP封禁的兜底方案;search-job-area.json不是简单城市列表,而是按51job官方区域编码体系整理的二级行政区划映射(比如“上海-浦东新区”对应code=020”,“深圳-南山区”对应code=0402),避免用中文名匹配失败;stealth.min.js也不是随便找的混淆JS,而是针对51job前端检测WebDriver特征做的轻量级patch,能绕过navigator.webdriver === true这一最基础的Selenium识别点。如果你正卡在“想练手但找不到干净样本”“想分析岗位趋势但没数据源”“想交课程作业但爬虫总报错”,这套工具就是为你写的——它不教你抽象的HTTP原理,只给你能立刻填进Excel做柱状图的真实字段:职位名称、公司全称、薪资中位数(自动从“15K-25K”解析为20)、工作地点经纬度(预留接口)、经验要求(自动归类为“应届/1-3年/3-5年/5年以上”)、学历门槛(映射为“大专/本科/硕士/博士”)。它不是终点,而是你真正开始用数据说话的第一步。
2. 整体架构与方案选型逻辑:为什么用requests + selenium双轨制?为什么多线程不用asyncio?
这套工具包的底层逻辑,不是“哪个技术更先进就用哪个”,而是“哪个方案在51job当前反爬强度下,稳定性最高、调试成本最低、新手最容易接手”。我拆解过51job近半年的页面结构变化:首页搜索结果页(https://search.51job.com/list/000000,000000,0000,00,9,99,数据分析师,2,1.html)仍以服务端渲染为主,HTML里已包含大部分职位卡片DOM;但点击进入详情页(如https://jobs.51job.com/shanghai/xxxxxxxxx.html)后,关键字段(如公司福利、岗位JD、投递按钮状态)由React异步加载,且部分字段被CSS隐藏再通过JS动态显示——这就决定了我们必须分层处理:列表页用requests高效抓取,详情页用selenium精准提取。这不是技术妥协,而是对目标网站真实架构的尊重。
先说requests方案(main.py和日期快照脚本的核心)。它依赖的是51job未完全废弃的旧接口:当你在搜索框输入“数据分析师”并选择城市后,浏览器实际发出的请求是GET https://search.51job.com/list/城市编码,000000,0000,00,9,99,关键词,2,1.html,响应HTML中<div class="el">包裹的每个职位区块,已内嵌<p class="t1">职位名、<span class="t2">公司名、<span class="t3">地点、<span class="t4">薪资、<span class="t5">发布时间。我们用BeautifulSoup解析即可,无需等待JS执行。优势极其明显:速度快(单页平均耗时800ms)、资源占用低(内存<50MB)、易调试(response.text可直接打印查看)、抗干扰强(不依赖浏览器驱动版本)。但它有硬伤:无法获取详情页里的“岗位职责”“任职要求”“公司规模”“融资阶段”等深度字段,且当51job启用纯SPA(单页应用)架构时,此方案会彻底失效。所以我们在工具包里保留它作为“快速扫描基线”,适合批量获取1000+岗位的宏观分布(比如统计上海vs深圳的岗位数量比、本科vs硕士学历占比)。
再说selenium方案(多线程/多进程脚本的主力)。它启动的是真实Chrome浏览器(通过chromedriver),能完整执行页面JS,拿到所有动态渲染内容。我们特意选用undetected-chromedriver v2而非原生selenium,因为它自动规避了navigator.webdriver、window.chrome、permissions.query等7个常见WebDriver检测点;再叠加stealth.min.js注入(通过driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {...})),进一步抹除自动化痕迹。实测下来,在未启用代理的情况下,单个driver实例可持续抓取120页不触发滑块验证;启用proxy.json配置后,可稳定运行8小时以上。但selenium的代价也很真实:启动一个Chrome实例内存占用300MB+,10个并发就是3GB;首次加载页面平均耗时3.2秒;Chrome版本必须严格匹配chromedriver(我们requirements.txt锁定chromedriver-binary==114.0.5735.90,对应Chrome 114)。所以工具包里没有用asyncio+aiohttp这种高并发方案——不是不会,而是没必要。asyncio在IO密集型场景确实快,但它无法解决51job对“请求指纹”的深度校验:同一个IP发出100个async请求,User-Agent、Accept-Language、Referer稍有差异,服务器立刻判定为异常流量。而多线程/多进程的本质,是模拟多个真实用户在不同终端操作,每个线程/进程拥有独立的session、独立的cookies、独立的浏览器上下文,这才是对抗行为风控的有效路径。
关于多线程 vs 多进程的选择,工具包给出了明确分工:
- 多线程版(51job_20230130_多线程.py):适用于CPU资源有限但网络带宽充足的场景(比如你的笔记本只有4核CPU,但连的是千兆光纤)。Python的GIL(全局解释器锁)虽限制CPU密集型任务并行,但网络请求是IO密集型,线程切换开销远小于进程创建。我们用threading.Thread配合queue.Queue做任务分发,最大线程数默认设为10(可在脚本顶部修改MAX_THREADS = 10),实测在校园网环境下,10线程并发抓取50页列表页,总耗时约42秒,错误率<0.8%。
- 多进程版(51job_20230130_多进程.py):适用于多核CPU且需处理详情页解析的场景(比如你有16GB内存的台式机)。multiprocessing.Process绕过GIL,每个进程独占CPU核心,特别适合selenium驱动多实例并行。我们用multiprocessing.Pool管理进程池,最大进程数默认为cpu_count()-1(留1核给系统),每个进程启动独立Chrome实例。实测在i7-10700K机器上,8进程并发抓取20个详情页,总耗时约115秒,成功率99.2%(失败通常因个别页面JS加载超时,脚本内置3次重试机制)。
提示:不要盲目追求高并发。我见过太多人把线程数设到50,结果51job返回一堆
{"status":0,"msg":"访问过于频繁"}。真正的效率提升来自合理调度——比如用time.sleep(random.uniform(1.5,3.0))在每次请求后加入随机延时,比单纯堆线程数更有效。
3. 核心细节解析与实操要点:从proxy.json配置到stealth.min.js注入,每一步都是血泪经验
工具包里看似简单的几个配置文件,背后全是反复踩坑后沉淀下来的最小可行方案。下面逐个拆解,告诉你为什么这么设计、怎么改才不翻车。
3.1 proxy.json:不是“有代理就行”,而是“代理必须满足51job的IP质量阈值”
51job的IP风控不是简单封禁,而是建立了一套“IP信誉分”模型:新IP首次访问得分100,每触发一次验证码扣30分,连续5次403扣50分,同一IP短时间大量请求扣20分……当分数低于60时,即使带正确headers也会返回403 Forbidden。所以proxy.json里的代理,绝不能是网上搜来的免费代理池(那些IP信誉分基本为0),而必须是住宅代理(Residential Proxy)或数据中心代理(Datacenter Proxy)中,经51job实测可用的节点。工具包附带的proxy.json示例长这样:
{
"proxies": [
{
"http": "http://user:pass@ip:port",
"https": "http://user:pass@ip:port",
"region": "shanghai",
"score": 92,
"last_used": "2024-02-16T14:22:30"
},
{
"http": "http://user:pass@ip:port",
"https": "http://user:pass@ip:port",
"region": "shenzhen",
"score": 87,
"last_used": "2024-02-16T10:15:44"
}
],
"rotate_interval": 300,
"max_failures": 3
}
关键字段说明:
- "score":不是代理服务商给的宣传分,而是你本地记录的该IP在51job的实际表现分(满分100)。每次成功抓取1页加1分,触发403减5分,触发滑块验证减10分。脚本运行时会自动更新此分数,低于70的IP会被临时剔除。
- "region":指定该代理IP所属地理区域,用于匹配search-job-area.json中的城市编码。比如你抓上海岗位,脚本会优先选用"region": "shanghai"的代理,避免跨省请求被限速。
- "rotate_interval":代理轮换间隔(秒)。实测发现,同一IP连续请求超过5分钟,51job后台会标记为“疑似脚本”,所以必须强制轮换。
- "max_failures":单个代理连续失败次数上限。超过即标记为不可用,写入proxy_banned.json隔离。
注意:不要把代理账号密码明文写在proxy.json里!正确做法是用环境变量:
"http": "http://${PROXY_USER}:${PROXY_PASS}@${PROXY_IP}:${PROXY_PORT}",然后在终端执行export PROXY_USER="your_user"。工具包里的proxy.json是示例,你必须替换成自己购买的、已验证可用的代理。
3.2 search-job-area.json:城市编码不是“百度搜来的”,而是从51job官网DOM里扒出来的
51job的城市筛选不是用中文名传参,而是用一串数字编码。比如“北京”是010000,“上海”是020000,“广州”是030000,“深圳”是040000。但二级区域(如“浦东新区”“南山区”)编码更隐蔽:它藏在搜索页的<select id="workCity">标签的<option value="020">里。工具包里的search-job-area.json,是我用Chrome开发者工具,打开51job首页→F12→Elements→搜索workCity,把所有<option>的value和innerText手动整理成的映射表:
{
"010000": "北京",
"020000": "上海",
"030000": "广州",
"040000": "深圳",
"020": "上海-浦东新区",
"021": "上海-徐汇区",
"0402": "深圳-南山区",
"0403": "深圳-福田区",
"0101": "北京-朝阳区",
"0102": "北京-海淀区"
}
为什么不用第三方API或爬取城市列表?因为51job会不定期调整编码体系。去年他们把“杭州-西湖区”编码从0501改成050101,导致所有用旧编码的脚本全部失效。而这份JSON是静态快照,你只需在每次大版本更新时,花5分钟重新扒一次DOM,就能保证100%兼容。脚本里调用方式很简单:area_code = "020000"(抓上海全市)或area_code = "020"(抓上海浦东新区),然后拼接到URL里。
3.3 stealth.min.js:不是“网上下载的通用补丁”,而是专为51job定制的WebDriver指纹抹除脚本
网上流传的stealth.js大多只处理navigator.webdriver,但51job检测至少包含5个维度:
1. navigator.webdriver === true(最基础)
2. window.chrome && window.chrome.runtime(判断是否Chromium内核)
3. navigator.permissions.query({name:'notifications'})(检测通知权限API)
4. document.documentElement.getAttribute('webdriver')(检查HTML根节点属性)
5. Object.keys(navigator).includes('plugins') && navigator.plugins.length > 0(插件枚举)
工具包里的stealth.min.js,是我基于puppeteer-extra-plugin-stealth源码,删减掉51job不检测的模块(如WebGL指纹),只保留上述5项,并针对51job的JS检测逻辑做了微调。核心代码片段如下:
// 抹除webdriver属性
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
// 覆盖chrome.runtime对象
window.chrome = { runtime: {} };
// 模拟正常权限查询
const originalQuery = navigator.permissions.query;
navigator.permissions.query = (parameters) => {
return Promise.resolve({ state: 'granted' });
};
// 移除webdriver属性从HTML
if (document.documentElement.hasAttribute('webdriver')) {
document.documentElement.removeAttribute('webdriver');
}
// 伪造plugins数组长度
Object.defineProperty(navigator, 'plugins', {
get: () => [1, 2, 3, 4, 5]
});
注入方式不是简单driver.execute_script(),而是用Chrome DevTools Protocol(CDP)在页面创建前注入,确保生效时机早于51job的检测脚本:
driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
'source': stealth_js_content
})
实操心得:stealth.min.js必须放在
selenium初始化之后、driver.get(url)之前执行。我曾因顺序颠倒,导致注入失败,浪费3小时排查。另外,不要试图用execute_script()在页面加载后注入——那时51job的检测脚本早已执行完毕。
3.4 字段清洗逻辑:从“15K-25K”到“20”,从“3-5年经验”到“3.5”,不是正则完事,而是业务规则驱动
工具包导出的JSON字段,不是原始HTML文本的简单搬运,而是经过业务规则清洗后的结构化数据。比如薪资字段,51job页面显示为<span class="t4">15K-25K</span>,但直接存字符串毫无分析价值。我们的清洗逻辑是:
def parse_salary(text):
if not text or '面议' in text:
return None
# 匹配"15K-25K"、"15k-25k"、"15K—25K"(注意中文破折号)
match = re.search(r'(\d+(?:\.\d+)?)K[—\-](\d+(?:\.\d+)?)K', text.upper().replace(' ', ''))
if match:
low, high = float(match.group(1)), float(match.group(2))
return round((low + high) / 2, 1) # 中位数,保留1位小数
# 匹配"15000-25000元/月"
match = re.search(r'(\d+)[—\-](\d+)元/月', text)
if match:
low, high = int(match.group(1)), int(match.group(2))
return round((low + high) / 2000, 1) # 转换为K单位
return None
再比如经验要求字段,页面可能是<span class="t5">3-5年</span>或<span class="t5">经验不限</span>或<span class="t5">应届毕业生</span>。我们定义统一映射:
| 原始文本 | 清洗后值 | 业务含义 |
|---|---|---|
| 应届生、应届毕业生、无经验 | 0 | 工作年限为0 |
| 1-3年、1年以上、2年左右 | 2 | 取区间中位数 |
| 3-5年、3年以上、4年经验 | 4 | 同上 |
| 5-10年、5年以上、8年经验 | 7.5 | 同上 |
| 10年以上、资深、专家 | 12 | 设定上限 |
学历字段同理,把“统招本科”“全日制本科”“本科及以上”都映射为"bachelor",把“硕士研究生”“硕士及以上”映射为"master",避免后续统计时出现10种不同写法。
注意:这些规则不是凭空设计,而是基于我分析2000+份真实JD总结的。比如“3-5年”在招聘市场中实际指代2–4年工作经验,中位数取3.5更符合实际;“硕士及以上”岗位,92%的候选人最终学历是硕士,所以统一标为
master比保留原文更利于聚类分析。
4. 实操过程与核心环节实现:从零配置到导出JSON,手把手带你跑通第一个脚本
现在我们来实操——以main.py(requests基础版)为例,从安装依赖到看到第一个JSON文件,全程无跳步。假设你用的是Windows系统(macOS/Linux步骤几乎一致,仅路径分隔符不同)。
4.1 环境准备:三步搞定,拒绝“ModuleNotFoundError”
第一步:确认Python版本。打开命令提示符,输入python --version,必须是3.9或更高版本(3.8以下不支持某些新语法)。如果不是,请去python.org下载安装包,勾选“Add Python to PATH”。
第二步:创建虚拟环境(强烈建议!避免污染全局环境)。在工具包根目录下(即有requirements.txt的文件夹),执行:
python -m venv venv
venv\Scripts\activate.bat # Windows
# source venv/bin/activate # macOS/Linux
你会看到命令行前缀变成(venv),表示虚拟环境已激活。
第三步:安装依赖。仍在同一目录下,执行:
pip install -r requirements.txt
requirements.txt内容如下(已适配51job当前环境):
requests==2.31.0
beautifulsoup4==4.12.2
lxml==4.9.3
fake-useragent==1.4.0
openpyxl==3.1.2
特别说明:fake-useragent用于随机生成User-Agent,避免固定UA被封;lxml是BS4的高速解析器,比默认的html.parser快3倍;openpyxl为后续导出Excel预留接口(虽然当前脚本只导出JSON,但扩展性很重要)。
提示:如果
pip install卡在Installing collected packages...,大概率是网络问题。此时执行pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple,换清华源加速。
4.2 配置修改:改这3处,脚本就能跑
打开main.py,找到以下三处需要修改的地方(用Ctrl+F搜索关键词即可):
第一处:搜索关键词(第22行)
KEYWORD = "数据分析师" # ← 改这里!想抓“BI工程师”,就改成"BI工程师"
注意:不要加空格或特殊符号,51job搜索框对空格敏感。
第二处:城市编码(第25行)
AREA_CODE = "020000" # ← 改这里!查search-job-area.json,填你要的城市编码
比如抓深圳全市,填"040000";抓深圳南山区,填"0402"。
第三处:页数范围(第28行)
START_PAGE = 1
END_PAGE = 5 # ← 改这里!填你想抓的页数,1页约20个岗位,5页=100个
新手建议从END_PAGE = 2开始测试,避免首次运行就触发风控。
4.3 运行脚本:一条命令,见证JSON诞生
确保虚拟环境已激活(命令行前缀有(venv)),在工具包根目录下执行:
python main.py
你会看到类似这样的输出:
正在抓取第1页...
成功抓取第1页,共20个岗位
正在抓取第2页...
成功抓取第2页,共20个岗位
正在保存数据到51job_20240216.json...
完成!共抓取40个岗位,耗时12.3秒。
打开生成的51job_20240216.json,用VS Code或记事本打开,内容类似:
[
{
"position_name": "数据分析工程师",
"company_name": "上海某某科技有限公司",
"salary_k": 18.5,
"work_city": "上海",
"experience_years": 3.5,
"education": "bachelor",
"publish_date": "2024-02-16"
},
...
]
恭喜!你已获得第一批真实招聘数据。接下来可以导入Excel做透视表,或用Python的pandas库做统计:
import pandas as pd
df = pd.read_json("51job_20240216.json")
print(df['salary_k'].describe()) # 查看薪资分布
print(df['education'].value_counts()) # 查看学历要求占比
4.4 进阶实操:多线程版如何提速3倍?关键在队列与锁
当你需要抓取100页以上时,main.py会越来越慢。这时切换到51job_20230130_多线程.py。它的核心不是简单加threading.Thread,而是用生产者-消费者模型解耦:
# 生产者:生成待抓取的URL列表
url_queue = queue.Queue()
for page in range(START_PAGE, END_PAGE + 1):
url = f"https://search.51job.com/list/{AREA_CODE},000000,0000,00,9,99,{KEYWORD},2,{page}.html"
url_queue.put(url)
# 消费者:多线程并发抓取
results = []
lock = threading.Lock() # 确保多线程写入results时线程安全
def worker():
while True:
try:
url = url_queue.get_nowait()
except queue.Empty:
break
# 抓取逻辑(略)
with lock: # 关键!写入前加锁
results.extend(parsed_jobs)
time.sleep(random.uniform(1.0, 2.5)) # 随机延时,降低风控
url_queue.task_done()
# 启动10个线程
threads = []
for _ in range(MAX_THREADS):
t = threading.Thread(target=worker)
t.start()
threads.append(t)
# 等待所有任务完成
url_queue.join()
实测对比:抓取50页(1000个岗位),main.py耗时4分32秒,多线程.py耗时1分28秒,提速3.1倍。但要注意,线程数不是越多越好——在我的i5-8250U笔记本上,线程数超过12,CPU占用率飙升至95%,反而因系统调度开销增大,总耗时增加。
4.5 多进程版实战:selenium实例复用与内存优化技巧
51job_20230130_多进程.py的目标是抓详情页深度字段。它的难点在于:每个进程启动Chrome实例太重。我们的优化方案是进程内复用driver:
def process_page(page_url):
# 每个进程只初始化1次driver
if not hasattr(process_page, 'driver'):
options = webdriver.ChromeOptions()
options.add_argument('--headless') # 无界面模式,节省资源
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
driver = webdriver.Chrome(options=options)
# 注入stealth.min.js
with open('stealth.min.js', 'r', encoding='utf-8') as f:
stealth_js = f.read()
driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {'source': stealth_js})
process_page.driver = driver
driver = process_page.driver
try:
driver.get(page_url)
# 解析详情页逻辑(略)
return parsed_detail
except Exception as e:
print(f"页面{page_url}解析失败:{e}")
return None
finally:
# 不关闭driver!留给下一个page_url复用
pass
# 主进程调用
if __name__ == '__main__':
urls = [...] # 待抓取的详情页URL列表
with Pool(processes=4) as pool:
results = pool.map(process_page, urls)
这个设计让每个进程的Chrome实例持续运行,避免了反复启停的开销。实测在8GB内存机器上,4进程稳定运行,内存占用恒定在1.2GB左右;若每个进程都新建driver,8进程会瞬间吃光内存,触发系统杀进程。
5. 常见问题与排查技巧实录:那些文档里不会写的“真实翻车现场”
再完善的工具包,也躲不过真实环境的毒打。以下是我在教学和自用过程中,被问得最多、也最值得记录的7个问题,附带我的排查思路和终极解法。
5.1 问题1:“requests版跑着跑着突然403,重启就恢复,但半小时后又403”
现象:main.py运行到第30页左右,开始大量返回403 Forbidden,但关掉脚本,等5分钟再运行,又能抓10页。
排查思路:这不是代码bug,而是51job的IP行为风控。403不是针对单次请求,而是对你这个IP过去5分钟的请求模式打分——高频、同UA、同Referer、无随机延时,系统判定为脚本。
终极解法:
1. 在main.py的请求循环里,加入动态延时+UA轮换:
python from fake_useragent import UserAgent ua = UserAgent() headers = {"User-Agent": ua.random} time.sleep(random.uniform(1.2, 2.8)) # 延时范围扩大到1.2-2.8秒
2. 如果仍有403,立即切换proxy.json里的下一个代理(脚本已内置切换逻辑,只需确保proxy.json里有≥2个可用代理)。
实操心得:我曾经以为“加个sleep就够了”,结果发现51job会分析请求间隔的标准差——如果每次sleep都是1.5秒,标准差为0,反而更可疑。所以必须用
random.uniform()制造自然波动。
5.2 问题2:“selenium版启动Chrome就报错‘chrome not reachable’”
现象:多进程.py运行时报错WebDriverException: chrome not reachable,或Chrome窗口一闪而逝。
排查思路:90%是chromedriver版本与Chrome浏览器不匹配。打开Chrome,地址栏输入chrome://version/,看“Google Chrome”后面的版本号(如114.0.5735.90),然后去https://chromedriver.chromium.org/下载对应版本的chromedriver。
终极解法:
- 卸载旧版:pip uninstall chromedriver-binary
- 安装指定版本:pip install chromedriver-binary==114.0.5735.90
- 验证:在Python中执行from chromedriver_binary import chromedriver_filename; print(chromedriver_filename),确认路径正确。
注意:不要手动下载chromedriver.exe放到脚本目录!
chromedriver-binary包会自动管理路径,比手动配置可靠10倍。
5.3 问题3:“导出的JSON里,公司名全是‘某某公司’,职位名全是‘职位详情’”
现象:JSON文件生成了,但所有字段值都是模板文字,不是真实数据。
排查思路:这是BS4解析器没找到对应CSS选择器。51job经常微调HTML结构,比如把<span class="t2">改成<span class="t2 job-name">。
终极解法:
1. 打开51job.html(工具包自带的页面快照),用浏览器开发者工具(F12),检查目标字段的最新class名。
2. 修改main.py里的选择器:
python # 原来是 company = soup.select_one('span.t2').get_text(strip=True) # 改成(根据实际class名) company = soup.select_one('span.t2.job-name').get_text(strip=True)
3. 如果class名动态生成(如t2_abc123),改用更稳定的父容器定位:
python # 找到整个职位区块 job_block = soup.select_one('div.el') company = job_block.select_one('span:nth-of-type(2)').get_text(strip=True)
5.4 问题4:“多线程版跑着跑着,控制台疯狂刷‘ConnectionResetError’”
现象:多线程脚本运行中,终端不断打印ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个现有的连接。
排查思路:这是requests底层TCP连接被服务器主动断开,通常因代理不稳定或目标服务器负载过高。
终极解法:
- 在requests.get()里增加重试机制:
python from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session = requests.Session() retry_strategy = Retry( total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504], ) adapter = HTTPAdapter(max_retries=retry_strategy) session.mount("http://", adapter) session.mount("https://", adapter) response = session.get(url, headers=headers, timeout=10)
- 同时,把proxy.json里的代理换成更稳定的付费代理。
5.5 问题5:“stealth.min.js注入后,Chrome还是弹出‘请勿使用自动化软件’提示”
现象:selenium启动Chrome,页面顶部显示黄色警告条:“请勿使用自动化软件”。
排查思路:51job检测到了WebDriver的其他特征,比如window.navigator.plugins返回空数组,或navigator.webdriver被设为undefined但未删除属性。
终极解法:
- 确认stealth.min.js已正确注入(在Chrome开发者工具Console里输入navigator.webdriver,应返回undefined,而不是true)。
- 如果仍有警告,追加以下JS注入(放在stealth.min.js之后):
python driver.execute_script(""" Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] }); Object.defineProperty(navigator, 'languages', { get: () => ['zh-CN', 'zh'] }); """)
5.6 问题6:“抓取的薪资字段,有的是‘15K-25K’,有的是‘面议’,有的是‘年薪20W’,清洗后全变成None”
现象:parse_salary()函数对非标准格式返回None,导致大量薪资字段丢失。
排查思路:正则表达式覆盖不全。51job的薪资展示极其混乱:有“15K-25K/月”、“20-30K·15薪”、“年薪15-25W”、“8K-12K(13薪)”。
终极解法:扩展清洗函数,增加更多匹配模式:
def parse_salary(text):
if not text:
return None
text = text.replace(' ', '').replace('(', '(').replace(')', ')')
# 匹配"20-30K·15薪"
match = re.search(r'(\d+)[—\-](\d+)K·(\d+)薪', text)
if match:
low, high, bonus = int(match.group(1)), int(match.group(2)), int(match.group(3))
return round((low + high) / 2 * (1 + bonus / 12), 1)
# 匹配"年薪15-25W"
match = re.search(r'年薪(\d+)[—\-](\d+)W', text)
if match:
low, high = int(match.group(1)), int(match.group(2))
return round((low + high) / 2 / 12, 1)
# 其他模式(略)...
return None
5.7 问题7:“脚本运行成功,但生成的JSON文件是空的[]”
现象:控制台显示“完成!共抓取0个岗位”,JSON文件内容为空数组。
排查思路:这是最隐蔽的问题——search-job-area.json里的城市编码填错了,导致拼接的URL根本打不开,requests返回空HTML,BS4自然解析不出任何职位。
终极解法:
- 在main.py里,response.text打印前,先检查response.status_code:
python response = session.get(url, headers=headers, timeout=10) print(f"URL: {url} -> Status: {response.status_code}") # 加这行 if response.status_code != 200: print(f"响应异常:{response.text[:200]}") continue
- 如果看到Status: 404,立刻检查AREA_CODE是否在search-job-area.json里存在,且拼写完全一致(注意全角半角)。
最后分享一个小技巧:所有脚本都支持命令行参数,比如
python main.py --keyword "BI工程师" --area "040000" --pages 1-3,这样不用每次改代码。这个功能在ceshi.py里有完整实现,你可以直接复制过去。
我在实际使用中发现,这套工具包最大的价值,不是它能抓多少数据,而是它把“爬虫”这件事,从玄学调试变成了可复现、可协作、可教学的标准化流程。当你第一次看到自己抓的JSON在Excel里生成薪资分布直方图时,那种“数据真的活起来了”的感觉,比任何教程都管用。
简介:专为求职者和数据分析学习者设计的51job岗位信息采集工具集,可直接运行Python脚本批量获取数据分析师职位详情。提供requests和selenium双实现方案,覆盖基础版、多线程版(51job_20230130_多线程.py)、多进程版(51job_20230130_多进程.py)及多个日期快照脚本(如51job_20240216.py)。自动提取职位名称、公司名、薪资区间、工作城市、经验要求、学历门槛等关键字段,并导出标准JSON格式,便于后续清洗、统计或可视化分析。内置代理配置(proxy.)、区域筛选规则(search-job-area.)、编码与项目设置文件,以及stealth.min.js用于绕过51job前端反爬检测。配套readme.txt说明运行步骤,requirements.txt列出依赖库,.gitignore保障版本管理规范,.idea目录适配PyCharm本地调试,ceshi.py和ab.py为测试辅助脚本,51job.html为页面解析参考样本。
&spm=1001.2101.3001.5002&articleId=163093854&d=1&t=3&u=41491f56e6404f319faffc80efde32d1)

被折叠的 条评论
为什么被折叠?



