资讯动态

Python招聘数据爬取与可视化:从岗位抓取到交互看板实战

发布时间:2026/10/20 12:55:23 来源:尧图企业网站定制
简介本资源是一套基于Python的招聘岗位数据爬取与可视化分析系统面向数据分析初学者、人力资源研究者及求职者帮助其快速获取智联招聘、前程无忧、拉勾网、BOSS直聘等平台的实时招聘信息并完成数据清洗与图形化展示。压缩包共61个文件约10.32MB包含9个py源码、12个pyc编译文件、7个xml配置、7张png与6张jpg图表、5个txt说明、5个js脚本、2个sql数据库文件及xls、pptx、md等覆盖爬虫、分析、展示与文档全流程。已有66人学习下载。系统采用Requests库实现多平台适配抓取结合Matplotlib、Seaborn或Plotly生成薪资分布、热门职位、地区就业等可视化图表并附数据库脚本与演示文稿便于读者直接运行、二次开发或作为课程设计参考同时提供模块化架构与容错思路帮助理解反爬应对与数据合规获取。1. 招聘数据爬取与可视化从岗位信息到决策看板招聘网站上的岗位数据表面看只是职位名称和薪资区间但把时间轴拉长、把城市和技能标签交叉起来它就是一张能反映行业冷热的“体温图”。很多做数据分析的同行第一步就卡在数据获取上手动复制粘贴效率太低写脚本又容易被反爬拦截好不容易爬下来几千条字段还乱七八糟没法直接用。这套基于 Python 的招聘岗位数据爬取与可视化分析系统解决的就是从“实时抓取”到“看得懂”的完整链路。它适合有 Python 基础、想用真实数据练手的数据分析新手也适合需要定期监控岗位趋势的 HR 或行业研究人员。核心思路不复杂用 Requests 库模拟浏览器请求拿到招聘平台的公开岗位列表页和详情页解析出岗位名称、薪资、城市、学历要求、技能关键词等字段清洗后存入本地数据库或 CSV再用可视化库生成图表。难点不在写代码而在理解每个平台的反爬节奏、字段结构差异以及怎么把非结构化的薪资文本变成可计算的数字。接下来我会按实际落地顺序把选型、实现、参数和踩坑点逐一拆开。2. 爬取前的技术选型与目标平台拆解2.1 为什么用 Requests BeautifulSoup 而不是 Scrapy面对招聘数据采集常见做法有两种一是用 Scrapy 这类重型框架二是用 Requests 配合解析库手写轻量爬虫。我一般会先评估目标站点的页面结构和反爬强度。招聘平台的列表页大多是服务端渲染或简单的 JSON 接口翻页逻辑清晰不需要分布式调度和复杂的去重管道。Scrapy 的优势在于大规模并发和内置的中间件机制但它的学习曲线和调试成本对新手不够友好一个下载中间件配置错误就可能让整个爬虫静默失败。Requests 库的优势是直观发一个 GET 请求拿到 HTML 或 JSON剩下的解析和存储逻辑完全由自己控制。配合 BeautifulSoup 或 lxml 解析 HTML代码量少出问题容易定位。对于日抓取量在几千到几万条之间的场景Requests 完全够用。如果后续需要扩展到几十万条并涉及增量抓取再考虑迁移到 Scrapy 或加入异步请求库也不迟。选型的关键不是哪个框架更“高级”而是当前阶段哪个能让你最快跑通闭环。2.2 主流招聘平台页面结构与字段差异不同招聘平台的页面组织方式差异很大直接决定了你的解析策略。以常见的几类平台为例有的平台列表页直接返回 JSON 数据岗位名称、薪资、城市、公司名都在一个接口里解析起来最省事有的平台列表页是 HTML 表格需要定位到具体的 class 或 XPath 才能提取还有的平台把详情页的岗位描述放在独立的富文本区域技能关键词需要从大段文字里做分词或正则匹配。字段命名也不统一比如“薪资”可能叫 salary、provideSalary、salaryReal单位可能是“千/月”“万/年”或“元/天”。城市字段有的给城市名有的给城市编码需要额外维护一张映射表。学历要求有的写“本科”有的写“统招本科”还有的写“学历不限”。这些差异意味着你不能指望一套解析规则通吃所有平台必须为每个目标平台单独写解析函数并在配置层做好字段映射。我的习惯是先手动打开目标平台的列表页和详情页用浏览器开发者工具查看网络请求确认数据是直接嵌在 HTML 里还是通过 XHR 异步加载再决定用 HTML 解析还是直接请求接口。2.3 请求头、Cookie 与频率控制的参数设置Requests 库默认的 User-Agent 是 python-requests很多招聘平台会直接拒绝这类请求。所以第一步是伪装请求头。下面是一个我常用的请求头配置模板关键字段都做了注释说明。import requests import time import random # 请求头配置模拟主流浏览器降低被识别为爬虫的概率 HEADERS { 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: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Accept-Encoding: gzip, deflate, br, Connection: keep-alive, Upgrade-Insecure-Requests: 1, } # 可选如果你从浏览器里复制了有效的 Cookie可以加进来 # 注意 Cookie 有时效性过期后需要重新获取 COOKIES { # sessionid: your_session_id_here, } def fetch_page(url, retry3): 带重试和随机延迟的页面抓取函数 for i in range(retry): try: resp requests.get( url, headersHEADERS, cookiesCOOKIES, timeout10, # 超时时间避免卡死 ) if resp.status_code 200: return resp.text elif resp.status_code 403: print(f被拒绝访问状态码 403第 {i1} 次重试) time.sleep(random.uniform(3, 6)) else: print(f状态码 {resp.status_code}第 {i1} 次重试) time.sleep(random.uniform(2, 4)) except requests.RequestException as e: print(f请求异常{e}第 {i1} 次重试) time.sleep(random.uniform(2, 5)) return None这段代码里timeout10是必须设置的否则遇到慢响应会一直挂起。retry3配合随机延迟能在遇到临时封禁时争取恢复机会。random.uniform(2, 5)的延迟范围是我多次调整后觉得比较稳妥的区间太短容易触发频率限制太长会拖慢整体抓取速度。Cookie 不是必须的但有些平台在未登录状态下只展示前几页数据登录后能看到更多。如果你选择携带 Cookie要注意它有时效性通常几小时到几天就会失效需要重新从浏览器获取。频率控制方面我的经验是单 IP 对同一平台的请求间隔不要低于 2 秒如果目标平台反爬严格可以拉长到 5 到 8 秒或者引入代理池轮换 IP。代理池的搭建涉及额外成本新手可以先从控制频率做起。3. 岗位数据解析与字段清洗的完整实现3.1 列表页与详情页的解析函数编写解析函数要针对每个平台单独写但结构可以统一输入是 HTML 文本输出是字典列表。下面以一类常见的 HTML 列表页为例展示解析逻辑。from bs4 import BeautifulSoup import re def parse_job_list(html): 解析招聘列表页提取岗位基础字段 soup BeautifulSoup(html, lxml) jobs [] # 假设岗位卡片在 class 为 job-card 的 div 中 for card in soup.select(.job-card): job {} # 岗位名称 title_tag card.select_one(.job-name a) job[title] title_tag.get_text(stripTrue) if title_tag else # 薪资文本后续需要清洗 salary_tag card.select_one(.job-salary) job[salary_raw] salary_tag.get_text(stripTrue) if salary_tag else # 城市 city_tag card.select_one(.job-city) job[city] city_tag.get_text(stripTrue) if city_tag else # 公司名称 company_tag card.select_one(.company-name a) job[company] company_tag.get_text(stripTrue) if company_tag else # 详情页链接用于后续抓取岗位描述 link_tag card.select_one(.job-name a) job[detail_url] link_tag[href] if link_tag and link_tag.has_attr(href) else jobs.append(job) return jobs这段代码的关键在于选择器的准确性。soup.select(.job-card)里的 class 名需要你根据目标平台的实际 HTML 结构替换。select_one返回第一个匹配元素如果页面结构变化返回 None后面的get_text就会报错所以每个字段都加了空值判断。detail_url用于后续抓取详情页通常需要拼接成完整 URL。解析详情页时重点提取岗位描述、学历要求、经验要求、技能关键词等字段。岗位描述往往是一大段富文本需要先去掉 HTML 标签再用正则或分词工具提取技能词。我一般会维护一个技能词典比如 Python、Java、SQL、Excel、沟通能力等用简单的字符串匹配先跑通后续再考虑用 jieba 分词做更细的提取。3.2 薪资文本的标准化清洗薪资字段是清洗环节最麻烦的部分。原始文本可能是“1.5-2万/月”“15-20K·13薪”“200元/天”“面议”等多种格式。要把它变成可计算的数字需要一套解析规则。下面是我常用的清洗函数。import re def parse_salary(salary_raw): 将薪资文本解析为月薪下限和上限单位元 if not salary_raw or 面议 in salary_raw: return None, None # 统一单位万 - 10000千/K - 1000元 - 1 text salary_raw.replace( , ).upper() # 处理“·13薪”这类后缀先提取薪数 months 12 month_match re.search(r·(\d)薪, text) if month_match: months int(month_match.group(1)) text text.replace(month_match.group(0), ) # 提取数字范围 numbers re.findall(r(\d\.?\d*), text) if len(numbers) 2: return None, None low float(numbers[0]) high float(numbers[1]) # 判断单位 if 万 in text: low * 10000 high * 10000 elif K in text or 千 in text: low * 1000 high * 1000 # 元/天的情况按每月 21.75 个工作日折算 if 天 in text: low low * 21.75 high high * 21.75 # 折算年薪到月薪 if 年 in text and 月 not in text: low low / 12 high high / 12 # 如果薪数不是 12按比例折算成 12 个月的标准月薪 if months ! 12: low low * months / 12 high high * months / 12 return round(low, 2), round(high, 2)这个函数的逻辑是先统一大小写和空格再提取“·13薪”这类信息然后找出所有数字。单位判断的顺序很重要先判断“万”再判断“K”或“千”最后处理“元/天”和“年”的情况。21.75是法定的月均工作日用来把日薪折算成月薪。如果薪数不是 12比如 13 薪意味着每年多拿一个月工资折算成标准月薪时要乘以 13/12。这个函数不能覆盖所有情况比如“底薪提成”这种复合结构遇到时我会先记录原始文本后续人工抽样处理。清洗后的薪资字段建议同时保留原始文本和解析结果方便回溯核对。3.3 数据存储与去重策略抓取到的数据需要落地存储。小规模场景用 CSV 就够了方便用 Excel 或 pandas 直接打开。数据量超过十万条或者需要频繁按条件查询建议用 SQLite 或 MySQL。下面是一个用 pandas 去重并写入 CSV 的示例。import pandas as pd import os def save_jobs(jobs, filepathjobs.csv): 将岗位列表保存为 CSV并按岗位链接去重 df pd.DataFrame(jobs) # 按详情页链接去重保留最新抓取的一条 if detail_url in df.columns: df.drop_duplicates(subset[detail_url], keeplast, inplaceTrue) # 如果文件已存在合并后再次去重 if os.path.exists(filepath): old_df pd.read_csv(filepath) df pd.concat([old_df, df], ignore_indexTrue) df.drop_duplicates(subset[detail_url], keeplast, inplaceTrue) df.to_csv(filepath, indexFalse, encodingutf-8-sig) print(f已保存 {len(df)} 条岗位数据到 {filepath})去重的依据选择detail_url而不是岗位名称因为同一岗位名称在不同公司可能重复而详情页链接是唯一的。keeplast表示保留最新抓取的那条适合薪资或岗位描述有更新的场景。encodingutf-8-sig是为了让 Excel 打开 CSV 时不乱码。如果数据量很大每次全量读取 CSV 再合并会越来越慢这时候应该换成数据库用INSERT OR REPLACE或ON DUPLICATE KEY UPDATE来实现增量更新。我的习惯是每天抓取一次按日期分文件存储比如jobs_20250101.csv这样既方便追溯也避免了单文件过大的问题。4. 可视化分析与看板搭建的落地路径4.1 用 pandas 做岗位数据的聚合统计可视化之前先用 pandas 把数据聚合到合适的粒度。常见的分析维度包括按城市统计岗位数量、按薪资区间统计分布、按学历要求统计占比、按技能关键词统计出现频率。下面是一段聚合统计的代码示例。import pandas as pd def analyze_jobs(filepathjobs.csv): 读取岗位数据并生成聚合统计结果 df pd.read_csv(filepath) # 按城市统计岗位数量取前 10 city_counts df[city].value_counts().head(10) # 按薪资下限划分区间 df[salary_low] pd.to_numeric(df[salary_low], errorscoerce) bins [0, 5000, 10000, 15000, 20000, 30000, float(inf)] labels [5K以下, 5K-10K, 10K-15K, 15K-20K, 20K-30K, 30K以上] df[salary_range] pd.cut(df[salary_low], binsbins, labelslabels) salary_dist df[salary_range].value_counts().sort_index() # 按学历要求统计 education_counts df[education].value_counts() return { city_counts: city_counts, salary_dist: salary_dist, education_counts: education_counts, }pd.cut用于把连续数值分箱bins定义了区间边界labels是每个区间的可读名称。value_counts()默认按数量降序排列sort_index()则按区间顺序排列适合画柱状图。学历字段如果原始数据里是“本科”“硕士”等文本直接value_counts()就能得到分布。技能关键词的统计需要先把岗位描述里的技能词提取出来可以用简单的字符串包含判断也可以用 jieba 分词后过滤停用词。聚合结果建议先打印出来看一眼确认没有明显的空值或异常值再进入可视化环节。4.2 用 pyecharts 生成交互式图表静态图表用 matplotlib 就够了但如果想让图表可以悬停查看数值、支持缩放pyecharts 是更合适的选择。它生成的 HTML 文件可以直接在浏览器里打开也方便嵌入看板。下面是一个生成城市岗位数量柱状图的示例。from pyecharts.charts import Bar from pyecharts import options as opts def plot_city_bar(city_counts, outputcity_bar.html): 生成城市岗位数量柱状图 cities city_counts.index.tolist() counts city_counts.values.tolist() bar ( Bar() .add_xaxis(cities) .add_yaxis(岗位数量, counts, color#5470c6) .set_global_opts( title_optsopts.TitleOpts(title城市岗位数量 Top 10), xaxis_optsopts.AxisOpts(name城市, axislabel_optsopts.LabelOpts(rotate30)), yaxis_optsopts.AxisOpts(name岗位数量), toolbox_optsopts.ToolboxOpts(is_showTrue), ) ) bar.render(output) print(f图表已保存到 {output})add_xaxis接收城市列表add_yaxis接收对应的数量列表。rotate30是为了防止城市名太长导致重叠。toolbox_opts开启了工具栏用户可以在浏览器里下载图片或切换视图。pyecharts 还支持折线图、饼图、词云等多种图表类型薪资分布适合用柱状图或饼图技能关键词适合用词云。生成多个图表后可以用Page类把它们组合成一个看板页面或者用Grid做多图联动。我的习惯是每个图表单独生成一个 HTML再用一个简单的导航页把它们串起来这样调试和分享都方便。4.3 用 Streamlit 快速搭一个可交互看板如果不想手动拼 HTMLStreamlit 是更快的选择。它用纯 Python 写交互式页面几行代码就能把 pandas 的聚合结果变成可筛选的图表。下面是一个最小可用的 Streamlit 看板示例。import streamlit as st import pandas as pd import plotly.express as px st.set_page_config(page_title招聘数据看板, layoutwide) st.title(招聘岗位数据可视化看板) st.cache_data def load_data(): df pd.read_csv(jobs.csv) df[salary_low] pd.to_numeric(df[salary_low], errorscoerce) return df df load_data() # 侧边栏筛选器 city_options [全部] sorted(df[city].dropna().unique().tolist()) selected_city st.sidebar.selectbox(选择城市, city_options) if selected_city ! 全部: df df[df[city] selected_city] # 薪资分布图 fig_salary px.histogram(df, xsalary_low, nbins30, title薪资下限分布) st.plotly_chart(fig_salary, use_container_widthTrue) # 学历要求饼图 edu_counts df[education].value_counts().reset_index() edu_counts.columns [学历, 数量] fig_edu px.pie(edu_counts, names学历, values数量, title学历要求分布) st.plotly_chart(fig_edu, use_container_widthTrue)st.cache_data装饰器用于缓存数据加载结果避免每次交互都重新读取 CSV。st.sidebar.selectbox在侧边栏生成下拉筛选器用户选择城市后后续图表会自动更新。px.histogram和px.pie是 Plotly Express 的函数生成的图表自带悬停提示和缩放功能。use_container_widthTrue让图表自适应页面宽度。运行方式是在命令行执行streamlit run app.py浏览器会自动打开看板页面。这种方式的优点是开发速度快适合快速验证分析思路缺点是定制化程度不如手写前端但对大多数内部看板场景已经足够。5. 避坑与常见问题排查5.1 请求返回 403 或验证码页面现象脚本运行一段时间后返回的 HTML 里出现“访问过于频繁”或验证码图片状态码变成 403。原因通常是请求频率过高或者请求头里的 User-Agent 被识别为爬虫。解决方法是先降低请求频率把间隔从 2 秒拉长到 5 秒以上并加入随机抖动。如果仍然被拦截检查请求头是否缺少 Referer 字段有些平台会校验来源页。另外Cookie 过期也会导致 403需要重新从浏览器获取。如果验证码频繁出现说明单 IP 已经进入风控名单这时候要么换 IP要么暂停一段时间再试。我的血泪经验是不要等到被封了才想起来加延迟一开始就把频率控制在保守范围反而整体效率更高。5.2 解析到的字段为空或错位现象岗位名称能拿到但薪资或城市字段是空的或者所有岗位的薪资都解析成了同一个值。原因通常是页面结构发生了变化或者选择器写错了。排查方法是先把请求到的 HTML 保存到本地文件用浏览器打开手动检查目标元素的 class 或 id 是否和代码里写的一致。另一个常见原因是列表页和详情页的字段来源不同比如列表页只有岗位名称和薪资城市信息在详情页里这时候需要先抓详情页再补充字段。还有一种情况是页面用了懒加载滚动到可视区域才渲染Requests 拿到的 HTML 里根本没有那些元素这种就需要分析 XHR 请求直接请求数据接口。5.3 薪资清洗结果出现极端值现象清洗后的薪资下限出现 0 或者几百万的异常值。原因可能是原始文本里包含了非薪资的数字比如“3-5年经验”被误提取成了薪资。解决方法是先检查salary_raw字段的内容确认没有把经验要求、招聘人数等数字混进来。可以在解析列表页时就把薪资字段限定在特定的 class 里避免抓错元素。另外“面议”和“薪资面议”要提前过滤掉否则re.findall可能匹配到空值。对于“200元/天”这种日薪折算成月薪后可能低于最低工资标准这属于正常情况但要在分析时单独标注避免拉低整体均值。5.4 数据去重后仍然出现重复岗位现象明明按detail_url去重了但 CSV 里还是有重复的岗位名称和公司。原因可能是同一个岗位在不同城市或不同站点发布了多次详情页链接不同但内容相似。解决方法是增加复合去重键比如用title company city组合去重。但要注意有些公司确实会在不同城市招同一个岗位这种不应该被去掉。我的做法是保留detail_url去重作为第一层再用title company做第二层但第二层只标记不删除后续分析时按需过滤。另外如果每次抓取都追加到同一个 CSV去重逻辑要在合并后执行否则旧数据里的重复项不会被清理。5.5 可视化图表中文显示为方块现象用 matplotlib 生成的图表里中文标题和标签显示成方块。原因是 matplotlib 默认字体不支持中文。解决方法是在绘图前指定中文字体比如plt.rcParams[font.sans-serif] [SimHei]同时把axes.unicode_minus设为 False 以正常显示负号。如果系统里没有 SimHei 字体可以换成Microsoft YaHei或WenQuanYi Micro Hei。pyecharts 和 Streamlit 通常没有这个问题因为它们渲染在浏览器里用的是系统字体。如果非要用 matplotlib建议在代码开头统一设置字体避免每个图表单独处理。6. 把抓取频率压到最低的增量更新技巧增量更新是让这套系统长期跑下去的关键。全量抓取不仅慢而且容易触发反爬。我的做法是维护一个已抓取链接的集合每次只请求列表页的前几页发现新链接就加入待抓队列已存在的链接直接跳过。具体实现上可以用 SQLite 建一张jobs表把detail_url设为主键插入时用INSERT OR IGNORE这样重复数据自动被忽略。下面是一个增量抓取的调度示例。import sqlite3 import time import random def init_db(db_pathjobs.db): conn sqlite3.connect(db_path) conn.execute( CREATE TABLE IF NOT EXISTS jobs ( detail_url TEXT PRIMARY KEY, title TEXT, company TEXT, city TEXT, salary_raw TEXT, salary_low REAL, salary_high REAL, education TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) conn.commit() return conn def save_job_incremental(conn, job): 增量插入岗位已存在的链接自动跳过 try: conn.execute( INSERT OR IGNORE INTO jobs (detail_url, title, company, city, salary_raw, salary_low, salary_high, education) VALUES (?, ?, ?, ?, ?, ?, ?, ?) , ( job.get(detail_url), job.get(title), job.get(company), job.get(city), job.get(salary_raw), job.get(salary_low), job.get(salary_high), job.get(education) )) conn.commit() return True except sqlite3.Error as e: print(f插入失败{e}) return FalseINSERT OR IGNORE是 SQLite 的语法遇到主键冲突时直接跳过不会报错。这样每次抓取只需要处理新出现的岗位链接老数据不动。调度上我一般把抓取任务放在凌晨执行这时候目标平台的压力小反爬策略也相对宽松。每次抓取前先请求列表页的第一页对比数据库里最新记录的created_at如果超过 24 小时没有新数据就适当增加抓取页数。另外给每个平台的抓取任务单独设置间隔比如平台 A 间隔 3 秒平台 B 间隔 5 秒避免同时请求多个平台导致 IP 被整体限流。这套增量方案跑顺之后每天新增数据量通常只有几百条几分钟就能跑完对目标平台的影响也降到最低。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