资讯中心

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

📅 2026/10/10 0:00:21
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 在需要登录态的场景下必须携带。翻页参数通常有两种形式页码型page1,2,3和游标型last_idxxx。页码型简单但容易被限制游标型更稳定但需要从上一页响应里提取游标值。下面是一个通用的请求封装包含重试和随机延迟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( total3, # 最多重试3次 backoff_factor1.5, # 退避因子重试间隔递增 status_forcelist[429, 500, 502, 503], # 这些状态码触发重试 ) adapter HTTPAdapter(max_retriesretry) 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, paramsparams, timeout15) resp.raise_for_status() return resp.json()逻辑说明Retry的total3表示最多重试三次backoff_factor1.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_asciiFalse), 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(市, , regexFalse) return df def plot_skill_top(df, top_n15): 技能词频柱状图 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, dpi150)font.sans-serif设成 SimHei 是 Windows 下的做法Linux 环境要换成系统里实际存在的中文字体否则中文全是方块这是新手最常翻车的地方。dpi150保证导出图片清晰度够用屏幕展示 100 就够打印或报告用 150 到 200。技能统计前要过滤掉空值和异常标签比如有些岗位的 skills 字段是空数组dropna处理不了空数组需要在展开时加判断。城市分布图建议用横向柱状图而不是饼图因为城市数量多的时候饼图根本看不清。薪资分布用箱线图按城市分组能直观看出不同城市的薪资中位数和离散程度。4. 避坑指南采集分析链路上的五个血泪教训4.1 翻页翻到一半返回空列表现象前几页正常返回翻到第 5 页或第 10 页突然返回空数组但手动在浏览器里翻页是正常的。原因多数平台的列表接口有深度分页限制页码超过阈值后不再返回数据或者需要额外的游标参数。另一个可能是触发了频率限制服务端静默返回空数据而不是报错。解决翻页循环里加空列表检测一旦返回空就停止并记录当前页码。同时把页码型翻页改成游标型从响应里提取下一页的游标值。如果平台确实有深度限制就按城市或关键词拆分采集任务每个子任务只翻前几页用维度拆分绕过深度限制。4.2 薪资统计结果明显偏高现象算出来的平均薪资比招聘网站上显示的水平高出一大截。原因薪资字段拆解时把“·14薪”的月数当成了薪资数字或者把“元/天”的日薪直接当成了月薪。还有一种情况是“20-40K”这种宽区间岗位拉高了均值而这类岗位往往是销售或管理岗不代表整体水平。解决拆解函数里先剥离“·N薪”部分再提取数字日薪单独判断单位。统计时用中位数而不是均值或者按岗位类别分组统计。宽区间岗位可以单独标记分析时排除或单独展示。4.3 中文图表全是方块现象matplotlib 生成的图表里中文标题和标签显示为一个个方块。原因matplotlib 默认字体不支持中文而代码里没有指定中文字体或者指定的字体在系统里不存在。解决先查系统里有哪些中文字体用fc-list :langzh命令在 Linux 下查看Windows 下直接看C:\Windows\Fonts目录。把font.sans-serif设成实际存在的字体名。如果部署环境不确定可以把字体文件打包进项目用font_manager动态加载。4.4 数据库写入越来越慢现象采集初期写入很快跑到几千条以后每条插入都要等好几秒。原因每次插入都单独 commitSQLite 在频繁小事务下性能急剧下降。另一个原因是主键冲突检查随着数据量增长变慢。解决改成批量插入攒够 500 条 commit 一次。建表时给查询频繁的字段加索引比如 city 和 publish_time。如果数据量超过十万条考虑换 PostgreSQL 或 MySQLSQLite 在这个量级下并发写入会吃力。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这个校验函数跑一次不到一秒但能省掉后面大量排查时间。我现在的习惯是采集完先跑校验校验通过再进分析环节不通过就先修数据。这个习惯帮我避开了好几次“分析结论看起来合理但其实是脏数据导致的”这种隐蔽翻车。希望帮到你。本文还有配套的精品资源点击获取

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案