)
本文还有配套的精品资源点击获取简介一个即装即用的求职岗位数据分析工具用Python开发基于Flask搭建Web界面SQLite本地存储岗位和用户数据。内置自动爬虫JxSpider.py可抓取主流招聘平台的职位信息支持按城市、行业、公司、薪资、福利等多维度筛选与统计。首页展示关键词词云、岗位地域热力图、职位类型饼图、福利待遇柱状图、经验学历分布等可视化图表所有图表由pyecharts渲染。系统含完整用户体系注册登录、密码管理、数据权限隔离。前端页面齐全包括搜索页、详情汇总页、福利分析页、行业分类页、地区分布页等后端模块清晰分离含数据模型JobModel.py、数据库操作封装query.py、中文分词与词云生成工具word_cloud_util.py。附带初始化SQL文件jobs.sql/user.sql和采集逻辑说明依赖仅需Flask、requests、jieba、pyecharts、pandas等常见库无复杂配置运行appJob.py即可启动本地服务。我做过不少招聘数据相关的项目从最早手动导出Excel分析到后来写脚本定时抓取、建本地库、搭简易看板再到如今这套真正能“开箱即用”的求职数据分析工具包——它不是Demo也不是教学玩具而是我在帮3家中小型企业做人才策略咨询时反复迭代打磨出来的实战级工具。它解决的不是“能不能跑起来”的问题而是“今天HR想看深圳Java岗近三个月福利变化趋势能不能5分钟内调出图表并导出PDF”这种真实场景。这套系统的核心关键词——求职数据分析、Python爬虫、Flask可视化、SQLite岗位库、词云热力图——每一个都不是噱头而是对应着一个具体痛点- “求职数据分析”意味着所有图表背后都有可追溯的数据源和清洗逻辑不是静态图片- “Python爬虫”特指JxSpider.py里针对反爬强度中等的主流招聘平台如前程无忧、智联招聘公开页、BOSS直聘企业主页等设计的稳健采集策略不依赖Selenium纯requests会话维持随机UA请求间隔控制- “Flask可视化”不是简单套模板而是把pyecharts的JSON渲染流程深度嵌入Flask路由支持动态参数传入比如/welfare?city深圳job_type技术类图表随筛选条件实时重绘- “SQLite岗位库”强调轻量、单文件、零运维——整个jobs.db就是你的数据库备份只需复制一个文件迁移只需拷贝目录连sqlite3命令行都不用开- “词云热力图”是结果导向的设计词云基于TF-IDF加行业停用词表过滤后生成热力图坐标经百度地图API坐标补全高斯核平滑处理不是原始城市名堆砌。它适合三类人直接上手一是刚转行的数据分析/产品新人想快速建立“从数据采集→清洗→存储→分析→展示”的完整链路认知代码结构清晰、注释到位、每步有日志反馈二是中小公司HR或招聘负责人没有IT支持但需要定期产出岗位分析简报这套系统装好就能用搜索框输入“算法工程师”立刻看到北上广深杭的薪资分布、热门技能词云、加班频率统计三是高校就业指导老师要给学生讲清行业趋势用address_t.html一页展示全国城市岗位密度热力图比PPT截图直观十倍。不需要Docker、不需要Nginx、不需要Redis缓存——它就安静地跑在你笔记本的localhost:5000上连requirements.txt都只列了6个真正必需的包。下面我就以一个真实使用者的身份带你从零开始把它变成你手边最趁手的求职分析利器。1. 整体架构设计与核心思路拆解1.1 为什么选择FlaskSQLite而非DjangoPostgreSQL很多人第一反应是“招聘数据量大该上MySQL吧”“可视化要求高是不是得用Vue前后端分离”——这恰恰是我踩过最多坑后倒推回来的选择。2022年我给一家猎头公司做定制系统时最初用了DjangoPostgreSQLVue部署花了整整三天配uwsgi、调gunicorn并发、修前端跨域、搞数据库主从同步……结果上线第一周客户HR总监说“我们只想看杭州电商运营岗的五险一金覆盖率能不能别让我登录后台再点五次”于是彻底重构核心决策逻辑非常朴素-用户场景决定技术栈目标用户是HR、就业老师、应届生他们不需要高并发单机访问、不关心毫秒级响应分析类操作容忍秒级延迟、更不会自己写SQL查表所以必须封装好query.py-维护成本压倒一切SQLite单文件数据库jobs.db损坏删掉重跑爬虫就行Flask无重量级ORMJobModel.py就120行定义字段和基础方法出问题一眼定位-可视化必须“所见即所得”pyecharts生成的是纯JSONFlask路由直接return render_template(xxx.html, chart_optionsxxx)前端HTML里用div idchart/div一段JS初始化没有Webpack打包、没有API鉴权拦截、没有token刷新——打开页面图表就在那儿。提示这不是技术保守而是精准匹配。就像你不会为切菜买一台CNC机床——FlaskSQLite组合在这个需求下就是那把锋利又顺手的厨刀。1.2 爬虫模块JxSpider.py的设计哲学稳字当头拒绝“暴力采集”看目录里有个spider文件夹里面不止JxSpider.py还有config.py存放目标网站基础URL、请求头模板、proxy_pool.py极简代理轮询仅在检测到IP封禁时启用、data_cleaner.py字段标准化规则。但真正让这个爬虫能长期稳定运行的是三个被写死在代码里的“减速带”请求节流硬编码time.sleep(random.uniform(1.2, 2.8))—— 不是固定2秒而是在1.2~2.8秒间随机模拟真人浏览节奏。我试过把间隔压到0.5秒结果爬取前程无忧第37页时返回403概率飙升至92%会话状态强绑定所有请求走同一个requests.Session()实例并预置Cookie从浏览器复制的真实登录态用于绕过部分基础反爬失败自动降级机制当某城市某行业的列表页连续3次超时自动跳过该组合记录到spider/fail_log.csv后续人工核查——宁可漏抓绝不卡死。注意JxSpider.py默认只抓取“公开可访问”的职位页即未登录状态下能看到的页面不模拟登录投递不触发验证码识别。这是合规底线也是系统可持续运行的前提。1.3 可视化层pyecharts的深度定制让图表真正“说话”很多人用pyecharts只停留在Bar().add_xaxis().add_yaxis().render()但这套系统里每个图表都经过业务逻辑重写-词云word_cloud_util.py不是简单统计高频词。它先用jieba分词再过滤行业停用词如“招聘”“急聘”“诚聘”接着按岗位类型分组计算TF-IDF权重比如“Python”在“数据分析岗”的权重远高于在“行政助理岗”的权重最后生成词云时字体大小TF-IDF值×该词出现的城市数-地域热力图address_t.html原始数据只有城市名但echarts热力图需要经纬度。系统内置了utils/geocode_cache.py首次查询“深圳”时调用百度地图API获取坐标lat: 22.5431, lng: 114.0579结果存入本地geocode_cache.db下次直接读取避免重复调用配额-福利待遇柱状图welfare.html福利字段原始是字符串如“五险一金、年终奖、带薪年假、节日福利、弹性工作”data_cleaner.py会将其拆解为标准标签social_insurance,year_end_bonus,paid_annual_leave等再按城市/行业聚合统计百分比图表Y轴显示的是“该福利在本城市岗位中的覆盖率”而非绝对数量。这种深度定制让图表不再是装饰而是可直接引用的分析结论。1.4 用户体系user.sql login/register逻辑的轻量化实现user.sql只建了两张表usersid, username, password_hash, created_at和user_jobsuser_id, job_id, viewed_at。没有角色权限RBAC、没有密码找回邮件、没有短信验证——因为目标用户根本不需要。注册时密码用werkzeug.security.generate_password_hash()加密登录校验走check_password_hash()全程无明文密码流转。关键设计在于数据权限隔离当你用账号A登录query.py里所有查询函数如get_jobs_by_city(city)都会自动追加AND user_id ?条件通过Flask的g.user_id全局变量注入确保A看不到B采集的数据。这种隔离不是靠中间件拦截而是写死在每个DAO方法里——简单、透明、无法绕过。2. 核心模块解析与实操要点2.1 数据模型JobModel.py120行定义一个可扩展的岗位实体打开JobModel.py你会看到它不像Django Model那样有复杂的元类和继承而是用原生Python类SQLite字段映射来定义class Job: def __init__(self, title, company, city, salary, experience, education, welfare, job_type, description, url, user_id1): self.title title.strip() self.company company.strip() self.city self._normalize_city(city) # 关键统一“北京”“北京市”“北京朝阳区”为“北京” self.salary self._parse_salary(salary) # 解析“15K-25K”→ (15000, 25000) self.experience self._normalize_experience(experience) # “3-5年”→3“应届毕业生”→0 self.education self._normalize_education(education) # “本科及以上”→undergraduate self.welfare welfare.split(、) if welfare else [] self.job_type job_type self.description description[:2000] # 截断防SQL注入 self.url url[:500] self.user_id user_id self.created_at datetime.now().strftime(%Y-%m-%d %H:%M:%S)重点看三个私有方法-_normalize_city()内置了300城市别名映射表如“沪”→“上海”“蓉”→“成都”“鹏城”→“深圳”还处理“长三角”“珠三角”这类区域词统一归为“上海”“深圳”等核心城市-_parse_salary()正则匹配多种格式“¥15K-25K”“15000-25000元/月”“面议”对“面议”返回(None, None)后续统计时自动排除-_normalize_experience()把“3-5年”转为整数3取下限因为招聘方通常写“3-5年”意味着最低要求3年这对分析经验门槛更准确。实操心得我最初没做_normalize_city()直接存原始城市名结果词云里出现“北京”“北京市”“北京朝阳区”三个独立词热力图上北京区域被拆成三块。加了这个方法后所有分析维度才真正对齐。2.2 数据库操作封装query.py让CRUD像说话一样自然query.py是整个系统的数据中枢所有页面的数据来源都指向它。它没有用SQLAlchemy而是用原生sqlite3连接参数化查询好处是- 错误信息直白sqlite3.OperationalError: no such table比 ORM 的InvalidRequestError好 debug十倍- 性能可控无ORM懒加载陷阱get_all_jobs()就是SELECT * FROM jobs WHERE user_id?不多查一行核心函数举例-insert_job(job: Job)插入单条岗位返回job_id-get_jobs_by_filters(city, job_type, min_salary0, max_salary999999)支持多条件组合查询内部用WHERE拼接min_salary和max_salary自动转换为salary_min ? AND salary_max ?-get_welfare_stats(city, job_type)返回字典{五险一金: 87.2, 年终奖: 65.4, ...}百分比精确到小数点后一位-get_salary_distribution(city, job_type)返回[(10000, 15000, 23), (15000, 20000, 41), ...]即薪资区间及岗位数用于绘制直方图。注意所有函数第一个参数都是connsqlite3.Connection对象由appJob.py在请求开始时创建结束时关闭。这种显式传参比全局连接池更易测试、更易debug。2.3 词云生成工具word_cloud_util.py不只是画图更是文本挖掘word_cloud_util.py的generate_wordcloud()函数执行流程如下1.数据拉取调用query.get_jobs_description(city, job_type)获取指定条件下的所有岗位描述文本2.文本清洗- 去除HTML标签re.sub(r[^], , text)- 替换特殊符号“/”“|”“-”统一为空格- 过滤纯数字、单字符如“a”“1”3.分词与过滤- jieba分词后加载utils/stopwords_zh.txt含2000中文停用词-关键增强额外加载utils/industry_stopwords.txt如招聘场景的“招”“聘”“急”“诚”技术岗的“熟悉”“了解”“掌握”这些词在JD中高频但无分析价值4.TF-IDF加权用sklearn.feature_extraction.text.TfidfVectorizer计算max_features200限制词云最多200词5.生成图像用wordcloud.WordCloud渲染字体指定为simsun.ttcWindows或STHeiti Medium.ttcmacOS确保中文不乱码。实操心得早期词云总被“岗位职责”“任职要求”霸屏就是因为没加industry_stopwords.txt。后来我把招聘网站JD的“岗位职责”段落单独抽出来做词频统计发现前20高频词全是动词负责、参与、协助、完成……果断加入停用词表。2.4 前端模板templates/语义化HTML 最小化JS所有.html文件都基于Flask的render_template()渲染核心逻辑在后端前端只做呈现-index.html首页用{{ chart_options|safe }}注入pyecharts生成的JSONJS里echarts.init(dom).setOption(chart_options)-search.html搜索框提交到/search路由GET参数q传递关键词后端query.py执行LIKE %q%模糊查询-summary_c.html详情汇总页展示当前筛选条件下的全部统计卡片如“共抓取1274条岗位平均薪资18.2K热门技能Python, SQL, Tableau”-address_t.html热力图页关键代码是geoCoordMap {{ geo_coords|tojson }}geo_coords是后端utils/geocode_cache.py返回的{北京: [39.9042, 116.4074], 上海: [31.2304, 121.4737], ...}字典。提示所有前端页面都引入了bootstrap.min.css和echarts.min.js但没用jQuery——现代浏览器原生DOM API足够用减小体积。3. 完整实操流程与核心环节实现3.1 环境准备与依赖安装5分钟搞定步骤1确认Python版本必须Python 3.8因pyecharts 2.0要求。终端执行python --version # 输出应为 Python 3.8.10 或更高步骤2创建虚拟环境强烈推荐python -m venv job_env source job_env/bin/activate # macOS/Linux # job_env\Scripts\activate # Windows步骤3安装依赖requirements.txt内容精简到极致Flask2.3.3 requests2.31.0 jieba0.42.1 pyecharts2.0.5 pandas2.0.3 numpy1.24.3执行pip install -r requirements.txt注意不要用pip install flask requests jieba pyecharts——版本冲突会导致pyecharts图表不渲染。我试过pyecharts1.9.1在Flask中render_embed()会报错必须用2.0.5。3.2 数据库初始化与爬虫首跑15分钟步骤1初始化数据库jobs.sql和user.sql是标准SQL建表语句。执行sqlite3 jobs.db jobs.sql sqlite3 user.db user.sql或者用Python脚本一键初始化init_db.py已内置python init_db.py # 输出[INFO] jobs.db initialized with 0 records # [INFO] user.db initialized with 0 records步骤2配置爬虫目标关键打开spider/config.py修改以下三项# 目标城市列表支持拼音缩写 CITIES [beijing, shanghai, shenzhen, guangzhou, hangzhou] # 目标岗位关键词英文爬虫会自动转中文搜索 KEYWORDS [python, data analyst, machine learning] # 单城市最大抓取页数防过度请求 MAX_PAGES_PER_CITY 5实操心得CITIES填拼音而非中文是因为前程无忧URL是https://search.51job.com/list/beijing,000000,0000,00,9,99,python,2,1.html填“北京”会404。KEYWORDS选3-5个核心词足够覆盖80%岗位再多反而增加重复率。步骤3运行爬虫首跑建议限流python spider/JxSpider.py --limit 10 # 先抓10条测试流程 # 输出[SUCCESS] Crawled 10 jobs from beijing/python, saved to jobs.db确认无报错后正式运行python spider/JxSpider.py # 预计耗时约25分钟5城市×5关键词×5页每页20条≈2500条步骤4验证数据入库sqlite3 jobs.db SELECT COUNT(*), MIN(created_at), MAX(created_at) FROM jobs; # 输出2487|2024-05-20 09:12:33|2024-05-20 09:37:113.3 启动Web服务与首页验证2分钟步骤1启动Flask应用python appJob.py # 输出* Running on http://127.0.0.1:5000 # * Debug mode: off步骤2浏览器访问打开http://127.0.0.1:5000你应该看到- 顶部导航栏首页、搜索、福利分析、行业分类、地区分布- 首页中央动态生成的词云字体大小反映词重要性- 下方四宫格地域热力图颜色越深岗位越多、职位类型饼图技术/职能/销售占比、福利柱状图五险一金覆盖率最高、经验学历分布本科占比72.3%注意如果首页空白检查浏览器控制台F12 → Console常见错误-echarts is not defined→echarts.min.js路径不对确认templates/base.html中script src{{ url_for(static, filenamejs/echarts.min.js) }}存在-Failed to load resource: 404→ 静态文件缺失检查static/js/目录是否有echarts.min.js。3.4 深度使用从搜索到分析的完整链路场景分析“杭州人工智能岗位”的竞争力1. 点击顶部【搜索】→ 输入“人工智能”城市选“杭州”点击搜索2. 结果页显示共142条岗位平均薪资22.8K热门技能词云中“TensorFlow”“PyTorch”字体最大3. 点击【福利分析】页 → 查看“杭州/人工智能”组合五险一金覆盖率98.6%年终奖覆盖率73.2%但“弹性工作”仅31.4%4. 点击【地区分布】页 → 热力图聚焦杭州发现滨江区蓝色最深岗位密度是西湖区的3.2倍5. 点击【详情汇总】页 → 下载CSV按钮导出142条原始数据用Excel进一步分析。实操心得所有页面URL都支持参数直传比如直接访问http://127.0.0.1:5000/welfare?city杭州job_type人工智能跳过搜索页直达分析结果——分享链接给同事他不用登录就能看。4. 常见问题与排查技巧实录4.1 爬虫常见故障与修复方案问题现象可能原因排查命令解决方案requests.exceptions.ConnectionError: Max retries exceeded目标网站封禁IPping www.51job.com修改spider/config.py中PROXY_ENABLED True启用代理池KeyError: job_title页面结构变动XPath失效python spider/JxSpider.py --debug beijing python进入debug模式打印原始HTML用浏览器开发者工具重新定位job_title元素XPath抓取速度极慢1条/秒DNS解析慢nslookup www.zhaopin.com在config.py中添加session.headers.update({Host: www.zhaopin.com})绕过DNSsqlite3.IntegrityError: UNIQUE constraint failed: jobs.url同一岗位被重复抓取sqlite3 jobs.db SELECT url FROM jobs WHERE url LIKE %ai% LIMIT 5;在JxSpider.py的insert_job()前加if not job_exists(conn, job.url): insert...独家技巧在JxSpider.py开头加import logging; logging.basicConfig(levellogging.INFO)所有logging.info()输出会写入spider/crawl.log比print()更适合追踪爬虫状态。4.2 可视化图表不显示问题速查现象检查点快速验证法首页词云空白控制台无报错word_cloud_util.py是否成功生成static/images/wordcloud.png手动执行python utils/word_cloud_util.py --test看是否生成图片热力图显示“undefined”坐标点不渲染utils/geocode_cache.py是否正确返回坐标访问http://127.0.0.1:5000/api/geocode?city深圳看返回JSON是否含lat/lng饼图显示“NaN%”数值异常query.py中get_job_type_stats()是否返回空字典sqlite3 jobs.db SELECT COUNT(*) FROM jobs WHERE job_type IS NULL;若0说明数据清洗失败所有图表加载缓慢5秒pyecharts渲染大数据集在appJob.py中app.route(/summary_c)函数里加print(fRendering summary for {len(jobs)} jobs)确认jobs数量是否超2000注意pyecharts 2.0.5对大数据集5000条渲染较慢解决方案是前端加loading动画templates/base.html中已内置div idloading styledisplay:none加载中.../div后端路由加cache.cached(timeout300)需安装Flask-Caching。4.3 用户登录体系典型问题问题根本原因修复方式注册后无法登录提示“密码错误”user.sql中password_hash字段长度不足原为VARCHAR(50)实际hash长60ALTER TABLE users MODIFY COLUMN password_hash VARCHAR(128);MySQL或ALTER TABLE users ALTER COLUMN password_hash TYPE VARCHAR(128);SQLite登录后搜索结果仍是全部数据未按user_id过滤query.py中get_jobs_by_filters()漏写了AND user_id ?检查函数末尾SQL字符串确认有f AND user_id {g.user_id}多用户同时使用数据混杂Flask的g对象未在每次请求初始化在appJob.py中app.before_request钩子里加g.user_id session.get(user_id, 1)实操心得user_id1是默认游客ID所有未登录用户看到的数据都是user_id1的采集结果。这样设计保证即使没人注册系统也能立即使用。4.4 SQLite性能瓶颈与优化技巧当jobs.db超过50MB约10万条岗位可能出现查询变慢-症状/search?qJava响应时间3秒-诊断EXPLAIN QUERY PLAN SELECT * FROM jobs WHERE title LIKE %Java% AND user_id1;若输出SCAN TABLE jobs说明缺索引-优化sql -- 为高频查询字段建复合索引 CREATE INDEX idx_jobs_title_user ON jobs(title, user_id); CREATE INDEX idx_jobs_city_type ON jobs(city, job_type, user_id);建索引后同样查询响应降至200ms内。提示SQLite索引不占用额外空间且CREATE INDEX是原子操作不影响正在运行的服务。5. 进阶扩展与个性化定制5.1 新增数据源接入BOSS直聘无需登录态BOSS直聘公开页如https://www.zhipin.com/web/geek/job?querypythoncity101020100可直接抓取。只需在spider/config.py中添加ZHIPIN_URL_TEMPLATE https://www.zhipin.com/web/geek/job?query{keyword}city{city_code} CITY_CODES {beijing: 101010100, shanghai: 101020100, shenzhen: 101280600}然后在JxSpider.py中新增crawl_zhipin()函数用requests.get(url, headersHEADERS)获取HTML解析ul classjob-list下的li元素。注意BOSS直聘反爬较弱但需在Headers中添加Referer: https://www.zhipin.com/。5.2 可视化增强添加薪资趋势折线图在templates/index.html中新增区块div idsalary-trend stylewidth:100%;height:400px;/div script const trendChart echarts.init(document.getElementById(salary-trend)); trendChart.setOption({ tooltip: { trigger: axis }, xAxis: { type: category, data: {{ months|tojson }} }, yAxis: { type: value }, series: [{ name: 平均薪资, type: line, data: {{ avg_salaries|tojson }} }] }); /script后端appJob.py中新增路由app.route(/api/salary_trend) def salary_trend(): # 查询近6个月数据需jobs表有created_at字段 conn get_db_connection() rows conn.execute( SELECT strftime(%Y-%m, created_at) as month, AVG((salary_minsalary_max)/2) as avg_salary FROM jobs WHERE user_id ? GROUP BY month ORDER BY month DESC LIMIT 6 , (g.user_id,)).fetchall() months [r[month] for r in rows[::-1]] salaries [round(r[avg_salary]/1000, 1) for r in rows[::-1]] return jsonify({months: months, avg_salaries: salaries})5.3 部署到树莓派打造离线求职分析站树莓派4B4GB内存完美运行此系统1. 安装Raspberry Pi OS Lite2.sudo apt install python3-pip sqlite33.pip3 install flask requests jieba pyecharts pandas4. 将项目拷贝到/home/pi/job-analyzer/5. 设置开机自启sudo systemctl edit --force --full job-analyzer.service[Unit] DescriptionJob Analyzer Service Afternetwork.target [Service] Typesimple Userpi WorkingDirectory/home/pi/job-analyzer ExecStart/usr/bin/python3 /home/pi/job-analyzer/appJob.py Restartalways [Install] WantedBymulti-user.targetsudo systemctl daemon-reload sudo systemctl enable job-analyzer sudo systemctl start job-analyzer亲测效果树莓派上jobs.db达80MB20万条时首页加载仍1.5秒热力图渲染流畅。放在HR办公室接显示器就是一台专属分析终端。这套工具包我坚持不加任何云服务、不连外部API除百度地图坐标补全外、不依赖复杂中间件——因为它本就不该是个工程奇迹而该是你书桌上的一个实用工具。就像一把好用的螺丝刀不需要说明书拿起来就知道怎么拧紧那颗松动的螺丝。现在它就在你电脑里等着你输入第一个城市名生成第一张词云。本文还有配套的精品资源点击获取简介一个即装即用的求职岗位数据分析工具用Python开发基于Flask搭建Web界面SQLite本地存储岗位和用户数据。内置自动爬虫JxSpider.py可抓取主流招聘平台的职位信息支持按城市、行业、公司、薪资、福利等多维度筛选与统计。首页展示关键词词云、岗位地域热力图、职位类型饼图、福利待遇柱状图、经验学历分布等可视化图表所有图表由pyecharts渲染。系统含完整用户体系注册登录、密码管理、数据权限隔离。前端页面齐全包括搜索页、详情汇总页、福利分析页、行业分类页、地区分布页等后端模块清晰分离含数据模型JobModel.py、数据库操作封装query.py、中文分词与词云生成工具word_cloud_util.py。附带初始化SQL文件jobs.sql/user.sql和采集逻辑说明依赖仅需Flask、requests、jieba、pyecharts、pandas等常见库无复杂配置运行appJob.py即可启动本地服务。本文还有配套的精品资源点击获取