news 2026/10/3 3:36:38

Python招聘数据分析与可视化:从爬虫到看板的完整项目实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python招聘数据分析与可视化:从爬虫到看板的完整项目实践

简介:一份基于Python实现的北京市大数据岗位招聘数据分析与可视化展示项目,内含完整源代码、爬虫脚本及采集数据,覆盖网络爬虫、数据处理、分析与可视化全流程。项目来自个人毕业设计,答辩评审98分,代码经过调试测试,确保可运行,适合计算机、通信、人工智能、自动化等相关专业学生作为期末大作业、课程设计或毕业设计参考。资源包共110个文件,约6.64MB,以Python脚本、CSV/XLS/DB数据文件及HTML/CSS/JS可视化页面为主,配有地图、图表字体与图片素材,可支撑招聘数据看板展示。已有88人浏览学习。通过该项目可掌握招聘信息爬取、字段清洗、需求分析与可视化呈现的完整实现思路,理解北京大数据岗位的分布特征、技能要求与薪资水平,是提升数据获取、处理和解读能力的综合实战资源。

1. 北京市大数据岗位招聘数据项目:不用跑通全流程,你很难信一张图

做得再漂亮的图表,也救不了一锅没洗干净的招聘数据。这是我从这个基于Python实现的北京市大数据岗位招聘数据分析及可视化项目里得到的最直接结论——它把爬虫、数据清洗、可视化三件事完整串成一条流水线,产物包含一份北京市大数据方向岗位数据集、基于 requests 的 Python 爬虫脚本、清洗脚本和一张可本地直接打开的招聘数据分析看板。如果你正在准备大数据方向的岗位求职,想摸清北京市场对不同经验、不同学历的人开什么价;或者你在做大数据毕业设计,需要一套能讲清“数据从哪来、怎么变成表、表怎么变成图”的完整技术链条,这套项目的价值不在于直接抄源码,而在于跟着数据走一遍,知道每张图是从哪个字段、哪一步清洗里来的。

2. 招聘数据爬虫:用 requests + lxml 写一个能扛反爬的最小采集器

招聘网站是爬虫里比较难缠的一类目标,但它非常适合做数据分析项目的采集层,因为岗位列表天然是结构化卡片:标题、公司、薪资、经验、学历都摆在同一个 DOM 节点里,不需要像新闻爬虫那样处理长正文。常见的做法是先抓列表页拿到岗位链接,再进详情页补 JD 文本;也有只抓列表页的轻量版本。这个项目我建议做完整版,因为后面词云分析需要 JD 原文。

2.1 采集前先定三件事:字段口径、翻页规律、请求身份

先别写代码,把这三件事想清楚,爬虫写一半断在奇怪位置的概率会小很多。

第一是字段口径。你要分析的岗位画像,需要哪些列,每一列的取值长什么样,决定了解析规则怎么写。我做这类项目时一般先定这张表:

字段取值示例用途
job_title大数据开发工程师岗位筛选与词频
company北京某某科技有限公司公司维度分析
location北京-朝阳区区县归类
experience3-5年经验分组
education本科及以上学历归一化
salary_text1.5-2.5万/月薪酬解析原始输入
job_urlhttps://...去重与复核

第二是翻页规律。很多招聘网站的翻页不是 URL 里改页码,而是 POST 一个pn或pageNo参数,列表数据以 JSON 返回。先用浏览器开发者工具看一次真实的页面请求,确认是 GET 还是 POST、参数名是什么,比盲写十行代码有用得多。requests 直接抓 HTML 的方式,只对服务端渲染或者能伪造 Ajax 请求的站点有效。

第三是请求身份。只带 User-Agent 抓不了几页,必须把 Referer、Accept、Cookie 一起带上。没有 Cookie 往往只能拿到前两页,后面直接跳验证码。我一般用 requests.Session 把请求头固定住,让整个采集过程保持同一个会话,这样 Cookie 也自动维护,代码不会到处散落请求头。

2.2 列表页采集与翻页循环:requests 的三个核心参数

一个能用的最小列表页采集函数长这样。域名我用示例地址占位,实际上你只需要把 URL 和参数名替换成目标站点的真实结构,这个骨架不用大改。

