ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

招聘数据可视化实战:从Python清洗到Flask交互面板

招聘数据可视化实战:从Python清洗到Flask交互面板 简介针对招聘信息可视化分析场景这份基于Python的完整实践文档主要面向数据分析初学者、求职者以及企业HR等读者旨在解决如何从招聘平台获取职位信息并挖掘市场需求、薪资水平、技能要求等关键洞察的问题。文档内容源自《计算机与网络》2020年第02期系统覆盖Python基础、Scrapy/BeautifulSoup数据抓取、Pandas数据清洗与转换、Matplotlib/Seaborn/Plotly可视化展示、结果解读以及定时动态更新等环节并配合条形图、直方图、词云图、地理热图等图表案例便于读者对照学习对求职选择、企业招聘策略和行业观察均有参考价值。整个资源仅含1个docx文档压缩包大小约198KB轻量易读可快速下载使用。目前已有129人学习适合具备基础Python语法、希望掌握招聘数据全链路分析并输出可视化报告的入门与进阶学习者。1. 为什么招聘数据分析要先解决“脏”而不是“多”在一堆“精通Python、3-5年经验、薪资面议”的职位里真正值得分析的往往不是岗位数量而是岗位描述里的细微差异——薪资范围是8k-15k还是15k-30k“熟悉”和“精通”背后对应的技能组合以及同样标题在不同城市的薪酬带宽。招聘信息可视化分析的难点从来不在画图而在于把非结构化的JD文本、混乱的薪资格式、多义词技能标签整理成一张能查、能算、能过滤的干净表格。这篇内容按一条常见可落地的路径展开先从公开招聘页面抓取原始数据再用Python清洗出结构化字段接着用pyecharts做交互可视化最后落到一个Flask小面板上让非技术人员也能自己筛选条件看图。适合已经会写Python基础语法、想走一遍完整数据分析流程的人也适合需要在简历里展示一个完整数据作品的求职者。2. 数据获取与清洗先用Python把“千岗千面”洗成一张表2.1 抓到原始数据的第一步选源与请求头招聘数据不会凭空出现在Excel里常见做法是爬取公开招聘平台的职位搜索页。不推荐直接抓网页正文因为反爬严重、结构变动快更稳的做法是先检查目标站点是否有公开API接口很多招聘网站的前端搜索功能背后就是一个JSON接口返回字段比解析HTML稳定得多。就算没有开放API用requests直接请求列表页也比用Selenium轻量至少能省掉浏览器渲染的开销。一个最朴素的请求写法长这样import requests headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: application/json, text/plain, */*, Referer: https://example.com/jobs, } resp requests.get(https://api.example.com/job/search, params{keyword: Python, city: 101010100, pn: 1}, headersheaders, timeout10) data resp.json()这段代码里真正值得注意的不是requests本身而是headers里的三个字段User-Agent用来告诉服务器“我是一个正常浏览器”Referer用于通过部分站点的来源校验Accept决定了API是否愿意返回JSON而不是HTML。timeout10是必须的不设超时的爬虫会在网络抖动时挂死整个任务。拿到JSON之后立刻落盘成原始文件别直接处理。常见做法是把每次请求的结果追加到一个JSONL文件里每行一条原始记录。这样后续清洗脚本可以反复重跑而不用重新请求服务器。2.2 清洗的第一步用正则抽取薪资区间招聘平台的“15K-25K·14薪”这类字段看起来比“面议”好处理但真正解析时会遇到各种形态12-18K15k-25k·13薪8千-1.2万1-1.5万/月25-35K/月清洗逻辑不能写死在一条正则里正确做法是先做薪资单位归一化再提取数字边界。我一般会先写一个函数把“万”转成“k”再统一匹配所有数字import re def normalize_salary(text): if not text or 面议 in text: return None, None text text.replace(万, k) # 统一“15k”和“15K” text text.lower() nums re.findall(r(\d\.?\d*)k, text) if len(nums) 2: low float(nums[0]) high float(nums[1]) elif len(nums) 1: low high float(nums[0]) else: return None, None return low * 1000, high * 1000这里有几个容易踩的坑第一“1-1.5万/月”如果不先替换成k正则匹配不到数字第二有的职位写“20-30K·14薪”薪资范围后面跟着年终月数用findall把所有带k的数值都取出来正常情况下只有前两个是薪资边界第三解析结果建议以“元/月”为单位存两个数字字段而不是存原始字符串排序和区间筛选都会方便得多。2.3 从JD文本里抽取技能标签JD里的“熟悉Python、flask、sql”这类句子是可视化分析里技能分布和技能-薪资交叉分析的核心原料。抽取方式看数据规模几千条数据的量级用规则匹配就够了如果是几万条以上再考虑用jieba分词加词典维护。规则匹配的做法是维护一个技能词典然后逐条去JD文本里in判断SKILLS [python, flask, django, fastapi, sql, mysql, postgresql, redis, docker, kubernetes, linux, git, spark, hadoop, tensorflow, pytorch, scrapy, numpy, pandas, vue, react] def extract_skills(jd_text): text jd_text.lower() matched [skill for skill in SKILLS if skill in text] return matched这段代码的局限在于“python”会误匹配“python开发工程师”这种职位名称但考虑到JD本身就会写“熟练使用python”误匹配比例不高。更可控的做法是把技能词典换成dict给每个词加上权重命中后按权重算技能分但这个设计在爬虫项目里属于优化项第一版直接返回命中列表就行。2.4 数据落库为什么用SQLite而不是直接存CSV清洗完的数据存在哪直接影响后续可视化的查询效率。几千行数据用CSV当然可以但一旦想“按城市筛再按薪资排序”pandas每次都要全表读一遍。我一般用Python内置的sqlite3模块落库省去安装数据库服务的依赖又能享受SQL的查询效率import sqlite3 conn sqlite3.connect(jobs.db) conn.execute( CREATE TABLE IF NOT EXISTS job ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, company TEXT, city TEXT, salary_low INTEGER, salary_high INTEGER, skills TEXT, jd TEXT ) )建表时把skills存成逗号分隔的字符串查询时再用pandas的str.contains做筛选。虽然这不符合数据库第三范式但数据分析场景下这样反而简单。数据量在十万行以内这个设计不会出现性能问题。3. 可视化设计用pyecharts把“岗位-城市-薪资”装进一张图3.1 选pyecharts而不是matplotlib的理由招贴信息分析师的核心产出是让业务方看明白“哪个城市给钱多、什么技能值钱”这类问题天然适合交互式图表。pyecharts生成的是HTML文件图表的tooltip、图例筛选、数据缩放都是自带交互的导出发给任何人都能用浏览器打开不用装Python环境。相比之下matplotlib是静态图适合论文不适合在团队里快速传阅。安装直接走pippip install pyecharts注意pyecharts 2.x版本自带同步的图表类型不需要额外装echarts包。如果你看到网上老教程里写的from pyecharts import Bar这种导入方式那是0.5.x老版本的写法新版本导入方式完全不同。3.2 第一张图城市平均薪资横向柱状图招聘分析里最常用的第一张图是“目标岗位各城市平均薪资Top 10”。这张图能快速回答一个基础问题同样写Python在哪个城市拿到的薪资更高以及高多少。import pandas as pd from pyecharts.charts import Bar from pyecharts import options as opts df pd.read_sql(SELECT city, AVG((salary_low salary_high)/2) AS avg_salary FROM job GROUP BY city ORDER BY avg_salary DESC LIMIT 10, conn) bar ( Bar() .add_xaxis(df[city].tolist()) .add_yaxis(平均薪资(元/月), df[avg_salary].round(0).tolist()) .set_global_opts( title_optsopts.TitleOpts(titlePython岗位城市薪资Top10), yaxis_optsopts.AxisOpts(name元/月), ) ) bar.render(city_salary.html)这段代码的数据库查询是核心AVG((salary_low salary_high)/2) 用薪资中位数近似平均月薪。用AVG而不是GROUP BY之后在pandas里算能让SQL少返回数据量十万行以内响应速度差距不大但养成在数据库层做聚合的习惯对后续处理更大数据集有好处。3.3 第二张图技能薪资箱线图比城市分布更能体现招聘质量分析价值的是技能维度。把“要求会Docker的岗位薪资”和“完全不提Docker的岗位薪资”放在一起比较能看出一个技能到底值多少钱。箱线图是合适的选择因为它能展示中位数、四分位数和离群点不会被少数超高薪岗位拉偏均值。在pyecharts里Boxplot需要先用pandas把数据整理成嵌套列表格式每个技能对应一组薪资数组from pyecharts.charts import Boxplot skill_salary [] for skill in [docker, spark, flask, django]: salaries df[df[skills].str.contains(skill)][salary_mid].tolist() skill_salary.append(salaries) boxplot ( Boxplot() .add_xaxis([docker, spark, flask, django]) .add_yaxis(薪资分布, boxplot.prepare_data(skill_salary)) .set_global_opts(title_optsopts.TitleOpts(title技能要求与薪资分布), yaxis_optsopts.AxisOpts(name元/月)) )注意boxplot.prepare_data(skill_salary)这步是Boxplot特有的它会把传入的原始数组列表自动计算成五个分位数值。如果忘了调用prepare_data直接传列表图表会渲染成空白或报错。3.4 地图组件用Map画省份岗位密度城市维度的数据还能用地图表达把“每个城市的岗位数量”映射到省份地图上颜色越深代表岗位越多。pyecharts的Map组件需要先准备省份中文名和对应数值的列表from pyecharts.charts import Map province_count df.groupby(province).size().reset_index(namecount) data_pair list(zip(province_count[province], province_count[count])) map_chart ( Map() .add(岗位数, data_pair, china) .set_global_opts( title_optsopts.TitleOpts(titlePython岗位全国分布), visualmap_optsopts.VisualMapOpts(max_int(province_count[count].max())), ) )地图有个常见坑部分招聘平台返回的城市字段是“北京”“上海”这种直辖市名称而Map组件的china地图要求的省份列表里包含的是“北京市”“上海市”。如果不做归一化地图上对应的区域会显示为空。解决方法是准备一份城市到省份的映射字典并且处理“市”“省”后缀。3.5 词云JS版本组件别做成静态图词云是招聘JD可视化里最讨喜的一张图但pyecharts内置的WordCloud组件在新版本里依赖pyecharts-snapshot渲染比较麻烦。我一般直接在HTML模板里引入echarts-wordcloud的JS文件用pyecharts的Page()把词云和其他图合并输出不单独渲染词云脚本。更主流、更容易搜到资料的做法是直接使用pyecharts的WordCloud类from pyecharts.charts import WordCloud word_freq df[skills].str.split(,).explode().value_counts().head(50) wc ( WordCloud() .add(, [list(item) for item in word_freq.items()], word_size_range[20, 100]) .set_global_opts(title_optsopts.TitleOpts(titleJD技能标签词云)) )这里把series展开成每个技能一行的数据再value_counts统计频次生成的是“技能名出现次数”的二元组列表。词云的可视化价值是让业务方一眼看到“这个岗位最看重的技能排序”在图旁边建议放一个文字说明标注这是出现频次统计而不是权重分析。4. 把可视化做成可筛选的页面Flask ECharts联动查询4.1 为什么需要一层后端页面单张HTML图表的局限是筛选条件写死在代码里。实际使用场景是业务方打开页面想只看“北京的前端岗位”不想看全国数据。在用户不碰Python代码的前提下这需要做一层Web界面。常见做法是用Flask搭一个轻量服务提供两个能力一个页面用于配置筛选条件一个JSON接口用于按条件查询数据并返回图表所需的数据结构。整个系统的技术栈是Flask做路由和APIpyecharts生成图表JSON前端用原生ECharts渲染。所谓“Python爬虫可视化界面”的最简落地形态就是这么一套东西。4.2 先写好查询接口再写页面后端代码的核心是一个接收GET参数返回JSON的数据接口from flask import Flask, request, jsonify import sqlite3, json app Flask(__name__) app.route(/api/jobs) def jobs_api(): city request.args.get(city, ) skill request.args.get(skill, ) where [] params [] if city: where.append(city ?) params.append(city) if skill: where.append(skills LIKE ?) params.append(f%{skill}%) sql SELECT city, salary_low, salary_high, title FROM job if where: sql WHERE AND .join(where) conn sqlite3.connect(jobs.db) df pd.read_sql(sql, conn, paramsparams) result { count: len(df), avg_salary: round(float(df[[salary_low, salary_high]].mean().mean()), 2), } return jsonify(result) if __name__ __main__: app.run(port5000)这里用了参数化查询而不是f-string拼接字符串原因很简单招聘数据里既然包含了“skill”这种用户可输入字段就必须防SQL注入。?占位符配合params参数列表是sqlite3标准的防注入写法没有理由不这么做。注意df[[salary_low,salary_high]].mean().mean()是先算列均值再算整体均值结果是可读的月薪水平。4.3 前端页面一个下拉框加一张图HTML页面里用原生fetch调上面这个接口把返回数据渲染进ECharts!DOCTYPE html html head meta charsetutf-8 title招聘信息可视化面板/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script /head body select idcitySelect option value全部城市/option option value北京北京/option option value上海上海/option option value深圳深圳/option /select button onclickloadData()筛选/button div idchart stylewidth:800px;height:500px;/div script function loadData() { const city document.getElementById(citySelect).value; fetch(/api/jobs?city${encodeURIComponent(city)}) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(chart)); chart.setOption({ title: { text: 平均薪资${data.avg_salary} 元/月 }, series: [{ type: bar, data: [data.avg_salary] }] }); }); } loadData(); /script /body /html这个前端页面没引入任何框架用原生JS就够了避免把项目复杂化成前后端分离架构。实际项目里会把图表做得更丰富但这个架构说明了每一层都在干什么前端发条件、后端查库、数据库聚合、图表更新。4.4 自动化定时更新数据的设计招聘数据不是一次性分析完就结束的。每天抓一次数据累计两周之后才能看出“Python岗位的平均薪资是涨了还是跌了”。定时更新的常见做法是用APScheduler在Flask里加一个后台任务或者更轻量地在服务器上配置cron30 8 * * * cd /path/to/project python crawl.py crawl.log 21这里两个要点crawler脚本必须用绝对路径或先cd进项目目录否则相对路径的SQLite库会写错位置输出重定向到log文件是为了排查爬虫被封或数据异常。加一个简单的增量策略每次抓取前先查数据库里最新的职位ID只抓新ID的数据这样不会产生重复记录。5. 四个影响分析结论的隐藏坑5.1 薪资字段的“迷信”数据要单独处理招聘平台经常出现“15-18K·14薪”和“15-18K”两个看似不同的岗位实际年总收入差异很大。按之前的正则解析逻辑14薪的信息会被丢弃导致分析结果有偏差。重要处理方式是增加salary_months字段正则匹配出薪字后面的数字削成一个月度工资乘数。months re.search(r(\d{2})薪, text) salary_months int(months.group(1)) if months else 12分析“目标薪资”时把月薪乘以月份数算的是“预期年收入”分析“基本工资”时用月薪原值。两种口径分开出图不然业务方会质疑数据真实性。这个字段的清洗逻辑不需要写进第一版但在项目文档里必须标注“未处理多薪字段”否则接手的人会误用。5.2 城市字段的别名问题比想象中严重爬虫拿到的城市字段可能是“北京市”“北京”“朝阳区”三种形态地图组件和分组统计都对不上。处理方式不复杂但要警惕“朝阳区”这种区级数据这个字段在招聘平台里通常是“工作地址所在区”不能当成城市用需要从完整地址里反推城市名。最可靠的做法是以职位页里的城市ID为基准用ID映射城市名而不是解析文本地址。5.3 词云的中心词污染技能词云容易出现一个反直觉现象“python”本身永远排在第一位甚至盖过所有其他真实技能。原因很简单招聘标题本身就带python它的频次天然偏高并不能说明它是岗位的核心技能。排查方法是把“python”“岗位名里的核心词”这类词从技能词典里剔除或者在词云旁边注明“频次Top榜含标题自带词”。否则业务方会问“我已经知道要python到底还要会什么”。5.4 动态更新的数据先落原始库再做清洗爬虫改成每半小时跑一次之后清洗脚本也随频率重跑会出现“正在处理的表被另一个进程写入”的问题。SQLite在默认事务隔离级别下可能报database is locked。稳妥做法是爬虫写入staging表清洗脚本把staging表读入pandas清洗后写入正式表最后清空staging表。这个架构在数据量小时略微增加复杂度但避免了脏数据、锁竞争、重复清洗三个问题。最后再验证一个点可视化面板上线后用一条SQL检查两个数字——数据总数是否每天递增、清洗后的有效薪资占比是否稳定在90%以上。有效薪资占比突降多半是页面结构变了我的习惯是在爬虫脚本里加一个if len(job_list) 0: send_alert()的告警在数据源出问题之前先收到通知。本文还有配套的精品资源点击获取
返回列表