2026/10/29 17:36:13

Python招聘网站爬取与可视化课程设计实战:从数据清洗到交互看板

Python招聘网站爬取与可视化课程设计实战:从数据清洗到交互看板 简介这份资源面向计算机相关专业的在校学生与编程初学者提供一套可直接用于毕业设计、期末大作业或课程设计的招聘网站数据采集与可视化项目。项目以Python为核心完成招聘信息的爬取、清洗与多维度图表呈现代码注释详尽新手也能读懂并快速部署运行。压缩包共12个文件约996KB包含3个py脚本分别负责爬虫抓取、箱型图绘制与词云生成另有5张jpg与3张png可视化结果图、1份docx项目手册以及数据库脚本等配套内容前后端代码齐全。目前已有236人学习下载。通过该项目读者可获得完整的爬取与可视化实现方案涵盖城市与工资箱型图、工作经验与工资关系、学历与地区职位数量分布、Python技能词云等分析维度并附有手册说明与图表样例便于对照理解数据流转与图表生成逻辑适合作为课程设计参考或数据分析入门练手项目。1. 从招聘网站到可视化大屏这套课程设计代码到底能跑出什么如果你正在为课程设计发愁或者想找一个能写进简历的 Python 实战项目招聘网站爬取加可视化这个方向几乎每年都会被翻出来做一遍。原因很直接数据源公开、字段结构清晰、可视化效果直观答辩时老师一眼就能看懂你在干什么。但真正动手时你会发现事情没那么简单——列表页翻几页就被限制、详情页字段对不上、薪资写的是“15-25K·13薪”这种非结构化文本、城市名带“·”后缀、经验要求混着“应届生”和“1年以内”。这套课程设计代码的价值就在于它把这些脏活累活都处理过了你拿到的是一个从爬取、清洗、存储到可视化展示的完整链路而不是一个只能跑通 demo 的玩具脚本。适合谁用如果你是计算机相关专业的学生正在找课程设计或毕业设计的底稿这套代码能帮你省掉至少两周的试错时间。如果你已经工作但想补一个数据采集加可视化的完整项目经验它也能作为一个可扩展的起点。需要说明的是这套代码默认你本地有 Python 环境知道怎么装库能看懂基本的 HTML 结构。完全零基础的话建议先补一下 requests 和 pandas 的基本用法再来看。2. 环境准备与目标站点结构拆解先看清页面再动手2.1 依赖库选型与安装这套代码用到的核心库不多但每个都有明确的选型理由。requests 负责发请求BeautifulSoup 负责解析静态 HTMLpandas 负责清洗和聚合pyecharts 负责出图。为什么不直接用 scrapy因为课程设计的场景下scrapy 的工程结构反而增加了理解成本而且目标站点页面结构相对规整没必要上重型框架。为什么不选 selenium因为招聘网站的列表页和详情页大部分内容是服务端渲染的静态请求就能拿到用 selenium 属于杀鸡用牛刀还会拖慢速度。安装命令如下pip install requests beautifulsoup4 pandas pyecharts lxml openpyxl这里重点说一下 lxml 和 openpyxl。lxml 是 BeautifulSoup 的解析器后端比默认的 html.parser 快很多尤其是在批量解析详情页的时候差距明显。openpyxl 是用来把清洗后的数据导出成 Excel 的方便你在答辩时直接展示原始数据表。pyecharts 选的是 1.x 版本和 0.5.x 的 API 完全不兼容网上很多老教程用的是旧版写法你如果混着抄会报错。提示pyecharts 1.x 生成的是 HTML 文件不是直接弹窗显示。你需要用浏览器打开生成的 HTML 才能看到图表或者用 Jupyter Notebook 的 render_notebook() 方法内嵌显示。2.2 目标页面结构分析在写任何爬虫代码之前我习惯先用浏览器开发者工具把目标页面的 DOM 结构过一遍。以招聘网站常见的搜索列表页为例你需要定位几个关键容器职位卡片的外层 div、职位名称的 a 标签、薪资的 span、公司名称的 a 标签、城市和经验的 span。不同网站的 class 名不一样但结构逻辑大同小异。打开开发者工具后按 F12 或者右键检查元素用选择器工具点一下职位卡片看看它的 class 是什么。常见的有job-list-item、job-card、sojob-item这类命名。找到卡片容器后再逐层往下找子元素。这里有个血泪经验不要直接复制浏览器里看到的 class 名就完事有些网站会用随机化的 class 或者动态生成的属性你复制下来的选择器第二天可能就失效了。更稳妥的做法是找那些语义化的、稳定的属性比如>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-Language: zh-CN,zh;q0.9, } url https://example-recruit-site.com/jobs?keywordpythoncity北京page1 resp requests.get(url, headersheaders, timeout10) resp.encoding utf-8 soup BeautifulSoup(resp.text, lxml) # 定位职位卡片class 名根据实际站点替换 cards soup.select(div.job-card) print(f本页抓到 {len(cards)} 个职位卡片) for card in cards[:3]: title card.select_one(a.job-name) salary card.select_one(span.salary) company card.select_one(a.company-name) print(title.text.strip() if title else N/A, salary.text.strip() if salary else N/A, company.text.strip() if company else N/A)这段代码的逻辑很直白构造请求头、发请求、指定编码、解析 HTML、用 CSS 选择器提取字段。参数方面timeout 设 10 秒是防止某个请求卡死导致整个脚本挂起。encoding 必须手动指定因为很多招聘网站返回的响应头里不带 charsetrequests 会默认用 ISO-8859-1 解码中文全是乱码。select 方法返回的是列表select_one 返回单个元素或 None所以每个字段都要做空值判断不然遇到缺失字段直接抛 AttributeError。2.3 翻页逻辑与请求频率控制翻页有两种常见做法一种是改 URL 里的 page 参数另一种是构造 POST 请求带分页参数。大部分招聘网站用的是第一种URL 里能看到page1、page2这样的规律。你只需要写一个循环把 page 从 1 递增到目标页数就行。但这里有个坑有些网站的分页参数不是 page 而是 start 或 offset而且步长不一定是 1。比如每页 30 条第二页的 start 就是 30第三页是 60。你得先手动翻两页看看 URL 变化规律。请求频率控制是必须做的不然轻则被封 IP重则整个 IP 段被拉黑。我一般会在每次请求之间加一个随机延时范围设在 1 到 3 秒之间。不要用固定延时固定延时反而容易被识别出是脚本行为。import time import random for page in range(1, 11): url fhttps://example-recruit-site.com/jobs?keywordpythoncity北京page{page} try: resp requests.get(url, headersheaders, timeout10) resp.encoding utf-8 # 解析逻辑同上 print(f第 {page} 页抓取完成) except requests.RequestException as e: print(f第 {page} 页请求失败: {e}) time.sleep(random.uniform(1.5, 3.5))random.uniform(1.5, 3.5) 生成的是浮点数比固定 sleep(2) 更自然。try-except 包裹的是整个请求和解析过程任何一页失败不会影响后续页面。如果你发现连续多页返回空列表或者状态码变成 403说明已经被限流了这时候应该停下来换个时间再跑或者降低频率。3. 数据解析与清洗把非结构化文本变成能画图的表3.1 详情页字段提取与薪资解析列表页能拿到的字段有限通常只有职位名、公司名、薪资、城市、经验、学历这几项。如果你想做更丰富的可视化比如分析岗位职责里的技能关键词就需要进详情页。详情页的 URL 一般在列表页的职位标题 a 标签的 href 属性里。进详情页后重点提取职位描述、公司规模、融资阶段、福利标签这些字段。薪资解析是清洗环节里最麻烦的一步。招聘网站上的薪资写法五花八门“15-25K”、“15-25K·13薪”、“200-300元/天”、“面议”、“8千-1.2万”。你需要写一个解析函数把这些统一转换成月薪范围单位元。import re def parse_salary(salary_str): if not salary_str or 面议 in salary_str: return None, None # 处理 15-25K·13薪 这种格式 salary_str salary_str.split(·)[0] # 处理 8千-1.2万 salary_str salary_str.replace(千, 000).replace(万, 0000) # 提取数字 nums re.findall(r[\d.], salary_str) if len(nums) 2: low, high float(nums[0]), float(nums[1]) # 如果是 K 为单位 if K in salary_str.upper(): return int(low * 1000), int(high * 1000) return int(low), int(high) return None, None # 测试 print(parse_salary(15-25K·13薪)) # (15000, 25000) print(parse_salary(8千-1.2万)) # (8000, 12000) print(parse_salary(面议)) # (None, None)这个函数的逻辑是先去掉“·13薪”这种后缀再把“千”和“万”统一替换成数字零然后用正则提取所有数字。注意“1.2万”替换后变成“1.20000”正则提取出来是 1.2 和 0000 两个数所以要先判断单位再乘系数。实际跑的时候你会发现还有“15K-25K”这种写法两个数字后面都带 K这时候正则提取出来还是两个数但单位判断要改成检查第一个数字后面有没有 K。这种边界情况需要你根据实际抓到的数据不断补充。3.2 城市与经验字段的标准化城市字段常见的问题是带后缀比如“北京·朝阳区”、“上海·浦东新区”。做城市维度的聚合时你需要把“·”后面的部分去掉只保留城市名。经验字段的问题更大“应届生”、“1年以内”、“1-3年”、“3-5年”、“5-10年”、“10年以上”这些是标准写法但有些网站会写成“经验不限”、“无经验”、“1年以上”。你需要定义一个映射表把它们归一到几个固定档位。def clean_city(city_str): if not city_str: return 未知 return city_str.split(·)[0].strip() def clean_experience(exp_str): if not exp_str: return 未知 exp_str exp_str.strip() mapping { 应届生: 应届/1年以内, 1年以内: 应届/1年以内, 无经验: 应届/1年以内, 1-3年: 1-3年, 3-5年: 3-5年, 5-10年: 5-10年, 10年以上: 10年以上, 经验不限: 不限, } for key, val in mapping.items(): if key in exp_str: return val return 其他映射表的好处是后续画图时类别不会太碎。如果你直接把原始字段丢进 pyecharts 的饼图可能会出现十几个扇区每个扇区占比不到 3%图看起来就很乱。归一成 6 个档位之后饼图或柱状图的可读性会好很多。城市字段同理去掉区域后缀后北京、上海、深圳、广州、杭州这几个城市的岗位数量对比会非常清晰。3.3 数据存储与导出清洗完的数据我一般会存两份一份 CSV 用于后续分析一份 Excel 用于答辩展示。CSV 的好处是轻量、通用pandas 读写都快。Excel 的好处是你可以手动调整列宽、加筛选器答辩时老师翻起来方便。import pandas as pd # 假设 all_jobs 是前面抓取和清洗后的列表每个元素是一个字典 df pd.DataFrame(all_jobs) # 去重按职位链接或职位 ID df.drop_duplicates(subset[job_link], inplaceTrue) # 导出 CSV df.to_csv(jobs_clean.csv, indexFalse, encodingutf-8-sig) # 导出 Excel df.to_excel(jobs_clean.xlsx, indexFalse, engineopenpyxl) print(f共导出 {len(df)} 条记录) print(df[[title, company, city, salary_low, salary_high, experience]].head())encodingutf-8-sig 是为了让 Excel 直接打开 CSV 时不乱码带 BOM 头的 UTF-8 编码 Excel 能正确识别。如果你用 utf-8 不带 sigExcel 打开中文会变成乱码这是很多人踩过的坑。drop_duplicates 按 job_link 去重是因为翻页过程中可能会有重复职位不去重的话统计结果会偏大。4. 可视化实现用 pyecharts 把数据变成能讲的图4.1 城市岗位数量对比图城市维度的对比最适合用柱状图或地图。柱状图简单直接地图视觉冲击力强但需要额外的地图文件支持。课程设计答辩的场景下我建议两个都做柱状图用来讲具体数字地图用来撑场面。from pyecharts.charts import Bar from pyecharts import options as opts city_counts df[city].value_counts().head(10) bar ( Bar() .add_xaxis(city_counts.index.tolist()) .add_yaxis(岗位数量, city_counts.values.tolist()) .set_global_opts( title_optsopts.TitleOpts(titlePython 岗位城市分布 Top10), xaxis_optsopts.AxisOpts(name城市, axislabel_optsopts.LabelOpts(rotate30)), yaxis_optsopts.AxisOpts(name岗位数量), ) ) bar.render(city_bar.html)add_xaxis 接收城市名列表add_yaxis 接收对应的数量列表。rotate30 是让 x 轴标签倾斜 30 度防止城市名太长挤在一起。render 生成的是 HTML 文件用浏览器打开就能看到交互式图表鼠标悬停会显示具体数值。如果你想让图表直接嵌入 Jupyter Notebook把 render 换成 render_notebook() 就行。4.2 薪资分布与经验要求交叉分析薪资分布用直方图或者箱线图都行但更有讲头的是把薪资和经验要求做成交叉表然后画分组柱状图。这样你能看出“3-5年经验的岗位薪资中位数比1-3年高多少”答辩时这是一个很好的分析点。# 计算每个经验档位的薪资中位数 exp_salary df.groupby(experience)[salary_mid].median().sort_values() from pyecharts.charts import Bar bar2 ( Bar() .add_xaxis(exp_salary.index.tolist()) .add_yaxis(薪资中位数元, exp_salary.values.astype(int).tolist()) .set_global_opts( title_optsopts.TitleOpts(title不同经验要求的薪资中位数), xaxis_optsopts.AxisOpts(name经验要求), yaxis_optsopts.AxisOpts(name薪资中位数元), ) ) bar2.render(salary_by_exp.html)这里用到了 salary_mid 字段它是 salary_low 和 salary_high 的均值。在清洗阶段就要算好这一列不然到可视化阶段再算会多一步。groupby 之后取 median 而不是 mean是因为薪资数据里可能有极端值中位数比平均数更稳健。sort_values 让柱子按薪资从低到高排列视觉上更有递进感。4.3 技能关键词词云如果你抓了详情页的职位描述可以做一个技能关键词的词云。这一步需要先分词中文分词用 jieba然后统计词频最后用 pyecharts 的 WordCloud 出图。import jieba from collections import Counter from pyecharts.charts import WordCloud # 假设 df[description] 是职位描述文本 all_text .join(df[description].dropna().tolist()) words jieba.lcut(all_text) # 过滤停用词和短词 stopwords {的, 和, 与, 或, 等, 有, 在, 对, 为, 了, 是, 我, 你, 他} filtered [w for w in words if len(w) 1 and w not in stopwords] word_counts Counter(filtered).most_common(50) wc ( WordCloud() .add(, word_counts, word_size_range[12, 60]) .set_global_opts(title_optsopts.TitleOpts(title岗位技能关键词词云)) ) wc.render(skill_wordcloud.html)jieba.lcut 返回的是列表Counter 统计词频后 most_common(50) 取前 50 个。word_size_range 控制词云中字体大小的范围最小值 12 保证小词也能看清最大值 60 防止某个词太大把其他词挤没了。停用词表需要根据实际结果调整比如“负责”、“要求”、“熟悉”这些词在职位描述里出现频率很高但对分析技能没有意义应该加进停用词表。5. 避坑与常见问题排查那些让你卡半天的坑5.1 请求返回 403 或验证码页面现象脚本跑了几页之后resp.status_code 变成 403或者返回的 HTML 里出现了验证码相关的元素。原因目标站点识别出了你的请求是脚本发出的。常见触发因素包括请求头缺少 Referer、User-Agent 是 python-requests 默认值、请求频率过高、同一个 IP 短时间内请求次数太多。解决首先检查 headers 里有没有带 User-Agent 和 Referer。Referer 设成目标站点的首页地址就行。其次把请求间隔拉长到 3 到 5 秒并且用 random.uniform 做随机化。如果还是被封可以尝试轮换 User-Agent准备几个常见浏览器的 UA 字符串每次请求随机选一个。最后如果目标站点对 IP 限制很严可以考虑在本地跑的时候控制总请求量比如只抓前 10 页不要一次性抓几百页。5.2 中文乱码现象打印出来的职位名称和公司名全是乱码类似“招蘔这种。原因requests 自动推断的编码不对。很多招聘网站的响应头里不带 charsetrequests 会默认用 ISO-8859-1 解码。解决在 resp.text 之前手动设置 resp.encoding utf-8。如果设了 utf-8 还是乱码试试 gbk 或 gb2312。判断方法是用 resp.apparent_encoding 看一下 requests 推测的编码是什么但不要直接用它因为它有时候也会猜错。最稳妥的方式是打开浏览器开发者工具看 Response Headers 里的 Content-Type 字段有没有 charset。5.3 选择器返回空列表现象soup.select(div.job-card) 返回空列表但浏览器里明明能看到这些元素。原因可能是页面内容是 JavaScript 动态渲染的requests 拿到的 HTML 里没有这些元素。也可能是 class 名写错了或者页面结构和你看到的不一样。解决先在浏览器里右键查看网页源代码不是检查元素搜索一下你要找的 class 名在不在源代码里。如果不在说明是 JS 渲染的requests 搞不定需要考虑用 playwright 或 selenium。如果在那就是选择器写错了检查一下有没有多余的空格、大小写是否匹配、是不是用了错误的 CSS 选择器语法。另外注意有些网站会对未登录用户隐藏部分内容你看到的页面和脚本拿到的不一样。5.4 薪资解析结果异常现象parse_salary 返回的数值明显不对比如“15-25K”解析出来是 15000 和 25000 没问题但“1.5-2万”解析出来变成了 1.5 和 20000。原因替换逻辑有漏洞。“1.5-2万”里的“万”只出现在第二个数字后面替换后变成“1.5-20000”正则提取出来是 1.5 和 20000第一个数字没有乘 10000。解决不要用简单的字符串替换改成先判断单位再分别处理。更稳妥的做法是用正则匹配“数字单位”的组合然后逐个转换。比如用re.findall(r(\d\.?\d*)([Kk万千])?, salary_str)把数字和单位一起提取出来再根据单位做乘法。这个正则能处理“15K-25K”、“1.5万-2万”、“8千-1.2万”这些常见格式。5.5 pyecharts 图表不显示或空白现象render 生成的 HTML 用浏览器打开后一片空白或者只有标题没有图。原因最常见的是数据为空。如果 df 是空的value_counts() 返回空 Seriesadd_xaxis 和 add_yaxis 接收到空列表图表自然没内容。另一个原因是 pyecharts 版本不对1.x 和 0.5.x 的 API 差异很大网上很多代码是旧版的。解决在 render 之前先 print 一下数据确认 city_counts 和 exp_salary 不是空的。如果是空的往回查爬取和清洗环节哪里出了问题。版本问题用 pip show pyecharts 看一下版本号确保是 1.x。如果是从网上抄的代码注意看它的 import 语句1.x 是from pyecharts.charts import Bar0.5.x 是from pyecharts import Bar两者不兼容。6. 进阶技巧把静态图表变成可交互的数据看板前面生成的图表都是独立的 HTML 文件答辩时你得一个个打开切换起来很麻烦。更好的做法是用 pyecharts 的 Page 组件把所有图表拼成一个页面或者用 Grid 做多图联动。Page 的用法很简单创建一个 Page 对象把各个图表 add 进去最后 render 成一个 HTML。from pyecharts.charts import Page page Page(layoutPage.DraggablePageLayout) page.add(bar, bar2, wc) page.render(dashboard.html)layoutPage.DraggablePageLayout 允许你在浏览器里拖动图表位置调整好之后点击左上角的“Save Config”按钮会下载一个 JSON 文件。然后你可以用这个 JSON 文件重新渲染固定布局。Page.save_resize_html(dashboard.html, cfg_filechart_config.json, destdashboard_final.html)这样生成的 dashboard_final.html 就是你调整好布局的最终版本所有图表在一个页面里答辩时直接全屏展示效果比一个个打开 HTML 好得多。另一个进阶方向是把数据存到 SQLite 数据库里然后用 Flask 或 FastAPI 做一个简单的 Web 接口前端用 ECharts 从接口拉数据渲染。这样做的好处是数据可以增量更新不用每次重新爬。但课程设计的场景下除非老师明确要求 Web 展示否则 pyecharts 的静态 HTML 已经足够用了。我一般会建议先跑通爬取和清洗把数据存成 CSV可视化用 pyecharts 快速出图等这些都没问题了再考虑上 Web 框架。还有一个容易被忽略的点是数据量。如果你只抓了 100 条数据画出来的图其实没什么分析价值城市分布可能就三四个城市薪资分布也很集中。建议至少抓 500 条以上最好能覆盖多个关键词比如 python、java、前端各抓一些这样交叉分析的时候才有东西可讲。但也不要贪多抓几千条的话请求频率控制不好很容易被封而且清洗和调试的时间会成倍增加。我自己的习惯是每个关键词抓 10 页每页 30 条三个关键词加起来 900 条左右足够撑起一个课程设计的分析量了。从那以后我每次做这类项目都会先把目标页面的 DOM 结构用开发者工具完整过一遍把选择器写在注释里再开始写代码。这样后面即使页面改版我也能快速定位到需要修改的地方。希望帮到你。本文还有配套的精品资源点击获取