import requests import time import random session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/122.0 Safari/537.36", "Referer": "https://www.example.com/jobs", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9," "image/avif,image/webp,*/*;q=0.8", }) def load_page(keyword: str, page: int) -> str: url = "https://www.example.com/jobs" params = { "query": keyword, "pn": page, "city": "北京" } resp = session.get(url, params=params, timeout=10) resp.raise_for_status() resp.encoding = resp.apparent_encoding or "utf-8" return resp.text

三个核心参数分别是timeout、params和headers。timeout=10是必须的,不设超时的话,某个连接假死会让整个采集脚本卡到天亮;resp.raise_for_status()让 HTTP 4xx、5xx 直接抛异常,方便定位反爬触发点;params把翻页参数交给 requests 做 URL 编码,比手拼字符串少踩很多编码坑。resp.encoding这行也有讲究:有些站点响应头没写明 charset,取apparent_encoding能减少中文乱码,它内部用 chardet 探测编码,速度慢一点,但列表页数据量小可以接受。

翻页循环里真正要控制的是节奏,不是循环本身。

all_pages = {} for page in range(1, 11): html = load_page("大数据", page) all_pages[page] = html time.sleep(random.uniform(1.5, 3.5))

间隔不要写死成固定 2 秒,random.uniform(1.5, 3.5)让请求间隔在一个区间内抖动,比规则间隔更难被风控识别。另外,前 10 页大概能拿到几百个岗位,做市级岗位分析已经够用,没必要一口气抓 50 页。招聘数据分析的核心是后续的清洗和可视化,不是数据量越大越好。

2.3 提取岗位字段与去重:XPath 比正则更适合解析卡片

列表页拿到手后,用 lxml 的 XPath 提取字段。招聘网站的 DOM 结构比较规整,卡片节点通常有统一的 class 前缀,写解析规则比写正则容易维护。

from lxml import etree def parse_page(html: str) -> list[dict]: tree = etree.HTML(html) cards = tree.xpath("//div[contains(@class, 'job-item')]") rows = [] for card in cards: title = card.xpath(".//span[contains(@class, 'job-name')]/text()") company = card.xpath(".//div[contains(@class, 'company')]/text()") salary = card.xpath(".//div[contains(@class, 'salary')]/text()") if not title: continue rows.append({ "job_title": title[0].strip(), "company": company[0].strip() if company else "", "salary_text": salary[0].strip() if salary else "", "job_url": card.xpath(".//a/@href")[0] }) return rows

XPath 返回的是列表,所以每个字段都要做空值判断:title[0].strip()之前先确认title不为空。job_url用来做去重主键,这是整个采集流程里最重要的字段——没有它,重复抓取会把同一条岗位写进 CSV 好几遍,后面做统计分析时出现神奇的双倍计数。如果你观察到列表页有“下一页”按钮在最后一页会消失,可以用这个特征来判断是否翻到了末尾。

2.4 落盘与断点续采:给爬虫加一剂后悔药

爬虫过夜跑是常态,网络抖动、IP 被临时限制都可能中断。我一开始图省事,把所有结果攒在内存里最后一次性写文件,结果凌晨三点脚本崩了,当天的数据全部白抓。后来改成每页解析完立即追加写入,同时用一个seen_urls.txt文件记住哪些岗位已经采过,重新运行时跳过它们。

import csv import os def append_rows(rows: list[dict], seen_path: str = "seen_urls.txt"): seen = set() if os.path.exists(seen_path): with open(seen_path, encoding="utf-8") as f: seen.update(f.read().splitlines()) with open("jobs.csv", "a", encoding="utf-8-sig", newline="") as f: writer = csv.DictWriter(f, fieldnames=list(rows[0].keys())) for row in rows: if row["job_url"] in seen: continue writer.writerow(row) seen.add(row["job_url"]) with open(seen_path, "w", encoding="utf-8") as f: f.write("\n".join(seen))

编码用utf-8-sig而不是utf-8,是专门留给后面 Excel 打开的。用 Excel 直接打开 UTF-8 的 CSV 会乱码,utf-8-sig带 BOM 头,Excel 能正确识别中文。这个细节很小,但项目给同事或导师看数据时会省很多解释成本。追加写入配合flush,崩溃时最多丢最后一条,不会丢掉全部进度。

提示:断点续采的seen_urls.txt只记 URL,不记完整数据。这样即使你后来改了字段结构,旧的去重记录仍然有效,不用重新采一遍。

3. 数据清洗与岗位画像:把采集结果变成可分析的二维表

