news 2026/10/22 4:56:18

Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

简介:这是一套面向计算机相关专业学生与项目实战学习者的Python数据采集与分析可视化完整项目,以Boss直聘岗位数据为对象,适合用作毕业设计、课程设计或期末大作业。资源包共38个文件,约246KB,以13个py源码文件为核心,涵盖爬虫配置、数据清洗与管道处理等模块,另含12个js、4个xml、1个html与1个css构成前端展示部分,并附1个csv数据文件、1个md说明文档及png图片等辅助材料,目录结构清晰,便于按模块阅读与二次开发。目前已有162人学习下载。项目经导师指导并获评审99分,代码完整可运行,读者可据此掌握从数据抓取、清洗存储到可视化呈现的完整链路,理解Scrapy框架配置、中间件与管道协作方式,并参考说明文档快速上手,适合作为实战练习与排错思路的参考范本。

1. 招聘数据采集这件事,门槛到底在哪

很多人第一次听到“岗位数据采集与分析可视化”,脑子里浮现的是几行 requests 加一个 pandas 就完事。真上手才发现,采集端要处理登录态、翻页参数、动态渲染和频率控制,分析端要面对岗位名称不统一、薪资区间格式混乱、城市层级不一致这些脏活。这个项目标题指向的是一套完整链路:用 Python 把招聘平台的岗位数据抓下来,清洗成结构化表格,再做薪资分布、技能词频、城市热力这类可视化。它适合两类人——想入门数据采集但不想碰灰色地带的初学者,以及手里有分析需求、缺一份可复用采集模板的从业者。核心难点从来不是写代码,而是让整条链路稳定、可复现、不翻车。

2. 采集链路怎么搭:从请求构造到数据落库

2.1 先想清楚采什么,再决定怎么采

动手之前必须把字段清单定死,否则代码写到一半改字段,清洗脚本全部重来。岗位数据采集常见字段分三层:基础信息(岗位名称、公司、城市、区域)、薪资信息(薪资区间、薪资月数、学历要求、经验要求)、描述信息(岗位职责、技能标签、福利列表)。这三层的采集难度递增,基础信息通常在列表页就能拿到,描述信息往往要进详情页。

我一般会先做一次人工采样,打开目标页面,用浏览器开发者工具看接口返回的 JSON 结构。如果列表页有 XHR 接口直接返回结构化数据,优先走接口而不是解析 HTML,因为接口字段稳定、解析成本低。如果页面是服务端渲染,那就老老实实解析 DOM。判断标准很简单:接口返回的字段能不能覆盖你字段清单的 80% 以上,能就走接口。

字段定完之后要确定存储格式。小规模采集(几千条以内)用 CSV 足够,字段里有换行和逗号时注意转义。上万条建议上 SQLite,方便去重和增量更新。下面是一个字段定义的示例结构:

# 岗位数据字段定义,采集前先对齐,避免后期返工 JOB_FIELDS = { "job_name": "岗位名称", # 列表页可获取 "company": "公司名称", # 列表页可获取 "city": "城市", # 列表页可获取,注意层级 "district": "区域", # 部分列表页有 "salary_raw": "薪资原始文本", # 如 "15-25K·14薪" "education": "学历要求", "experience": "经验要求", "skills": "技能标签", # 详情页或标签区 "description": "岗位描述", # 详情页获取 "publish_time": "发布时间", }

这段定义的作用是给后续所有环节一个统一契约。采集脚本按这个字典写解析逻辑,清洗脚本按这个字典做类型转换,可视化脚本按这个字典取列。参数上唯一要注意的是salary_raw保留原始文本,不要在采集阶段就拆成数字,因为薪资格式变体太多,拆解逻辑放在清洗阶段单独维护。

2.2 请求构造与翻页:参数怎么设才不会被拦

采集请求的核心是模拟正常用户行为,而不是追求速度。请求头里 User-Agent 要带,Referer 建议带上列表页地址,Cookie 在需要登录态的场景下必须携带。翻页参数通常有两种形式:页码型(page=1,2,3)和游标型(last_id=xxx)。页码型简单但容易被限制,游标型更稳定但需要从上一页响应里提取游标值。

下面是一个通用的请求封装,包含重试和随机延迟:

