☰
招聘数据爬取与可视化课程设计:从爬虫到ECharts大屏的工程化实战
2026/10/29 7:48:23 网站建设 项目流程

简介:这份资源是面向高校学生与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负责解析 HTML,pandas负责数据清洗和落库,openpyxl让 pandas 能写 Excel,pyecharts负责生成可视化图表,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,但每个站点不一样。把关键字段的选择器记下来,后面写解析代码时直接用。这一步偷懒,后面调试选择器会花更多时间。

还要注意分页规则。有的站点用?page=2,有的用?start=25,有的把分页信息放在接口参数里。先手动翻两页,观察 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;q=0.9", } def fetch_page(url): """抓取单页 HTML,带简单重试""" for attempt in range(3): try: resp = requests.get(url, headers=HEADERS, timeout=10) 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(strip=True), "company": item.select_one("div.company-name").get_text(strip=True), "salary": item.select_one("span.salary").get_text(strip=True), "city": item.select_one("span.city").get_text(strip=True), "edu": item.select_one("span.edu").get_text(strip=True), "exp": item.select_one("span.exp").get_text(strip=True), } jobs.append(job) return jobs

逻辑说明:fetch_page做了三件事——带请求头、设超时、失败重试。请求头里的 User-Agent 是必须的,很多站点会拦截空 UA 的请求。resp.apparent_encoding让 requests 自动推断编码,避免中文乱码。parse_list用 CSS 选择器逐字段提取,get_text(strip=True)去掉首尾空白。

参数说明:timeout=10是单次请求超时,网络慢可以调到 15。重试次数 3 次是经验值,再多会拖慢整体速度。random.uniform(1, 3)是请求间隔,后面讲反爬时会展开。选择器div.job-item这类写法要按你实际分析的页面结构替换,不要照抄。

3.2 分页循环与请求频率控制

单页跑通后,把它套进分页循环。关键是控制请求频率,既不要快到被封,也不要慢到跑不完。

def crawl_all(base_url, max_pages=20): """按页循环抓取,返回全部岗位""" 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_pages=20是课程设计的合理上限,按每页 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?keyword=python", max_pages=20) df = build_dataframe(jobs) df.to_csv("jobs_raw.csv", index=False, encoding="utf-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(headless=True) 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_until="networkidle", timeout=30000) page.wait_for_selector("div.job-item", timeout=10000) html = page.content() browser.close() return html

逻辑说明:headless=True表示不弹出浏览器窗口,适合后台跑。wait_until="networkidle"等网络请求基本停止后再取内容,wait_for_selector确保岗位列表已经渲染出来。page.content()拿到渲染后的完整 HTML,后面可以复用前面的parse_list解析。

参数说明:timeout=30000是页面加载超时,异步页面加载慢可以调到 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_opts=opts.TitleOpts(title="Python 岗位城市分布 Top10"), xaxis_opts=opts.AxisOpts(name="城市", axislabel_opts=opts.LabelOpts(rotate=30)), yaxis_opts=opts.AxisOpts(name="岗位数"), ) ) bar.render("city_distribution.html")

逻辑说明:value_counts()统计每个城市的岗位数,head(10)取前 10 名避免图表太挤。rotate=30让 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_opts=opts.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_opts=opts.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(layout=Page.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 时用encoding="utf-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?keyword=python", "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,哪怕是小脚本。踩过的坑是早期把参数写死在代码里,换一个数据源要改十几处,改漏一处就报错。后来统一抽出来,改一处全生效,省下的时间够多跑几轮实验。希望这个习惯也能帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询