爬虫落盘的 CSV 只能算原材料,里面充满了“1.5-2.5万/月”“本科及以上”“3-5年”这类给人看、不给机器算的文本。数据清洗在这类项目里的工作量占比远高于爬虫本身,大概六成时间要花在这一步。Python 数据分析的第一步,永远是先把字段变成统一的、可聚合的、有业务含义的数值或枚举。

3.1 薪酬字段解析:把“1.5-2.5万/月”拆成三个数值

薪酬文本是最典型的需要解析的字段,也是整个项目里正则最容易翻车的地方。常见写法有“1.5-2.5万/月”“15K-25K·14薪”“200-300/天”“面议”。第一版清洗脚本只写了最标准的正则,结果第二天数据里混进了“月薪2-4万·16薪”,解析结果全是 0,我花了半小时才发现是单位缩写没匹配上。

import re import pandas as pd pat = re.compile(r"(\d+\.?\d*)\s*[-~至]?\s*(\d+\.?\d*)\s*(万|k)?/?(月|年)?") def parse_salary(text): m = pat.search(str(text)) if not m: return None, None, None low, high = float(m.group(1)), float(m.group(2)) unit = (m.group(3) or "k").lower() period = m.group(4) or "月" if unit == "万": low, high = low * 10000, high * 10000 elif unit == "k": low, high = low * 1000, high * 1000 if period == "年": low, high = low / 12, high / 12 return low, high, (low + high) / 2 df["salary_min"], df["salary_max"], df["salary_median"] = zip( *df["salary_text"].map(parse_salary) )

正则里[-~至]?专门兼容“1.5-2.5”“1.5~2.5”“1.5至2.5”三种写法。单位只处理“万”和“k”,“/天”这类日薪字段我建议直接丢弃,或者单独标记为缺失,不要混进月薪分析里。salary_median是后面可视化的主力字段,它代表这条岗位的市场中间价,比只用最低值或最高值更稳。计算完成后我建议保留原始salary_text列不删除,遇到离群值可以随时回去核对,这是给自己留的后悔药。

3.2 学历、经验与公司规模的枚举归一化

学历字段的原始值通常是“本科及以上”“统招本科”“硕士/博士”“大专学历“。如果不归一化,pandas 的value_counts()会给你列出一二十种写法,图表根本没法看。

def norm_education(x): s = str(x) if "博士" in s: return "博士" if "硕士" in s or "研究生" in s: return "硕士" if "本科" in s: return "本科" if "大专" in s or "专科" in s: return "大专" return "不限" df["education_group"] = df["education"].map(norm_education)

这里判断“硕士”要在“本科”之前,因为“本科及以上”的文本里没有“硕士”字样,但“硕士学历”里也没有“本科”,顺序写反了会把硕士归成本科。经验字段同理,把“经验不限”映射为“不限”,把“1年以内”映射成“1年以下”,把“3-5年”拆成数值3,这样后面做薪酬与经验关系的箱线图时才有可排序的 x 轴。

归一化肯定丢信息,比如“统招本科”和“本科”在招聘方眼里可能有差别,但在这类宏观岗位分析项目里,保住可聚合性比保住细节更重要。

3.3 类型转换与缺失值处理:先记账再动手

字段归一化做完,要把该转数值的转数值。pandas 里最常见的问题是“看起来是数字、其实是字符串”的列,比如地区里的数字是区号,“3-5年”转成 3 之后要强制pd.to_numeric。

df["experience_year"] = df["experience"].str.extract(r"(\d+)").astype(float) df.loc[df["experience"].str.contains("不限"), "experience_year"] = 0 df["experience_year"] = pd.to_numeric(df["experience_year"], errors="coerce") missing_report = df.isna().sum() missing_report[missing_report > 0]

缺失值处理我的原则很简单:先统计缺失量,再决定怎么处理。如果 2000 条数据里薪酬缺失 50 条,直接dropna无伤大雅;如果缺失 800 条,就要回头检查爬虫解析环节是不是漏了字段,而不是在清洗层硬填。errors="coerce"能保证整列不因为一两条脏数据变成 object 类型,但代价是脏数据会被静默变成 NaN,所以一定要看一眼missing_report。

对于 JD 文本缺失的岗位,我一般保留行,词云分析时再过滤。删除整行会连带丢掉薪酬和区县信息,得不偿失。

3.4 入库 MySQL:pandas + sqlalchemy 一键建表

