news 2026/10/10 9:09:32

Django招聘数据分析系统:爬虫采集到可视化全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Django招聘数据分析系统:爬虫采集到可视化全流程

1. 为什么选这个题目:招聘网站里的"人才需求数据金矿"

1.1 说句实话,最初是受毕业设计题目清单刺激的

每年毕业设计选题季,信息类专业的学生都会收到一张很长的题目清单。我扫了一圈,最常见的是图书管理系统、宿舍管理系统、学生信息管理,这些题目不能说没用,但做完除了熟悉一遍增删改查,基本学不到什么。

我当时换了个思路:什么系统做完之后能回答一些"真问题"?正好那阵子在帮几个学弟准备简历,发现大家根本不清楚企业到底需要大数据方向的毕业生掌握哪些技能。招聘网站上的岗位描述,其实就是企业用真金白银投出来的"需求说明书",只是数据散落在页面里,没有人系统地把它收集、清洗、分析出来。这个题目就定了:做一个Django系统,从招聘网站抓取大数据相关岗位,分析市场需求并用数据大屏展示。整个项目从爬虫一路做到可视化,前后花了我三周时间,中间踩了不少坑。如果你也在找Django项目实战的方向,或者毕业设计想做点跟就业市场真正结合的东西,这篇文章应该能帮上忙。

1.2 开工前先把"要回答的问题"钉死

很多项目做到一半烂尾,不是因为技术不行,而是需求一直漂。我开工前花了半天,把系统最终要回答的问题写在项目文档首页:

  • 大数据方向岗位在哪些城市最集中?数量占比如何?
  • 企业要求频率最高的20个技能词是什么?
  • 算法工程师、数据分析师、大数据开发、数据运维这几类岗位,技能要求差异到底在哪里?
  • 不同城市、不同经验年限、不同学历对应的薪资中位数是多少?

这四个问题看着简单,实际上决定了后面数据库表怎么建、爬虫抓哪些字段、可视化页面放哪些图表。每一条都能拉出一张分析报表,而且数据来源都是招聘网站的原始字段,不需要额外造数据。我特别想强调一点:不要一上来就写代码,先把"这个系统要给谁看、回答什么问题"用文字写清楚,后面所有工作都是在给这份需求文档补细节。

2. 技术选型:为什么Django是这类项目的"省心之选"

2.1 Django比Flask更适合这个场景的理由

我见过有人用Flask做类似的招聘分析系统,也见过有人用前后端分离的方案。Flask轻是真轻,但页面一多,路由、模板继承、数据库迁移、后台管理全得自己拼。我们这个项目虽然不大,功能面却挺全:有爬虫模块、有数据模型、有统计接口、有可视化页面,还得登录后台看原始数据表。

Django的MTV框架把这些事情都安排好了:

  • 自带ORM,建表、查询、迁移一条龙,不用写原生SQL,数据模型一改,makemigrations自动生成迁移文件
  • Admin后台免费送,导入原始Job数据后,直接在后台筛选查看,省去写管理页面的工作量
  • URL路由和模板引擎成熟,页面结构清晰,模板继承能避免大量重复的HTML
  • session/auth机制是保底的,后面想加"管理员登录"功能,也就是几行配置的事

有人可能觉得Django重,"重"在这个项目里恰恰是优点。三周时间要出完整系统,Django的默认功能帮你省下的时间,远比学它配置所花的时间多。对比Flask+ECharts的常见组合,Django这边只是把接口从render变成JsonResponse,前端图表那一套逻辑完全通用。

2.2 采集层:requests + BeautifulSoup为主,Selenium兜底

数据是系统的地基。招聘网站的页面分两类:一类是服务端渲染的静态HTML,一类是前端JS动态渲染的页面。我的方案很直接:

  • 首选requests + BeautifulSoup:请求快、解析稳定,适合列表页和大部分详情页
  • Selenium只在遇到JS动态渲染、直接用requests拿不到数据时补充使用
  • 实测下来,80%的页面用静态解析就能搞定