import requests import time import random from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def build_session(): """构造带重试策略的会话对象""" session = requests.Session() retry = Retry( total=3, # 最多重试3次 backoff_factor=1.5, # 退避因子,重试间隔递增 status_forcelist=[429, 500, 502, 503], # 这些状态码触发重试 ) adapter = HTTPAdapter(max_retries=retry) session.mount("https://", adapter) session.headers.update({ "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, */*", "Referer": "https://example.com/list", # 替换为实际列表页 }) return session def fetch_page(session, url, params): """单页请求,带随机延迟""" time.sleep(random.uniform(1.5, 3.5)) # 随机延迟,避免固定节奏 resp = session.get(url, params=params, timeout=15) resp.raise_for_status() return resp.json()

逻辑说明:Retry的total=3表示最多重试三次,backoff_factor=1.5让每次重试间隔按 1.5 的倍数增长,避免密集重试触发风控。status_forcelist里放的是服务端临时错误码,429 是频率限制,500 系列是服务端异常,这些值得重试;403 和 404 不要重试,重试也没用。随机延迟的范围 1.5 到 3.5 秒是经验值,低于 1 秒容易被识别为脚本,高于 5 秒采集效率太低。参数上唯一需要按实际情况调的是timeout,网络差的环境可以放到 20 秒。

翻页循环要设终止条件,不能无限翻。常见终止条件有三个:返回列表为空、返回条数小于页大小、达到预设最大页数。三个条件满足任一就停。我一般会设一个max_pages兜底,防止接口异常时死循环。

2.3 数据落库与增量更新

采集到的数据不能直接覆盖写,要做增量。最简单的增量策略是用岗位唯一标识(比如岗位链接里的 ID)做去重。SQLite 建表时给这个字段加 UNIQUE 约束,插入时用INSERT OR IGNORE,重复数据自动跳过。

import sqlite3 import json def init_db(db_path): """初始化数据库表结构""" conn = sqlite3.connect(db_path) conn.execute(""" CREATE TABLE IF NOT EXISTS jobs ( job_id TEXT PRIMARY KEY, -- 岗位唯一标识 job_name TEXT, company TEXT, city TEXT, salary_raw TEXT, education TEXT, experience TEXT, skills TEXT, -- JSON 数组存为文本 description TEXT, publish_time TEXT, crawl_time TEXT DEFAULT CURRENT_TIMESTAMP ) """) conn.commit() return conn def save_jobs(conn, jobs): """批量插入,重复岗位自动跳过""" sql = """INSERT OR IGNORE INTO jobs (job_id, job_name, company, city, salary_raw, education, experience, skills, description, publish_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""" rows = [ (j["job_id"], j["job_name"], j["company"], j["city"], j["salary_raw"], j["education"], j["experience"], json.dumps(j.get("skills", []), ensure_ascii=False), j.get("description", ""), j.get("publish_time", "")) for j in jobs ] conn.executemany(sql, rows) conn.commit()

INSERT OR IGNORE配合主键约束实现幂等写入,同一岗位重复采集不会产生重复行。skills字段用 JSON 文本存储是因为技能标签数量不固定,单独建关联表对这个小规模场景来说过度设计。crawl_time用数据库默认值自动填充,方便后续做采集批次分析。参数上要注意executemany的批量大小,一次插入 500 到 1000 条比较合适,太大容易锁表,太小写入效率低。

3. 清洗与可视化:把脏数据变成能看的图

3.1 薪资字段拆解:三种格式和四个边界

薪资原始文本的格式比想象中乱。常见的有三种:区间型(15-25K)、固定型(20K)、面议型(薪资面议)。区间型还要处理“·14薪”这种附加信息。拆解逻辑要覆盖这些变体,同时处理边界情况。

import re def parse_salary(raw): """解析薪资文本,返回 (最低, 最高, 月数) 单位K""" if not raw or "面议" in raw: return None, None, None # 提取薪月数,默认12 months = 12 m = re.search(r"·(\d+)薪", raw) if m: months = int(m.group(1)) # 提取薪资数字部分 nums = re.findall(r"(\d+\.?\d*)", raw.split("·")[0]) if len(nums) == 2: low, high = float(nums[0]), float(nums[1]) elif len(nums) == 1: low = high = float(nums[0]) else: return None, None, None # 处理“元/天”这类日薪,统一折算为月薪K if "元/天" in raw: low = low * 22 / 1000 high = high * 22 / 1000 return low, high, months

四个边界要注意:第一,“15-25K”和“15K-25K”写法不同但含义一样,正则要能兼容;第二,“1.5-2万”这种单位是万,需要乘 10 转成 K;第三,日薪按 22 个工作日折算月薪;第四,面议岗位返回 None,后续统计时单独归为“未披露”类别,不要直接丢弃,否则样本有偏。

3.2 技能词频与城市分布的可视化

清洗完的数据用 pandas 做聚合,matplotlib 或 pyecharts 出图。技能词频的做法是把 skills 字段的 JSON 数组展开,用 Counter 统计。城市分布按城市分组计数,注意城市名要统一,比如“北京”和“北京市”要合并。

import pandas as pd from collections import Counter import matplotlib.pyplot as plt # 设置中文字体,否则图表中文显示为方块 plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False def load_clean_data(db_path): """从数据库读取并做基础清洗""" conn = sqlite3.connect(db_path) df = pd.read_sql("SELECT * FROM jobs", conn) # 薪资拆解 parsed = df["salary_raw"].apply(parse_salary) df["salary_low"] = [p[0] for p in parsed] df["salary_high"] = [p[1] for p in parsed] # 城市名统一 df["city"] = df["city"].str.replace("市", "", regex=False) return df def plot_skill_top(df, top_n=15): """技能词频柱状图""" all_skills = [] for s in df["skills"].dropna(): all_skills.extend(json.loads(s)) counter = Counter(all_skills) top = counter.most_common(top_n) names, counts = zip(*top) plt.figure(figsize=(10, 6)) plt.barh(names[::-1], counts[::-1]) plt.xlabel("出现次数") plt.title(f"技能标签 Top {top_n}") plt.tight_layout() plt.savefig("skill_top.png", dpi=150)

font.sans-serif设成 SimHei 是 Windows 下的做法,Linux 环境要换成系统里实际存在的中文字体,否则中文全是方块,这是新手最常翻车的地方。dpi=150保证导出图片清晰度够用,屏幕展示 100 就够,打印或报告用 150 到 200。技能统计前要过滤掉空值和异常标签,比如有些岗位的 skills 字段是空数组,dropna处理不了空数组,需要在展开时加判断。

城市分布图建议用横向柱状图而不是饼图,因为城市数量多的时候饼图根本看不清。薪资分布用箱线图按城市分组,能直观看出不同城市的薪资中位数和离散程度。

4. 避坑指南:采集分析链路上的五个血泪教训

4.1 翻页翻到一半返回空列表

现象:前几页正常返回,翻到第 5 页或第 10 页突然返回空数组,但手动在浏览器里翻页是正常的。

原因:多数平台的列表接口有深度分页限制,页码超过阈值后不再返回数据,或者需要额外的游标参数。另一个可能是触发了频率限制,服务端静默返回空数据而不是报错。

解决:翻页循环里加空列表检测,一旦返回空就停止并记录当前页码。同时把页码型翻页改成游标型,从响应里提取下一页的游标值。如果平台确实有深度限制,就按城市或关键词拆分采集任务,每个子任务只翻前几页,用维度拆分绕过深度限制。

4.2 薪资统计结果明显偏高

现象:算出来的平均薪资比招聘网站上显示的水平高出一大截。

原因:薪资字段拆解时把“·14薪”的月数当成了薪资数字,或者把“元/天”的日薪直接当成了月薪。还有一种情况是“20-40K”这种宽区间岗位拉高了均值,而这类岗位往往是销售或管理岗,不代表整体水平。

解决:拆解函数里先剥离“·N薪”部分再提取数字,日薪单独判断单位。统计时用中位数而不是均值,或者按岗位类别分组统计。宽区间岗位可以单独标记,分析时排除或单独展示。

4.3 中文图表全是方块

现象:matplotlib 生成的图表里中文标题和标签显示为一个个方块。

原因:matplotlib 默认字体不支持中文,而代码里没有指定中文字体,或者指定的字体在系统里不存在。

解决:先查系统里有哪些中文字体,用fc-list :lang=zh命令在 Linux 下查看,Windows 下直接看C:\Windows\Fonts目录。把font.sans-serif设成实际存在的字体名。如果部署环境不确定,可以把字体文件打包进项目,用font_manager动态加载。

4.4 数据库写入越来越慢

现象:采集初期写入很快,跑到几千条以后每条插入都要等好几秒。

原因:每次插入都单独 commit,SQLite 在频繁小事务下性能急剧下降。另一个原因是主键冲突检查随着数据量增长变慢。

解决:改成批量插入,攒够 500 条 commit 一次。建表时给查询频繁的字段加索引,比如 city 和 publish_time。如果数据量超过十万条,考虑换 PostgreSQL 或 MySQL,SQLite 在这个量级下并发写入会吃力。

4.5 采集脚本跑一夜被中断

现象:挂机采集,第二天发现脚本半夜就停了,日志里只有一条连接超时。

原因:没有做异常兜底,单次请求超时直接抛异常终止了整个循环。网络抖动、服务端临时不可用都会导致这种情况。

解决:把单页请求包在 try-except 里,超时或异常时记录日志并跳过当前页,继续下一页。同时加一个断点续采机制,把已完成的页码或游标存到文件里,脚本重启后从断点继续。这样即使中断也不会丢进度。

5. 让分析结果真正可用的两个进阶技巧

第一个技巧是给岗位名称做聚类归一。原始数据里“Python开发工程师”“Python后端开发”“高级Python工程师”其实是同一类岗位,直接按名称分组会得到几百个细碎类别,图表完全没法看。做法是用 jieba 分词后提取关键词,或者用简单的规则匹配把包含“Python”和“开发”的归为一类。更稳的做法是维护一个映射表,把常见变体映射到标准岗位名,映射表随采集批次逐步补充。这个映射表是整条链路里最有复用价值的资产,比采集脚本本身更值得维护。

第二个技巧是加一个数据质量校验环节。每次采集完成后跑一遍校验脚本,检查字段缺失率、薪资解析成功率、城市字段的取值分布。缺失率超过 30% 的字段要在分析时标注出来,薪资解析成功率低于 80% 说明解析规则需要更新。校验结果存成一份简短的报告,和采集批次绑定。这样当分析结论出现异常时,能快速定位是数据质量问题还是分析逻辑问题。

def quality_check(df): """数据质量校验,返回各字段缺失率和解析成功率""" report = {} for col in ["job_name", "company", "city", "salary_raw"]: report[f"{col}_missing_rate"] = df[col].isna().mean() salary_parsed = df["salary_low"].notna().mean() report["salary_parse_rate"] = salary_parsed report["total_rows"] = len(df) report["city_count"] = df["city"].nunique() return report

这个校验函数跑一次不到一秒,但能省掉后面大量排查时间。我现在的习惯是采集完先跑校验,校验通过再进分析环节,不通过就先修数据。这个习惯帮我避开了好几次“分析结论看起来合理但其实是脏数据导致的”这种隐蔽翻车。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 13:48:17

从Vibe Coding到Spec-Driven:让AI编程不再失控

这段时间“vibe coding”这个词在开发者圈子里是真的火——用自然语言描述个大致想法,让AI噼里啪啦把代码写了,自己也懒得逐行看,跑通了就感觉“人机合一”。我在不少技术社区看到新手用这种方式一晚上搭出个小工具,确实爽。但爽完…

作者头像 李华
网站建设 2026/10/21 3:35:07

Trash-ICRA19数据集YOLO格式转换与训练全流程指南

简介:本资源为面向计算机、电子信息工程等专业学生及算法初学者的YOLO目标检测实战数据集,基于Trash-ICRA19海洋垃圾检测场景构建,可直接用于课程设计、期末大作业与毕业设计。压缩包共2288个文件,包含1144张jpg图像与1144个同名x…

作者头像 李华
网站建设 2026/10/21 3:36:30

MySQL省市区数据表实战:建表、导入与递归查询避坑指南

简介:这份 MySQL 版中国省市区数据表 SQL 资源,面向需要处理行政区划信息的后端开发者、电商与物流系统设计人员,以及正在学习数据库表结构设计的学习者。它提供了一张可直接导入使用的 db_yhm_city 表,通过 class_id 、 cla…

作者头像 李华
网站建设 2026/10/21 10:28:28

电机工程师实战手册:绕组结构、磁路设计与故障检测全解析

1. 这不是教科书,是电机工程师的“手边笔记”“吃透电机知识,从入门到精通”——看到这个标题,我第一反应不是翻教材,而是想起去年在某工业自动化实验室调试一台异步电机驱动系统时的情景:PLC信号已发,变频…

作者头像 李华
网站建设 2026/10/19 21:30:55

SQL Server人事管理系统课程设计:从建表到存储过程实战指南

简介:面向数据库课程设计的人事管理系统完整资料包,适合计算机专业学生及SQL Server、Java GUI初学者。资源以SQL Server数据库为核心,构建员工、部门、职位等表结构,并配套Java图形界面实现员工信息的增删改查与查询统计&#xf…

作者头像 李华