news 2026/9/18 16:57:10

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
招聘信息可视化分析:从爬虫到Word报告的一站式Python实践

简介:docx文档《基于Python语言的招聘信息可视化分析》面向数据分析初学者、互联网行业求职者及人力资源从业者,利用招聘平台数据讲解从采集到决策的完整链路。文档从Python基础工具链入手,介绍Pandas、NumPy、Matplotlib、Seaborn、Scrapy、BeautifulSoup等库的用途,并重点演示通过爬虫抓取在线招聘信息后,如何用Pandas完成缺失值、异常值处理以及非结构化文本的结构化转换。在分析阶段,文档依次展开职位需求量排序、薪资水平分布、技能关键词频次统计等常用方法,并以直方图、词云图、地理热图等可视化形式呈现;此外还补充了定时抓取和历史对比等动态更新机制,使得分析结果能够持续反映市场变化。整个资源压缩包仅含1个docx文件,约198KB,内容源自《计算机与网络》2020年第02期,适合高效阅读和离线学习。目前该资源已有129人学习,文档结构较为完整,读者可参照其中思路开展自己的招聘数据分析项目。

1. 招聘信息可视化分析:从爬虫到报告的一站式链路

招聘信息可视化分析,说白了一句话:把招聘网站上零散的岗位数据抓下来,清洗成结构化表格,再用图表把“哪个城市Python岗多、薪资分布长什么样、什么技能最值钱”这类问题回答清楚,最后把图和分析结论装进一个.docx文档。很多人做这个选题是因为求职,但真正做下来你会发现,它的核心价值不在帮你找工作,而在让你理解数据分析从获取、清洗到呈现的完整链路,这套链路放到商品评论、舆情监测、行业报告里照样用得通。这篇文章适合刚学完Python语法、想拿一个真实项目练手的人,也适合要快速搭建一套数据采集与可视化报表的工程师。全文按“采集→清洗→可视化→出报告”四段展开,每段都会给出可运行的代码、参数解释和常见坑。

2. 数据采集:用requests+BeautifulSoup抓取招聘公开页

2.1 为什么选requests+BeautifulSoup这套组合

招聘信息抓取最常见的入门方案就是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;q=0.9", }) url = (f"https://search.某招聘网站.com/list/{city_code},000000,0000,00,9,99," f"{keyword},2,{page}.html") resp = session.get(url, timeout=10) 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(strip=True), "company": company.get_text(strip=True), "salary": salary.get_text(strip=True), }) 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选择器定位职位标题、公司名和薪资,解析成字典后放进列表。两个关键参数值得展开:timeout=10是请求超时,设太短在弱网下会频繁抛异常,设太长会让整个爬虫卡死;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(subset="job_id", keep="first") print(df.shape) print(df.head())

df.shape返回的是几行几列,用来快速确认数据规模是否合理——比如翻10页怎么也有两三百条,如果只有几条,多半是HTML结构变了、选择器失效了。drop_duplicates(subset="job_id", keep="first")是按职位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.0
1.5-2万/月1.5 ~ 2,单位万18.0
2-2.5万·15薪2 ~ 2.5,单位万23.0
面议不匹配None
200-300/天不匹配None

3.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(), bins=20, color="#4C72B0", edgecolor="white") axes[0].set_xlabel("月薪(千元)") axes[0].set_title("Python岗位薪资分布") # 右图:岗位数量Top10城市 city_counts = df["city"].value_counts().head(10) city_counts.plot(kind="bar", ax=axes[1], color="#DD8452") axes[1].set_xlabel("城市") axes[1].set_title("岗位数量Top10城市") axes[1].tick_params(axis="x", rotation=45) plt.tight_layout() plt.savefig("job_analysis.png", dpi=150)

两个参数在这段里最值得说:bins=20控制直方图的分箱数量,箱数太少会把薪资的集中趋势抹平,太多会看到一堆毛刺,20左右对几百条数据是比较稳的起点;dpi=150是保存分辨率,屏幕上看100就够,但后面要插进Word报告打印出来,150以上才能保证文字不发虚。另外SimHei字体是解决中文乱码的常见方案,换成别的字体前要确认系统中确实装了对应字体文件。

4.2 静态图之外为什么还要留一手Pyecharts

Matplotlib 出的是静态图,适合打印、插入文档、放进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_opts=opts.TitleOpts(title="岗位数量Top10城市"), xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=45)), ) ) bar.render("city_bar.html")

Pyecharts 的核心是链式调用:add_xaxisadd_yaxis分别设置X轴类目和Y轴数值,set_global_opts统一设置标题和坐标轴。这里的rotate=45是X轴标签旋转角度,城市名一般是两三个字,30度够用,遇到“石家庄”“呼和浩特”这种就得45度甚至更陡。注意render()默认输出HTML文件,如果在Jupyter里跑,换成render_notebook()可以直接嵌入单元格展示。