这里有个重要提醒:写爬虫前先花半小时用浏览器开发者工具看目标页面的HTML结构,确定数据藏在哪个标签里,是直接在HTML里还是通过JS接口异步返回。很多人一上来就写代码,结果解析规则反复改,浪费大量时间。另外,采集务必限定在合法合规、公开数据的范围内,做好请求频率控制,别把目标站点搞崩,更不要尝试任何绕过正常访问限制的操作。

2.3 分析与可视化:pandas负责清洗,ECharts负责展示

分析层我用的是python + pandas + jieba这条经典组合。pandas处理清洗和聚合,jieba处理中文分词提取技能词。可视化层选了ECharts,核心原因有两点:

  • 图表类型足够多:柱状图、折线图、词云、雷达图全覆盖,正好对应毕业设计里"数据大屏"的需求
  • 配置项成熟、社区例子多,遇到不会的图表,搜一下就有现成代码能改

我见过一个项目把所有分析都丢给数据库SQL写,分析指标一变就要改SQL改接口,非常痛苦。正确的分工是:pandas做一次性的清洗和聚合,把结果存成新的统计表,Django只负责读取统计结果转JSON给前端。这样职责清晰,改图表时根本不用动原始数据。

3. 数据库建模与爬虫采集:数据地基决定上层分析

3.1 models设计:把字符串拆成数字字段

建表是整个项目里最需要想清楚的一步。我第一版把薪资直接存成字符串"10-15K",结果分析时傻眼了,还得回来重新解析。第二版改成拆开存,后面所有计算都顺畅了。当时的核心模型大概是这样的:

# app/models.py from django.db import models class Company(models.Model): name = models.CharField(max_length=128, unique=True) industry = models.CharField(max_length=64, blank=True) scale = models.CharField(max_length=32, blank=True) def __str__(self): return self.name class Job(models.Model): title = models.CharField(max_length=128, db_index=True) company = models.ForeignKey(Company, on_delete=models.CASCADE) city = models.CharField(max_length=32, db_index=True) salary_min = models.IntegerField(default=0) # 单位:K salary_max = models.IntegerField(default=0) experience_min = models.IntegerField(default=0) experience_max = models.IntegerField(default=0) education = models.CharField(max_length=16, blank=True) skills = models.TextField(blank=True) description = models.TextField(blank=True) publish_date = models.DateField(null=True, blank=True) source = models.CharField(max_length=32, blank=True) created_at = models.DateTimeField(auto_now_add=True) class SkillStat(models.Model): skill = models.CharField(max_length=64, db_index=True) job_type = models.CharField(max_length=32, db_index=True) count = models.IntegerField(default=0)

几个关键设计决策:

  • salary_min/salary_max用IntegerField,单位统一为K,这样排序、分组、聚合都能直接用,不用再做字符串转换
  • experience_min/experience_max同理,拆成数字
  • company单独建表,通过外键关联,方便统计"哪些公司在大量招大数据人才"
  • skills字段存原始文本,留给后面做技能词挖掘
  • publish_date建索引,做增量更新时按日期筛选
  • SkillStat是为了可视化专门生成的统计表,不要把分析结果混进原始表,这是避免数据混乱的好习惯

这里插一句Django执行查询时容易出事的点:删除对象。测试时我习惯用Job.objects.all().delete()清空表,但好几次因为外键约束报错,原因就是Company表里还有关联数据。规范做法是从被引用的外键表开始删:先删Job,再删Company。如果只想删满足条件的记录,务必先看一眼filter条件,别把全表删了。

3.2 爬虫主流程和字段解析

爬虫主流程本身不复杂:请求列表页,抽出每个岗位的详情链接,再逐个请求详情页抽字段。这里贴一个简化版本:

import random import time import requests from bs4 import BeautifulSoup UA_LIST = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', ] def fetch_page(url, session): headers = {'User-Agent': random.choice(UA_LIST)} resp = session.get(url, headers=headers, timeout=10) resp.raise_for_status() return resp.text def parse_job_list(html): soup = BeautifulSoup(html, 'html.parser') links = [] for a in soup.select('a.job-title'): href = a.get('href') if href and not href.startswith('javascript'): links.append(href) return links def crawl_pages(start_urls): session = requests.Session() for url in start_urls: html = fetch_page(url, session) for job_url in parse_job_list(html): detail_html = fetch_page(job_url, session) # 这里交给解析函数,把字段写入Job表 time.sleep(random.uniform(1, 3))

动态渲染的页面用Selenium时,主要是等待元素出现、取page_source再交给同一套解析逻辑。这个抽取方案可维护性很高的地方在于:解析函数只收HTML字符串,不关心它从哪来,requests还是Selenium都无所谓。

真正的难点在薪资解析。招聘页面里薪资表达方式五花八门:"10-15K"、"1.5-2万"、"面议"、"20K以上"。我写了一个统一函数:

import re def parse_salary(text): """把薪资文本转成(月薪下限K, 月薪上限K),无效返回(0,0)""" if not text or '面议' in text: return 0, 0 text = text.strip().upper().replace('万', 'K') nums = re.findall(r'[\d.]+', text) if not nums: return 0, 0 # 凡是出现'万'统统按倍数换算成K,1.5万 => 15K factor = 10 if '万' in text else 1 values = [round(float(n) * factor) for n in nums] if len(values) >= 2: return min(values[:2]), max(values[:2]) return values[0], values[0]

注意这里"月薪1.5万"和"年薪18万"是不同的。招聘网站主流的默认口径是月薪,所以我只处理月薪场景。另外像"15K-25K·14薪"这种带薪结构的数据,在需求分析场景里优先级不高,暂时忽略,分析阶段过滤掉即可。

3.3 清洗去重:一份能用的数据集是怎么来的

采集完成后,原始数据大概会有几个问题:重复、城市叫法不一、学历写法不一、技能大小写不一。我的清洗流水线是这样的:

  1. 指纹去重:用(title + company.name + publish_date)生成MD5,存一个unique字段,同一岗位重复抓到就跳过
  2. 城市归一:"北京"、"北京市"、"北京·朝阳区"全部归成"北京"
  3. 学历归一:"本科及以上"归成"本科","大专及以下"归成"大专"
  4. 技能归一:Python、python、PYTHON统一转小写再入库
  5. 经验字段确认格式:把"3-5年"解析成min=3、max=5,"不限"则min=max=0单独标记

清洗这一步直接决定分析结论靠不靠谱。我第一版跳过清洗,直接用原始数据跑词频,结果"北京"和"北京市"被当成两个实体统计,图表特别难看。花两小时做清洗,能省下后面十倍返工时间。

4. 需求分析核心:从岗位描述到人才画像

4.1 岗位归族:别用原始职位名做统计

"大数据方向"的职位名五花八门:大数据开发工程师、数据挖掘工程师、数据分析师、算法工程师、BI工程师、数仓开发……如果直接拿原始title做统计,光"数据分析师"和"高级数据分析师"就会被当成两个岗位。我的做法是写一个简单的归族规则:

def normalize_job(title): if any(w in title for w in ['算法', '机器学习', '深度学习']): return '算法工程师' if any(w in title for w in ['分析', 'BI', '挖掘']): return '数据分析师' if any(w in title for w in ['数仓', 'ETL', '开发']): return '数据开发' if any(w in title for w in ['运维', '平台']): return '数据运维' return '其他'

规则不需要复杂,能覆盖80%的岗位就行。归族之后,所有统计都基于规范化后的岗位类型,图表才具备可比性。这个"先定字典、再做映射"的思路,在处理任何非结构化文本时都适用。

4.2 技能词挖掘:词频统计前的两个关键准备

技能词是招聘描述里的核心信息。直接用jieba切全文本,然后统计词频,效果会很差:因为"spark"可能被切成"spar"和"k","tensorflow"可能完全没切出来。正确做法是先建一个领域技能词表,再做匹配统计:

