
简介这份资源是面向高校学生与Python初学者的招聘网站数据采集与可视化课程设计项目可作为毕业设计、期末大作业或技能实训的完整参考方案。项目围绕招聘信息爬取、数据清洗与多维度图表展示展开涵盖词云、箱型图、词频统计等典型分析场景代码注释详尽新手也能读懂并快速部署。压缩包共12个文件约996KB包含3个Python脚本、5张jpg与3张png图表、1份docx说明文档脚本负责爬取与绘图图片呈现技能词云、工作经验与工资箱型图、地区职位数量分布等结果文档则补充项目说明。目前已有236人学习下载具备一定参考热度。读者可获得可直接运行的源码、数据库脚本与完整目录结构既能用于课程设计答辩也能作为数据分析入门练手素材帮助理解从数据获取到可视化呈现的完整链路。1. 招聘数据爬取与可视化课程设计怎么做出能拿高分的工程感很多同学做课程设计第一反应是找个现成模板改改界面结果答辩时被老师问一句「数据从哪来的、字段怎么对齐的」就卡住了。招聘网站爬取加可视化这个题目之所以容易拿高分恰恰因为它能串起一条完整链路真实数据采集、字段清洗、结构化存储、图表呈现。老师想看的不是你会不会调matplotlib而是你能不能把一份脏数据变成一张能讲出结论的图。这篇笔记按一线做数据项目的思路拆一遍从环境准备、爬取策略、反爬应对、数据落库到可视化大屏每一步都给可复现的命令和参数。适合正在做课程设计、想做出工程感而不是玩具感的同学也适合刚入门 Python 爬虫想找一个完整练手项目的人。核心词 Python 爬取、可视化、课程设计会贯穿始终但重点永远落在「怎么做、参数怎么设、坑在哪」。2. 动手前先把技术选型和数据源想清楚2.1 为什么招聘数据适合做课程设计招聘网站的数据结构天然规整岗位名称、公司、薪资、城市、学历要求、经验要求、技能标签几乎每个岗位都带这些字段。这意味着你爬下来的数据可以直接进表格不需要做太复杂的非结构化解析省下来的时间可以花在可视化和分析上。相比之下爬电商评论要处理大量自然语言爬视频弹幕要处理时间轴对齐对课程设计的周期来说性价比不高。另一个优势是招聘数据有天然的「可讲故事」维度。你可以按城市看岗位分布按薪资区间看行业热度按技能标签看技术栈趋势。这些结论用 ECharts 或 Pyecharts 一画就很有说服力答辩时老师一眼能看懂。我一般建议学生优先选「岗位 薪资 城市 技能」这四个维度因为它们既能体现爬虫的字段提取能力又能撑起一张完整的可视化大屏。选数据源时要注意一点不同招聘网站的页面结构差异很大。有的用服务端渲染HTML 里直接有数据有的用前端框架异步加载需要抓接口有的对未登录用户隐藏部分字段。课程设计阶段建议先选一个结构相对简单、对爬虫容忍度较高的站点做通再考虑扩展。不要一上来就同时爬三四个站点字段对齐和反爬策略会把你的时间吃光。2.2 环境准备与依赖安装先把环境搭好避免后面写到一半发现库版本冲突。推荐 Python 3.9 以上用虚拟环境隔离依赖。下面这套命令在 Windows 和 macOS 上都能跑Linux 把激活命令换成source venv/bin/activate即可。# 创建虚拟环境 python -m venv venv # Windows 激活 venv\Scripts\activate # macOS / Linux 激活 source venv/bin/activate # 安装核心依赖 pip install requests beautifulsoup4 lxml pandas openpyxl pip install pyecharts jieba pip install playwright playwright install chromium这里解释一下每个库的角色。requests负责发 HTTP 请求beautifulsoup4加lxml负责解析 HTMLpandas负责数据清洗和落库openpyxl让 pandas 能写 Excelpyecharts负责生成可视化图表jieba用于技能标签的中文分词。playwright是备选方案当目标站点是异步渲染、requests 拿不到数据时用它驱动浏览器。提示playwright install chromium会下载一个 Chromium 浏览器大约一百多兆网络慢的话多等一会。如果公司或学校网络有限制可以先跳过 playwright用 requests 方案跑通再说。参数上有一个容易忽略的点lxml解析器比 Python 自带的html.parser快很多但安装时依赖 C 库。如果pip install lxml报错先升级 pip 再试或者退而用html.parser只是解析速度会慢一些。课程设计数据量不大慢一点也能接受。2.3 目标页面结构分析方法动手写代码前先花十分钟分析页面。打开目标招聘网站的搜索结果页按 F12 打开开发者工具切到 Network 面板刷新页面看第一个文档请求返回的 HTML 里有没有岗位列表。如果有说明是服务端渲染直接用 requests 抓如果没有看 XHR 或 Fetch 里有没有返回 JSON 的接口。判断字段位置时用 Elements 面板的选取工具点一下岗位标题看它在 DOM 树里的层级。常见结构是div.job-list div.job-item a.job-name但每个站点不一样。把关键字段的选择器记下来后面写解析代码时直接用。这一步偷懒后面调试选择器会花更多时间。还要注意分页规则。有的站点用?page2有的用?start25有的把分页信息放在接口参数里。先手动翻两页观察 URL 变化规律把规律写进代码的循环里。如果分页是异步加载的就要在 Network 里找到对应的接口请求看它的参数和返回结构。3. 爬取层实现从单页请求到批量入库3.1 用 requests 抓取列表页并解析字段先写一个最小可用的抓取函数把单页数据拿到手。下面这段代码以服务端渲染的列表页为例字段包括岗位名、公司、薪资、城市、学历和经验。import requests from bs4 import BeautifulSoup 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-Language: zh-CN,zh;q0.9, } def fetch_page(url): 抓取单页 HTML带简单重试 for attempt in range(3): try: resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding return resp.text except requests.RequestException as e: print(f第 {attempt 1} 次请求失败: {e}) time.sleep(random.uniform(1, 3)) return None def parse_list(html): 解析岗位列表返回字典列表 soup BeautifulSoup(html, lxml) jobs [] for item in soup.select(div.job-item): job { title: item.select_one(a.job-name).get_text(stripTrue), company: item.select_one(div.company-name).get_text(stripTrue), salary: item.select_one(span.salary).get_text(stripTrue), city: item.select_one(span.city).get_text(stripTrue), edu: item.select_one(span.edu).get_text(stripTrue), exp: item.select_one(span.exp).get_text(stripTrue), } jobs.append(job) return jobs逻辑说明fetch_page做了三件事——带请求头、设超时、失败重试。请求头里的 User-Agent 是必须的很多站点会拦截空 UA 的请求。resp.apparent_encoding让 requests 自动推断编码避免中文乱码。parse_list用 CSS 选择器逐字段提取get_text(stripTrue)去掉首尾空白。参数说明timeout10是单次请求超时网络慢可以调到 15。重试次数 3 次是经验值再多会拖慢整体速度。random.uniform(1, 3)是请求间隔后面讲反爬时会展开。选择器div.job-item这类写法要按你实际分析的页面结构替换不要照抄。3.2 分页循环与请求频率控制单页跑通后把它套进分页循环。关键是控制请求频率既不要快到被封也不要慢到跑不完。def crawl_all(base_url, max_pages20): 按页循环抓取返回全部岗位 all_jobs [] for page in range(1, max_pages 1): url f{base_url}page{page} html fetch_page(url) if not html: print(f第 {page} 页抓取失败跳过) continue jobs parse_list(html) if not jobs: print(f第 {page} 页无数据可能已到末页) break all_jobs.extend(jobs) print(f第 {page} 页抓到 {len(jobs)} 条累计 {len(all_jobs)} 条) time.sleep(random.uniform(2, 5)) return all_jobs逻辑说明循环从第 1 页到max_pages每页抓完 sleep 2 到 5 秒。如果某页返回空列表说明已经翻到末页直接 break避免无意义请求。累计计数打印出来方便你判断进度。参数说明max_pages20是课程设计的合理上限按每页 30 条算能拿到 600 条左右足够做可视化。random.uniform(2, 5)的间隔比单页时更长因为连续翻页更容易触发风控。如果你发现抓了十几页后开始返回空数据或验证码把间隔调到 5 到 8 秒或者加一个更长的随机停顿。注意不要用固定间隔比如每次都 sleep 3 秒。固定间隔的请求特征很明显随机间隔更接近真人操作。这是血泪经验固定间隔跑到几百条后被封的概率明显更高。3.3 数据落库与字段清洗抓下来的数据先落成 CSV 或 Excel方便后面 pandas 处理。但原始数据往往有脏字段比如薪资写成「15-25K·13薪」城市写成「上海·浦东新区」需要拆开。import pandas as pd import re def clean_salary(s): 把 15-25K·13薪 拆成最低、最高、月数 if not s or 面议 in s: return None, None, None match re.search(r(\d)-(\d)K, s) if not match: return None, None, None low, high int(match.group(1)), int(match.group(2)) months 12 m re.search(r·(\d)薪, s) if m: months int(m.group(1)) return low, high, months def clean_city(s): 把 上海·浦东新区 拆成城市和区域 if not s: return None, None parts s.split(·) return parts[0], parts[1] if len(parts) 1 else def build_dataframe(jobs): df pd.DataFrame(jobs) df[[salary_low, salary_high, salary_months]] df[salary].apply( lambda x: pd.Series(clean_salary(x)) ) df[[city_main, city_district]] df[city].apply( lambda x: pd.Series(clean_city(x)) ) df df.dropna(subset[salary_low]) df[salary_avg] (df[salary_low] df[salary_high]) / 2 return df if __name__ __main__: jobs crawl_all(https://example.com/jobs?keywordpython, max_pages20) df build_dataframe(jobs) df.to_csv(jobs_raw.csv, indexFalse, encodingutf-8-sig) print(f共保存 {len(df)} 条有效数据)逻辑说明clean_salary用正则提取薪资区间和薪数面议的直接返回空值。clean_city按「·」拆分城市和区域。build_dataframe把清洗结果拼回原表丢掉薪资缺失的行再算一个平均薪资字段。最后用utf-8-sig编码写 CSV这样 Excel 打开不会乱码。参数说明正则(\d)-(\d)K只匹配「数字-数字K」格式如果你的目标站点薪资写法不同比如「15k-25k」或「15-25千」要相应调整。dropna会丢掉面议岗位如果这类岗位占比高可以考虑单独统计而不是直接丢。4. 反爬应对与异步渲染页面的兜底方案4.1 常见反爬手段与应对策略招聘网站常见的反爬手段有几种请求头校验、IP 频率限制、验证码、字段混淆。请求头校验最容易解决把 User-Agent、Referer、Accept-Language 补全就行。IP 频率限制靠控制请求间隔和并发数课程设计阶段单线程加随机 sleep 基本够用。验证码比较麻烦遇到就降速或换时间段再跑。字段混淆是指页面里用自定义字体或图片替换数字这种在招聘网站上不常见但电商价格里很多。我一般会先跑 20 页测试观察是否出现空数据、验证码页或 403。如果 20 页内稳定再放大到 50 页。如果中途被封先停半小时把间隔调大再从小页数重新测。不要被封了还硬跑只会让封禁时间更长。提示有些站点对未登录用户只返回前几页数据后面要求登录。这种情况不要尝试绕过登录课程设计换个数据源或者只取前几页即可。合规采集比多拿几百条数据重要。4.2 用 Playwright 处理异步加载页面如果目标页面的岗位列表是前端异步渲染的requests 拿到的 HTML 里没有数据这时候用 Playwright 驱动浏览器。下面是一个最小示例。from playwright.sync_api import sync_playwright import time def fetch_with_playwright(url): 用无头浏览器抓取异步渲染页面 with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() page.set_extra_http_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 }) page.goto(url, wait_untilnetworkidle, timeout30000) page.wait_for_selector(div.job-item, timeout10000) html page.content() browser.close() return html逻辑说明headlessTrue表示不弹出浏览器窗口适合后台跑。wait_untilnetworkidle等网络请求基本停止后再取内容wait_for_selector确保岗位列表已经渲染出来。page.content()拿到渲染后的完整 HTML后面可以复用前面的parse_list解析。参数说明timeout30000是页面加载超时异步页面加载慢可以调到 60000。wait_for_selector的选择器要和实际页面一致超时设 10 秒如果 10 秒还没出现说明页面结构变了或加载失败。Playwright 比 requests 慢很多每页大概多花 2 到 3 秒所以只在必要时用。4.3 抓取失败时的排查顺序抓取过程中遇到问题按这个顺序排查先看返回的 HTML 里有没有数据没有就检查是不是异步渲染再看状态码403 说明被拦截需要补请求头或降速然后看选择器用浏览器的 Elements 面板重新确认最后看编码中文乱码就手动设resp.encoding。这个顺序能覆盖大部分问题比盲目改代码高效。如果 Playwright 也拿不到数据可能是页面需要登录或有更复杂的风控。课程设计阶段建议换一个数据源不要在单一站点上死磕。把时间花在数据分析和可视化上比跟反爬斗智斗勇更划算。5. 可视化落地从字段到图表的完整链路5.1 用 Pyecharts 做城市岗位分布图数据清洗完先做最直观的城市分布。用 Pyecharts 的地图或柱状图都行课程设计推荐柱状图因为城市数量多时地图上的标签会挤在一起。from pyecharts.charts import Bar from pyecharts import options as opts import pandas as pd df pd.read_csv(jobs_raw.csv) city_count df[city_main].value_counts().head(10) bar ( Bar() .add_xaxis(city_count.index.tolist()) .add_yaxis(岗位数量, city_count.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_distribution.html)逻辑说明value_counts()统计每个城市的岗位数head(10)取前 10 名避免图表太挤。rotate30让 x 轴标签倾斜防止城市名重叠。render输出一个 HTML 文件浏览器打开就能看也可以嵌入到你的课程设计报告里。参数说明head(10)的数字按你的数据量调整城市多的话取 15 也行。rotate角度 30 到 45 之间比较合适太小还是会重叠太大不好读。5.2 薪资分布与技能标签词云薪资分布用直方图技能标签用词云。薪资字段前面已经算过salary_avg直接画分布。技能标签需要从岗位描述或标题里用 jieba 分词提取。from pyecharts.charts import Histogram, WordCloud import jieba # 薪资分布 salary_data df[salary_avg].dropna().tolist() hist ( Histogram() .add_xaxis([str(i) for i in range(0, 60, 5)]) .add_yaxis(岗位数, [ sum(1 for s in salary_data if i s i 5) for i in range(0, 60, 5) ]) .set_global_opts(title_optsopts.TitleOpts(title薪资分布K/月)) ) hist.render(salary_dist.html) # 技能词云 text .join(df[title].dropna().tolist()) words jieba.lcut(text) stopwords {Python, 工程师, 开发, 岗位, 招聘} filtered [w for w in words if len(w) 1 and w not in stopwords] word_freq pd.Series(filtered).value_counts().head(50) wc ( WordCloud() .add(, list(zip(word_freq.index.tolist(), word_freq.values.tolist()))) .set_global_opts(title_optsopts.TitleOpts(title技能关键词)) ) wc.render(skill_wordcloud.html)逻辑说明薪资直方图按 5K 一档统计range(0, 60, 5)生成 0、5、10 到 55 的区间。词云部分用 jieba 分词过滤掉单字和常见停用词取前 50 个高频词。zip把词和频次配对传给 WordCloud。参数说明薪资区间上限 60K 是按常见岗位设的如果你的数据里有更高薪资把 range 上限调大。停用词表要按你的数据补充比如「高级」「资深」这类词如果没意义就加进去。词云取前 50 个词是经验值太多会看不清。5.3 把多个图表拼成可视化大屏课程设计如果只交几个独立 HTML观感一般。可以用 Pyecharts 的Page或Grid把图表拼成一页再配一个简单的 HTML 布局就有大屏的感觉了。from pyecharts.charts import Page page Page(layoutPage.DraggablePageLayout) page.add(bar, hist, wc) page.render(dashboard.html)逻辑说明Page把多个图表对象放进一个页面DraggablePageLayout允许你在浏览器里拖动调整位置调好后保存布局。如果不想拖动用默认布局也行。参数说明DraggablePageLayout适合调试阶段定稿后可以换成SimplePageLayout让布局固定。大屏的配色和字体可以在每个图表的set_global_opts里统一设置保持风格一致。6. 避坑与常见问题排查6.1 中文乱码与编码问题现象CSV 用 Excel 打开是乱码或者图表里的中文显示成方块。原因requests 默认编码推断不准或者写文件时没指定编码。解决抓取时用resp.apparent_encoding写 CSV 时用encodingutf-8-sig。图表中文方块是字体问题Pyecharts 生成的 HTML 一般没这个问题如果嵌入到其他框架里检查字体配置。6.2 选择器失效导致抓到空数据现象代码跑通但jobs列表是空的或者某些字段是 None。原因页面结构变了或者选择器写错了。解决用浏览器的 Elements 面板重新确认选择器注意 class 名有没有动态后缀。有些站点会给 class 加随机字符串这种情况改用相对结构或属性选择器。6.3 请求被封或返回验证码现象跑了几十页后开始返回 403、空页面或验证码页。原因请求频率过高或者请求头特征太明显。解决把 sleep 间隔调大补全 Referer 和 Accept 头降低并发。如果已经被封停一段时间再跑不要连续重试。6.4 薪资字段格式不统一导致清洗失败现象clean_salary返回大量 None有效数据变少。原因薪资写法多样正则没覆盖全。解决先把所有薪资值打印出来看一遍统计有几种格式再逐个写正则。常见格式有「15-25K」「15k-25k」「15-25千」「面议」「200-300元/天」日薪和月薪要分开处理。6.5 可视化图表数据量太大导致卡顿现象词云或柱状图渲染很慢浏览器卡死。原因数据点太多或者词云词数过多。解决柱状图取 Top10 到 Top15词云取前 50 个词薪资直方图分档不要超过 15 档。课程设计的图表要的是清晰不是数据全。7. 让课程设计加分的一个技巧把爬取过程做成可复现的配置最后分享一个我做数据项目时养成的习惯把所有可变参数抽到一个配置文件里而不是散落在代码各处。这个习惯在课程设计答辩时特别加分因为老师能一眼看出你考虑了可维护性。# config.py CONFIG { base_url: https://example.com/jobs?keywordpython, max_pages: 20, sleep_range: (2, 5), timeout: 10, retry: 3, output_csv: jobs_raw.csv, top_cities: 10, salary_bins: list(range(0, 60, 5)), wordcloud_top: 50, }然后在爬取和可视化代码里from config import CONFIG所有参数从字典里取。这样做的好处是换数据源只改base_url调频率只改sleep_range答辩演示时想跑快一点就把 sleep 调小想跑稳一点就调大。老师问「你这个参数怎么定的」你可以直接说「抽成配置了方便对比不同设置的效果」这比说「我随便设的」强太多。验证方法也很简单把max_pages改成 2 跑一遍确认流程通再改成 20 跑完整版对比两次的数据量和耗时。如果两次结果字段一致、只是数量不同说明你的流程是稳定的。这个对比过程可以写进课程设计报告作为「系统可配置性验证」一节。我自己的习惯是每个项目都留一个config.py哪怕是小脚本。踩过的坑是早期把参数写死在代码里换一个数据源要改十几处改漏一处就报错。后来统一抽出来改一处全生效省下的时间够多跑几轮实验。希望这个习惯也能帮到你。本文还有配套的精品资源点击获取