数据量大到一万条以上,或者你想用 SQL 做临时筛选,清洗完的 DataFrame 可以整表灌进 MySQL。这个项目里用 pandas 的to_sql结合 SQLAlchemy 是最省事的路径,不需要手写建表语句。

from sqlalchemy import create_engine, String engine = create_engine( "mysql+pymysql://root:yourpassword@localhost:3306/recruitment?charset=utf8mb4" ) df.to_sql( "bj_bigdata_jobs", engine, if_exists="replace", index=False, dtype={"job_title": String(300), "company": String(300), "job_url": String(500)} )

to_sql默认会把所有字符串列映射成 TEXT 类型,TEXT 不能加索引,而且字段过长会浪费存储。这里用dtype把核心文本列显式指定为String(300)、String(500)。如果你后续要用WHERE job_title LIKE '%数据仓库%'做筛选,最好在建表后补一个索引,SQL 层面才跑得动。if_exists="replace"适合每次全量重灌;如果要做增量更新,就改成append,配合爬虫阶段的seen_urls去重,不会产生重复数据。

4. 可视化展示:用 pyecharts 拼一块北京市岗位分析看板

可视化在这类项目里最容易被误解:不是把图表画出来就完事,而是每一张图都要回答一个招聘市场上的具体问题。薪酬给到多少、经验门槛在哪、哪些区岗位最密集、JD 里反复出现哪些技能,这是本科毕业设计或者个人作品集里最常用的四个维度。我用的工具是 pyecharts,它的图表是纯 HTML/JS 渲染,能嵌入 Flask 模板,也能直接打开成单页看板。

4.1 薪酬与工作年限:箱线图比散点图更适合回答问题

很多人第一反应画散点图,但散点图在几千条样本下会糊成一团。箱线图能一次性展示每个经验段的薪酬中位数、四分位距和离群值,招聘市场的信息量更加清楚。

from pyecharts import options as opts from pyecharts.charts import Boxplot exp_groups = ["1年以下", "1-3年", "3-5年", "5-10年"] data = [ df.loc[df["experience_group"] == g, "salary_median"].dropna().tolist() for g in exp_groups ] boxplot = Boxplot() boxplot.add_xaxis(exp_groups) boxplot.add_yaxis( "月薪中位数", boxplot.prepare_data(data), tooltip_opts=opts.TooltipOpts(trigger="item") ) boxplot.set_global_opts( title_opts=opts.TitleOpts(title="北京市大数据岗位薪酬随经验分布"), yaxis_opts=opts.AxisOpts(name="元/月") )

pyecharts 的 Boxplot 有一个容易踩的细节:prepare_data方法必须单独调用,它会把多组数据计算成五数概括的嵌套数组。如果你只是把原始列表直接塞给add_yaxis,图表会画成柱状图而不是箱线图。每个经验组里的缺失值要先dropna(),否则 pyecharts 序列化时会把 NaN 变成 null,前端图表不报错但箱子是空的,看着像数据全丢了,实际只是没清理。

4.2 北京市各区岗位数量:Geo 地图组件的地名匹配

区县分布是这个项目里最有北京辨识度的图。招聘网站上的地点通常写成“北京-朝阳区”“朝阳区·望京”这种混合格式,第一步要从原始字段里提取出标准区名。

from pyecharts.charts import Geo district_counts = df["district"].value_counts() data_pair = [(name, int(count)) for name, count in district_counts.items()] geo = Geo().add_schema(maptype="北京") geo.add_coordinate("昌平区", 116.135, 40.217) geo.add("岗位数", data_pair) geo.set_series_opts(label_opts=opts.LabelOpts(is_show=False)) geo.set_global_opts( visualmap_opts=opts.VisualMapOpts(max_=data_pair[0][1]), )

pyecharts 内置了北京市的地图边界,但对区县地名的匹配比较死板,只认“朝阳区”“海淀区”这种标准写法,不认“北京朝阳”“亦庄”。add_coordinate可以手动补充坐标,前提是你知道那个地方的经纬度,这个函数本质上是给 pyecharts 的坐标注册表打补丁。实操里我一般是先对照district_counts的列表,把“北京经济技术开发区”这类非行政区归并到“大兴区”,再做配对,而不是在图表渲染阶段临时改名字。

4.3 JD 词频与技能词云:分词白名单比停用词表更靠谱