import jieba from collections import Counter SKILL_WORDS = [ 'python', 'java', 'scala', 'sql', 'hadoop', 'spark', 'flink', 'kafka', 'hive', 'mysql', 'linux', 'docker', 'kubernetes', 'tensorflow', 'pytorch', 'redis', 'elasticsearch', 'airflow', ] for w in SKILL_WORDS: jieba.add_word(w) def extract_skills(text): words = jieba.lcut(text.lower()) skill_set = set(SKILL_WORDS) # 匹配时只关心技能词表里的词 return [w for w in words if w in skill_set] # 示例:统计某个岗位族的所有技能词频 skill_counter = Counter() for desc in job_type_df['description']: skill_counter.update(extract_skills(desc))

这一步的产出是一个技能词频表,对应前面模型里的SkillStat。我最终统计出的排名前20技能,和行业直觉基本一致:SQL、Java、Spark、Hadoop、Python是高频技能。但拆到岗位族层面差异就出来了:算法岗位的TensorFlow/PyTorch频次高,数据开发岗位的Kafka/Flink频次高,这正是岗位画像的价值。

4.3 薪资分析:为什么用中位数而不是平均值

很多人做薪资分析,直接groupby城市求薪资中值的均值。这样做有坑:招聘网站上有些岗位标着"50K以上",如果解析函数没过滤,就会把一个异常大值带进均值计算,把结果拉得离谱。我改用中位数:

import pandas as pd df['salary_mid'] = (df['salary_min'] + df['salary_max']) / 2 # 过滤无效薪资和明显异常值 df = df[(df['salary_mid'] > 0) & (df['salary_mid'] < 100)] city_salary = df.groupby('city')['salary_mid'].median().sort_values(ascending=False)

经验年限同理:把"3-5年"转成中值4年,再按0-1、1-3、3-5、5年以上四档分组,看每个档位的薪资中位数曲线。最后我生成了一张"经验-薪资"折线图,走势基本符合市场认知:0-1年起步较低,3-5年出现明显跃升,5年以上涨幅趋缓。这个结果能从侧面验证数据清洗做得到位。

4.4 画像矩阵:岗位、技能、薪资怎么关联

为了回答"不同岗位的技能要求差异",我构造了一个"岗位类型×技能词"的频次矩阵。行是岗位族,列是技能词,单元格是岗位描述里出现该技能词的次数。pandas的crosstab一行就能搞定:

matrix = pd.crosstab(df['job_type'], df['skill'])

这个矩阵既能直接作为雷达图的数据源:选出某个岗位族频次最高的5个技能画五维雷达图;也是后面做"简历匹配"功能的底座。矩阵数据写入SkillStat表后,Django视图只需要按job_type过滤,转成JSON给前端,整条分析链路就闭环了。

5. 可视化大屏:Django里塞ECharts的正确姿势

5.1 大屏布局与页面结构

大屏页面我没用重型前端框架,就是简单的Grid布局,加一点CSS。页面结构分两层:

  • 顶部一行指标卡片:岗位总量、技能词种类数、覆盖城市数、最新采集时间
  • 中部四块图表区:城市岗位Top10柱状图、技能词云、岗位类型占比饼图、经验-薪资折线图

指标卡片的数据在Django视图里统计完后直接塞进模板上下文,图表数据则走异步接口。这样页面首次渲染只要几毫秒,图表数据回来后再绘制,视觉上的加载体验也更好。

5.2 视图返回JSON,模板只负责初始化图表

一开始我也习惯在模板里用for循环渲染表格数据,然后让ECharts去读data属性,折腾半天发现数据格式不对。后来改成标准做法:Django视图返回JsonResponse,前端用fetch请求。

from django.http import JsonResponse from django.views import View from app.models import SkillStat class SkillChartData(View): def get(self, request): rows = list(SkillStat.objects .filter(count__gt=0) .order_by('-count')[:100] .values('skill', 'count')) return JsonResponse({'data': rows})