4.3 可视化前先想清楚:给谁看、要什么结论

做可视化分析时最常见的问题不是代码报错,而是“图都画出来了却不知道在说什么”。画之前先把自己当成读者问三个问题:这张图想证明什么?读者能不能在三秒内看懂?数字单位会不会让读者误解?比方说薪资分布直方图,如果画出来是正偏态,峰值集中在15-25K,结论就是“这个岗位的中位薪资在某区间、高位机会集中在少数资深岗位”,而不是一句“薪资很高”那种没信息量的话。想清楚结论再去调图表的细节,效率高得多,也避免了反复返工。

5. 报告落地:用python-docx把图表和分析写进Word

5.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("招聘信息可视化分析报告", level=0) title.alignment = WD_ALIGN_PARAGRAPH.CENTER doc.add_paragraph("数据来源:某招聘网站公开岗位信息") doc.add_paragraph(f"统计周期:近30天 | 分析时间:{pd.Timestamp.now():%Y-%m-%d}") doc.add_heading("一、整体薪资水平", level=1) 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", width=Cm(15)) doc.paragraphs[-1].alignment = WD_ALIGN_PARAGRAPH.CENTER

add_heading("xxx", level=0)的 level=0 是文档标题样式,level=1 是一级标题,往上级数递增字号递减。add_picturewidth=Cm(15)控制图片宽度,A4纸正文宽度约16厘米,设15厘米刚好占满又不会溢出。doc.paragraphs[-1]取的是最后添加的那个段落——也就是刚插入的图片段落,把它设为居中对齐,报告布局才显得端正。

5.2 把分析结论写成可读性强的段落和表格

报告不能只堆图,得有“看到这个图你该怎么解读”的文字。我的习惯是每张图跟一段结论文字,结论里只写一眼看得出的事实,不写推测和臆断。比如“北京、深圳、上海、杭州四城的岗位数合计占比超过 60%”“月薪 20K 以上的岗位比例约为三成,且主要集中在5年以上经验要求”。这类句子的价值在于:第三方看报告时不需要自己读图就能抓到要点,报告的说服力也因此更强。

table = doc.add_table(rows=1, cols=3) 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_retries=3, pool_connections=10, pool_maxsize=10) session.mount("https://", adapter) def fetch_with_retry(url: str) -> str: for attempt in range(3): try: resp = session.get(url, timeout=15) resp.raise_for_status() return resp.text except requests.RequestException: if attempt == 2: raise time.sleep(2 * (attempt + 1))

HTTPAdapter(max_retries=3)给连接挂重试,但它重试的是连接级别的错误,不是HTTP状态码错误,raise_for_status()抛出的异常仍要靠外层for循环兜底。2 * (attempt + 1)是退避策略:第一次失败等2秒,第二次等4秒,递进式地拉开重试间隔,避免失败后立刻又撞上同一个问题。

6.2 用cron或系统计划任务定时触发

参数齐全后,整个流程可以落成一个main()函数:抓数据→清洗→出图→生成docx。手工跑一遍验证没问题,再挂计划任务。Linux服务器上用cron,Windows上用任务计划程序,想完全零服务器维护就用GitHub Actions的schedule触发。cron表达式的最简写法:

0 3 * * 1 cd /opt/job-analysis && /usr/bin/python3 main.py >> run.log 2>&1

这段cron表示每周一凌晨3点执行一次main.py>> run.log 2>&1把标准输出和错误信息都追加进日志。这里有两点经验:路径要写绝对路径,cron环境下PATH和交互式终端不一样,直接写python3可能找不到解释器;定好的任务一定要有日志可查,否则某周数据源页面改了结构,你会过两三周才发现任务一直在悄悄失败——这正是无人值守任务最需要防的问题。

6.3 每次运行自动对比上期数据

最后分享一个实用技巧:把历史薪资中位数和本次结果写入一个JSON文件,每次生成报告时做一个环比说明。“本月薪资中位数环比上涨5%”这句话出现在报告里,比一堆静态图表更有增量价值,也最容易让读者感受到这套分析持续更新的意义。实现上不过是用json.load读上次存档、计算差异、再把本次结果写回,十几行代码就能让这份可视化分析从一次性快照变成持续运行的监控工具。

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

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

查重报告里 AI 率飘红?TaoToken 这样改 Codex 的模型通道再复检

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 16:47:40

LL(1)预测分析表从零构造:FIRST/FOLLOW集计算与冲突排查实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 16:45:24

全连接层深度解析:从矩阵乘法到CNN与Transformer应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华