词云的常规做法是jieba.cut()全量分词,然后靠停用词表过滤“我们”“公司”“要求”这类词。但招聘 JD 里真正干扰结果的不是停用词,而是“熟练”“精通”“负责”“优先”这类修饰性动词。它们频率极高,对技能分析毫无价值,却又不在通用停用词表里。

import jieba from collections import Counter skill_set = { "Python", "Java", "Spark", "Hadoop", "Hive", "SQL", "Flink", "Kafka", "爬虫", "数据仓库", "数据挖掘", "机器学习", "深度学习", "ETL", "可视化", "Linux" } words = Counter() for jd in df["job_desc"].dropna(): segs = [w.strip() for w in jieba.cut(str(jd)) if len(w.strip()) > 1] words.update([w for w in segs if w in skill_set or w.isdigit()])

加一个技能白名单,让词云只统计和岗位强相关的技能词。这个方法牺牲了发现未知技能的能力,但换来了词云的可解释性。如果你想保留一点探索能力,可以再加一个 Top 30 高频词的兜底统计,两个结果对照着看。词云属于锦上添花的图,它没法精确表达技能出现次数的数量级差异,只能做定性展示,所以我把全部 JD 文本放进去,而不做经验段拆分,保证词云图上的词频差异足够大、视觉上不糊。

4.4 用 Flask 把图表组装成看板:最简路由与模板

pyecharts 单张图打开是 HTML 文件,但招聘数据分析的产物应该是多张图在同一屏的看板。用 Flask 起一个本地服务,把前面三张图通过模板拼进同一个页面,是这类项目最常见的交付形式。

from flask import Flask, render_template app = Flask(__name__) @app.route("/") def dashboard(): return render_template( "dashboard.html", box_html=make_boxplot().render_embed(), geo_html=make_geo().render_embed(), cloud_html=make_wordcloud().render_embed(), ) if __name__ == "__main__": app.run(debug=True, port=5000)

render_embed()是 pyecharts 提供的核心方法,它把图表所需的全部 JS 依赖和图表配置直接内联成一个完整的 HTML 片段,不需要额外的静态文件目录。模板里只需要用 Jinja2 的安全过滤器渲染即可:

<!doctype html> <html> <head><meta charset="utf-8"><title>北京大数据岗位分析</title></head> <body> <h1>北京市大数据岗位招聘分析看板</h1> <div>{{ box_html | safe }}</div> <div>{{ geo_html | safe }}</div> <div>{{ cloud_html | safe }}</div> </body> </html>

Flask 的render_template默认从templates目录找dashboard.html,所以项目结构里的模板位置不能放错。整个看板是纯静态的,没有接口交互,就是一个直观的数据简历看板,跑起来只需要python app.py,浏览器打开http://localhost:5000就能看到。

5. 避坑:招聘数据分析项目最常见的五个翻车现场

这套流程看着线很短,但每个环节都有能让你卡一整天的暗坑。我挑五个真实发生过的问题,按“现象、原因、解决”写清楚,你照着排查能少走很多弯路。

5.1 动态页面试图用 requests 硬抓:拿到的 HTML 里没有岗位数据

现象:load_page()返回的 HTML 打印出来是正常的页面框架,但解析后 rows 为空列表,或者只解析出导航栏的文本。

原因:目标页面的岗位列表是 JavaScript 异步渲染的,requests 只能拿到空壳 HTML,真实数据在浏览器执行脚本后才插入 DOM。

解决:先打开浏览器开发者工具的 Network 面板,清空网络记录后手动翻一页,看有没有返回 JSON 数据的 XHR 请求。如果找到了这个接口,就用 requests 直接调它,这是最高效的方案;如果接口参数带加密签名,再退到 selenium 或 playwright 驱动真实浏览器。用 requests 抓动态页的代价是:排查时间远超直接上 selenium。我的习惯是,任何页面先print(len(html)),如果页面只有几十 KB 却号称有几百条岗位数据,大概率是空壳页,别急着写解析规则。

5.2 请求头只写 User-Agent 还是被识别:缺 Referer 和 Cookie

现象:前 5 页抓得好好的,第 6 页开始返回验证码页面,或者返回的 JSON 里 code 字段变成错误状态。

原因:反爬系统识别出请求身份不完整。单独一个 User-Agent 能通过最基本的 UA 校验,但请求频率一上来,缺少 Referer 的请求会直接被判定为伪装浏览器。