模板对应部分:

<div id="skillCloud" style="width:100%;height:360px;"></div> <script> fetch('/api/skill_chart/') .then(res => res.json()) .then(result => { const chart = echarts.init(document.getElementById('skillCloud')); chart.setOption({ series: [{ type: 'wordCloud', data: result.data }] }); }); </script>

这里有个容易踩的坑:Django模板引擎会尝试解析{{ }},而ECharts的option里大括号非常多,两者混在同一个script标签里很容易出问题。我的解决方案就是让模板里不直接出现数据,所有数据都走JSON接口,模板里的script块只写init和setOption逻辑,彻底绕开冲突。

5.3 ECharts引入方式与CDN注意事项

ECharts可以通过static目录放本地文件,也可以引CDN。我建议下载到static/js/echarts.min.js,避免演示现场网络出现问题。如果要做技能词云,还需要额外的echarts-wordcloud插件,记得一并下载到static目录。

页面加载性能方面,图表接口返回的都是之前聚合好的统计表,量级在几百行以内,前端秒开。真正的性能瓶颈是原始Job表查询,比如后台管理列表页。所以优化重点放在:原始表页面用分页器、加索引,统计表查询不做多余关联。这里顺便提一句,如果你在Django模板里用{% static %}标签引用文件,务必在settings里把STATICFILES_DIRS配好,这是新手最容易卡住的地方之一。

6. 开发中踩过的坑:从ORM到爬虫的完整复盘

6.1 N+1查询:循环里取外键对象的大坑

岗位列表页最初慢得离谱。一查原因,是我在for循环里逐条访问job.company.name,每条记录都要多一次SQL查询,50条数据就是51次查询。用Django提供的select_related解决:

jobs = Job.objects.select_related('company').all()[:100]

这个改动把响应时间从秒级降到毫秒级。凡是涉及外键且要展示关联表字段的地方,都要检查是否加了select_related。如果碰到的是多对多关系,则用prefetch_related,原理类似但实现不同。

6.2 爬虫被限制时的合理应对

采集过程中遇到被限制的情况是常态,我的应对经验全部建立在合法合规、非侵入式访问的前提下:

  • 请求头伪装:不要用默认的python-requests UA,准备一个常见浏览器的UA池随机切换
  • 请求频率控制:每次请求间sleep 1-3秒随机延时,模拟人工浏览节奏
  • 失败重试:requests配好Retry机制,连续失败5次以上就自动暂停,防止死循环
  • 错峰采集:每天固定凌晨时段跑增量采集,避开平台流量高峰

这些操作的意义是让爬虫表现得像一个普通访客,而不是"绕边界"。采集范围严格限定在自己做学术分析所需的公开信息,不采集任何需要登录才能看到的私有数据,不批量下载文件,不影响平台正常运行。这个边界必须守住。

6.3 删除对象和自增ID重置的细节

前面提过Django删除对象时要小心外键关联,这里再说一个容易被忽略的点:delete()不会重置自增ID。测试时清空表后,新增数据的ID会接着上次继续涨,演示时前几条数据ID从100多开始,观感不好。

SQLite下重置ID序列的办法是:

from django.db import connection with connection.cursor() as cursor: cursor.execute("DELETE FROM sqlite_sequence WHERE name='app_job'")

MySQL则是直接TRUNCATE TABLE。不同数据库语法不一样,写测试脚本时要注意区分环境。如果不重置也行,就是纯观感问题,不影响功能。

6.4 MySQL环境下的编码坑

如果数据库选了MySQL而不是SQLite,建库的时候务必指定utf8mb4字符集。我最初用默认utf8,存技能词里带特殊符号的岗位描述时直接报错:Incorrect string value。统一改成utf8mb4之后就正常了。

另外,如果要做中文检索,LIKE '%技能%'在数据量大时会全表扫描,性能很差。在学校项目这个量级倒是勉强能用,但要清楚这个限制在哪。真要升级,可以把数据库换成PostgreSQL,用Django的全文搜索能力,或者提前把技能词拆好存进单独的索引表。

