ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Django+Python+ECharts招聘数据可视化实战

Django+Python+ECharts招聘数据可视化实战 简介这是一份面向高校计算机专业学生与Python Web开发初学者的招聘数据可视化实战项目源码聚焦Django后端框架、Python数据处理与ECharts前端图表库的协同应用适用于期末大作业、课程设计及毕业设计选题参考。资源包共537个文件包含40个核心Python脚本含数据爬取、清洗、分析逻辑、109个HTML页面含Django模板、102个JS文件封装ECharts配置与交互逻辑、24个CSS样式文件及多种静态资源SVG、GIF、ICO等整体压缩包32.32MB结构完整、模块清晰开箱即部署。已有396人学习下载项目已获96分高分通过提供从原始数据接入、Django视图路由、数据库建模到多维度图表渲染的全流程实现涵盖职位分布热力图、薪资区间柱状图、城市岗位占比环形图等典型可视化场景附带venv环境激活脚本activate.bat、start.bat与Scrapy配置文件便于快速复现与二次开发。1. 用 Django Python ECharts 做招聘数据可视化不是交作业是练出「数据闭环」硬功夫你交的期末大作业.zip解压后跑不起来模板里写死的 JSON 数据改了就报错ECharts 图表加载空白、控制台只有一行Uncaught ReferenceError: echarts is not defined别急——这不是代码有 bug而是你还没真正理解这个组合的底层协作逻辑Django 是数据管道的调度中枢Python 是清洗与聚合的引擎ECharts 是前端渲染的画笔三者缺一不可但又各自承担不可替代的职责。这套方案真正价值不在“能画图”而在于它逼你亲手打通「原始招聘 Excel → 后端清洗 → API 接口 → 前端动态渲染 → 用户交互反馈」的完整链路。适合正在学 Web 开发、但对“数据怎么从数据库走到浏览器”还停留在return render(request, index.html, context)阶段的同学也适合想快速验证招聘平台用户画像、岗位热度、薪资分布等业务假设的 HR 或运营同学——只要你会改几行 Python 和 JS就能把静态报表变成可下钻、可筛选、可导出的决策看板。它不追求高并发或微服务架构但每一步都踩在真实项目落地的关节上比如 Django 的 QuerySet 如何避免 N1 查询拖垮页面Python 的 Pandas 分组统计怎样保留原始字段语义ECharts 的 dataset 与 series 如何配合实现“点击城市自动刷新岗位列表”这种轻量级交互。下面我们就从零开始把这份“期末作业”真正跑通、调优、用起来。2. 搭建最小可行环境避开 pip 版本陷阱与 Django 静态资源黑洞2.1 环境隔离与版本锁定为什么pip install django会翻车很多同学解压源码后直接pip install -r requirements.txt结果卡在django.core.exceptions.ImproperlyConfigured: Requested setting INSTALLED_APPS, but settings are not configured.。这不是配置错了而是Django 4.x 默认启用ASGI模式但老项目settings.py里可能还残留WSGI_APPLICATION myproject.wsgi.application且未声明ASGI_APPLICATION。更隐蔽的是 Python 版本兼容性ECharts 官方推荐使用pyecharts2.0.0基于 v2 渲染引擎但它依赖jinja23.0而 Django 3.2 要求jinja23.1——直接pip install会触发版本冲突。提示不要用系统自带 Python用pyenv或conda创建独立环境命令如下# 推荐用 conda对中文路径和 Windows 更友好 conda create -n recruitviz python3.9 conda activate recruitviz pip install Django3.2,4.0 pandas1.3 openpyxl3.0 pyecharts2.0.22.2 静态资源路径生死线STATIC_URL与STATICFILES_DIRS的真实含义源码里常看到STATIC_URL /static/但前端引用echarts.min.js时写script src/static/js/echarts.min.js/script却 404。原因在于Django 的collectstatic不是“复制文件”而是“按规则搬运”。STATICFILES_DIRS是开发时告诉 Django“这些目录里的文件你启动时要能直接 serve”而STATIC_ROOT是生产部署时collectstatic打包的目标目录。新手常误把echarts.min.js放进myapp/static/myapp/js/却忘了在settings.py中注册该 app 的静态路径# settings.py INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, recruit, # 你的招聘分析 app 名 ] # 关键必须显式添加 app 的 static 目录到 STATICFILES_DIRS import os BASE_DIR Path(__file__).resolve().parent.parent STATIC_URL /static/ STATICFILES_DIRS [ BASE_DIR / static, # 项目根目录下的 static放全局 js/css BASE_DIR / recruit / static, # app 自带的 static放业务 js ] STATIC_ROOT BASE_DIR / staticfiles # 生产打包目标开发时不用管验证方法启动开发服务器后访问http://127.0.0.1:8000/static/js/echarts.min.js能直接下载文件即成功。2.3 数据库初始化用 SQLite 快速启动但必须绕开makemigrations的坑招聘数据通常来自 Excel 或 CSV源码里可能直接用pandas.read_excel()加载但若需持久化如用户上传新数据必须建模型。常见错误是python manage.py makemigrations报错No changes detected即使你刚在models.py里写了JobPost(models.Model)。这是因为Django 只跟踪INSTALLED_APPS列表中已注册的 app 的模型变更。检查INSTALLED_APPS是否包含你的 app 名如recruit且recruit/models.py内容类似# recruit/models.py from django.db import models class JobPost(models.Model): title models.CharField(max_length200) company models.CharField(max_length100) city models.CharField(max_length50) salary_min models.IntegerField() salary_max models.IntegerField() experience models.CharField(max_length50) # 如 1-3年 education models.CharField(max_length30) # 如 本科 publish_date models.DateField() class Meta: db_table job_post # 显式指定表名避免 migration 冲突然后执行python manage.py makemigrations recruit # 必须指定 app 名 python manage.py migrate注意SQLite 在 Windows 下路径含空格或中文会报错unable to open database file。解决方案settings.py中DATABASES配置改为绝对路径import os BASE_DIR Path(__file__).resolve().parent.parent DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, # 确保路径无空格、无中文 } }3. 数据清洗与 API 设计用 Pandas 做真实招聘数据的“脏活”3.1 从 Excel 到 DataFrame处理常见脏数据的 4 个必做动作招聘 Excel 表常含合并单元格、空行、薪资格式混乱如 “15K-25K”、“面议”、“15000-25000元/月”。直接pd.read_excel()会崩。正确流程是# recruit/utils.py import pandas as pd import re def clean_salary(text): 提取薪资数字单位统一为 元/月面议返回 None if pd.isna(text) or 面议 in str(text): return None # 匹配数字范围如 15K-25K → [15000, 25000] match re.search(r(\d\.?\d*)[kK]?\s*[-~—]\s*(\d\.?\d*)[kK]?, str(text)) if match: low, high float(match.group(1)), float(match.group(2)) return (int(low * 1000), int(high * 1000)) if k in str(text).lower() else (int(low), int(high)) # 单值如 20000 或 2万 single_match re.search(r(\d\.?\d*)[万kK]?, str(text)) if single_match: val float(single_match.group(1)) return (int(val * 10000), int(val * 10000)) if 万 in str(text) else (int(val), int(val)) return None def load_and_clean_data(file_path): df pd.read_excel(file_path, skiprows1) # 跳过标题行 # 1. 删除全空行 df df.dropna(howall) # 2. 重命名列适配不同 Excel 表头 df.columns df.columns.str.strip() rename_map { 职位名称: title, 公司名称: company, 工作地点: city, 薪资: salary_raw, 经验要求: experience, 学历要求: education, 发布时间: publish_date } df df.rename(columnsrename_map) # 3. 清洗薪资 salary_series df[salary_raw].apply(clean_salary) df[salary_min] salary_series.apply(lambda x: x[0] if x else None) df[salary_max] salary_series.apply(lambda x: x[1] if x else None) # 4. 标准化城市去除“市”“区”等冗余 df[city] df[city].str.replace(r[市区县], , regexTrue).str.strip() return df3.2 构建 RESTful API用 Django View 而非 DRF轻量够用期末作业无需复杂权限用函数视图即可。关键点JSON 响应必须序列化日期和 NaN且避免循环引用# recruit/views.py from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from .utils import load_and_clean_data import json import numpy as np csrf_exempt def get_job_stats(request): if request.method ! GET: return JsonResponse({error: Only GET allowed}, status405) try: # 读取 Excel实际项目应存入 DB此处演示文件读取 file_path data/jobs.xlsx # 放在项目根目录 data/ 下 df load_and_clean_data(file_path) # 统计各城市岗位数 city_count df[city].value_counts().to_dict() # 薪资分布分段统计 salary_bins [0, 5000, 10000, 15000, 20000, 30000, 50000, 100000] salary_labels [5K, 5-10K, 10-15K, 15-20K, 20-30K, 30-50K, 50K] df[salary_range] pd.cut(df[salary_max], binssalary_bins, labelssalary_labels, include_lowestTrue) salary_dist df[salary_range].value_counts().sort_index().to_dict() # 返回结构化 JSON处理 NaN 和日期 result { city_count: city_count, salary_distribution: salary_dist, total_jobs: len(df), avg_salary: round(df[salary_max].mean(), 0) if not df[salary_max].isna().all() else 0 } # 手动序列化避免 json.dumps 报错 return JsonResponse(result, encoderCustomJSONEncoder) except Exception as e: return JsonResponse({error: str(e)}, status500) class CustomJSONEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, np.integer): return int(obj) elif isinstance(obj, np.floating): return float(obj) elif isinstance(obj, np.ndarray): return obj.tolist() elif isinstance(obj, pd.Timestamp): return obj.strftime(%Y-%m-%d) return super().default(obj)3.3 URL 路由与跨域调试开发阶段如何让前端 AJAX 正常请求urls.py中必须注册该视图# recruit/urls.py from django.urls import path from . import views urlpatterns [ path(api/job-stats/, views.get_job_stats, namejob_stats), ]# myproject/urls.py from django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(, include(recruit.urls)), # 主页路由 ]注意开发时前端在http://localhost:3000Django 在http://127.0.0.1:8000AJAX 会因跨域被拦截。临时解决仅开发pip install django-cors-headerssettings.py中INSTALLED_APPS [corsheaders] MIDDLEWARE.insert(0, corsheaders.middleware.CorsMiddleware) CORS_ALLOW_ALL_ORIGINS True # 仅开发生产必须设为白名单4. ECharts 前端集成告别“复制粘贴 demo”掌握动态数据绑定4.1 初始化 ECharts 实例DOM 尺寸与异步加载的先后逻辑常见错误HTML 里写div idchart stylewidth:600px;height:400px;/divJS 中echarts.init(document.getElementById(chart))却报Cannot read property getAttribute of null。原因是DOM 元素未加载完成就执行 JS。正确写法!-- templates/index.html -- !DOCTYPE html html head meta charsetutf-8 title招聘数据可视化/title !-- ECharts CDN确保在 body 之前加载 -- script srchttps://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js/script /head body div idcityChart stylewidth: 100%; height: 500px;/div !-- JS 放在 body 底部确保 DOM 已就绪 -- script // 等待 DOM 加载完成 document.addEventListener(DOMContentLoaded, function() { const chartDom document.getElementById(cityChart); if (!chartDom) return; const myChart echarts.init(chartDom); // 发起 AJAX 获取数据 fetch(/api/job-stats/) .then(response response.json()) .then(data { // 构造 ECharts option const option { title: { text: 各城市岗位数量分布 }, tooltip: { trigger: item }, legend: { top: bottom }, series: [{ name: 岗位数, type: pie, radius: [40%, 70%], avoidLabelOverlap: false, itemStyle: { borderRadius: 10, borderColor: #fff, borderWidth: 2 }, label: { show: true, formatter: {b}: {c} 个 }, data: Object.entries(data.city_count).map(([city, count]) ({ name: city, value: count })) }] }; myChart.setOption(option); }) .catch(error console.error(获取数据失败:, error)); }); /script /body /html4.2 动态更新图表用setOption实现“点击城市切换岗位列表”ECharts 的核心优势是响应式更新。例如左侧饼图点击某城市右侧柱状图显示该城市热门岗位。关键在myChart.on(click, ...)事件监听与setOption的增量更新// 绑定饼图点击事件 myChart.on(click, function (params) { const selectedCity params.name; // 发起新请求获取该城市的岗位详情 fetch(/api/job-by-city/?city${encodeURIComponent(selectedCity)}) .then(response response.json()) .then(data { // 更新右侧柱状图假设 DOM ID 为 jobListChart const listChart echarts.getInstanceByDom(document.getElementById(jobListChart)); if (listChart) { listChart.setOption({ title: { text: ${selectedCity} 热门岗位 Top 10 }, xAxis: { type: category, data: data.jobs.map(j j.title) }, yAxis: { type: value }, series: [{ data: data.jobs.map(j j.count), type: bar, label: { show: true } }] }); } }); });后端对应视图views.pydef get_jobs_by_city(request): city request.GET.get(city) if not city: return JsonResponse({error: 缺少 city 参数}, status400) # 从数据库查该城市岗位统计简化版 from .models import JobPost jobs JobPost.objects.filter(citycity).values(title).annotate(countmodels.Count(id)).order_by(-count)[:10] return JsonResponse({ jobs: [{title: j[title], count: j[count]} for j in jobs] })4.3 响应式与主题让图表适配手机屏幕与暗色模式ECharts 5 原生支持响应式但需手动触发// 监听窗口大小变化 window.addEventListener(resize, () { myChart.resize(); // 必须调用 resize() }); // 暗色模式检测现代浏览器 if (window.matchMedia window.matchMedia((prefers-color-scheme: dark)).matches) { myChart.setOption({ backgroundColor: #1e1e1e, textStyle: { color: #eee } }); }5. 避坑指南那些让期末作业卡住 3 天的血泪问题5.1 现象ECharts 图表渲染后文字模糊、线条锯齿原因Canvas 渲染分辨率与设备像素比DPR不匹配尤其在 MacBook Retina 屏或高 DPI Windows 上。解决在echarts.init()时传入devicePixelRatio参数const myChart echarts.init(chartDom, null, { renderer: canvas, devicePixelRatio: window.devicePixelRatio || 1 });补充若仍模糊检查 CSS 是否对#chart设置了transform: scale(0.5)类似缩放这会导致 Canvas 像素丢失。5.2 现象fetch(/api/job-stats/)返回 403 Forbidden原因Django 的 CSRF 保护默认开启而fetch不携带 CSRF token。解决两种方案任选其一方案 A推荐在视图上加csrf_exempt仅限内部 API如本例方案 B前端从 Cookie 读取csrftoken并放入请求头fetch(/api/job-stats/, { headers: { X-CSRFToken: getCookie(csrftoken) // 需先定义 getCookie 函数 } })5.3 现象Pandasread_excel()报错xlrd.biffh.XLRDError: Excel xlsx file; not supported原因xlrd库 2.0 版本只支持.xls不支持.xlsx。解决卸载xlrd改用openpyxlpip uninstall xlrd pip install openpyxl然后pd.read_excel()会自动选用openpyxl引擎。5.4 现象Django 启动时报ModuleNotFoundError: No module named recruit原因recruitapp 目录下缺少__init__.py文件或INSTALLED_APPS中 app 名拼写错误如Recruit大写。解决确认recruit/__init__.py存在可为空文件且settings.py中INSTALLED_APPS写的是recruit小写与目录名完全一致。5.5 现象ECharts 饼图标签重叠、显示不全原因label配置未启用formatter或rotate且avoidLabelOverlap: false导致文字挤在一起。解决强制旋转标签并精简内容label: { show: true, rotate: 45, // 旋转 45 度 formatter: {b}\n{c}个, // 换行显示 fontSize: 12, lineHeight: 16 }, avoidLabelOverlap: true, // 必须设为 true6. 进阶技巧把“期末作业”升级成可交付的轻量分析工具6.1 添加数据上传功能让用户自己导入 Excel前端加上传表单form iduploadForm enctypemultipart/form-data input typefile namefile accept.xlsx,.xls required button typesubmit上传招聘数据/button /form div iduploadStatus/div后端处理views.pyfrom django.views.decorators.csrf import csrf_exempt from django.http import JsonResponse from .utils import load_and_clean_data import os csrf_exempt def upload_jobs(request): if request.method POST and request.FILES.get(file): uploaded_file request.FILES[file] # 保存到 media 目录需配置 MEDIA_ROOT file_path os.path.join(media, uploads, uploaded_file.name) with open(file_path, wb) as destination: for chunk in uploaded_file.chunks(): destination.write(chunk) try: df load_and_clean_data(file_path) # 写入数据库批量插入 job_objects [] for _, row in df.iterrows(): job_objects.append(JobPost( titlerow[title], companyrow[company], cityrow[city], salary_minrow[salary_min], salary_maxrow[salary_max], experiencerow[experience], educationrow[education], publish_daterow[publish_date] )) JobPost.objects.bulk_create(job_objects, batch_size1000) return JsonResponse({success: True, count: len(df)}) except Exception as e: return JsonResponse({error: f解析失败: {str(e)}}, status400) return JsonResponse({error: 无效请求}, status400)注意MEDIA_ROOT配置settings.pyimport os MEDIA_ROOT os.path.join(BASE_DIR, media) MEDIA_URL /media/urls.py中添加from django.conf import settings from django.conf.urls.static import static urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)6.2 实现“一键导出 PNG”用 ECharts 的getDataURL()生成图片用户常需截图汇报但手动截屏质量差。ECharts 提供原生导出document.getElementById(exportBtn).addEventListener(click, function() { const url myChart.getDataURL({ type: png, pixelRatio: 2, // 提高清晰度 backgroundColor: #fff }); const link document.createElement(a); link.href url; link.download 招聘分析_城市分布.png; link.click(); });6.3 性能优化大数据量时的分页与懒加载当岗位超 10 万条JobPost.objects.all()会内存溢出。用Paginator分页from django.core.paginator import Paginator def get_job_stats_paginated(request): page request.GET.get(page, 1) jobs JobPost.objects.all() paginator Paginator(jobs, 1000) # 每页 1000 条 page_obj paginator.get_page(page) # 仅对当前页数据做统计牺牲全局精度换性能 df pd.DataFrame(list(page_obj.object_list.values())) # ... 后续统计逻辑6.4 部署到轻量云用gunicornnginx替代runserverpython manage.py runserver仅限开发。生产部署步骤# 1. 安装 gunicorn pip install gunicorn # 2. 启动后台运行 gunicorn myproject.wsgi:application --bind 0.0.0.0:8000 --workers 2 --daemon # 3. nginx 配置/etc/nginx/sites-available/recruit upstream django_app { server 127.0.0.1:8000; } server { listen 80; server_name your-domain.com; location /static/ { alias /path/to/your/staticfiles/; } location /media/ { alias /path/to/your/media/; } location / { proxy_pass http://django_app; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }我带过 3 届学生做这类作业最深的教训是别急着写图表先花 2 小时把manage.py runserver跑通、/admin登录成功、/api/xxx返回 JSON——这三步走通后面全是填空题。很多同学卡在static404 或CSRF错误反复重装包其实只是STATICFILES_DIRS少写了一行路径。还有人把echarts.min.js放错目录层级折腾半天不如打开浏览器开发者工具 Network 标签页看哪个文件 404直接定位。这套 DjangoPythonECharts 流程表面是期末作业内核是训练你建立“数据流动”的肌肉记忆Excel 里的数字 → Python 的 DataFrame → Django 的 QuerySet → JSON API → ECharts 的 option → 浏览器的像素。当你能闭着眼改完这整条链路再遇到任何数据可视化需求第一反应不再是“百度怎么画饼图”而是“数据在哪API 怎么设计前端怎么接”——这才是技术落地的真实手感。希望帮到你。本文还有配套的精品资源点击获取
返回列表