解决:用requests.Session统一维护请求头,Referer 写成列表页地址,Cookie 从浏览器登录态复制过来,然后重点把请求间隔拉大。招聘网站的数据量需求其实不高,一天抓几百条足够分析,我一般会把间隔放大到 5 到 8 秒,宁可慢一点也不要触发风控。另外,如果你需要登录后才能看完整 JD 信息,Cookie 里的某些字段(比如用户标识)会在登录后变化,抓取中途失效的话,要检查是不是 Cookie 过期,而不是盲目换代理。

5.3 薪酬字段解析出来一半是空值:单位、写法、周期全都不统一

现象:salary_min列有相当一部分是 NaN,画箱线图时这些组直接消失,看起来像“应届生没薪资数据”。

原因:薪资文本里出现了“面议”“200-300/天”“15薪”“25K-30K·14薪”这些变体。正则只匹配“数字-数字”的标准格式,遇到“200/天”会提取出 200 和 300 吗?不会,因为日薪数据的格式是“200-300/天”,单位不是万也不是 k,但数量级是日薪,直接乘 1000 会得到离谱的月薪值。

解决:在parse_salary里把日薪识别出来,返回 None,不纳入月薪统计。对于“·14薪”“·15薪”这类带薪月数的,解析时先忽略后缀,只取基本月薪区间,并在字段名里注明“不含年终奖”。这种取舍要在清洗代码的注释里写清楚,否则几个月后看这份代码,你会以为是自己写错了。

5.4 区县字段对不上地图:Geo 图表画出来是空白

现象:图例显示朝阳区 300 条、海淀区 280 条,但地图上只有零星几个点/块,大部分区是空白的。

原因:数据里的区名写法是“北京朝阳”,而 pyecharts 的北京地图数据里区名是“朝阳区”,两者匹配不上。更隐蔽的是“亦庄”“回龙观”“望京”这些片区名,它们不是行政区划,地图数据里根本没有。

解决:建立一个区县别名映射表,在清洗阶段就完成统一。常见的映射规则是:包含“朝阳”就归“朝阳区”,包含“亦庄”或“经开区”就归“大兴区”,包含“回龙观”或“天通苑”就归“昌平区”。映射表用字典写在脚本里,不要写一长串if-else,方便根据新数据随时补条目。这一步必须放在 DataFrame 阶段做,等图表渲染才发现匹配失败,返工成本高得多。

5.5 pyecharts 图表在浏览器里空白:JS 资源加载失败

现象:图表 HTML 文件打开后页面是空白的,浏览器控制台报错Failed to load resource: 404,网络标签里能看到https://cdn.../echarts.min.js无法加载。

原因:pyecharts 生成的 HTML 默认引用在线 CDN 的 JS 资源。如果你所在环境网络受限,或者交付给别人的时候对方没有外网,图表就会白屏。这跟图表代码本身没有关系,是资源引用问题。

解决:最简单的方案是在有网的环境打开 HTML;更稳的方案是把 echarts.min.js 下载到本地,修改模板里的资源引用路径。Flask 看板则不需要处理这个问题,因为render_embed()内联了全部 JS 依赖,本地跑 Flask 服务不依赖外网。这个坑在 Jupyter Notebook 里也有变体——直接用chart.render_notebook()会要求 notebook 环境先加载扩展,报错信息比较误导人。

6. 进阶:把静态 Flask 看板升级成带筛选器的 Streamlit 应用

Flask 看板做出来后,你会发现一个问题:每次想换个岗位关键词看结果,都要改代码重新渲染。这是因为 Flask 看板是静态的,图表在服务端生成完毕才输出给浏览器。想让看板支持“选关键词、拖薪资区间、图表跟着变”,最省力的方案是把整个展示层迁到 Streamlit 上。

Streamlit 的核心逻辑是“声明式交互”:你写好筛选组件,任何状态变化都会触发下方脚本重新执行。迁移成本很低,因为图表还是 pyecharts,只是嵌进 Streamlit 的方式变了。

import streamlit as st from streamlit.components.v1 import html import pandas as pd df = pd.read_csv("jobs_cleaned.csv") keyword = st.selectbox("岗位关键词", ["大数据", "数据分析", "数据仓库", "数据挖掘"]) sal_range = st.slider("月薪中位数区间(元)", 5000, 50000, (10000, 35000)) filtered = df[ df["job_title"].str.contains(keyword, na=False) & df["salary_median"].between(*sal_range) ] st.subheader(f"{keyword} 岗位薪酬分布") html(make_boxplot(filtered).render_embed(), height=520)