7. 项目扩展的三个落地方向:从数据分析走向数据服务

7.1 方向一:用现有数据做薪资预测接口

系统里已经积累了城市、岗位类型、技能、经验、学历这些特征,加上薪资标签,完全可以训练一个简单的回归模型预测薪资区间。具体落地方式:

  • 用pandas把特征编码成数值型,比如城市做one-hot,学历按高低映射成0、1、2
  • 用scikit-learn的RandomForestRegressor或简单的线性回归训练,效果不要求多高,能体现趋势就行
  • 训练完用joblib.dump保存模型文件
  • Django里写一个POST接口,接收技能、城市、经验参数,加载模型返回预测结果

这个方向的好处是代码量不大,但项目一下子从"统计展示"升级成"智能预测",答辩时的亮点完全不同。

7.2 方向二:简历匹配度和技能缺口评估

现有数据已经有岗位画像矩阵,把一份简历文本做同样的分词和技能词提取,然后和岗位族画像做匹配,就能算出一个覆盖度百分比。具体实现:

  • 把SkillStat表读进来,构造"技能词→岗位族权重"字典
  • 简历文本用同一套extract_skills提取技能词
  • 命中技能的权重和除以岗位全部技能权重和,得出匹配度

这个功能对求职者非常有价值:可以把自己简历放进去,看看离目标岗位还差哪些技能,也就是"技能缺口"。从单纯展示市场数据,到直接给用户提供个性化建议,产品价值立刻不一样。

7.3 方向三:多数据源融合和定时增量采集

目前系统接的是一两个招聘网站的数据,如果增加更多数据源,可以做一个交叉验证:同一个岗位在两个平台的薪资是否有差异,某个城市岗位总量的趋势是否一致。这样系统输出的分析结果会更可靠。

配套要做的是定时任务。在Django里写一个management command,然后用操作系统的定时任务每天凌晨执行一次增量抓取和重算统计表:

# app/management/commands/update_jobs.py from django.core.management.base import BaseCommand class Command(BaseCommand): help = '增量抓取招聘数据并重算统计' def handle(self, *args, **options): from app.services import run_incremental_crawler, recompute_stats run_incremental_crawler() recompute_stats() self.stdout.write(self.style.SUCCESS('数据更新完成'))

命令行里执行python manage.py update_jobs,就能完成一次完整的采集、清洗、重算流程。把这个命令挂进系统的定时任务,系统就变成一个每天自动更新的"人才需求监测站"。

说到底,这类"爬虫+存储+分析+展示"的项目,核心收获不是某一个函数写得漂亮,而是把一条完整的数据链路跑通:从网页到数据库,从数据库到统计表,从统计表到图表。这个过程里建立的工程直觉,比任何单独的课程知识点都更值钱。以后再做数据类项目,你自然会先问:数据从哪来、要回答什么问题、清洗规则是什么、结果怎么呈现。这四个问题想清楚了,选什么框架反而变成了次要的事。

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

基于PageRank的社交网络用户分析与预测flask框架机器学习算法

✅源码获取&#xff1a; &#x1f345;--------------------【点击左上方头像&#xff0c;在置顶文章上方的wx】联系我们-------------&#x1f345;✌网站介绍&#xff1a;✌10年项目辅导经验、专注于计算机技术领域学生项目实战辅导。✌服务范围&#xff1a;大数据、机器学习…

作者头像 李华
网站建设 2026/10/10 9:07:22

SpringBoot报刊厅书刊订购系统:从数据库设计到答辩的全流程解析

做计算机毕业设计&#xff0c;最怕的不是技术难&#xff0c;而是方向太虚。这个SpringBoot报刊厅实体书刊订购系统&#xff0c;表面上是把线下报刊亭“搬上线”&#xff0c;实际里面牵扯到期刊多期订阅、现货库存扣减、配送单据生成、用户权限分配一整套业务闭环&#xff0c;复…

作者头像 李华