基于Django的智能职位推荐系统设计与实现:从数据爬取到协同过滤推荐
本文详细介绍了如何使用Django框架构建一个完整的智能职位推荐系统,涵盖数据爬取、数据处理、推荐算法实现和Web界面开发的全流程。
📚 文章目录
- 一、项目背景与需求分析
- 二、技术选型与架构设计
- 三、数据库设计
- 四、数据爬取模块实现
- 五、推荐算法核心实现
- 六、Web界面开发
- 七、数据分析与可视化
- 八、系统部署与优化
- 九、项目总结与思考
一、项目背景与需求分析
1.1 项目背景
在互联网招聘快速发展的今天,求职者往往面临信息过载的问题。如何在海量职位信息中找到最适合自己的职位,成为求职者的一大痛点。本项目旨在构建一个智能职位推荐系统,通过分析用户的投递行为和求职意向,为用户提供个性化的职位推荐服务。
1.2 功能需求
- 数据获取:从招聘网站爬取职位信息
- 数据管理:职位信息的存储、查询和更新
- 智能推荐:基于用户行为提供个性化推荐
- 搜索筛选:多条件职位搜索和筛选
- 数据分析:职位数据的多维度统计分析
- 用户管理:用户注册、登录、投递记录管理
1.3 技术难点
- 反爬虫机制:招聘网站的反爬虫策略需要处理
- 推荐算法:协同过滤算法的实现和优化
- 大数据处理:海量职位数据的高效查询和存储
- 用户体验:快速响应的界面和流畅的交互体验
二、技术选型与架构设计
2.1 技术栈
后端技术
- Django 3.2.23:成熟的Python Web框架,提供完整的MVC架构
- MySQL 8.0:关系型数据库,存储结构化数据
- Selenium 4.3.0:浏览器自动化工具,用于数据爬取
- Pandas + NumPy:数据处理和分析
- PyEcharts:数据可视化
前端技术
- Layui Admin:轻量级前端UI框架
- ECharts:图表可视化库
2.2 系统架构
┌─────────────────┐
│ 用户浏览器 │
└────────┬────────┘
│ HTTP请求
┌────────▼────────┐
│ Django Web层 │
│ (Views/URLs) │
└────────┬────────┘
│
┌────┴────┐
│ │
┌───▼───┐ ┌──▼──────┐
│业务逻辑│ │推荐算法 │
└───┬───┘ └──┬──────┘
│ │
┌───▼────────▼───┐
│ Django ORM │
└───────┬───────┘
│
┌───────▼───────┐
│ MySQL数据库 │
└───────────────┘
2.3 项目目录结构
JobRecommend/
├── JobRecommend/ # 项目配置
│ ├── settings.py # 项目设置
│ └── urls.py # 路由配置
├── job/ # 主应用
│ ├── models.py # 数据模型
│ ├── views.py # 视图函数
│ ├── job_recommend.py # 推荐算法
│ └── tools.py # 爬虫工具
├── templates/ # 模板文件
└── static/ # 静态文件
三、数据库设计
3.1 数据模型设计
职位信息表 (job_data)
python
class JobData(models.Model):
job_id = models.AutoField('职位ID', primary_key=True)
name = models.CharField('职位名称', max_length=255)
salary = models.CharField('薪资', max_length=255)
place = models.CharField('工作地点', max_length=255)
education = models.CharField('学历要求', max_length=255)
experience = models.CharField('工作经验', max_length=255)
company = models.CharField('公司名称', max_length=255)
label = models.CharField('职位标签', max_length=255)
scale = models.CharField('公司规模', max_length=255)
href = models.CharField('职位链接', max_length=255)
key_word = models.CharField('关键词', max_length=255)
设计要点:
- 使用
AutoField作为主键,自动递增 - 所有字段允许为空,提高数据容错性
key_word字段用于推荐算法的关键词匹配
用户表 (user_list)
python
class UserList(models.Model):
user_id = models.CharField('用户ID', primary_key=True, max_length=11)
user_name = models.CharField('用户名', max_length=255)
pass_word = models.CharField('密码', max_length=255)
投递记录表 (send_list)
python
class SendList(models.Model):
id = models.AutoField(primary_key=True)
user = models.ForeignKey('UserList', on_delete=models.CASCADE)
job = models.ForeignKey('JobData', on_delete=models.CASCADE)
class Meta:
unique_together = (('user', 'job'),) # 防止重复投递
设计要点:
- 使用
unique_together约束防止用户重复投递同一职位 on_delete=models.CASCADE确保数据一致性
用户意向表 (user_expect)
python
class UserExpect(models.Model):
expect_id = models.AutoField(primary_key=True)
user = models.ForeignKey('UserList', models.DO_NOTHING)
key_word = models.CharField(max_length=255) # 期望职位关键词
place = models.CharField(max_length=255) # 期望工作地点
3.2 数据库索引优化
为了提高查询性能,建议在以下字段上创建索引:
sql
-- 职位表索引
CREATE INDEX idx_keyword ON job_data(key_word);
CREATE INDEX idx_place ON job_data(place);
CREATE INDEX idx_education ON job_data(education);
-- 投递记录表索引
CREATE INDEX idx_user_job ON send_list(user_id, job_id);
四、数据爬取模块实现
4.1 Selenium爬虫实现
核心爬虫函数
python
def lieSpider(city="", all_page="", spider_code=0):
"""爬取猎聘网职位信息"""
# 1. 配置Chrome浏览器
chrome_options = Options()
chrome_options.add_argument('--headless') # 无头模式
chrome_options.add_argument('--disable-gpu')
chrome_options.add_argument('--no-sandbox')
# 2. 初始化WebDriver
driver = webdriver.Chrome(
service=Service(ChromeDriverManager().install()),
options=chrome_options
)
# 3. 关键词列表
key_list = ['python', 'AI', '数据分析', '数据挖掘',
'大数据', '人工智能', '算法', 'java']
# 4. 遍历关键词和页数
for key in key_list:
for page in range(all_page):
# 构建URL
url = f"https://www.liepin.com/zhaopin/?currentPage={page}&key={key}"
# 爬取页面数据
get_pages(url, driver)
# 随机休眠,避免被封IP
time.sleep(random.uniform(1, 3))
driver.quit()
页面数据提取
python
def get_pages(url, driver):
"""从页面提取职位信息"""
driver.get(url)
time.sleep(2) # 等待页面加载
# 使用lxml解析HTML
html = driver.page_source
tree = etree.HTML(html)
# 提取职位列表
job_items = tree.xpath('//div[@class="job-info"]')
for item in job_items:
job_data = {
'name': extract_text(item, './/h3/a/text()'),
'salary': extract_text(item, './/span[@class="salary"]/text()'),
'place': extract_text(item, './/span[@class="place"]/text()'),
'company': extract_text(item, './/div[@class="company"]/text()'),
# ... 更多字段
}
# 保存到数据库
save_job_to_db(job_data)
4.2 反爬虫应对策略
- 请求频率控制:在每次请求之间添加随机延迟
- User-Agent轮换:模拟不同浏览器的请求头
- Cookie管理:维护会话状态,避免频繁登录
- 代理IP池:大规模爬取时使用代理IP轮换
4.3 数据清洗与存储
python
def save_job_to_db(job_data):
"""保存职位数据到数据库"""
# 数据清洗
job_data['salary'] = clean_salary(job_data['salary'])
job_data['key_word'] = extract_keyword(job_data['name'])
# 去重检查
if not JobData.objects.filter(href=job_data['href']).exists():
JobData.objects.create(**job_data)
五、推荐算法核心实现
5.1 协同过滤算法原理
协同过滤分为两类:
- 基于用户的协同过滤:找到相似用户,推荐他们喜欢的物品
- 基于物品的协同过滤:找到相似物品,推荐给用户
本项目采用基于物品的协同过滤,因为职位数量相对用户数量更稳定。
5.2 相似度计算
使用余弦相似度计算两个职位的相似度:
python
def similarity(job1_id, job2_id):
"""计算两个职位的相似度(余弦相似度)"""
# 获取投递job1的用户集合
job1_users = set(SendList.objects.filter(job_id=job1_id)
.values_list('user_id', flat=True))
# 获取投递job2的用户集合
job2_users = set(SendList.objects.filter(job_id=job2_id)
.values_list('user_id', flat=True))
# 计算交集和并集
intersection = len(job1_users & job2_users)
union_size = len(job1_users) * len(job2_users)
# 余弦相似度公式
if union_size == 0:
return 0
similar_value = intersection / sqrt(union_size)
return similar_value
相似度公式说明:
similarity(A, B) = |A ∩ B| / √(|A| × |B|)
- |A|:投递职位A的用户数
- |B|:投递职位B的用户数
- |A ∩ B|:同时投递职位A和B的用户数
5.3 推荐算法实现
完整推荐函数
python
def recommend_by_item_id(user_id, k=9):
"""基于物品的协同过滤推荐算法"""
# 1. 检查用户是否存在
if not UserList.objects.filter(user_id=user_id).exists():
return get_random_jobs(k)
user = UserList.objects.get(user_id=user_id)
# 2. 获取用户已投递的职位
sent_job_ids = list(user.sendlist_set.values_list('job_id', flat=True))
if not sent_job_ids:
# 用户没有投递记录,使用关键词匹配
return recommend_by_keyword(user_id, k)
# 3. 提取用户偏好关键词
keywords = JobData.objects.filter(job_id__in=sent_job_ids)
.values_list('key_word', flat=True)
# 统计关键词出现频率
keyword_counts = Counter(keywords)
top_keywords = [k for k, _ in keyword_counts.most_common(3)]
# 4. 获取候选职位(未投递且包含热门关键词)
candidate_jobs = JobData.objects.filter(
~Q(job_id__in=sent_job_ids),
key_word__in=top_keywords
)[:30]
# 5. 计算相似度并排序
similarity_scores = []
for candidate_job in candidate_jobs:
max_similarity = 0
for sent_job_id in sent_job_ids:
sim = similarity(candidate_job.job_id, sent_job_id)
max_similarity = max(max_similarity, sim)
similarity_scores.append((max_similarity, candidate_job))
# 6. 按相似度降序排序,返回前k个
similarity_scores.sort(key=lambda x: x[0], reverse=True)
recommendations = [job for _, job in similarity_scores[:k]]
return recommendations
关键词匹配推荐(冷启动问题)
python
def recommend_by_keyword(user_id, k=9):
"""基于关键词的推荐(解决冷启动问题)"""
try:
# 获取用户意向
user_expect = UserExpect.objects.filter(user_id=user_id).first()
if user_expect:
# 根据意向匹配职位
jobs = JobData.objects.filter(
name__icontains=user_expect.key_word,
place__icontains=user_expect.place
)[:k]
if jobs:
return list(jobs.values())
# 无匹配则返回随机推荐
return get_random_jobs(k)
except:
return get_random_jobs(k)
5.4 算法优化建议
- 时间衰减因子:考虑投递时间,最近的投递权重更高
- 隐式反馈:不仅考虑投递,还可以考虑浏览、收藏等行为
- 混合推荐:结合内容过滤和协同过滤
- 实时更新:用户新投递后实时更新推荐列表
六、Web界面开发
6.1 Django视图函数
职位列表视图
python
def get_job_list(request):
"""获取职位列表(支持分页和多条件筛选)"""
page = int(request.GET.get("page", 1))
limit = int(request.GET.get("limit", 10))
keyword = request.GET.get("keyword", "")
price_min = request.GET.get("price_min", "")
price_max = request.GET.get("price_max", "")
# 构建查询
query = JobData.objects.filter(name__icontains=keyword)
# 薪资筛选
if price_min or price_max:
# 提取薪资格式并筛选
# ...
# 分页
start = (page - 1) * limit
jobs = query[start:start+limit]
return JsonResponse({
"code": 0,
"count": query.count(),
"data": list(jobs.values())
})
推荐接口
python
def get_recommend(request):
"""获取推荐职位"""
user_id = request.session.get("user_id")
recommend_list = job_recommend.recommend_by_item_id(user_id, 9)
# 检查投递状态
for job in recommend_list:
is_sent = SendList.objects.filter(
user_id=user_id,
job_id=job['job_id']
).exists()
job['send_key'] = 1 if is_sent else 0
return render(request, "recommend.html", {'recommend_list': recommend_list})
6.2 前端页面实现
Layui表格展示
javascript
// 职位列表表格初始化
table.render({
elem: '#jobTable',
url: '/get_job_list/',
page: true,
cols: [[
{field: 'name', title: '职位名称', width: 200},
{field: 'salary', title: '薪资', width: 120},
{field: 'company', title: '公司', width: 200},
{field: 'place', title: '地点', width: 120},
{title: '操作', toolbar: '#actionBar', width: 150}
]],
done: function(res, curr, count){
// 渲染投递按钮状态
renderSendButtons(res.data);
}
});
// 投递职位
function sendJob(jobId) {
$.ajax({
url: '/send_job/',
type: 'POST',
data: {job_id: jobId, send_key: 0},
success: function(res) {
if(res.Code === 0) {
layer.msg('投递成功');
table.reload('jobTable');
}
}
});
}
6.3 用户认证
python
def login(request):
"""用户登录"""
if request.method == "POST":
user_id = request.POST.get('user')
password = request.POST.get('password')
user = UserList.objects.filter(
user_id=user_id,
pass_word=password
).first()
if user:
# 设置session
request.session['user_id'] = user_id
request.session['user_name'] = user.user_name
return JsonResponse({'code': 0, 'msg': '登录成功'})
else:
return JsonResponse({'code': 1, 'msg': '账号或密码错误'})
return render(request, "login.html")
七、数据分析与可视化
7.1 数据统计接口
python
def get_pie(request):
"""获取统计数据(学历、薪资、经验、城市分布)"""
# 学历分布
edu_list = ['博士', '硕士', '本科', '大专', '不限']
edu_data = []
for edu in edu_list:
count = JobData.objects.filter(education__icontains=edu).count()
edu_data.append({'name': edu, 'value': count})
# 薪资分布
salary_ranges = [
('5K及以下', lambda x: x <= 5),
('5-10K', lambda x: 5 < x <= 10),
('10K-15K', lambda x: 10 < x <= 15),
# ...
]
salary_data = []
for name, condition in salary_ranges:
count = count_by_salary_condition(condition)
salary_data.append({'name': name, 'value': count})
return JsonResponse({
'edu_data': edu_data,
'salary_data': salary_data,
# ...
})
7.2 PyEcharts可视化
python
from pyecharts.charts import Pie, Bar
from pyecharts import options as opts
def generate_edu_pie_chart(edu_data):
"""生成学历分布饼图"""
pie = (
Pie()
.add(
series_name="学历要求",
data_pair=edu_data,
radius=["40%", "70%"]
)
.set_global_opts(
title_opts=opts.TitleOpts(title="学历要求分布"),
legend_opts=opts.LegendOpts(orient="vertical", pos_left="left")
)
)
return pie.render_embed()