ARTICLE DETAIL

资讯详情

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

招聘信息可视化分析:从爬虫到Word报告的一站式Python实践

招聘信息可视化分析:从爬虫到Word报告的一站式Python实践 简介docx文档《基于Python语言的招聘信息可视化分析》面向数据分析初学者、互联网行业求职者及人力资源从业者利用招聘平台数据讲解从采集到决策的完整链路。文档从Python基础工具链入手介绍Pandas、NumPy、Matplotlib、Seaborn、Scrapy、BeautifulSoup等库的用途并重点演示通过爬虫抓取在线招聘信息后如何用Pandas完成缺失值、异常值处理以及非结构化文本的结构化转换。在分析阶段文档依次展开职位需求量排序、薪资水平分布、技能关键词频次统计等常用方法并以直方图、词云图、地理热图等可视化形式呈现此外还补充了定时抓取和历史对比等动态更新机制使得分析结果能够持续反映市场变化。整个资源压缩包仅含1个docx文件约198KB内容源自《计算机与网络》2020年第02期适合高效阅读和离线学习。目前该资源已有129人学习文档结构较为完整读者可参照其中思路开展自己的招聘数据分析项目。1. 招聘信息可视化分析从爬虫到报告的一站式链路招聘信息可视化分析说白了一句话把招聘网站上零散的岗位数据抓下来清洗成结构化表格再用图表把“哪个城市Python岗多、薪资分布长什么样、什么技能最值钱”这类问题回答清楚最后把图和分析结论装进一个.docx文档。很多人做这个选题是因为求职但真正做下来你会发现它的核心价值不在帮你找工作而在让你理解数据分析从获取、清洗到呈现的完整链路这套链路放到商品评论、舆情监测、行业报告里照样用得通。这篇文章适合刚学完Python语法、想拿一个真实项目练手的人也适合要快速搭建一套数据采集与可视化报表的工程师。全文按“采集→清洗→可视化→出报告”四段展开每段都会给出可运行的代码、参数解释和常见坑。2. 数据采集用requestsBeautifulSoup抓取招聘公开页2.1 为什么选requestsBeautifulSoup这套组合招聘信息抓取最常见的入门方案就是requests作HTTP客户端、BeautifulSoup解析HTML。相比Scrapy它没有框架负担单文件就能跑通相比Selenium它不会因为启动浏览器而显得笨重。对于招聘网站这种“页面结构变化不快、反爬不算极端”的目标这套组合是性价比最高的选择。需要说明的是招聘网站的页面结构和接口随时可能调整下面的代码演示的是通用解析思路落到你的目标站点时要按实际HTML结构调整选择器。常见做法是先用浏览器开发者工具查看列表页结构定位每条职位信息所在的标签路径再回来写解析逻辑。这样能避免反复试错带来的时间损耗也让后续维护时知道该改哪里。2.2 一个最小可跑的职位列表抓取脚本import requests from bs4 import BeautifulSoup import time import random def fetch_page(city_code: str, keyword: str, page: int) - str: 抓取单个列表页返回HTML文本 session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36, Accept-Language: zh-CN,zh;q0.9, }) url (fhttps://search.某招聘网站.com/list/{city_code},000000,0000,00,9,99, f{keyword},2,{page}.html) resp session.get(url, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding return resp.text def parse_jobs(html: str) - list: soup BeautifulSoup(html, html.parser) jobs [] for item in soup.select(.joblist-item): title item.select_one(.job-title) company item.select_one(.company-name) salary item.select_one(.salary) if title and company and salary: jobs.append({ title: title.get_text(stripTrue), company: company.get_text(stripTrue), salary: salary.get_text(stripTrue), }) return jobs for page in range(1, 11): html fetch_page(020000, Python, page) page_jobs parse_jobs(html) print(f第{page}页抓取到{len(page_jobs)}条职位) time.sleep(random.uniform(1.5, 3.5))这段代码的逻辑是每次抓一页列表页HTML用CSS选择器定位职位标题、公司名和薪资解析成字典后放进列表。两个关键参数值得展开timeout10是请求超时设太短在弱网下会频繁抛异常设太长会让整个爬虫卡死time.sleep(random.uniform(1.5, 3.5))是请求间隔固定间隔容易被频率检测识别随机间隔模拟人的浏览节奏这是最基础也是最重要的一条反爬礼仪——不是绕过限制而是不去触发限制。2.3 分页与去重抓取环节最容易被忽略的两个问题分页的坑通常在“终止条件”。常见做法是写死页码范围但更稳妥的是先取第一页的总页数再循环比如在页面里搜索“共xx页”的文本节点解析出数字后动态生成页码范围。写死页码的坏处是岗位少的时候后面全是空页浪费请求岗位多的时候漏掉后面的数据分析结果失真。招聘数据还有一个天然特征——不同列表页之间存在重复职位职位ID才是唯一标识。我的习惯是在parse_jobs阶段顺带把职位详情页的URL一起抓下来拿URL末尾的ID作主键去重。这一步在数据采集阶段做比后面清洗阶段再处理要省事得多。重复数据一旦混入薪资中位数、城市占比这类聚合指标都会被不同程度地放大。提示如果目标网站的职位信息是异步加载的列表HTML里只有职位ID薪资和详情要二次请求才能拿到就要先确认数据是否在HTML源码里。判断方法很简单在浏览器里禁用JavaScript刷新页面如果职位信息仍然显示说明静态HTML里有数据直接用requests就能拿。3. 数据清洗字段规整与薪资区间的解析3.1 先把数据装进DataFrame爬虫拿到的是散装字典列表第一步是合并成一个DataFrame。这里有一个很常见的多关键词去重问题如果你同时爬了“Python”和“数据分析”两个关键词同一家公司同一个职位会出现两条记录清洗开头就要按职位ID去重。import pandas as pd raw_data [] for page in range(1, 11): html fetch_page(020000, Python, page) raw_data.extend(parse_jobs(html)) df pd.DataFrame(raw_data) df df.drop_duplicates(subsetjob_id, keepfirst) print(df.shape) print(df.head())df.shape返回的是几行几列用来快速确认数据规模是否合理——比如翻10页怎么也有两三百条如果只有几条多半是HTML结构变了、选择器失效了。drop_duplicates(subsetjob_id, keepfirst)是按职位ID去重保留第一条记录。这两步确认完再往下走能省很多后面排错的功夫。3.2 薪资字段的拆分与归一化招聘网站的薪资文本是给人读的不是给机器算的常见写法有“20-30K·14薪”“1.5-2.5万/月”“面议”。要让薪资能进图表必须做归一化统一单位到“千元/月”再用中位数代表区间的中间值。import re def parse_salary(text: str): 20-30K·14薪 - 26.0(千元/月) if not isinstance(text, str) or 面议 in text: return None # 提取数字部分兼容 1.5-2万、20-30K 两种写法 match re.search(r([\d.])\s*[-~]\s*([\d.])\s*([K万]), text) if not match: return None low, high, unit match.groups() low, high float(low), float(high) if unit 万: low, high low * 10, high * 10 # 万/月 - 千/月 # 加权平均偏向区间下限更贴近招聘薪资的真实分布 return low * 0.4 high * 0.6 df[salary_k] df[salary].apply(parse_salary) print(df[salary_k].describe())这段代码里正则表达式r([\d.])\s*[-~]\s*([\d.])\s*([K万])是核心第一组[\d.]匹配下限数字中间[-~]匹配连字符或波浪线第二组匹配上限数字最后[K万]捕获单位。三个括号分别对应group(1)、group(2)、group(3)。注意“万”字的口径是隐藏的坑有些网站写“1.5-2万/月”是月薪还有极少数写“万/年”清洗前要先抽样确认单位含义。low * 0.4 high * 0.6是我习惯的取法招聘薪资通常偏向区间下限简单平均反而会高估真实水平。薪资解析前后的对照关系大致如下原始文本正则匹配结果salary_k千元/月20-30K·14薪20 ~ 30单位K26.01.5-2万/月1.5 ~ 2单位万18.02-2.5万·15薪2 ~ 2.5单位万23.0面议不匹配None200-300/天不匹配None3.3 城市、学历、工作经验列的规整城市字段往往带着行政区后缀比如“上海-静安区”。工作经验的文本则是“3-4年经验”“在校生/应届生”混在一起。规整的思路是能映射就映射不能映射就归入“未知”。def normalize_city(raw: str) - str: 上海-静安区 - 上海 if not isinstance(raw, str): return 未知 if - in raw: return raw.split(-)[0] if · in raw: return raw.split(·)[0] return raw df[city] df[city_raw].apply(normalize_city)这里以城市规整为例其它字段同理套模式。规整过程的重点是“宁可少拆不要拆错”北京不带后缀上海-静安区拆出上海但如果站点写法是上海·静安用点号切分才不会误伤全角字符。做规整的通用经验是先执行df[field].value_counts().head(30)把所有取值看一遍再写规则盲写正则一定会漏掉你没见过的写法。4. 可视化分析用Matplotlib和Pyecharts看岗位趋势4.1 两张图解决“这个市场值不值得进”的问题数据洗干净后第一步要回答的问题通常是薪资分布长什么样、哪些城市岗位多。这两张图分别对应直方图和条形图也是整个可视化分析里信息量最大的两张图。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] # 解决中文乱码 plt.rcParams[axes.unicode_minus] False # 解决负号显示方块 fig, axes plt.subplots(1, 2, figsize(14, 5)) # 左图薪资分布直方图 axes[0].hist(df[salary_k].dropna(), bins20, color#4C72B0, edgecolorwhite) axes[0].set_xlabel(月薪千元) axes[0].set_title(Python岗位薪资分布) # 右图岗位数量Top10城市 city_counts df[city].value_counts().head(10) city_counts.plot(kindbar, axaxes[1], color#DD8452) axes[1].set_xlabel(城市) axes[1].set_title(岗位数量Top10城市) axes[1].tick_params(axisx, rotation45) plt.tight_layout() plt.savefig(job_analysis.png, dpi150)两个参数在这段里最值得说bins20控制直方图的分箱数量箱数太少会把薪资的集中趋势抹平太多会看到一堆毛刺20左右对几百条数据是比较稳的起点dpi150是保存分辨率屏幕上看100就够但后面要插进Word报告打印出来150以上才能保证文字不发虚。另外SimHei字体是解决中文乱码的常见方案换成别的字体前要确认系统中确实装了对应字体文件。4.2 静态图之外为什么还要留一手PyechartsMatplotlib 出的是静态图适合打印、插入文档、放进PDF发给大家。但交互式图表在探索阶段更顺手鼠标悬停能看具体数值缩放能看分布细节。Pyecharts 生成的是HTML文件浏览器打开就能交互和Flask、Streamlit 这类Web框架对接也顺。常见的做法是两种都出Matplotlib 的画进报告Pyecharts 的存成HTML随报告附上一份静态一份动态各派各的用场。from pyecharts.charts import Bar from pyecharts import options as opts bar ( Bar() .add_xaxis(city_counts.index.tolist()) .add_yaxis(岗位数, city_counts.values.tolist()) .set_global_opts( title_optsopts.TitleOpts(title岗位数量Top10城市), xaxis_optsopts.AxisOpts(axislabel_optsopts.LabelOpts(rotate45)), ) ) bar.render(city_bar.html)Pyecharts 的核心是链式调用add_xaxis和add_yaxis分别设置X轴类目和Y轴数值set_global_opts统一设置标题和坐标轴。这里的rotate45是X轴标签旋转角度城市名一般是两三个字30度够用遇到“石家庄”“呼和浩特”这种就得45度甚至更陡。注意render()默认输出HTML文件如果在Jupyter里跑换成render_notebook()可以直接嵌入单元格展示。4.3 可视化前先想清楚给谁看、要什么结论做可视化分析时最常见的问题不是代码报错而是“图都画出来了却不知道在说什么”。画之前先把自己当成读者问三个问题这张图想证明什么读者能不能在三秒内看懂数字单位会不会让读者误解比方说薪资分布直方图如果画出来是正偏态峰值集中在15-25K结论就是“这个岗位的中位薪资在某区间、高位机会集中在少数资深岗位”而不是一句“薪资很高”那种没信息量的话。想清楚结论再去调图表的细节效率高得多也避免了反复返工。5. 报告落地用python-docx把图表和分析写进Word5.1 初始化文档与标题层级python-docx 是目前生成 .docx 最主流的库。它的基本思路是三步创建Document对象往里添加段落、标题、图片最后save。看似简单但在“可视化分析”这个场景里布局是否专业、图片是否清晰、关键数据有没有被突出很大程度上决定了这份报告能不能直接拿去交付。from docx import Document from docx.shared import Cm from docx.enum.text import WD_ALIGN_PARAGRAPH doc Document() # 封面标题 title doc.add_heading(招聘信息可视化分析报告, level0) title.alignment WD_ALIGN_PARAGRAPH.CENTER doc.add_paragraph(数据来源某招聘网站公开岗位信息) doc.add_paragraph(f统计周期近30天 | 分析时间{pd.Timestamp.now():%Y-%m-%d}) doc.add_heading(一、整体薪资水平, level1) doc.add_paragraph( f本次共采集有效岗位 {len(df)} 条月薪中位数为 f{df[salary_k].median():.1f} 千元薪资区间集中在 f{df[salary_k].quantile(0.25):.1f} ~ f{df[salary_k].quantile(0.75):.1f} 千元之间。 ) doc.add_picture(job_analysis.png, widthCm(15)) doc.paragraphs[-1].alignment WD_ALIGN_PARAGRAPH.CENTERadd_heading(xxx, level0)的 level0 是文档标题样式level1 是一级标题往上级数递增字号递减。add_picture的widthCm(15)控制图片宽度A4纸正文宽度约16厘米设15厘米刚好占满又不会溢出。doc.paragraphs[-1]取的是最后添加的那个段落——也就是刚插入的图片段落把它设为居中对齐报告布局才显得端正。5.2 把分析结论写成可读性强的段落和表格报告不能只堆图得有“看到这个图你该怎么解读”的文字。我的习惯是每张图跟一段结论文字结论里只写一眼看得出的事实不写推测和臆断。比如“北京、深圳、上海、杭州四城的岗位数合计占比超过 60%”“月薪 20K 以上的岗位比例约为三成且主要集中在5年以上经验要求”。这类句子的价值在于第三方看报告时不需要自己读图就能抓到要点报告的说服力也因此更强。table doc.add_table(rows1, cols3) table.style Light Grid Accent 1 hdr table.rows[0].cells hdr[0].text 城市 hdr[1].text 岗位数 hdr[2].text 占比 for city, cnt in df[city].value_counts().head(5).items(): row table.add_row().cells row[0].text city row[1].text str(cnt) row[2].text f{cnt / len(df) * 100:.1f}%table.style Light Grid Accent 1是python-docx内置的表格样式名不同样式名取决于你用的Word版本或python-docx版本常用的有 “Table Grid”“Light Shading Accent 1”“Medium Shading 1 Accent 1”。如果指定的样式名不存在代码会直接抛KeyError所以不要盲目抄样式名最好先跑一遍确认它在你环境里可用。表格在这儿的作用是让Top城市的排名一眼可见图和表各承担各的职责。5.3 生成Word后必查的三件事doc.save(招聘信息可视化分析.docx) print(报告已生成)保存后第一件事不是看内容而是确认三件事图片有没有糊、表格有没有被截断、中文字体是否正常。python-docx默认中文字体可能是等线或宋体如果需要统一为微软雅黑要额外设置样式from docx.oxml.ns import qn style doc.styles[Normal] style.font.name 微软雅黑 style._element.rPr.rFonts.set(qn(w:eastAsia), 微软雅黑)这里qn(w:eastAsia)是设置东亚文字字体的固定写法处理中文字体时只改font.name不够必须同步设置 eastAsia 属性否则Word打开后中文字符仍然沿用默认字体。这是python-docx最常被忽略的细节之一。6. 进阶把分析流程封装成定时自动更新的脚本6.1 会话复用、超时重试与随机延迟爬虫跑一次容易跑得勤了才会遇到问题。招聘网站不像电商平台那么严苛但请求频率一旦上去很快会出现验证码或IP临时封禁。常见的应对措施是三个一起上用 requests.Session 复用连接池省去每次请求的TCP握手开销给请求挂上超时重试网络抖动时不至于整个流程崩溃相邻请求之间保持随机延时降低被识别为脚本的概率。from requests.adapters import HTTPAdapter session requests.Session() adapter HTTPAdapter(max_retries3, pool_connections10, pool_maxsize10) session.mount(https://, adapter) def fetch_with_retry(url: str) - str: for attempt in range(3): try: resp session.get(url, timeout15) resp.raise_for_status() return resp.text except requests.RequestException: if attempt 2: raise time.sleep(2 * (attempt 1))HTTPAdapter(max_retries3)给连接挂重试但它重试的是连接级别的错误不是HTTP状态码错误raise_for_status()抛出的异常仍要靠外层for循环兜底。2 * (attempt 1)是退避策略第一次失败等2秒第二次等4秒递进式地拉开重试间隔避免失败后立刻又撞上同一个问题。6.2 用cron或系统计划任务定时触发参数齐全后整个流程可以落成一个main()函数抓数据→清洗→出图→生成docx。手工跑一遍验证没问题再挂计划任务。Linux服务器上用cronWindows上用任务计划程序想完全零服务器维护就用GitHub Actions的schedule触发。cron表达式的最简写法0 3 * * 1 cd /opt/job-analysis /usr/bin/python3 main.py run.log 21这段cron表示每周一凌晨3点执行一次main.py run.log 21把标准输出和错误信息都追加进日志。这里有两点经验路径要写绝对路径cron环境下PATH和交互式终端不一样直接写python3可能找不到解释器定好的任务一定要有日志可查否则某周数据源页面改了结构你会过两三周才发现任务一直在悄悄失败——这正是无人值守任务最需要防的问题。6.3 每次运行自动对比上期数据最后分享一个实用技巧把历史薪资中位数和本次结果写入一个JSON文件每次生成报告时做一个环比说明。“本月薪资中位数环比上涨5%”这句话出现在报告里比一堆静态图表更有增量价值也最容易让读者感受到这套分析持续更新的意义。实现上不过是用json.load读上次存档、计算差异、再把本次结果写回十几行代码就能让这份可视化分析从一次性快照变成持续运行的监控工具。本文还有配套的精品资源点击获取
返回列表