Streamlit 的html()组件来自streamlit.components.v1,它会把 pyecharts 渲染出的完整 HTML 片段放进一个 iframe 里,这是 pyecharts 图表能正常显示的关键。st.slider返回的是一个元组,用between(*sal_range)展开成上下界参数,比手写两个比较条件更干净。dataset变成筛选器后,每次拖动滑块都会重新读取同一份 CSV 并重新计算图表,几万行数据没有任何压力。

我个人的习惯是把项目分成spider.py、clean.py、analysis.py、app.py四个独立脚本,彼此用 CSV 或 Parquet 文件传递数据,而不是在全流程里共享一个全局 DataFrame。这样做的好处是,你改爬虫采集策略后,只需要重跑spider.py和clean.py,可视化和看板代码完全不用动。有一回我为了省事把爬虫和清洗写在一个文件里,采集字段名一改,连带可视化脚本全部报错,排查半天,那次之后我就坚持脚本间只用文件解耦。这套基于 Python 的北京市大数据岗位招聘数据分析项目,难的不是某个算法,而是把每条数据从页面一路推到图表之间不断验证每一步。希望你跑通一遍之后,能比我还快地画出一张能用、敢给别人看的招聘数据看板。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 3:36:30

Flutter×OpenHarmony跨端实践:排序与创建功能实现指南

最近团队接了一个需求&#xff0c;要在OpenHarmony设备上做一款任务管理应用&#xff0c;其中列表排序和快捷创建选项是两个核心交互。客户端这边选型很直接——Flutter&#xff0c;原因也很简单&#xff1a;团队本身有Flutter技术储备&#xff0c;ArkTS侧的能力边界还在试错&a…

作者头像 李华
网站建设 2026/10/3 3:36:27

Agent记忆系统实战:基于MCP与Docker构建可检索的长期记忆

1. 从“hindsight”说起&#xff1a;为什么我们需要给Agent装上记忆“hindsight”这个词本身很有意思&#xff0c;字面意思是“事后的洞察力”&#xff0c;也就是我们常说的“后见之明”。放在AI Agent的语境里&#xff0c;它指向一个非常具体且关键的问题&#xff1a;Agent能不…

作者头像 李华
网站建设 2026/10/3 3:36:07

Python实现电池寿命预测:KNN、SVM与随机森林回归实战对比

电池寿命预测这个活&#xff0c;网上教程不少&#xff0c;但大多要么偏理论、要么代码全是英文注释、要么只跑一个模型就完事了&#xff0c;很难直接拿来上手。我这次用Python把KNN、SVM、随机森林三种回归模型完整跑了一遍&#xff0c;代码全部带中文注释&#xff0c;整理成一…

作者头像 李华
网站建设 2026/10/3 3:35:38

MySQL连接池爆满排查:从Too many connections到根因修复

公司业务半夜报警&#xff0c;数据库连接池直接打满&#xff0c;用着好好的服务突然就“Too many connections”&#xff0c;随后页面超时、接口504&#xff0c;紧接着一堆任务队列堆积告警。这种情况我处理过不止一次&#xff0c;每次原因都不完全相同&#xff0c;但排查思路是…

作者头像 李华
网站建设 2026/10/3 3:35:36

Agent稳定性收口:重试、幂等与并发控制的工程实践

周五晚上九点&#xff0c;飞书群里静悄悄的。按照设定&#xff0c;当日19:00应该准时出现汇总好的团队日报&#xff0c;但什么都没有。我打开Agent的后台日志&#xff0c;看到一行安静的报错&#xff1a;agent execution terminated due to error。再往前翻&#xff0c;周二早上…

作者头像 李华
网站建设 2026/10/3 3:35:35

Flutter for OpenHarmony电子合同App活动历史模块实现与踩坑总结

做 Flutter for OpenHarmony 电子合同签署App 的这段经历里&#xff0c;我一度以为最硬核的会是签名面板、证书解析、骑缝章渲染这些"看得见"的模块。结果真到了测试和交付阶段&#xff0c;卡住我时间最久的&#xff0c;反而是看起来平平无奇的"活动历史"功…

作者头像 李华