资讯动态

全国招聘岗位就业可视化系统:Flask+ECharts数据闭环实践

发布时间:2026/9/14 2:05:25 来源:尧图企业网站定制
简介这是一套基于Flask与Python构建的全国招聘岗位就业可视化系统适合计算机相关专业学生用于毕业设计、课程设计或项目初期演示。资源覆盖数据采集、清洗、存储到可视化展示的完整链路前端包含HTML页面与JavaScript交互逻辑后端由Python脚本驱动可直观呈现岗位区域分布、省级与地区划分等就业数据维度。压缩包共54个文件包含5个Python源码、10个JavaScript文件、1个CSS样式表、1个HTML页面以及36个文本说明和1个Markdown文档整体仅364KB结构紧凑便于快速部署与二次开发。目前已有202人学习下载代码均经运行验证并配有文档说明适合想掌握Flask开发与数据可视化流程的初学者参考进阶。1. 用 FlaskPython 做全国招聘岗位就业可视化核心不是“花哨大屏”而是“数据闭环”“全国招聘岗位就业可视化系统”这个标题同时押中了三件事招聘数据怎么来、统计接口怎么出、图表怎么联动。选 Flask 而不是 Django 或 Spring Boot不是因为 Flask 简单而是这类项目的价值集中在查询参数设计和数据联动的自由度上Flask 的轻路由刚好能把“数据导入—统计接口—图表渲染”拆成互不干扰的三条线。“源代码文档说明”的后缀意味着要按课程设计或内部看板交付拿到手要能一次跑通、讲得清设计。下文按存储设计、Flask 接口、ECharts 可视化、部署排错四个阶段展开章节顺序即开发顺序适合有 Web 基础、想补全数据可视化链路的人也适合正在准备答辩材料的开发者。2. 招聘数据采集与存储用 Python 先定表结构再决定爬取还是导入这一章的顺序刻意反着来先建表再写爬虫。招聘网站的字段表达差异很大同一岗位薪资可能是“8千-1.2万”也可能是“15K-20K”如果先写采集器再去迁表结构改动成本会翻倍。先定好存储模型采集脚本和可视化 SQL 都围绕它展开后面所有接口才有稳定的数据基础。2.1 招聘数据源选型公开列表页、开放数据集与采集合规边界常见的数据来源有三类招聘网站公开搜索页的列表数据、人社或统计机构发布的开放数据集、以及手工整理的企业招聘明细。第一类最贴合“实时”语义但要注意目标站点的服务条款与 robots 协议演示项目只采集公开列表页字段不采集个人简历、不绕过登录态这是采集代码的底线。第二类干净但更新慢适合做历史趋势分析第三类适合做单元测试和演示兜底。另一个务实的建议是如果系统用于答辩或团队演示第一版直接用 CSV 导入最稳妥。爬虫写好后可能因为页面改版随时失效而 CSV 导入能保证演示时刻的数据完整性。我一般会同时保留两种方式scripts/import_csv.py负责批量入库scripts/crawler.py负责增量补充README 里把两条命令都写清楚接手的人就不会纠结“数据到底从哪来”。2.2 jobs 表结构设计薪资上下限字段与索引怎么定SQLite 对课程设计和中小型看板都够用避免引入 MySQL 的安装和账号成本。核心表结构如下CREATE TABLE jobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, -- 岗位名称 company TEXT, -- 公司名称 city TEXT NOT NULL, -- 市如 北京/上海地图聚合键 industry TEXT, -- 所属行业 salary_min INTEGER, -- 月薪下限单位 K salary_max INTEGER, -- 月薪上限单位 K education TEXT, -- 学历要求大专/本科/硕士/不限 experience TEXT, -- 经验要求1-3年/3-5年/不限 publish_date TEXT, -- 发布日期 yyyy-mm-dd source TEXT DEFAULT csv -- 数据来源标记 ); CREATE INDEX idx_jobs_city ON jobs(city); CREATE INDEX idx_jobs_edu ON jobs(education);表的三个设计点值得展开。第一薪资不存原始字符串而是拆成salary_min和salary_max两个整数字段单位为 K/月这样聚合 SQL 可以直接AVG((salary_min salary_max) / 2)算平均薪资区间筛选也能用salary_min 8表达“月薪 8K 以上”。第二城市字段单独一列而不是省市区拼一个字符串因为后面 ECharts 地图要按市级粒度聚合字段越干净GROUP BY越省事。第三为 city 和 education 建了普通索引统计接口的查询条件都落在这些列上数据量到几万条时索引收益明显。薪资文本解析是入库前最需要统一口径的地方常见的原文形态和落库规则如下原始文本salary_minsalary_max处理方式15K-20K1520统一单位 K 后拆分8千-1.2万812先转 K 再拆分20K以上2020下限当作上下限面议 / 未标注NULLNULL跳过该条并记日志提示薪资解析失败时不要中断整轮采集把异常记录到logs/failed_parse.log事后统一补采比逐条重跑整批数据效率高。2.3 requestslxml 爬虫脚本的限速参数与薪资文本解析用 requests 配合 lxml 做列表页采集是常见做法核心代码如下import random import time import requests from lxml import html HEADERS {User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)} def parse_salary(text): # 将 15K-20K 或 8千-1.2万 解析为 (min, max)单位 K text text.replace(K, k).replace(千, k).replace(万, *10k) numbers re.findall(r[\d\.], text) if 万 in text: return (float(numbers[0]) * 10, float(numbers[1]) * 10) return (float(numbers[0]), float(numbers[1])) def fetch_job_list(page): url https://example-jobsite.com/api/jobs/list params {page: page, size: 50} resp requests.get(url, paramsparams, headersHEADERS, timeout10) resp.raise_for_status() for item in resp.json().get(data, []): yield { title: item[title], company: item[company_name], city: item[city], salary_min: parse_salary(item[salary_text])[0], salary_max: parse_salary(item[salary_text])[1], education: item.get(education, 不限), } for page in range(1, 6): for job in fetch_job_list(page): insert_job(job) time.sleep(random.uniform(3, 6)) # 限速避免对目标站点造成压力parse_salary负责把“15K-20K”这类文本解析成 (15, 20)要兼容“8千-1.2万”就要先统一单位再拆分用re.findall提取数字后按单位换算这是爬虫里最容易出 bug 的部分。time.sleep(random.uniform(3, 6))是限速的关键参数固定 sleep 3 秒容易被识别成脚本特征随机区间更接近人工浏览节奏。resp.raise_for_status()在请求失败时直接抛出异常避免把错误页当列表页解析出空数据。2.4 pandas 批量导入 CSV演示场景的保底数据入口import pandas as pd import sqlite3 df pd.read_csv(data/jobs.csv, encodingutf-8) print(df.isna().sum()) # 先看缺失值分布 conn sqlite3.connect(instance/jobs.db) df.to_sql(jobs, conn, if_existsappend, indexFalse) conn.close()pandas 的to_sql按 DataFrame 列名直接映射表字段要求 CSV 表头与 jobs 表列名一致。导入前一定要用df.isna().sum()检查缺失值缺薪资的记录可以过滤掉或写进jobs_invalid临时表避免主表里出现 NULL 导致聚合接口报错。if_existsappend表示追加而不是覆盖重复导入时要先去重否则统计数字会翻倍。3. Flask 后端接口把统计逻辑收敛进查询参数而不是写死在前端后端是整个系统的“翻译层”把数据库里的明细记录翻译成图表需要的聚合结果。这一章讲清楚 Flask 项目怎么组织、统计接口怎么写、参数怎么设以及为什么不要把聚合逻辑散落在前端。3.1 Flask 项目结构用 Blueprint 拆分统计路由课程设计里最常见的反面写法是十几个路由全写在app.py里。当接口数量超过 8 个后调试和答辩讲解都会变困难。常见做法是用应用工厂加 Blueprint 拆分app/ ├── __init__.py # create_app 工厂函数 ├── blueprints/ │ ├── __init__.py │ └── stats.py # 统计聚合接口 ├── db.py # get_db 连接管理 ├── models.py # 查询 SQL 封装 ├── templates/ └── static/工厂函数的好处是测试和部署可以传入不同配置比如测试时用内存数据库、生产用磁盘数据库不用改业务代码。db.py里用sqlite3.Row作为行工厂查询结果就能按字典取字段JSON 序列化时省一轮字段映射。答辩时讲这个结构也比讲“所有代码在一个文件里”有说服力。3.2 Flask 统计接口 /api/overview 的查询参数与防注入写法from flask import Blueprint, jsonify, request, g import sqlite3 stats_bp Blueprint(stats, __name__) def query_db(sql, args()): g.db.row_factory sqlite3.Row return g.db.execute(sql, args).fetchall() stats_bp.route(/api/overview) def overview(): city request.args.get(city, ).strip() keyword request.args.get(keyword, ).strip() conditions, params [], [] if city: conditions.append(city ?) params.append(city) if keyword: conditions.append((title LIKE ? OR company LIKE ?)) params.extend([f%{keyword}%, f%{keyword}%]) where fWHERE { AND .join(conditions)} if conditions else rows query_db(f SELECT COUNT(*) AS total, ROUND(AVG((salary_min salary_max) / 2.0), 1) AS avg_salary FROM jobs {where} , params) return jsonify({code: 0, data: dict(rows[0])})city和keyword是两个最核心的筛选参数city走等值匹配用于地图点击下钻keyword走 LIKE 模糊匹配用于搜索框。这里用列表拼接 where 子句而不是直接拼字符串参数全部通过?占位符传入从源头避开 SQL 注入。ROUND(AVG(...), 1)把平均薪资保留一位小数前端 tooltip 直接显示不需要再格式化。接口参数的口径要写进文档方便前后端对齐参数类型默认值含义citystring空表示全国城市等值筛选keywordstring空岗位或公司名模糊匹配edustring不限学历要求筛选3.3 GROUP BY 聚合地域分布与薪资区间的两个接口写法地图和柱状图对应的接口都是单表聚合差别只在分组维度stats_bp.route(/api/jobs/geo) def geo_distribution(): city request.args.get(city) where, params , [] if city: where, params WHERE city ?, [city] rows query_db(f SELECT city, COUNT(*) AS job_count, ROUND(AVG((salary_min salary_max) / 2.0), 1) AS avg_salary FROM jobs {where} GROUP BY city ORDER BY job_count DESC LIMIT 100 , params) return jsonify({code: 0, data: [dict(r) for r in rows]})GROUP BY city输出的是“城市→岗位量→平均薪资”三元组直接可以喂给地图的 data 数组。LIMIT 100防止城市数量过多导致前端渲染卡顿。薪资分布接口GET /api/jobs/salary?city北京则用GROUP BY salary_max统计薪酬区间上的岗位数量前端画柱状图。两个接口都复用query_db查询逻辑集中在models.py而不是散在路由里答辩时能讲清“路由管参数校验、模型管 SQL、模板管展示”的分层。3.4 Flask 接口的统一响应、缓存与 CORS三个接口的返回结构统一用{code: 0, data: ...}错误时 code 非 0 并带 msg 字段前端 fetch 只需要判断code 0。聚合接口在数据量大时会有明显耗时可以用functools.lru_cache包一层带参数的查询函数from functools import lru_cache lru_cache(maxsize128) def geo_cache(city): return compute_geo(city)geo_cache的缓存键是参数字符串城市筛选变化时自动命中不同的缓存项。maxsize控制在 128防止筛选组合过多撑爆内存。如果前端单独跑在另一个调试端口还需要给 Flask 加flask-cors的CORS(app)否则浏览器会拦截跨域请求这是前后端分离调试中最高频的报错之一。4. 可视化大屏ECharts 全国地图与 Flask 接口联动的完整写法系统到了这一步才真正“可视化”。选 ECharts 而不是 Chart.js 或 AntV是因为它对中国地图、散点图和 visualMap 热力分层的支持最省事招聘行业画像类图表几乎都能覆盖。4.1 大屏布局与本地静态资源不依赖 CDN 的页面组织templates/dashboard.html 用 CSS Grid 分三栏左侧行业和学历分布、中间全国地图、右侧薪资和经验分布顶部放筛选栏。静态资源用本地文件而不是 CDN是因为答辩现场的展示环境可能没有外网CDN 加载失败会让整个页面白屏。图表与接口的对应关系要在一开始就定好区域图表类型数据接口返回字段顶部统计卡片/api/overviewtotal, avg_salary中央全国岗位分布地图/api/jobs/geocity, job_count右侧薪资区间柱状图/api/jobs/salarysalary_max, count4.2 全国地图 geoJSON 注册ECharts 5 地图加载的坑ECharts 5 移除了内置的全国地图直接geo: { map: china }会静默失败只显示空白底图。需要先把 GeoJSON 下载到本地并注册fetch(/static/js/china.json) .then(res res.json()) .then(geoJson { echarts.registerMap(china, geoJson); initMap(); });registerMap的第一个参数是注册名后续geo和series.map都要引用同一个名字。china.json 约几十 KB属于项目必需资源README 里必须写清来源否则其他人拿到项目时地图区域会一直是灰的。地图上的散点用effectScatter效果更直观但视觉层次主要靠 visualMap 的分段设色visualMap: { min: 0, max: 60, text: [高, 低], inRange: { color: [#e0f3f8, #abd9e9, #74add1, #4575b4] }, seriesIndex: 0 }inRange.color是一个从低到高的渐变数组低值浅色高值深色比默认的单一色更有层次。max不要写死成 60最好根据接口返回的job_count最大值动态生成写死会导致部分城市的数据溢出到最高档色图表语义就被破坏了。4.3 fetch 接口联动城市下钻与参数拼装代码async function refreshCharts() { const params new URLSearchParams(); if (currentCity) params.set(city, currentCity); if (keyword) params.set(keyword, keyword); const res await fetch(/api/jobs/geo?${params.toString()}); const json await res.json(); if (json.code ! 0) { showError(json.msg); return; } mapChart.setOption({ series: [{ type: effectScatter, data: json.data.map(d ({ name: d.city, value: [cityCoord[d.city][0], cityCoord[d.city][1], d.job_count], salary: d.avg_salary })) }] }); }URLSearchParams负责拼装 city 和 keyword比手写字符串拼接干净得多。setOption用增量更新的方式传入新数据ECharts 会自动 diff 并保留其他配置。地图点击下钻通过mapChart.on(click, params { currentCity params.name; refreshCharts(); })实现某个城市被点击后overview、薪资柱状图、行业饼图全部按该城市重新请求——这就是标题里“联动”的完整语义。提示cityCoord是城市名到经纬度的映射表如果 GeoJSON 的 features 里带了 properties可以用它动态生成避免手写几百个城市坐标。4.4 tooltip formatter 格式化图表可读性的最后一道工序默认 tooltip 直接显示value数组会很乱用 formatter 收口tooltip: { trigger: item, formatter(params) { const v params.data.value || []; return ${params.name}br/岗位数量${v[2] || 0} 个br/平均薪资${params.data.salary || -} K; } }params.name是城市名v[2]是配置在 value 数组第三位的岗位数。这里把“平均薪资”放在params.data的扩展字段里而不是塞进 value是为了不破坏 ECharts 对 value 数组的解析。表单的筛选事件城市、关键词、学历统一走refreshCharts所有图表只从接口数据驱动页面里不缓存任何聚合结果这样前后端的数据口径始终一致。5. 把 Flask 可视化系统部署上线命令与三个高频排错点可视化系统开发完只跑了一半能部署到服务器上让别人打开才算交付。这一章给最小可用的上线方案以及三个最容易让项目跑不起来的排错点。5.1 gunicorn NginxFlask 可视化的最小上线配置pip install gunicorn gunicorn -w 4 -b 127.0.0.1:8000 app:create_app()-w 4是 worker 进程数按 CPU 核数乘 2 设置即可app:create_app()是应用工厂的调用写法注意不要带着 debug 模式上线。Nginx 负责请求转发和静态资源分发location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; }5.2 三个高频排错点地图白屏、SQLite 线程冲突、中文乱码地图白屏几乎都是registerMap没执行或 china.json 返回 404。先按 F12 开 Network 面板确认文件加载状态再确认注册发生在echarts.init之前两个顺序反了就会静默失败。SQLite 线程冲突Flask 默认多线程处理请求多线程下sqlite3.connect需要传check_same_threadFalse或者用 flask 的g对象按请求持有连接否则会报Database is locked。中文乱码检查响应头是否带charsetutf-8以及 CSV 导入时是否指定了encodingutf-8这两个地方不一致就会出现“页面里全是问号”的现象。5.3 文档说明怎么组织接口表是答辩的硬通货“源代码文档说明”里的文档不能只写环境安装至少要有环境要求、启动命令、数据导入命令和一张接口表。接口表按“方法、路径、参数、返回字段”四列给出示例方法路径关键参数返回字段GET/api/overviewcity, keywordtotal, avg_salaryGET/api/jobs/geocitycity, job_count, avg_salaryGET/api/jobs/salarycitysalary_max, count参数说明里要写清类型与默认值city 为空表示全国。这张表既是答辩时讲数据流的提纲也是新接手的人最快能上手的入口上面的三个排错点单独放在 FAQ 一节和接口表互相呼应文档就成了能跟着排查问题的活材料。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价