ARTICLE DETAIL

资讯详情

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

招聘岗位爬虫挖掘与可视化:期末大作业完整项目实战

招聘岗位爬虫挖掘与可视化:期末大作业完整项目实战 简介一份面向计算机专业学生及爬虫初学者的Python招聘岗位数据爬虫挖掘与可视化分析项目可直接用于课程设计或期末大作业参考完整覆盖数据采集、存储、分析与展示流程。压缩包共59个文件约10.33MB以Python源码为主多个.py与.pyc搭配SQL数据库文件、PPT演示文稿、HTML/CSS/JS可视化页面、PNG/JPG结果图及README说明结构清晰便于按模块学习。项目内置腾讯招聘与51job招聘数据采集相关脚本、数据表及分析代码配合演示文稿可快速理解爬虫设计思路、数据清洗与可视化方法。已有2083人学习下载代码经调试可直接运行适合需要完整项目实战或借鉴高分作业方案的学习者。1. 招聘岗位爬虫挖掘与可视化期末大作业里适用面最广的落地方向期末课程设计最怕的不是没代码而是代码跑不通、数据难看、答辩没话说。招聘岗位数据爬虫挖掘及可视化这个方向恰好能把这三件事一次解决它有明确的网页数据源能完整走通“抓取—清洗—挖掘—可视化”这条链路产出的图表又天生适合讲成答辩 PPT。这份资源就是按高分期末大作业的标准打包的Python 爬虫源码、全量数据、数据挖掘与可视化脚本、PPT 文档都在里面。适合正在赶课程设计的在校生也适合想拿一个完整数据项目做作品集的入门开发者。照着复现一遍从请求页面到最终图表都能自己跑通。提前说一个反直觉的结论这套项目最容易翻车的不是反爬而是编码、去重和图表离线加载这三处细枝末节。第五章会把这几条踩坑记录一条一条拆开讲。2. 项目全景与运行准备先看懂包结构再决定从哪一步开始跑2.1 包里都有什么源码、数据、PPT 三段分离的好处这类打包资源最忌讳一锅炖。把爬虫、分析和可视化拆成三个独立模块意思是换关键词、改图表、补数据都不用推倒重来这也是课程设计答辩时老师最愿意看到的工程化习惯。压缩包解压之后的目录结构大致是下面这个样子文件名可能略有出入但职责基本一致project/ ├── main.py # 一键执行入口 ├── spider/ │ └── job_spider.py # 爬虫抓取模块 ├── analysis/ │ └── analyze_jobs.py # 数据清洗与指标统计 ├── visual/ │ ├── charts.py # 可视化脚本 │ └── output/ # 生成的 HTML 图表 ├── data/ │ ├── jobs_raw.csv # 抓取后的原始数据 │ ├── jobs_clean.csv # 清洗后的分析数据 │ └── jobs.db # SQLite 中间库 └── docs/ └── 答辩PPT.pptx # 演示文档spider 模块的输出是 data 目录下的原始 CSV 和 SQLite 数据库analysis 模块读取原始数据清洗后写出 jobs_clean.csvvisual 模块只认清洗后的文件不碰原始数据。三个模块单向依赖每一步的产出都是下一步的输入所以单独调试任何一环都不影响其他环节。目录/文件职责主要产出打开优先级main.py按顺序调用三个模块控制台日志最后看spider/job_spider.py抓取招聘页列表与详情jobs_raw.csv、jobs.db先看analysis/analyze_jobs.py去重、薪资拆解、统计jobs_clean.csv第二看visual/charts.py生成交互图表output 下多个 HTML第三看docs/答辩PPT.pptx答辩展示与讲解演示文稿最后改拿到包之后不建议立刻双击 main.py。先用文本编辑器打开 data 目录下的原始数据确认字段名和样例值再打开 analysis 脚本看它做了哪些清洗动作。把这两件事做完你对这个资源的理解就已经超过大部分只会跑通的同学。2.2 环境搭建Python 3.8 加七个依赖版本锁死我一直建议课程设计项目不要贪多requests 加 BeautifulSoup 的组合足够应付招聘类页面的抓取完全没必要上 Scrapy。后者虽然功能强但配置文件、中间件、管道这些概念对期末作业来说学习成本高出问题也不好排查。先建虚拟环境再装依赖别直接往全局环境里塞。python -m venv venv # Windows 激活命令venv\Scripts\activate # macOS / Linux 激活命令source venv/bin/activate pip install -r requirements.txt这里给一个可用的依赖清单按我的习惯会把版本锁死免得半年后重新安装时某个库升级把代码搞挂requests2.31.0 beautifulsoup44.12.2 lxml4.9.3 pandas2.0.3 pyecharts2.0.3 jieba0.42.1 wordcloud1.9.2逐个说明用途。requests 负责发 HTTP 请求beautifulsoup4 做页面解析lxml 是解析器引擎比 Python 自带的 html.parser 容错性好、速度快pandas 承担数据清洗和统计pyecharts 负责生成交互式 HTML 图表jieba 用于把岗位描述切词配合 wordcloud 画词云。注意wordcloud 在新版 Python 上偶尔直接安装失败因为对应版本的 wheel 没跟上。遇到这种情况不用硬磕后面 4.3 我会给一个纯 pyecharts 的词云替代方案效果差距不大。2.3 执行链路main.py 按什么顺序做四件事main.py 的本质不是写逻辑而是把三个模块按依赖顺序串起来。我见过不少同学喜欢把抓取、清洗、画图全部塞进一个文件跑起来是省事可一旦页面结构变了改一处就牵动全身。这份资源的模块化拆分本来就是为了让你能单独替换某一段。import subprocess import sys def main(): # 第 1 步抓取默认抓 5 页关键词从命令行传入 subprocess.run( [sys.executable, -m, spider.job_spider, --pages, 5] ) # 第 2 步清洗并输出统计字段 subprocess.run([sys.executable, -m, analysis.analyze_jobs]) # 第 3 步生成图表 subprocess.run([sys.executable, -m, visual.charts]) print(三个模块执行完毕请检查 visual/output 下的 HTML 文件) if __name__ __main__: main()用 subprocess 而不是直接 import 函数好处是模块之间没有强耦合任何一步失败都不会让整个进程崩溃日志也能清晰定位到具体模块。如果你想改成抓取不同关键词按下面这种方式传参即可python main.py --keyword python --pages 10--keyword 指定搜索词--pages 控制翻页深度。这个参数设计很关键演示阶段先抓两三页验证流程确认没问题再加页数别一上来就抓几十页。提示如果你只想复现数据分析和可视化部分不想联网跑爬虫完全可以直接跳过第 1 步。data 目录下已有的原始数据足够支撑第 2、3 步跑通。这是这套资源“能跑、能交、能讲”的关键设计。3. 爬虫模块实战解析策略、翻页节奏与数据落盘3.1 页面分析列表页和详情页各取什么字段写爬虫之前先打开目标招聘网站按 F12 切到开发者工具找到岗位列表所在的 DOM 区域。列表页通常能拿到岗位名称、公司名称、薪资、城市、经验要求、学历要求、发布时间这些字段已经够做大部分分析详情页里才有职位描述和技能要求是词云分析的数据来源。抓取时我一般习惯先做一个小表格把字段和对应位置记清楚再动手写代码字段来源页面典型格式清洗后用途岗位名称列表页数据分析师岗位分类、词云公司名称列表页某某科技有限公司公司维度统计薪资列表页10-15K / 面议薪资拆解成数值城市列表页上海 / 北京城市分布图经验要求列表页1-3年经验维度统计学历要求列表页本科学历占比图职位描述详情页长文本技能词频、词云岗位链接列表页URL去重和详情补抓列表页的卡片结构一般是一个重复的 div 容器里面包含上述字段。详情页则是一整个文章区域。列表页和详情页分开解析是后面排查字段错位问题的基础。3.2 用 Requests BeautifulSoup 写岗位解析器抓取请求的核心不是把代码写得多花哨而是请求头、编码和超时这三件事。很多初学者只带一个 User-Agent 就发请求遇到反爬强的站点直接返回 403。常见做法是带上 Accept 和 Referer并统一通过 Session 保持会话。import requests from bs4 import BeautifulSoup 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, Referer: https://example.com/, } SESSION requests.Session() SESSION.headers.update(HEADERS) def fetch_page(keyword: str, page: int) - str: url https://example.com/jobs params {query: keyword, page: page} resp SESSION.get(url, paramsparams, timeout10) resp.encoding resp.apparent_encoding # 防止中文乱码 resp.raise_for_status() return resp.text逻辑说明把 headers 挂在 Session 上后续所有请求都会自动携带不用每个请求重复传。params 参数交给 requests 自动拼 URL比字符串拼接优雅也更安全特殊字符会被正确编码。timeout 设 10 秒是底线避免某个请求卡死导致整个脚本挂住。resp.apparent_encoding 是 requests 根据页面内容反推的编码比默认的 ISO-8859-1 可靠。不过它需要额外读取一部分内容对性能有一点影响如果确认目标站是 UTF-8也可以直接写死 resp.encoding utf-8。拿到 HTML 后解析def parse_jobs(html: str) - list[dict]: soup BeautifulSoup(html, lxml) result [] # 先定位到每一个岗位卡片再在卡片内部取字段 for card in soup.select(div.job-card): title_el card.select_one(.job-title) company_el card.select_one(.company-name) salary_el card.select_one(.salary) city_el card.select_one(.city) link_el card.select_one(a.job-link) if not all([title_el, company_el, salary_el, city_el]): continue # 字段不全的卡片直接跳过不塞垃圾数据 result.append({ title: title_el.get_text(stripTrue), company: company_el.get_text(stripTrue), salary: salary_el.get_text(stripTrue), city: city_el.get_text(stripTrue), url: link_el.get(href, ), }) return resultselect 定位所有岗位卡片select_one 在每个卡片内部找对应字段。先锁定卡片容器再取子元素比直接从全局页面里 select_one 要稳得多这也是后面避坑章节里字段错位问题的关键解法。get_text(stripTrue) 会去掉文本首尾空白和换行避免存进数据库的字段带着一堆 \n 和空格。字段不全时用 continue 跳过宁缺毋滥。3.3 翻页控制与去重请求频率、循环条件、断点续跑翻页的过程中最容易出问题的不是页面解析而是请求频率。我见过不少同学直接 for 循环秒抓几十页结果抓一半 IP 被限流整个脚本白跑。我一般会在每两次请求之间加一个 1 到 3 秒的随机停顿并且用页内是否有数据作为循环停止条件而不是盲目写死页数。import random import time KEYWORD python MAX_PAGES 10 all_jobs [] for page in range(1, MAX_PAGES 1): html fetch_page(KEYWORD, page) jobs parse_jobs(html) if not jobs: print(f第 {page} 页无数据提前停止翻页) break all_jobs.extend(jobs) print(f第 {page} 页抓取 {len(jobs)} 条累计 {len(all_jobs)} 条) # 随机停顿模拟真实浏览节奏 time.sleep(random.uniform(1, 3))random.uniform(1, 3) 是随机浮点数分布在 1 到 3 秒之间。随机间隔的意义在于让请求时间序列不那么规律大幅降低被风控识别的概率。如果站点对频率特别敏感可以把上限调整到 5 秒。还有个容易忽略的点翻页条件。有些站点到了最后一页会返回空列表但也有些站点会重复返回最后一页的内容。判断条件应该用“本页是否解析出新数据”而不是“是否达到设定页数”。去重建议在抓取阶段就做别等清洗时才做。岗位链接是天然的唯一键同一个招聘信息通常不会出现两条一样的 URL。seen_urls set() unique_jobs [] for job in all_jobs: url job[url] if url and url not in seen_urls: seen_urls.add(url) unique_jobs.append(job) print(f去重后剩余 {len(unique_jobs)} 条)seen_urls 是一个集合判断 url 是否出现过的时间复杂度是 O(1)。如果重复数据已经进入了 CSV 或者数据库也没关系下一章清洗部分会讲基于 pandas 的第二道去重防线。3.4 数据落盘为什么我选了 SQLite 而不是 CSV很多教程会把抓取结果直接写进 CSV对于小数据量确实够用。但课程设计的完整流程应该是可重复的今天抓一遍、明天再抓一遍CSV 文件就会越堆越乱。SQLite 的 UNIQUE 约束能在数据库层直接挡掉重复记录这是 CSV 做不到的。import sqlite3 conn sqlite3.connect(data/jobs.db) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS jobs ( url TEXT PRIMARY KEY, title TEXT, company TEXT, salary TEXT, city TEXT, experience TEXT, education TEXT, crawled_at TEXT DEFAULT (datetime(now, localtime)) ) ) cursor.executemany( INSERT OR IGNORE INTO jobs (url, title, company, salary, city, experience, education) VALUES (?, ?, ?, ?, ?, ?, ?) , [ (job[url], job[title], job[company], job[salary], job[city], job.get(experience, ), job.get(education, )) for job in unique_jobs ] ) conn.commit() conn.close()url 设为主键天然满足唯一性。INSERT OR IGNORE 的意思是遇到主键冲突就跳过这条记录不会报错中断这正是断点续跑需要的特性——中途失败了重新运行一次脚本已经存在的记录会被自动忽略。crawled_at 字段用数据库默认值记录抓取时间方便答辩时展示“这批数据是什么时候采集的”。executemany 批量插入比逐条 execute 快一个量级几百条数据虽然感觉不明显但养成习惯对以后处理更大规模的数据有帮助。4. 数据挖掘与可视化把原始岗位文本变成六张能讲的图4.1 数据清洗把“10-15K”拆成可计算的数值字段原始数据里的薪资是字符串比如“10-15K”“20-30K·14薪”“面议”。这种格式没法直接做均值、排序和分布统计所以清洗的第一步是把薪资文本拆成 salary_low 和 salary_high 两个数值字段再算一个中位数用于对比。import pandas as pd import re raw pd.read_csv(data/jobs_raw.csv, encodingutf-8-sig) clean raw.copy() def split_salary(s): if not isinstance(s, str) or 面议 in s: return None, None nums re.findall(r\d, s) if len(nums) 0: return None, None if len(nums) 1: return int(nums[0]), int(nums[0]) return int(nums[0]), int(nums[1]) clean[salary_low], clean[salary_high] zip( *clean[salary].map(split_salary) ) # 计算月薪中位数单位为 K clean[salary_mid] ( clean[salary_low].fillna(0) clean[salary_high].fillna(0) ) / 2 # 把城市里的“上海-徐汇区”统一成“上海” clean[city] clean[city].str.split(-).str[0] clean.to_csv(data/jobs_clean.csv, indexFalse, encodingutf-8-sig) print(clean.shape)逻辑说明split_salary 用正则把字符串里所有数字抽出来。一个数字表示固定薪资两个数字表示区间。“面议”或者其他非字符串值统一返回空值后续分析时可以用 fillna(0) 或者直接过滤掉。这里有个细节容易被忽略读 CSV 时指定 encodingutf-8-sig写入时也指定 utf-8-sig。utf-8-sig 会在文件开头写入 BOM 头Windows 的 Excel 打开时才不会乱码。这是一个血泪教训后面避坑章节会专门讲。4.2 挖掘维度城市、薪资、经验、技能四根分析主线招聘数据最值得挖掘的四根主线我归纳成下面这个表格。每一行对应答辩 PPT 里的一张图也对应一个能讲清楚的分析结论分析问题图表类型数据字段可讲解的结论哪些城市岗位最多条形图 TOP10city一线城市岗位集中度各城市薪资中位数差异横向条形图city salary_mid薪资地域差异经验要求分布饼图experience初级 vs 资深岗位比例学历要求分布环形图education行业门槛特征岗位名称关键词分布词云title热门岗位方向职位描述技能词频条形图 TOP20职位描述文本技术栈需求分析答辩时最容易冷场的时刻是老师问“你为什么做这张图”。提前把每张图的结论写死比如“北京岗位数量是成都的两倍但薪资中位数差距不大”比临场编理由强太多。这些结论都来自分析脚本输出不是编出来的。经验字段的清洗也要注意“经验不限”和“在校生/应届生”属于低门槛类别“5-10年”属于高门槛类别。分析时可以把经验字段先归一化成几个档位否则饼图上会冒出十几个类别看不出规律。4.3 可视化输出pyecharts 交互式 HTML 图表和答辩演示配合可视化我推荐 pyecharts原因有两个图表是交互式的鼠标悬停能看到具体数值答辩演示效果好其次是输出 HTML 文件不会遇到 matplotlib 中文乱码这种陈年问题。需要强调一点pyecharts 的 2.x 版本 API 是链式调用网上随便搜到的老代码大多是 1.x 风格直接复制大概率报错。from pyecharts import options as opts from pyecharts.charts import Bar # 按城市统计岗位数量取前 10 city_count ( clean.groupby(city) .size() .sort_values(ascendingFalse) .head(10) ) bar ( Bar( init_optsopts.InitOpts( width1000px, height600px, page_title招聘岗位城市分布 TOP10, ) ) .add_xaxis(city_count.index.tolist()) .add_yaxis(岗位数量, city_count.values.tolist()) .set_global_opts( title_optsopts.TitleOpts(title招聘岗位城市分布 TOP10), xaxis_optsopts.AxisOpts(axislabel_optsopts.LabelOpts(rotate30)), ) ) bar.render(visual/output/city_top10.html)逻辑说明groupby 之后按数量降序排列取前 10再把 index 和 values 分别转成列表传给 pyecharts。x 轴标签旋转 30 度是为了避免城市名重叠。init_opts 里的宽度和高度直接控制生成图表的画布尺寸答辩投屏之前先在自己电脑上打开看一眼比例不对再调。词云部分如果 wordcloud 装好了就用常规写法装不上就退一步用 pyecharts 内置的 WordCloudfrom pyecharts.charts import WordCloud # 假设 jieba 分词结果是一个 (词, 频次) 列表 words [(Python, 120), (数据分析, 98), (机器学习, 76)] cloud ( WordCloud() .add(series_name技能词频, data_pairwords, word_size_range[20, 100]) .set_global_opts(title_optsopts.TitleOpts(title岗位技能词频)) ) cloud.render(visual/output/skill_wordcloud.html)word_size_range 控制词的大小范围数值越大词越突出。data_pair 接收的是二元组列表格式别传错。这里建议把 jieba 分词结果过滤掉“要求”“负责”“岗位”这类无意义词否则词云里全是套话看不出技术栈特征。5. 常见问题与避坑四条实测踩坑记录5.1 CSV 中文乱码Excel 打开全是问号爬虫端和 pandas 端都要管现象用 Excel 直接打开 jobs_raw.csv中文字段全部变成乱码或者问号但在记事本里打开是正常的。原因Windows 版 Excel 默认用 GBK 编码读取 CSV而这套脚本写入时用的是 UTF-8两边对不上。另外 requests 抓取时如果没有主动设置 resp.encodingrequests 会按 HTTP 头里的 charset 解析很多招聘网站不返回这个头默认就可能解析出错。解决抓取时用 resp.encoding resp.apparent_encoding 或者直接写死 utf-8pandas 读写 CSV 时统一指定 encodingutf-8-sig。utf-8-sig 会在文件开头写 BOM 头Excel 识别到 BOM 就会自动按 UTF-8 解码这个细节一次设置之后所有环节都不再乱码。5.2 翻页到一半返回空白请求频率太快被限流别硬闯现象前 10 页抓得好好的第 11 页开始返回空列表或者返回一个带验证码提示的页面解析结果是全空。原因请求频率太高IP 触发了站点风控。很多招聘网站对搜索类接口有每分钟请求次数限制连续快速翻页最容易触发。解决抓取循环里强制加随机停顿时间间隔从 1 到 3 秒起步遇到风控严格的目标站拉到 3 到 6 秒。如果已经触发了验证码最好的做法是停止脚本等待一段时间不是换着 UA 继续撞。课程设计场景下完全没有必要和反爬死磕稍等十分钟再跑或者直接改用包里现成的 data 数据把时间花在分析上。5.3 字段错位select_one 选错节点时数据会串行现象岗位名称和公司名称互相串位比如“阿里巴巴”出现在岗位名称列或者某些记录的薪资字段是空的。原因页面结构里可能有两套相似的 DOM一套是真正的岗位卡片另一套是广告位或者推荐位。直接对全局页面用 select_one 取字段偶尔会命中错误节点尤其当字段缺失时select_one 会跳到别的卡片里去找。解决始终遵循“先定位卡片容器再在容器内取字段”的解析方式。每个字段取回来之后做一次格式校验最简单的办法是检查 salary 字段里是否包含数字或者“面议”不满足就跳过这条记录。清洗阶段再做一次字段完整性检查两个环节叠加错位数据基本进不了最终数据集。5.4 答辩机没网pyecharts 图表白屏现象在自己电脑上打开的 HTML 图表一切正常拿到答辩教室的电脑上双击打开图表区域一片空白。原因pyecharts 默认从公共 CDN 加载 ECharts 的 JavaScript 文件答辩教室断网或者网络受限时JS 加载不出来图表自然渲染不了。解决生成图表时指定本地 JS 文件路径或者直接截图保存成 PNG。本地化方式是在 Bar 的 init_opts 里加一个 js_host 参数指向本地 ECharts 文件所在目录。但更稳妥的做法是答辩前一晚把每张图都打开截图把图片插入 PPT 里HTML 只作为现场演示的补充。图片永远不会因网络问题失效这也是最保险的准备方式。from pyecharts import options as opts from pyecharts.charts import Bar bar Bar( init_optsopts.InitOpts( width1000px, height600px, js_host./echarts.min.js, # 本地化 JS 文件 ) )6. 数据验收技巧交作业前十分钟跑一遍自检脚本6.1 三分钟的自动化检查让数据口径经得起问答辩前一天最焦虑的事情永远是“老师问数据是不是真的怎么办”。与其到时候支支吾吾不如提前写一个自检脚本把数据量、缺失率、图表存在性全部检查一遍打印出一张清晰的结果清单。这个脚本看起来简单但每次跑完都能给你吃一颗定心丸。import os import sqlite3 import pandas as pd # 检查 1数据库记录数是否达标 conn sqlite3.connect(data/jobs.db) total pd.read_sql(SELECT COUNT(*) AS c FROM jobs, conn)[c][0] # 检查 2薪资缺失率是否在可解释范围 salary_missing pd.read_sql( SELECT COUNT(*) AS c FROM jobs WHERE salary面议, conn )[c][0] conn.close() print(f数据总量: {total} 条) print(f面议薪资占比: {salary_missing / max(total, 1):.1%}) # 检查 3关键图表文件是否存在 required_charts [ visual/output/city_top10.html, visual/output/skill_wordcloud.html, ] for path in required_charts: exists os.path.exists(path) print(f图表 {path}: {存在 if exists else 缺失}) assert exists, f缺少图表文件: {path} assert total 500, f数据量不足当前 {total} 条 print(自检通过可以安心答辩)逻辑说明前两个检查从 SQLite 里直接统计记录数和薪资缺失率第三个检查确认图表文件真实存在最后用一个总断言兜底。跑完之后输出的信息既给你看也是答辩时可以直接念给老师听的“数据质量说明”。这个脚本还能延伸出一个很实用的习惯每次重新抓完数据先跑一遍自检再生成 PPT确保所有图表对应的是最新数据而不是拿上一版图表临时顶替。数据口径不一致是答辩翻车的重灾区老师一旦发现图表里的数字和你口头表述对不上整个项目的可信度都会打折扣。从那以后我每做一个数据类项目都会强制在最后一步走一遍这套快速自检流程入库记录数、关键字段缺失率、图表文件存在性三件事缺一不可。希望帮到你。本文还有配套的精品资源点击获取
返回列表