简介这份资源是一套基于Python的招聘岗位数据爬取与可视化分析系统面向具备一定Python基础、希望实践数据采集与分析的学习者以及需要洞察招聘市场的人力资源从业者。系统以Requests库为核心针对智联招聘、前程无忧、拉勾网、BOSS直聘等主流平台完成职位名称、薪资范围、工作地点、经验与学历要求等字段的抓取并配套数据清洗、整理与可视化展示流程。压缩包共61个文件约10.32MB包含9个py源码、12个pyc编译文件、7个xml与7个png图表、6个jpg截图以及sql建表脚本、xls数据表、pptx演示文稿和README说明文档覆盖爬虫、分析、展示与文档多个环节。目前已有66人学习下载。读者可从中获得完整的爬虫与可视化项目结构、数据库脚本、分析图表与演示材料理解反爬应对、模块化设计与容错处理思路适合作为课程设计或数据分析练手案例。1. 招聘数据采集与分析系统从岗位信息到可视化洞察的完整落地路径招聘岗位数据爬取与可视化分析系统本质上解决的是一个很朴素的问题把分散在多个招聘平台上的岗位信息聚合起来做结构化分析回答“这个岗位在市场上到底值多少钱、需要什么技能、分布在哪些城市”这类问题。很多做数据分析和求职决策的人靠手动翻页看岗位效率极低且样本偏差大。用 Python 的 Requests 库配合解析工具可以批量拉取岗位标题、薪资、公司、城市、学历要求、技能标签等字段再通过 Pandas 清洗、Matplotlib 或 Pyecharts 做可视化最终产出一份可交互的岗位市场分析报告。这套方案适合有基础 Python 语法能力的数据分析初学者、求职策略研究者以及需要做行业薪酬调研的 HR 从业者。整套流程不依赖复杂框架一台普通笔记本就能跑通。2. 爬取目标拆解与 Requests 请求层设计2.1 先搞清楚要抓什么字段定义与页面结构分析在动手写代码之前必须先明确目标字段。招聘岗位页面通常包含以下核心信息岗位名称、薪资范围、工作城市、公司名称、公司规模、融资阶段、学历要求、经验要求、岗位职责描述、技能关键词。这些字段分布在列表页和详情页两个层级。列表页一般能拿到岗位名称、薪资、城市、公司名、学历和经验要求详情页才有完整的岗位职责和技能标签。常见做法是先用浏览器开发者工具F12打开目标平台的搜索列表页切换到 Network 面板刷新页面找到返回岗位列表数据的 XHR 请求。观察请求的 URL 结构、请求头中的关键字段Cookie、User-Agent、Referer、以及响应体的数据格式JSON 还是 HTML。这一步是整个项目的地基地基没打好后面解析全是白费功夫。我一般会先手动构造一个请求用 Requests 发出去看返回的内容和浏览器里看到的是否一致。如果不一致大概率是缺少了某个请求头或者 Cookie 过期了。2.2 用 Requests 构造一个可复用的请求函数下面是一个通用的请求封装包含重试机制、随机延迟和请求头管理import requests import time import random from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def build_session(): 构建带重试策略的 Session 对象 session requests.Session() retry Retry( total3, # 最多重试3次 backoff_factor1.5, # 重试间隔递增因子 status_forcelist[429, 500, 502, 503, 504] ) adapter HTTPAdapter(max_retriesretry) session.mount(https://, adapter) session.mount(http://, adapter) return session 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, Accept: application/json, text/plain, */*, Accept-Language: zh-CN,zh;q0.9, Referer: https://www.example-job-site.com/, } def fetch_page(session, url, paramsNone): 发送 GET 请求返回响应文本 try: resp session.get(url, headersHEADERS, paramsparams, timeout10) resp.raise_for_status() # 随机延迟降低被封风险 time.sleep(random.uniform(1.5, 3.5)) return resp.text except requests.exceptions.RequestException as e: print(f请求失败: {url}, 错误: {e}) return None这段代码的逻辑说明build_session创建了一个带重试策略的会话对象当遇到 429请求过多或 5xx 服务端错误时自动重试backoff_factor控制重试间隔按指数增长。HEADERS中 User-Agent 模拟浏览器Referer 告诉服务器请求来源页面这两个字段很多平台都会校验。fetch_page中time.sleep的随机延迟是关键固定间隔容易被识别为爬虫随机化后请求模式更接近真人操作。参数方面timeout10表示 10 秒无响应就放弃避免线程卡死。random.uniform(1.5, 3.5)的范围可以根据目标平台的反爬强度调整强度高的调到 3 到 6 秒。2.3 分页策略与增量采集招聘平台的列表页通常有两种分页方式一种是 URL 参数带页码如?page2另一种是滚动加载通过 POST 请求传偏移量。对于第一种直接循环拼接页码即可对于第二种需要构造 POST 请求体每次递增偏移量。def crawl_list_pages(session, base_url, max_pages50): 按页码循环采集列表页 all_items [] for page in range(1, max_pages 1): params {page: page, pageSize: 30} html fetch_page(session, base_url, paramsparams) if not html: break items parse_list_page(html) # 解析函数见下一章 if not items: print(f第 {page} 页无数据停止采集) break all_items.extend(items) print(f已采集第 {page} 页累计 {len(all_items)} 条) return all_items增量采集的思路是记录上一次采集到的岗位 ID 或发布时间下次采集时遇到已存在的 ID 就跳过。常见做法是在本地维护一个 SQLite 数据库每次插入前检查唯一键是否已存在。3. 数据解析、清洗与存储的工程化处理3.1 HTML 解析与 JSON 接口解析的两条路线如果目标平台返回的是 HTML 页面用 BeautifulSoup 或 lxml 解析如果返回的是 JSON 接口直接用resp.json()提取字段。两种路线的代码结构差异很大但核心思路一致定位到包含岗位信息的节点或键值逐条提取。from bs4 import BeautifulSoup import json def parse_list_page(html): 解析列表页兼容 HTML 和 JSON 两种格式 items [] # 先尝试 JSON 解析 try: data json.loads(html) for job in data.get(data, {}).get(list, []): items.append({ title: job.get(jobName, ), salary: job.get(salary, ), city: job.get(city, ), company: job.get(companyName, ), education: job.get(education, ), experience: job.get(experience, ), job_id: job.get(jobId, ), }) return items except (json.JSONDecodeError, AttributeError): pass # 回退到 HTML 解析 soup BeautifulSoup(html, lxml) for card in soup.select(.job-list-item): items.append({ title: card.select_one(.job-title).get_text(stripTrue), salary: card.select_one(.salary).get_text(stripTrue), city: card.select_one(.city).get_text(stripTrue), company: card.select_one(.company-name).get_text(stripTrue), education: card.select_one(.edu).get_text(stripTrue), experience: card.select_one(.exp).get_text(stripTrue), job_id: card.get(data-jobid, ), }) return items逻辑说明先尝试 JSON 解析因为 JSON 接口的数据结构更稳定字段提取更可靠。如果失败则回退到 HTML 解析。select_one使用 CSS 选择器定位节点.get_text(stripTrue)去除首尾空白。>import re def parse_salary(salary_str): 将各种薪资格式统一转换为 (最低月薪, 最高月薪) 元 if not salary_str or 面议 in salary_str: return (None, None) # 处理 15-25K 或 15-25k·13薪 match re.search(r(\d\.?\d*)-(\d\.?\d*)[Kk千], salary_str) if match: low float(match.group(1)) * 1000 high float(match.group(2)) * 1000 return (int(low), int(high)) # 处理 1.5-2万 match re.search(r(\d\.?\d*)-(\d\.?\d*)万, salary_str) if match: low float(match.group(1)) * 10000 high float(match.group(2)) * 10000 return (int(low), int(high)) # 处理 300元/天 match re.search(r(\d\.?\d*)元/天, salary_str) if match: daily float(match.group(1)) return (int(daily * 21.75), int(daily * 21.75)) return (None, None)逻辑说明用正则表达式匹配三种常见格式分别乘以对应系数转换成月薪。21.75是法定月计薪天数用于日薪转月薪。返回None表示无法解析后续分析时过滤掉。3.3 用 SQLite 做本地存储与去重采集的数据需要持久化SQLite 是最轻量的选择不需要额外安装服务。import sqlite3 def init_db(db_pathjobs.db): conn sqlite3.connect(db_path) conn.execute( CREATE TABLE IF NOT EXISTS jobs ( job_id TEXT PRIMARY KEY, title TEXT, salary_low INTEGER, salary_high INTEGER, city TEXT, company TEXT, education TEXT, experience TEXT, crawl_date TEXT ) ) conn.commit() return conn def save_jobs(conn, jobs): 批量插入job_id 冲突时忽略 cursor conn.cursor() for job in jobs: low, high parse_salary(job.get(salary, )) cursor.execute( INSERT OR IGNORE INTO jobs (job_id, title, salary_low, salary_high, city, company, education, experience, crawl_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, date(now)) , ( job[job_id], job[title], low, high, job[city], job[company], job[education], job[experience] )) conn.commit() print(f本次写入 {cursor.rowcount} 条新记录)INSERT OR IGNORE配合job_id主键实现自动去重重复采集同一岗位不会产生冗余数据。crawl_date记录采集日期方便后续做时间序列对比。4. 可视化分析与报告输出让数据开口说话4.1 薪资分布与城市对比的图表实现采集和清洗完成后最有价值的分析维度是薪资分布直方图、各城市平均薪资对比、学历与经验要求的占比、技能关键词词云。import pandas as pd import matplotlib.pyplot as plt import matplotlib # 设置中文字体避免乱码 matplotlib.rcParams[font.sans-serif] [SimHei] matplotlib.rcParams[axes.unicode_minus] False def analyze_salary_distribution(db_pathjobs.db): conn sqlite3.connect(db_path) df pd.read_sql(SELECT * FROM jobs WHERE salary_low IS NOT NULL, conn) conn.close() # 计算中位薪资 df[salary_mid] (df[salary_low] df[salary_high]) / 2 # 按城市分组统计 city_stats df.groupby(city)[salary_mid].agg([mean, count]) city_stats city_stats[city_stats[count] 10] # 过滤样本过少的城市 city_stats city_stats.sort_values(mean, ascendingFalse).head(15) fig, ax plt.subplots(figsize(12, 6)) ax.barh(city_stats.index, city_stats[mean] / 1000, color#4A90D9) ax.set_xlabel(平均月薪千元) ax.set_title(各城市岗位平均薪资对比样本≥10) plt.tight_layout() plt.savefig(city_salary.png, dpi150) plt.show() return city_stats逻辑说明salary_mid取薪资范围的中点作为该岗位的代表薪资。按城市分组后计算均值和样本数过滤掉样本少于 10 的城市避免统计偏差。barh画水平柱状图城市名在纵轴更易读。dpi150保证输出图片清晰度。4.2 技能关键词提取与词频统计岗位职责描述中的技能关键词是求职者最关心的信息之一。用 jieba 分词配合自定义词典提取。import jieba from collections import Counter # 自定义技能词典提高分词准确率 SKILL_WORDS [Python, Java, Go, SQL, 机器学习, 深度学习, 数据分析, 前端, 后端, 微服务, Docker, Kubernetes, Redis, MySQL, Linux, React, Vue, Spring] def extract_skills(descriptions): 从岗位描述列表中提取技能关键词频次 for word in SKILL_WORDS: jieba.add_word(word) counter Counter() for desc in descriptions: if not desc: continue words jieba.lcut(desc) for w in words: if w in SKILL_WORDS: counter[w] 1 return counter.most_common(20)jieba.add_word把技能词加入词典避免被切碎。Counter统计频次most_common(20)返回前 20 个高频技能。这个结果可以直接喂给 Pyecharts 做词云图或者做成柱状图展示。4.3 用 Pyecharts 生成可交互的 HTML 报告静态图片适合快速查看但交互式报告更适合分享和深入探索。from pyecharts.charts import Bar, Pie, WordCloud from pyecharts import options as opts def build_city_chart(city_stats): 生成城市薪资对比的交互式柱状图 bar ( Bar() .add_xaxis(city_stats.index.tolist()) .add_yaxis(平均月薪元, city_stats[mean].astype(int).tolist()) .set_global_opts( title_optsopts.TitleOpts(title城市薪资对比), xaxis_optsopts.AxisOpts(axislabel_optsopts.LabelOpts(rotate45)), yaxis_optsopts.AxisOpts(name月薪元), ) ) bar.render(city_salary_chart.html) print(图表已生成: city_salary_chart.html)rotate45让 x 轴标签倾斜 45 度避免城市名重叠。render输出独立 HTML 文件浏览器直接打开即可交互。5. 避坑指南采集与分析中最容易翻车的五个地方5.1 请求被重定向到登录页现象代码跑着跑着返回的 HTML 变成了登录页面解析出来全是空数据。原因目标平台检测到异常请求频率或缺少关键 Cookie将请求重定向到登录验证页。解决在fetch_page中检查响应 URL 是否被重定向如果resp.url与请求 URL 不一致且包含 login 关键字立即停止采集并打印警告。同时降低请求频率补充完整的 Cookie 字段。5.2 薪资解析出现负数或异常大值现象清洗后的薪资数据出现 0 或者几百万的异常值。原因正则匹配到了非薪资数字比如“3-5年经验”被误匹配为薪资。解决在正则前先判断字符串是否包含“K”“万”“元/天”等薪资单位关键词不包含则直接返回(None, None)。同时在入库后加一层校验过滤掉月薪低于 1000 或高于 500000 的记录。5.3 中文字体导致图表乱码现象Matplotlib 生成的图表中中文全部显示为方块。原因默认字体不支持中文。解决在绘图前设置matplotlib.rcParams[font.sans-serif] [SimHei]。Linux 环境下如果没有 SimHei可以换成[WenQuanYi Micro Hei]或安装中文字体包。Pyecharts 不受此影响因为它基于浏览器渲染。5.4 SQLite 并发写入报 database is locked现象多线程采集时写入数据库报错。原因SQLite 默认不支持高并发写入多个线程同时写会锁表。解决采集和写入分离采集线程只负责抓取把数据放入队列单独一个写入线程从队列取数据串行写入。或者改用 WAL 模式conn.execute(PRAGMA journal_modeWAL)。5.5 岗位 ID 不稳定导致去重失效现象同一岗位每次采集都产生新记录数据库快速膨胀。原因部分平台的岗位 ID 是动态生成的每次请求返回的 ID 不同。解决改用复合唯一键比如title company city的哈希值作为主键。或者用岗位详情页的 URL 作为唯一标识URL 通常比内部 ID 更稳定。6. 从采集到洞察几个让分析结果更可信的进阶技巧数据采集本身不是目的让分析结果经得起推敲才是。我在多次迭代这套流程后总结了几个真正提升数据可信度的做法。第一个是样本量控制。很多人在做城市薪资对比时某个城市只采到三五条数据就敢算平均值这种结论毫无意义。我的习惯是设置最小样本阈值低于 30 条的城市不单独展示合并到“其他城市”类别中。如果要做更严谨的对比可以计算中位数和四分位距比均值更能反映真实分布。第二个是时间窗口对齐。招聘数据有明显的季节性波动年初和年中的岗位结构差异很大。如果要做趋势分析确保对比的数据来自相同的时间窗口。我在数据库里保留了crawl_date字段分析时用WHERE crawl_date BETWEEN 2025-01-01 AND 2025-03-31来限定范围。第三个是技能关键词的动态更新。技术栈变化很快去年热门的技能今年可能已经过时。我一般每季度手动审查一次SKILL_WORDS列表根据采集到的新岗位描述补充新词删掉已经很少出现的旧词。这个维护成本不高但能让分析结果始终保持时效性。第四个是交叉验证。不要只看一个平台的数据就下结论。同一岗位在不同平台的薪资标注可能差异很大有的平台偏高有的偏低。如果条件允许至少采集两个平台的数据做交叉对比取交集或加权平均结论会更稳健。最后一个习惯是保留原始数据。清洗和转换过程中难免有信息损失把原始 HTML 或 JSON 响应存一份到本地文件或对象存储中后续发现解析逻辑有问题时可以重新处理不用重新采集。这个“后悔药”在排查数据异常时特别管用。这套流程从 Requests 请求封装到可视化报告输出核心代码不超过 300 行但覆盖了采集、清洗、存储、分析、展示的完整链路。真正花时间的不是写代码而是理解目标页面的结构、处理各种边界情况、以及反复验证数据的合理性。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?