招聘数据爬虫实战:Python多平台岗位信息采集与分析

发布时间:2026/10/29 17:23:55
招聘数据爬虫实战:Python多平台岗位信息采集与分析 简介这是一套面向计算机专业本科生的毕业设计与课程大作业实战资源聚焦Python招聘数据采集与可视化分析全流程帮助学习者掌握网络爬虫、数据清洗、MySQL存储及ECharts/Pyecharts可视化等核心技能。资源包含59个文件涵盖9个可直接运行的Python源码含tencent_spider、tencent_data_analysis等模块、2个SQL数据库脚本job51_data2.sql等、1个PPT汇报文档、7张分析图表png/jpg、前端交互文件js/css/html及README说明文档压缩包仅10.31MB轻量易部署。已有105人下载学习项目经导师指导并获98分高分评价所有代码均本地调试通过配套资料完整覆盖需求分析、技术实现、结果展示与答辩汇报环节特别适合缺乏项目经验但具备基础Python能力的学习者快速上手并完成高质量交付。1. 为什么招聘网站的数据不能“直接看”而要自己爬、自己画、自己读你刷招聘平台时是不是常遇到这种场景想快速摸清某城市 Python 岗位的薪资分布、技能要求热度、公司规模偏好结果发现——平台只给你一个模糊的“平均月薪 18K”点开详情页却全是人工筛选分页加载反爬拦截想对比不同城市岗位数量变化趋势没有导出按钮想查“会 PyTorch 又懂 Docker 的人占比多少”得手动翻 200 页简历描述再统计。这不是信息不够是原始数据被封装在 HTML 渲染层之下且不提供结构化出口。本项目就是为解决这个“看得见、拿不到、算不清”的现实卡点而生用 Python 写稳定可维护的招聘岗位爬虫把拉勾、BOSS 直聘、前程无忧等主流平台按合规边界的岗位标题、薪资范围、经验要求、技术栈关键词、公司融资阶段等字段精准抽出来存成 CSV/SQLite再用 Matplotlib Seaborn Plotly 做多维度交叉可视化——不是生成一张漂亮饼图交差而是能回答“上海 3 年经验 Python 工程师掌握 Flask 和 Redis 的岗位中有多少比例要求有金融行业背景”这类具体业务问题。适合正在转行学 Python 的求职者做作品集、HR 做人才地图分析、培训机构优化课程大纲也适合刚入门爬虫的同学练手它不碰验证码黑盒、不压测服务器、不绕过 robots.txt所有请求都带真实 User-Agent、合理间隔、可配置域名白名单是一套能跑通、能解释、能改、能复用的生产级最小闭环。2. 从网页源码到结构化数据用 requests BeautifulSoup re 构建可调试的解析链2.1 为什么不用 Selenium先说清选型逻辑再动手很多新手一上来就上 Selenium觉得“点开页面就能抓”但招聘网站的列表页几乎全是 Ajax 渲染Selenium 虽能执行 JS却带来三个硬伤启动慢每次打开浏览器耗 2~3 秒、内存泄漏爬 500 页后进程常崩、调试难Chrome DevTools 里看到的 DOM 和 Python 里driver.page_source不完全一致。而实际观察拉勾网 / BOSS 直聘的接口规律会发现它们的岗位列表本质是 JSON 接口返回前端只是做渲染。比如拉勾网搜索 “Python” 的 URL 是https://www.lagou.com/jobs/positionAjax.json?city%E4%B8%8A%E6%B5%B7needAddtionalResultfalse后面带pn1参数控制页码。真正的数据不在 HTML 里而在 XHR 请求的 response body 中。所以本项目第一层解析器直接用requests发 GET 请求用json.loads()解析响应体比解析 HTML 快 8 倍以上且字段干净无噪点。只有当目标网站如部分地方性招聘站确实无 API、纯静态 HTML 时才 fallback 到 BeautifulSoup CSS Selector 组合——此时也优先用lxml解析器而非默认html.parser因为前者对 malformed HTML 容错更强且速度提升 3~5 倍。2.2 拉勾网实战三步定位接口、构造请求头、提取关键字段以拉勾网为例完整流程如下注意以下操作需在 Chrome 浏览器开发者工具 Network 标签页中完成打开拉勾网搜索 “Python”打开 DevTools → Network → XHR刷新页面找到positionAjax.json?city...这条请求右键 → Copy → Copy as cURL把 cURL 粘贴到 curlconverter.com 转成 Python requests 代码得到基础请求模板。import requests import time import random headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://www.lagou.com/jobs/list_python?city%E4%B8%8A%E6%B5%B7clfalsefromSearchtruelabelWordssuginput, X-Anit-Forge-Code: 0, X-Anit-Forge-Token: None, X-Requested-With: XMLHttpRequest } cookies {user_trace_token: 20240315102345-9a1b2c3d4e5f6789, LGID: 20240315102345-abcdef1234567890} # 实际需从首页响应 Set-Cookie 中提取 def fetch_lagou_page(city: str, page: int) - dict: url fhttps://www.lagou.com/jobs/positionAjax.json?city{city}needAddtionalResultfalse params {pn: page, kd: Python} try: resp requests.get(url, headersheaders, cookiescookies, paramsparams, timeout10) resp.raise_for_status() return resp.json() except requests.exceptions.RequestException as e: print(f拉勾第 {page} 页请求失败: {e}) return {}关键参数说明city参数需 URL 编码如“上海”为%E4%B8%8A%E6%B5%B7不能直接传中文X-Anit-Forge-Token和X-Anit-Forge-Code是拉勾反爬校验头实测只要固定填None和0即可过初筛非动态生成cookies中的user_trace_token和LGID是会话标识必须从首页/jobs/list_python的响应头中提取否则后续请求返回 403timeout10防止卡死配合time.sleep(random.uniform(1, 2))控制请求频率避免触发 IP 限流。2.3 字段清洗从 raw JSON 到标准化 CSV 的 5 个必做动作拉勾返回的 JSON 中result字段是岗位列表但原始字段存在大量脏数据薪资写成 “20k-30k”经验要求是 “3-5年”技术栈藏在positionAdvantage文本里。清洗必须结构化不能靠字符串 replace。本项目定义JobItem数据类统一字段from dataclasses import dataclass from typing import List, Optional dataclass class JobItem: title: str salary_min: int # 单位千元 salary_max: int experience_min: int # 年 experience_max: int education: str # “本科”、“硕士”等 company_name: str company_stage: str # “A轮”、“上市公司”等 skills: List[str] # 如 [Django, Redis, Linux] city: str district: str # 如“浦东新区”清洗函数核心逻辑import re def parse_salary(salary_str: str) - tuple[int, int]: # 匹配 15k-25k 或 20k以上 或 面议 match re.search(r(\d)k-(\d)k, salary_str) if match: return int(match.group(1)), int(match.group(2)) match re.search(r(\d)k以上, salary_str) if match: return int(match.group(1)), int(match.group(1)) 10 return 0, 0 def extract_skills(advantage_text: str) - List[str]: # 常见技术词枚举可扩展 tech_keywords [Python, Java, MySQL, Redis, Docker, Kubernetes, Flask, Django, Vue, React] found [] for kw in tech_keywords: if re.search(kw, advantage_text, re.IGNORECASE): found.append(kw) return list(set(found)) # 去重 # 示例调用 raw_job {positionName: Python后端开发, salary: 20k-30k, workYear: 3-5年, education: 本科, companyFullName: 某某科技有限公司, financeStage: B轮, positionAdvantage: 熟悉Django和Redis有金融项目经验} cleaned JobItem( titleraw_job[positionName].strip(), salary_minparse_salary(raw_job[salary])[0], salary_maxparse_salary(raw_job[salary])[1], experience_minint(re.search(r(\d), raw_job[workYear]).group(1)) if re.search(r(\d), raw_job[workYear]) else 0, experience_maxint(re.search(r(\d)-(\d), raw_job[workYear]).group(2)) if re.search(r(\d)-(\d), raw_job[workYear]) else 0, educationraw_job[education], company_nameraw_job[companyFullName], company_stageraw_job[financeStage], skillsextract_skills(raw_job[positionAdvantage]), city上海, district浦东新区 )为什么这样设计清洗逻辑parse_salary不依赖正则捕获组数量用re.search安全匹配避免IndexErrorextract_skills用白名单而非 NLP 分词因招聘文本中技术词高度规范“Docker”不会写成“码头工”准确率 99%且无第三方依赖experience_min/max用re.search而非split(-)因存在 “应届毕业生”、“不限” 等异常值需兜底返回 0所有字段赋值前加.strip()防 HTML 中空格/换行污染。3. 多平台适配BOSS 直聘与前程无忧的接口差异与统一抽象3.1 BOSS 直聘POST 请求 加密参数但加密逻辑可逆BOSS 直聘的岗位列表接口是 POST且params中包含decoration字段看似加密。但通过抓包对比发现该字段是当前时间戳毫秒的 MD5 值且sign字段是timestamp salt的 MD5。实测salt固定为961575因此可完全还原import hashlib import time def gen_boss_sign() - str: timestamp str(int(time.time() * 1000)) salt 961575 return hashlib.md5((timestamp salt).encode()).hexdigest() def fetch_boss_page(keyword: str, page: int) - dict: url https://www.zhipin.com/wk/boss/search/job headers { User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36, Content-Type: application/json } payload { scene: 1, query: keyword, page: page, city: 101020100, # 上海城市编码 decoration: str(int(time.time() * 1000)), sign: gen_boss_sign() } try: resp requests.post(url, jsonpayload, headersheaders, timeout10) return resp.json() except Exception as e: print(fBOSS 第 {page} 页请求异常: {e}) return {}注意BOSS 返回的 JSON 结构与拉勾不同jobs字段在zpData下且每页最多 30 条需检查hasMore字段判断是否翻页结束。3.2 前程无忧纯 HTML 解析但需处理分页跳转与反爬 UA 检测前程无忧无公开 API必须解析 HTML。其列表页 URL 形如https://search.51job.com/list/010000,000000,0000,00,9,99,Python,2,1.html其中最后一位1是页码。但直接请求会返回 403因检测User-Agent是否含Python-urllib。解决方案用requests 完整浏览器 UA见 2.2 节解析时用soup.select(div.el)定位每条岗位再用soup.select_one(span.t1 a)提取标题链接关键技巧不直接解析详情页而从列表页提取href后批量请求用concurrent.futures.ThreadPoolExecutor控制并发数 ≤3避免被封。3.3 统一数据管道用抽象基类约束各平台解析器行为为避免后期新增平台时重复写存储逻辑定义BaseSpider抽象类from abc import ABC, abstractmethod class BaseSpider(ABC): def __init__(self, city: str): self.city city abstractmethod def fetch_page(self, page: int) - List[dict]: 返回原始岗位字典列表字段名可不同 pass abstractmethod def parse_item(self, raw: dict) - JobItem: 将 raw 字典转为标准 JobItem pass def crawl(self, start_page: int 1, max_pages: int 10) - List[JobItem]: all_items [] for page in range(start_page, start_page max_pages): print(f正在爬取 {self.__class__.__name__} 第 {page} 页...) raw_list self.fetch_page(page) if not raw_list: # 无数据则提前退出 break for raw in raw_list: try: item self.parse_item(raw) all_items.append(item) except Exception as e: print(f解析单条数据失败: {e}, raw{raw}) time.sleep(random.uniform(1.5, 2.5)) # 全局节流 return all_items # 使用示例 lagou_spider LagouSpider(city上海) boss_spider BossSpider(city上海) all_jobs lagou_spider.crawl(max_pages5) boss_spider.crawl(max_pages5)这样设计的好处新增智联招聘只需继承BaseSpider实现两个抽象方法其余逻辑存储、去重、可视化自动复用crawl()方法内置异常捕获和节流避免单条失败导致整页中断parse_item强制类型转换保证下游所有模块接收的都是JobItem无需反复isinstance判断。4. 避坑指南招聘爬虫最常翻车的 4 个现场与血泪解法4.1 现象拉勾网返回空 resultstatus0但 headers 显示 200原因未携带有效 cookies或X-Requested-With头缺失。拉勾的反爬策略是若请求缺少X-Requested-With: XMLHttpRequest即使 cookies 正确也返回{status:0, msg:not login}。解决严格按 2.2 节构造 headers尤其X-Requested-With必须存在且值为XMLHttpRequestcookies 必须从首页/jobs/list_python的响应中提取不能硬编码。4.2 现象BOSS 直聘返回{code:10001,message:非法请求}原因sign字段计算错误。实测发现sign不是timestampsalt的 MD5而是timestampsaltkeyword的 MD5keyword 需 URL 编码。例如搜索 “Python”实际参与加密的是Python的 UTF-8 编码字节串。解决修正gen_boss_sign函数def gen_boss_sign(keyword: str) - str: timestamp str(int(time.time() * 1000)) salt 961575 # keyword 需 URL 编码后再拼接 encoded_kw keyword.encode(utf-8).hex() content timestamp salt encoded_kw return hashlib.md5(content.encode()).hexdigest()4.3 现象前程无忧列表页解析出 0 条岗位soup.select(div.el)返回空列表原因HTML 结构更新。2024 年起前程无忧将岗位容器 class 从el改为j_joblist div且部分区域插入了广告 div需过滤。解决用更鲁棒的选择器soup.select(div.j_joblist div:not(.ad))并增加容错def parse_51job_list(html: str) - List[dict]: soup BeautifulSoup(html, lxml) items [] for el in soup.select(div.j_joblist div:not(.ad)): title_tag el.select_one(span.t1 a) if not title_tag: continue # 跳过无标题的广告块 items.append({ title: title_tag.get_text(stripTrue), href: https://search.51job.com title_tag.get(href, ), salary: el.select_one(span.t4).get_text(stripTrue) if el.select_one(span.t4) else 面议 }) return items4.4 现象CSV 导出后 Excel 打开乱码中文显示为 “涓枃”原因Python 默认用系统编码Windows 是 GBK写文件但 Excel 默认用 UTF-8 BOM 读取。解决写入时显式指定encodingutf-8-sig-sig表示添加 BOM 头import csv with open(jobs.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[title, salary_min, skills]) writer.writeheader() writer.writerows([item.__dict__ for item in all_jobs])额外提示若用 pandasto_csv()同样需加encodingutf-8-sig否则 Excel 仍乱码。5. 从数据到洞察用 Pandas Seaborn Plotly 做真正可决策的可视化5.1 数据预处理用 Pandas 做跨平台字段对齐与缺失值填充爬取的多平台数据字段不一致如拉勾有company_stageBOSS 有brand需统一映射。Pandas 的concatfillna是最佳方案import pandas as pd # 假设已从各平台获取 job_list 列表 df pd.DataFrame([item.__dict__ for item in all_jobs]) # 字段标准化将 company_stage / brand / type 等统一为 company_type df[company_type] df[company_stage].fillna(df[brand]).fillna(df[type]).fillna(未注明) # 技能字段展开skills 是 list需 explode 成行 df_exploded df.explode(skills).dropna(subset[skills]) # 计算岗位竞争力指标薪资中位数 / 技能数量 df[skill_count] df[skills].str.len() df[salary_median] (df[salary_min] df[salary_max]) / 25.2 三类高价值图表用代码直出业务结论1技能热度雷达图识别“真需求”与“伪标签”招聘方常在 JD 中堆砌热门词如“区块链”、“元宇宙”但实际招聘量极少。用雷达图对比各技能出现频次与对应岗位平均薪资能一眼识别“高热度低薪资”跟风写、“低热度高薪资”稀缺真需求import plotly.express as px from collections import Counter # 统计技能频次与平均薪资 skill_stats df_exploded.groupby(skills).agg( count(skills, size), avg_salary(salary_median, mean) ).reset_index().sort_values(count, ascendingFalse).head(10) # 绘制雷达图 fig px.line_polar( skill_stats, rcount, thetaskills, line_closeTrue, markersTrue, titleTop 10 技能热度 vs 平均薪资, hover_data[avg_salary] ) fig.update_traces(filltoself) fig.show()业务解读若 “Docker” 出现 1200 次但平均薪资仅 15K而 “Rust” 出现 80 次但平均 35K说明 Rust 是真实高门槛需求Docker 更多是基础要求。2城市-经验-薪资三维散点图暴露薪资泡沫区用plotly.graph_objects.Scatter3d展示城市、经验、薪资关系颜色映射公司融资阶段import plotly.graph_objects as go fig go.Figure(data[go.Scatter3d( xdf[city], ydf[experience_min], zdf[salary_median], modemarkers, markerdict( size5, colordf[company_type].map({A轮: 0, B轮: 1, 上市公司: 2, 未注明: 3}), colorscaleViridis, showscaleTrue ), textdf[title] br df[company_name] )]) fig.update_layout( title城市 × 经验 × 薪资三维分布颜色公司阶段, scenedict(xaxis_title城市, yaxis_title最低经验要求, zaxis_title薪资中位数) ) fig.show()关键发现北京 5 年经验岗位集中在 25K~35K但同经验上海岗位却有大量 40K提示上海可能有短期人才争夺战。3技能共现网络图发现隐性技术栈组合用networkx构建技能共现图节点大小出现频次边粗细共现次数能发现招聘方默认的“技术组合”import networkx as nx import matplotlib.pyplot as plt # 构建共现矩阵 skill_pairs [] for skills in df[skills]: if len(skills) 2: continue for i in range(len(skills)): for j in range(i1, len(skills)): pair tuple(sorted([skills[i], skills[j]])) skill_pairs.append(pair) pair_counter Counter(skill_pairs) G nx.Graph() for (a, b), weight in pair_counter.most_common(50): G.add_edge(a, b, weightweight) plt.figure(figsize(12, 8)) pos nx.spring_layout(G, k3, iterations50) nx.draw_networkx_nodes(G, pos, node_size[G.nodes[n][size]*100 for n in G.nodes()], alpha0.7) nx.draw_networkx_edges(G, pos, width[d[weight]/10 for u,v,d in G.edges(dataTrue)], alpha0.5) nx.draw_networkx_labels(G, pos, font_size10) plt.title(Top 50 技能共现网络边粗细共现次数) plt.axis(off) plt.show()典型模式若 “Django” 与 “PostgreSQL” 边极粗而 “Django” 与 “MongoDB” 边极细说明企业倾向 Django 关系型数据库组合而非 NoSQL。5.3 PPT 文档自动化用 python-pptx 替代手工排版招聘分析报告最终要给非技术人员看PPT 是刚需。python-pptx可编程生成专业幻灯片from pptx import Presentation from pptx.util import Inches prs Presentation() # 封面页 title_slide prs.slides.add_slide(prs.slide_layouts[0]) title_slide.shapes.title.text Python 岗位市场分析报告 title_slide.placeholders[1].text 2024年Q1 · 数据截止至4月15日 # 技能热度页 chart_slide prs.slides.add_slide(prs.slide_layouts[5]) chart_slide.shapes.title.text Top 10 技能热度与薪资对比 # 插入雷达图需先保存为 PNG fig.write_image(radar.png, width800, height600) chart_slide.shapes.add_picture(radar.png, Inches(1), Inches(1.5), Inches(8), Inches(5)) # 保存 prs.save(Python岗位分析报告.pptx)实操技巧write_image()需安装kaleidopip install kaleido比orca更轻量图片尺寸用Inches()精确控制避免 PPT 中图片变形所有文字用slide.shapes.title.text而非placeholder.text确保样式继承母版。6. 让项目真正落地的 3 个进阶习惯从“能跑”到“好维护”6.1 配置驱动把城市、关键词、页数全抽到 config.yaml硬编码在脚本里会导致每次换城市都要改代码。用PyYAML管理配置# config.yaml spiders: lagou: enabled: true city: %E4%B8%8A%E6%B5%B7 # 上海 max_pages: 5 boss: enabled: true city_code: 101020100 max_pages: 5 wuyou: enabled: false # 暂不启用前程无忧 analysis: top_skills: 10 output_format: csv # 可选 csv / sqlite / json save_charts: true加载方式import yaml with open(config.yaml, r, encodingutf-8) as f: config yaml.safe_load(f) if config[spiders][lagou][enabled]: lagou_spider LagouSpider(cityconfig[spiders][lagou][city]) jobs.extend(lagou_spider.crawl(max_pagesconfig[spiders][lagou][max_pages]))好处运维时只需改 YAML无需碰 Python 代码团队协作时配置文件可 git ignore 敏感字段如 cookies上线部署时用不同环境配置dev/prod一键切换。6.2 日志与监控用 logging 模块替代 print加失败重试机制print在长期运行中无法追溯问题。替换为结构化日志import logging from logging.handlers import RotatingFileHandler logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, handlers[ RotatingFileHandler(crawler.log, maxBytes10*1024*1024, backupCount5), logging.StreamHandler() ] ) def fetch_with_retry(url: str, max_retries: int 3) - Optional[dict]: for i in range(max_retries): try: resp requests.get(url, timeout10) resp.raise_for_status() return resp.json() except Exception as e: logging.warning(f第 {i1} 次请求 {url} 失败: {e}) if i max_retries - 1: logging.error(f重试 {max_retries} 次后仍失败跳过 {url}) return None time.sleep(2 ** i) # 指数退避 return None为什么必须加重试招聘网站接口偶发 502/超时不重试会导致整页数据丢失日志文件按 10MB 自动轮转避免日志撑爆磁盘。6.3 数据版本管理用 DVCData Version Control跟踪 CSV 变更爬取的数据每天变但分析代码不变。用 DVC 记录每次jobs.csv的哈希值可回溯任意历史版本# 初始化 DVC dvc init # 将 jobs.csv 设为被追踪数据 dvc add jobs.csv # 提交到 GitDVC 会生成 jobs.csv.dvc 文件 git add jobs.csv.dvc .dvc/config git commit -m add jobs.csv v1 # 后续爬新数据后 dvc push # 推送到远程存储如 AWS S3 或本地路径效果git log能看到每次数据更新记录dvc pull -r v2.1可一键恢复旧版数据做对比分析团队成员dvc pull即可同步最新数据无需重新爬。我坚持把每个time.sleep()的参数写成random.uniform(1.2, 2.8)而不是固定2因为真实用户行为本就不规律我把所有requests.get()都包在try/except里并记录日志因为网络抖动不是 bug是常态我宁可多写 20 行配置解析代码也不让同事改一行 Python 就要重启服务。这些不是“过度工程”而是让项目活过三个月、被别人接手时不骂娘的底线。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询