1 前言:
今天给大家分享一个基于PythonWeb的Django框架结合爬虫以及数据可视化Echarts以及协同过滤算法推荐和数据库综合技术的项目,该项目总体来说还是挺不错的,下面针对这个项目做具体介绍。
具体介绍之前我们可以先简单看一下项目的简介:
☀ 项目简介:
1⃣ 项目名称:基于django的boss直聘数据可视化分析.
2⃣ 项目实现功能:1、用户登录注册,2、个人信息编辑以及个人密码修改,3、数据分页总览以及实现了用户可以对心仪岗位进行收藏和删除,4、首页大屏展示了用户的注册数据以及数据库中所有岗位数据的基本属性数据,5、针对爬取的岗位数据的各个字段做可视化图表分析处理. 6、针对不同用户对岗位的收藏和评分个性化推荐岗位,7、admin后台管理。
3⃣ 项目涉及技术:Python、Django、mysql、Echarts、机器学习、协同过滤算法、爬虫.
2 项目介绍:
2.1 爬虫数据获取
数据针对boss直聘目标网站所获取,获取的内容对象为title工作名字,address工作城市,type工作类型,educational学历,workExperience工作经验,workTag工作标签,salary工资,salaryMonth多薪,companyTag公司福利,hrWorkHR岗位,hrNameHR姓名,pratice是否实习,companyTitle公司名字,companyAvatar公司头像,companyNature公司性质,companyStatus公司融资,companyPeopl公司人数,detailUrl岗位详情链接,companyUrl公司详情链接,dist行政区。
我们先从官网拿来两条页面的数据,照片如下:
与此同时上面的数据和我们想要存放在数据库里的最终数据格式不一样,对此我们对个人数据样式需要做一些处理。
比如薪资信息,如15-30k我们在数据库要展示的类型为[15000,30000],处理过程为首先判断该职位是不是实习岗位,因为实习岗位是没有月薪的,实习的职位是多少多少元/天,我们先用Xpath路径解析来获取该薪资标签的text文本内容也就是15-30k·16薪,可以通过判断该文本字符串内容是否包含K来知道该职位是否实习岗位,包含k则为非实习岗位,然后python分隔符split(·)来把工资和多薪分开,然后用len()方法来获取其长度,如果为1则说明其没有多薪,我们设置为0薪,否则说明为多薪,至于对少薪,我们可以用python的切片来获取并将其多少薪赋值给一个salaryMonth的变量。
对于工资值的转换如何从15-30k转换为[15000,30000],代码首先从 salaries 列表中获取第一个元素,并在该元素中将 "K" 替换为空字符串。接着,代码使用 split('-') 方法将该元素按照 "-" 进行分割,得到一个列表。最后,代码使用 map() 函数将该列表中的每个元素都转换成整数,并乘以 1000,得到一个整数类型的列表。例如,如果 salaries[0] 的值为 "10K-20K",那么经过该代码处理后,salary 的值将为 [10000, 20000]。
另外对于address城市和dist行政区的赋值就是用xpath来获取去地址标签的文本如武汉市·洪山区·光谷,那我们是需要用python的split函数进行分割,接着用索引的方法,即用下标为0和1来获取城市和地区的文本。
公司融资情况也需要做处理,因为我们浏览网站的数据不难发现有些有些公司的融资情况显示为无,有的显示已上市或者具体的融资对象等等,例如:下面的融资情况为无,有的则显示具体明确的融资对象。
所以我们需要做些处理再放入到我们的最终数据库里,处理过程为:先用selenium库的find_element方法获取目标网址中的标签元素,遍历标签元素数量,提取公司的性质、融资状态以及公司规模等信息。具体而言,它首先判断网页中是否同时存在包含公司性质、融资状态和公司规模的标签,如果存在,就按顺序提取出它们的文本信息,具体的操作方法和上述相同,即先分割再切片索引获取文本;否则,就认为公司的融资状态为“未融资”,然后提取公司规模信息,这里的公司人数也要做些处理。
公司人数也要做处理网站上显示的类型比如为20-99人,我们想要在数据库展示的是列表形式如[20,99],则处理的过程和上面一样,使用 Selenium 库的 find_element 方法找到网页中符合条件的 HTML 元素,然后使用 split 和 replace 方法对元素的文本进行处理,最终将公司规模信息转化为一个包含两个整数的列表 companyPeople。如果在处理过程中出现异常,比如无法获取到元素或者元素的文本不符合预期,那么这段代码就会跳转到 except 语句块中,使用默认值 [0, 10000] 来代替 companyPeople。
公司福利也需要在传入数据库前经行处理因为我们发现有的公司并没有贴上福利内容比如:
这个时候只需要做简单的内容存在判断即可,如果不存在就把字符串“无”赋值即可,避免mysql不允许插入空值的异常问题。
数据爬取处理完毕之后给数据赋值变量后,将其放入到一个jobData的列表,调用os模块的writerow方法写入将其写入到csv里面,接着再对数据做最后的处JobInfo.objects.create,将其将其存入数据库里,到此爬取的数据已按照要求全部存入数据库,也就意味着我们的数据获取工作告一段落。
代码如下:
import json
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
import csv
import pandas as pd
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'boss直聘数据可视化分析.settings')
django.setup()
from myApp.models import *
class spider(object):
def __init__(self, type, page):
self.type = type
self.page = page
self.spiderUrl = "https://www.zhipin.com/web/geek/job?query=%s&city=100010000&page=%s"
# self.spiderUrl = "https://www.zhipin.com/web/geek/job?query=%s&city=101210100&page=%s"
def startBrowser(self):
# 指定Chrome驱动程序的路径
browser = webdriver.Chrome(executable_path='./chromedriver129.exe')
return browser
def main(self, **info):
if info['page'] < self.page: return
browser = self.startBrowser() # 直接调用spider类中的方法
print('页表页面URL:' + self.spiderUrl % (self.type, self.page))
browser.get(self.spiderUrl % (self.type, self.page))
time.sleep(15)
job_list = browser.find_elements(by=By.XPATH, value="//ul[@class='job-list-box']/li")
for index, job in enumerate(job_list):
try:
print("爬取的是第 %d 条" % (index + 1))
jobData = []
# title 工作名字
title = job.find_element(by=By.XPATH,value=".//div[contains(@class,'job-title')]/span[@class='job-name']").text
# address 地址
addresses = job.find_element(by=By.XPATH,value=".//div[contains(@class,'job-title')]//span[@class='job-area']").text.split('·')
address = addresses[0]
# dist 行政区
dist = addresses[1] if len(addresses) != 1 else ''
# type 工作类型
job_type = self.type
tag_list = job.find_elements(by=By.XPATH,value=".//div[contains(@class,'job-info')]/ul[@class='tag-list']/li")
if len(tag_list) == 2:
educational = job.find_element(by=By.XPATH,value=".//div[contains(@class,'job-info')]/ul[@class='tag-list']/li[2]").text
workExperience = job.find_element(by=By.XPATH,value=".//div[contains(@class,'job-info')]/ul[@class='tag-list']/li[1]").text
else:
educational = job.find_element(by=By.XPATH,value=".//div[contains(@class,'job-info')]/ul[@class='tag-list']/li[3]").text
workExperience = job.find_element(by=By.XPATH,value=".//div[contains(@class,'job-info')]/ul[@class='tag-list']/li[2]").text
# hr
hrWork = job.find_element(by=By.XPATH,value=".//div[contains(@class,'job-info')]/div[@class='info-public']/em").text
hrName = job.find_element(by=By.XPATH,value=".//div[contains(@class,'job-info')]/div[@class='info-public']").text
# workTag 工作标签
# workTag = job.find_elements(by=By.XPATH,value="./div[contains(@class,'job-card-footer')]/ul[@class='tag-list']/li")
workTag = job.find_elements(by=By.XPATH,value=".//div[@class='job-card-footer clearfix']/ul[@class='tag-list']/li")
workTag = json.dumps(list(map(lambda x: x.text, workTag)))
# salary 薪资
salaries = job.find_element(by=By.XPATH,value=".//div[contains(@class,'job-info')]/span[@class='salary']").text
pratice = 0
if 'K' in salaries:
salaries = salaries.split('·')
salary = list(map(lambda x: int(x) * 1000, salaries[0].replace('K', '').split('-')))
salaryMonth = '0薪' if len(salaries) == 1 else salaries[1]
else:
salary = list(map(lambda x: int(x), salaries.replace('元/天', '').split('-')))
salaryMonth = '0薪'
pratice = 1
# companyTitle 公司名称
companyTitle = job.find_element(by=By.XPATH, value=".//h3[@class='company-name']/a").text
# companyAvatar 公司头像
companyAvatar = job.find_element(by=By.XPATH,value=".//div[contains(@class,'job-card-right')]//img").get_attribute("src")
companyInfoList = job.find_elements(by=By.XPATH,value=".//div[contains(@class,'job-card-right')]//ul[@class='company-tag-list']/li")
if len(companyInfoList) == 3:
companyNature = job.find_element(by=By.XPATH,value=".//div[contains(@class,'job-card-right')]//ul[@class='company-tag-list']/li[1]").text
companyStatus = job.find_element(by=By.XPATH,value=".//div[contains(@class,'job-card-right')]//ul[@class='company-tag-list']/li[2]").text
try:
companyPeople = list(map(lambda x: int(x), job.find_element(by=By.XPATH,value=".//div[contains(@class,'job-card-right')]//ul[@class='company-tag-list']/li[3]").text.replace('人', '').split('-')))
except:
companyPeople = [0, 10000]
else:
companyNature = job.find_element(by=By.XPATH,value=".//div[contains(@class,'job-card-right')]//ul[@class='company-tag-list']/li[1]").text
companyStatus = "未融资"
try:
companyPeople = list(map(lambda x: int(x), job.find_element(by=By.XPATH,value=".//div[contains(@class,'job-card-right')]//ul[@class='company-tag-list']/li[2]").text.replace('人', '').split('-')))
except:
companyPeople = [0, 10000]
# companyTag 公司标签
companyTag = job.find_element(by=By.XPATH,value="./div[contains(@class,'job-card-footer')]/div[@class='info-desc']").text
if companyTag:
companyTag = json.dumps(companyTag.split(','))
else:
companyTag = '无'
# 详情地址
detailUrl = job.find_element(by=By.XPATH,value="./div[@class='job-card-body clearfix']/a").get_attribute('href')
# 公司详情
companyUrl = job.find_element(by=By.XPATH, value="//h3[@class='company-name']/a").get_attribute('href')
print(title, address, job_type, educational, workExperience, workTag, salary, salaryMonth, companyTag,
hrWork, hrName, pratice, companyTitle, companyAvatar, companyNature, companyStatus, companyPeople,
detailUrl, companyUrl, dist)
jobData.extend(
[title, address, job_type, educational, workExperience, workTag, salary, salaryMonth, companyTag,
hrWork, hrName, pratice, companyTitle, companyAvatar, companyNature, companyStatus, companyPeople,
detailUrl, companyUrl, dist])
self.save_to_csv(jobData)
except Exception as e:
print(f"Error occurred: {e}")
pass
self.page += 1
self.main(page=info['page'])
def save_to_csv(self,rowData):
with open('./temp.csv', 'a', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(rowData)
def clear_numTemp(self):
with open('./numTemp.txt','w',encoding='utf-8') as f:
f.write('')
def init(self):
if not os.path.exists('./temp.csv'):
with open('./temp.csv','a',newline='',encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(["title","address","type","educational","workExperience","workTag","salary","salaryMonth",
"companyTags","hrWork","hrName","pratice","companyTitle","companyAvatar","companyNature",
"companyStatus","companyPeople","detailUrl","companyUrl","dist"])
def save_to_sql(self):
data = self.clearData()
for job in data:
JobInfo.objects.create(
title=job[0],
address = job[1],
type = job[2],
educational = job[3],
workExperience = job[4],
workTag = job[5],
salary = job[6],
salaryMonth = job[7],
companyTags = job[8],
hrWork = job[9],
hrName = job[10],
pratice = job[11],
companyTitle = job[12],
companyAvatar = job[13],
companyNature = job[14],
companyStatus = job[15],
companyPeople = job[16],
detailUrl = job[17],
companyUrl = job[18],
dist=job[19]
)
print("导入数据库成功")
os.remove("./temp.csv")
def clearData(self):
df = pd.read_csv('./temp.csv')
df.dropna(inplace=True)
df.drop_duplicates(inplace=True)
df['salaryMonth'] = df['salaryMonth'].map(lambda x:x.replace('薪',''))
print("总条数为%d" % df.shape[0])
return df.values
if __name__ == '__main__':
spiderObj = spider('电话销售',1);
spiderObj.init()
spiderObj.main(page=10)
# spiderObj.save_to_sql()
2.2 注册登录
登录注册的原理比较简单,就是纯粹的前端知识,建立一个form表单,把input输入框内容放在里面,建立一个属性为submit的button标签,在view.py视图写入后台逻辑代码,如果request.method == “GET”则键入url对应的地址(注册界面),如果请求方法为request.method == “POST”即提交表单,同时获取输入框提交的内容,然后用数据库User对象User.objects.create方法将数据写入到数据库的用户表里,使用md5 = hashlib.md5() .update(pwd.encode())对数据库的密码经行MD5加盐加密处理同时跳转到登录的url地址页面,登录的原理类似,就是同样获取login.html页面前端的form表单里面输入框的内容,这个时候不是创建存入,而是丛数据库比对改数据(账号,密码)是否存在了,如果存在则说明前端里面输入的账号密码正确则可以redirect重定向到index首页。
代码如下:
def login(request):
if request.method == 'GET':
return render(request, 'login.html')
else:
uname = request.POST.get('username')
pwd = request.POST.get('password')
md5 = hashlib.md5()
md5.update(pwd.encode())
pwd = md5.hexdigest()
try:
user = User.objects.get(username=uname,password=pwd)
request.session['username'] = user.username
return redirect('home')
except:
return errorResponse(request, '用户名或密码错误!')
# 注册页面
def registry(request):
if request.method == 'GET':
return render(request, 'register.html')
else:
uname = request.POST.get('username')
pwd = request.POST.get('password')
checkPWD = request.POST.get('checkPassword')
try:
User.objects.get(username=uname)
except:
if not uname or not pwd or not checkPWD:return errorResponse(request, '不允许为空!')
if pwd != checkPWD:return errorResponse(request, '两次密码不符合!')
md5 = hashlib.md5()
md5.update(pwd.encode())
pwd = md5.hexdigest()
User.objects.create(username=uname,password=pwd)
return redirect('login')
return errorResponse(request, '该用户已被注册')
2.3 首页
3-1:首页-时间&欢迎用户语:
在首页页面我们可以发现,有当前时间和欢迎用户语,我们使用time.localtime()函数方法获取当前时间,year = timeFormat.tm_year,month = timeFormat.tm_mon,day = timeFormat.tm_mday获取年月日,由于获取的月份是数字形式,而我们想要的是(June 1,2023)这种格式,所以在此我们创建一个新的列表放入1-12月的英文文本如下:monthList=["January","February","March","April","May","June","July","August","September","October","November","December"],然后我们需要的月份便可以用monthlist[得到的数字-1],因为函数方法得到的是当前具体数字,而列表的索引是从0开始表示,同时欢迎语中的返回显示的用户名便可使用使用django模板在前端html语法{{ username }}表示即可。
3-2:首页-用户创建时间饼图
在这里我们便开始我们的数据可视化展示了,数据可视化我们选择不使用python的绘图根据matplitlob,为此我们选择使用echars可视化工具,我们在echars官网找到需要的数据展示图拿来源码,我们在此举个例子做个简述说明,拿来echars.Js的源代码如下:
option = {
tooltip: {
trigger: 'item'
},
legend: {
top: '5%',
left: 'center'
},
series: [
{
name: 'Access From',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 40,
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: [
{ value: 1048, name: 'Search Engine' },
{ value: 735, name: 'Direct' },
{ value: 580, name: 'Email' },
{ value: 484, name: 'Union Ads' },
{ value: 300, name: 'Video Ads' }
]
}
]
};
在series列表的data字典里把用户创建时间字段属性值即何时时间(哪一天)创建了几个user用户,形式如{数量:几个;时间:具体日期}然后将其写入即可,注意写入的方式和方法,要遵循django框架的模板语言,为了防止转义在后面加上 | safe,tooltip: { trigger: 'item'},legend: { top: '5%',left: 'center'},则用来实现数据布置图的鼠标悬停动态显示和顶部导航的,具体便不再详细赘述。
3-3:首页-右侧数据实现
数据总量:获取数据库的用户对象,使用len()方法即可同理用户数量。
最高学历:创建一个educations = {"博士": 1, "硕士": 2, "本科": 3, "大专": 4, "高中": 5, "中专/中技": 6, "初中及以下":7,"学历不限": 8}字典,educations[job.educational]<educations[educationsTop]: educationsTop = job.educational对工作数据库的工作经行遍历,创建的educations 字典[工作学历]可以得到所创字典的value数值,用此来和每一个工作表里的工作条目的学历经行比较,找到最大的value数值也即对应所创字典最高的学历,最后就在前端使用django模板语法{{ 最高学历的变量 }}即可。
优势地点:if address.get(job.address,-1) == -1:address[job.address] = 1
else:address[job.address] += 1对job的对象的地址和经验进行计数,如果job对象的地址和经验在字典中不存在,则将其添加到字典中并赋值为1;如果已存在,则将其对应的值加1,这样便可得到如下形式{“地点a”:数值1,“地点b”:数值2.等等}然后调用内置的items()方法将其返回字典的所有键值对,最后addressStr=sorted(address.items(),key=lambda x:x[1],reverse=True)[:3]对其数值经行降序排列且切片到前3个地址并将其连接表示赋值给一个addressstr变量最后在前端规划写入django模板语法{{ addressstr }}即可,最高多薪以及岗位性质同理。
最高薪资:遍历工作表的每一个工资经行对比,找出最高的赋值即可。
3-4:首页的数据表格展示
我们使用Jobs=JobInfo.objects.all()操作获取数据库包含的所有工作数据条目,然后对jobs经行遍历得到每一个job,在前端页面的tbale标签里经行数据渲染,注意django框架里对数据库信息的操作方法,如获取job条目里的该公司人数,我们只需要使用i.companypeople即可(注意:i是job的循环遍历的数据,companypeople是创建数据库是表的字段名称)。另外对于工资数据我们保存在数据库里的是[xk,xk]的列表形式的,但在这里我们这里要显示的是最高工资每月形式如XXX/月,所以job.salary = json.loads(i.salary)[1]用json的loads方法把字典里的字符串转换为python对象然后使用切片拿到后面的数据(也即是最高工资值)即可,是否实习也要经行个简单处理,因为我们数据库放的是0或者1来表示是否实习的,这里我们只需要做个if else语句即可,以及人数,我们数据库使用[a,b]来表示的,这里我们要展示为a人-b人, json.loads(i.companyPeople)把数据库的属性人数的值拿过来转换python对象,i.companyPeople = list(map(lambda x:str(x) + '人',然后再使用'-'.join(i.companyPeople)把列表里的的两个数用-号连接拼接,便可得到需要的结果形式,呈现在效果前端界面中。
代码如下(说明:这是调用函数,获取函数的返回值,因为直接写在views.py后端里面代码量太大,为了规划严谨,将其处理的业务代码写在utils里面了,我们在views.py的index首页函数里面直接调用即可,下面的也类似):
def home(request):
username = request.session.get("username")
userInfo = User.objects.get(username=username)
year,month,day = getHomeData.getNowTime()
jobsLen,usersLen,educationsTop,salaryTop,salaryMonthTop,addressTop,praticeMax = getHomeData.getTagData()
userTime = getHomeData.getUserCreateTime()
newUser = getHomeData.getUserTop5()
# allJobsPBar = getHomeData.getAllJobsPBar()
tableData = getHomeData.getTableData()
return render(request, 'index.html',{
'username':username,
'userAvatar':userInfo.avatar,
'year':year,
"month":month,
'day':day,
'jobsLen':jobsLen,
'usersLen':usersLen,
'educationsTop':educationsTop,
'salaryTop':salaryTop,
'salaryMonthTop':salaryMonthTop,
'praticeMax':praticeMax,
'addressTop':addressTop,
'userTime':userTime,
'newUser':newUser,
'tableData':tableData
})
2.4 个人中心
4-1:个人中心-个人信息:
学历,工作经验以及意向岗位选择下拉框的实现是在select标签里循环数据库的学历,工作经验以及岗位类型字段对呀的值,用for 循环来遍历 educations 列表中的每一个元素 e,并使用 if 和 else 语句来判断当前遍历到的元素是否等于 userInfo.educational,如果等于则输出一个选中状态的 option 标签,否则输出一个普通的 option 标签。其中 educations 和 userInfo.educational 都是从后端传递到前端的数据。这段代码的作用是生成一个下拉框,让用户可以选择自己的教育程度。其中educations列表为我们定义的educations 列表= ["博士","硕士","本科","大专","高中","中专/中技","学历不限"]
userInfo.educationa为数据库的学历字段对应的内容值,工作经验和意向岗位选取原理同上。
图片的选取是调用了前端的文件input type=“file”
然后接着便是提交form表单执行后端视图的post方法调用修改信息函数,该函数接受两个参数newInfo和FileInfo,其中newInfo是一个字典,包含用户的新个人信息,FileInfo是一个字典,包含用户上传的文件信息。该函数使用Django框架中的ORM(对象关系映射)方式来更新用户的个人信息。具体地说,它首先通过get方法从数据库中获取指定用户名的User对象,然后更新该对象的educational、workExpirence、address和work属性。如果FileInfo中的avatar属性不为None,则将其设置为该User对象的avatar属性。最后,使用save方法将更新后的User对象保存回数据库。
def selfInfo(request):
username = request.session.get("username")
userInfo = User.objects.get(username=username)
educations,workExperience,jobsTypes = getSelfInfoData.getPageData()
if request.method == 'GET':
return render(request,'selfInfo.html',{
'username': username,
'userInfo': userInfo,
'educations':educations,
'workExperience':workExperience,
'jobsTypes':jobsTypes
})
else:
getSelfInfoData.changeSelfInfo(request.POST,request.FILES)
userInfo = User.objects.get(username=username)
return render(request, 'selfInfo.html', {
'username': username,
'userInfo': userInfo,
'educations': educations,
'workExperience': workExperience,
'jobsTypes': jobsTypes
})
4-2:个人中心-修改密码:
原理同上.
2.5 数据统计
5-1:数据统计-数据总览:
首先获取表格数据,并将其分页,每页显示 10 条数据。其中 使用编写的getTableData()
函数方法来获取表格数据并对数据经行规划整理,返回值是个可迭代的数据结构(列表);Paginator
是 Django 框架自带的分页器类,用于将数据分页。这段代码执行后会返回一个名为 paginator
的分页器对象。
然后将当前页码设置为 1,然后检查是否有 GET 请求参数中传递了页码信息,如果有,则将当前页码更新为 GET 请求参数中传递的页码。接着,它使用 Django 自带的分页器(paginator)对数据进行分页,将当前页面设置为 c_page 变量。如果没有传递页码信息,则将当前页面设置为第一页。这段代码的作用是根据用户请求的页码对数据进行分页,并返回对应页码的数据。
其次定义了一个空的 page_range
列表,定义了一个 visibleNumber
变量,表示最多显示多少个页码(10个)。接着计算了页码的起始位置 min
,这里将当前页码减去visibleNumber / 2
,然后向下取整并转换为整数。如果计算出来的 min
小于 1,则将其设置为 1,因为页码从 1 开始。接下来,计算了页码的结束位置 max
,这里是将 min
加上 visibleNumber
。如果 max
大于总页数,则将其设置为总页数。最后,使用一个循环将 min
到 max
之间的页码添加到 page_range
列表中,并返回该列表。这段代码的作用是生成一个适当范围内的分页页码列表,方便用户快速跳转到目标页码。
最后在前端页面循环遍历paginator.page(cur_page)的内容显示即可。
5-2:数据统计-岗位收藏:
点击前端页面的岗位收藏按钮(实际是一个超链接标签),点击按钮出发链接路由地址调用getHistoryData(userInfo)方法,该方法使用 Django 模型 History
来查询用户 userInfo
的历史记录数据,并按照 count
字段进行降序排序,将查询结果保存在一个列表 data
中。
定义一个名为 map_fn
的函数,用于处理每个历史记录数据项。其中,该函数会将 item.job.salary
、item.job.companyPeople
和 item.job.workTag
字段中的 JSON 字符串转化为 Python 对象,并将 item.job.companyTags
字段中的字符串转化为列表(如果为 "无" 则转化为空列表)。如果 item.job.pratice
字段为 False,则将 item.job.salary
中的每个元素除以 1000 并转化为字符串,否则直接将其转化为字符串。最后,将 item.job.salary
和 item.job.companyPeople
中的每个元素用 "-" 连接成一个字符串。处理完成后,返回处理后的数据项 item
。
使用 map
函数和 map_fn
函数将 data
中的每个历史记录数据项都进行处理,并将处理后的结果保存在一个列表中。以为在model数据库里我们使用了models.ForeignKey与职业数据库经行了关联,此处data为用户数据库的用户对象。
返回处理后的列表,然后就可以在前端使用django模板语言将数据经行遍历渲染展现了。
2.6 数据可视化
6-1:可视化图标-薪资情况:
遍历所有非实习的工作,将其岗位类型添加到jobsType里,将其薪资信息中的第二项加入到 jobsType
中对应类型的列表中。形式为{Java:[1,2,4,3]}这里列表的内容是java的各个工资合集
接着,代码创建了一个空字典 barData
,并遍历 jobsType
中的每个类型,将其薪资列表按照一定区间进行分组,并统计各个区间的数量,将结果存储在 barData
中。最后,代码返回 salaryList
、barData
和 barData
的键列表。此时barData形式为{java:[1,2,3,4]}此时列表的内容为不同薪资段的人数。
然后引入echars,在js代码的series: [ {% for k,v in barData.items %}
{ name: '{{ k }}', type: 'bar', data: {{ v }}, }, {% endfor %}]即可展现效果,其中的鼠标悬停的动态效果是tooltip
字段。
输入框的选择筛选根据前端页面的option的选择将值传入后端,然后对数据库的工作岗位的每一个对象内容经行object。fillter(筛选条件)即可获取需要的数据然后用前面所说的echars展示即可。
实习生薪资平均值饼图数据展示:使用Django 的 ORM 模块从数据库中获取 JobInfo 对象的所有实例。然后,它创建一个空字典 jobsType 用于存储每种工作类型以及对应的月平均工资,并遍历所有工作信息,筛选出实习经验(pratice=1(实习))的工作信息,并将它们的月平均工资添加到 jobsType 中。
接着创建一个空列表 result,用于存储每种工作类型的月平均工资的总和,然后遍历 jobsType 中的每个键值对,用 自定义的addLis函数(求平均)函数将该工作类型的月平均工资求和。最后,将每种工作类型的名称和月平均工资总和添加到 result 中,并将 result 返回。此时result形式为[{工作类型:该类型薪资均值}]最后在前端的js代码的series将其写入即可。
至于多薪图标的展示,处理JobInfo的对象,筛选出薪资月份数量大于 0 的工作信息,并统计每种薪资月份的数量,返回一个列表和一个字典。其中,列表包含所有薪资月份的字符串,字典包含每种薪资月份及其对应数量的键值对。然后在引入的echars的series列表的data字段设置 {{ louDouData | safe}}即可。
6-2:可视化图标-企业情况:
首先使用JobInfo.objects.all()
来获取数据库中所有职位信息的QuerySet对象,然后遍历QuerySet对象中的每个职位信息对象,将每个职位信息对象的类型属性添加到一个列表中。最后通过set()
函数将列表中的重复元素去除,再通过list()
函数将集合转换为列表并返回。
在前端页面显示选择条实现:使用了条件语句和循环语句来显示不同的按钮。根据变量"type"的不同值来显示相应的按钮,如果"type"等于"all",则显示一个绿色的按钮,否则显示一个默认的按钮。然后,它会使用一个循环语句来遍历"typeList"列表,并显示相应的按钮,如果"type"等于当前循环变量"i"的值,则显示一个绿色的按钮,否则显示一个默认的按钮。最终生成的HTML代码将根据这些条件和循环生成相应的按钮。
定义一个"getCompanyBar"的函数,该函数接受一个参数"type",用于过滤JobInfo对象。如果"type"等于"all",则从JobInfo中获取所有的对象,否则只获取"type"等于"type"的对象。然后,它创建一个空字典"natureData",用于存储不同公司性质的数量。对于每个对象,它将检查该对象的"companyNature"属性是否已经在字典中存在,如果不存在,则将其添加到字典中,并将其值设置为1,否则将其值增加1。最后,函数返回一个由字典键和值组成的元组列表,其中键表示公司性质,值表示该性质的对象数量。由于返回的列表只包含前30个元素,因为在数据量较大时我们对此进行截断。
最后在前端页面的echars.js代码里的series的字典写入模板语法data:{{ natureColumn }}即可出数据效果。至于鼠标悬停动态,以及显示行业最大最小值和平均分割线便是相对于的属性来实闲的比如前文对应下来便是:tooltip,markPoint,markline的基本属性设置了,不再解释。
公司地址数量饼状图:定义一个getCompanyPie(type)函数
获取一个职位信息的饼图数据。其中,参数 type
表示职位类型。如果 type
为 'all'
,则获取所有职位信息;否则,获取指定类型的职位信息。然后,程序遍历职位信息,统计每个地址对应的职位数量,并将结果存储在 addressData
字典中。若某个地址在字典中不存在,则将其初始值设为 1;否则,将其值加 1。最后,程序将 addressData
字典转换成一个列表 result
,其中每个元素都是一个字典,表示一个地址及其对应的职位数量。我这里为了美观就显示3列数据57个。
然后将其数据渲染到前端的echar.js代码里就欧克了。
6-3:可视化图标-词云产生
1.连接 MySQL 数据库,查询 jobinfo
表中的指定字段(field
参数)。
2.将查询结果拼接成一个字符串 text
。
3.使用 jieba 库对字符串进行中文分词,得到分词后的字符串 string
。
4.打开指定的遮罩图片(targetImgSrc
参数),将其转化为列表 img_arr
。
5.使用 WordCloud 库生成词云对象 wc
,并将分词后的字符串 string
作为输入,生成词云。
6.使用 Matplotlib 库绘制词云图片并保存到指定路径(resImgSrc
参数)。
6-4:可视化图标-学历分布
6-5:可视化图标-企业融资
2.7 岗位推荐
岗位推荐是基于用户来推荐的,更具不同用户的收藏和评分来个性化推荐用户可能感兴趣的岗位
代码如下:
from math import *
from myApp.models import Job_score,User,JobInfo
def getdata():#获取数据
alldata=Job_score.objects.values_list('user','job','score')
data={}
for user,job, score in alldata:
if user not in data:
data[user] = {}
data[user][job] = score
return data
#计算用户相似度
def Euclid(user1,user2):#尤几里得距离也就是欧式距离
#取出两个用户都查看过的的职位
data=getdata()
user1_data = data.get(user1, {})
user2_data = data.get(user2, {})
#默认距离,相似度越大距离越短
distance=0
#遍历找出相同职位
for key in user1_data.keys():
if key in user2_data.keys():
distance+=pow(float(user1_data[key])-float(user2_data[key]),2)
return 1/(1+sqrt(distance))
#计算某用户和其他用户相似度的比对
def top_simliar(user):
res=[]
data=getdata()
for userid in data.keys():
#首先排除当前用户
if not userid==user:
simliar=Euclid(user,userid)
res.append((userid,simliar))
res.sort(key=lambda val:val[1],reverse=True)
return res,data
def recommend_user(user):#给用户推荐职位方法
res,data=top_simliar(user.id)
print('res+data',res,data)
workid={}
try:
for i in range(5):
top_user=res[i][0]
workid.update(data[top_user])
except:
pass
#print(workid)
recommend_list=[]
workid=sorted(workid.items(),key=lambda x:x[1],reverse=True)[:10]#给列表按打分排序,取出前十个推荐
for i in workid:
recommend_list.append(i[0])
print(recommend_list)
# 意向岗位存在
if user.work:
# 如果推荐列表为空,则直接使用意向岗位筛选出的工作ID
if len(recommend_list) == 0:
joblist = JobInfo.objects.filter(workTag__icontains=user.work).values_list('id', flat=True)
recommend_list = list(joblist)
else:
# 如果推荐列表不为空,则将意向岗位筛选出的工作ID与推荐列表取并集
joblist = JobInfo.objects.filter(workTag__icontains=user.work).values_list('id', flat=True)
recommend_list = list(set(recommend_list) | set(joblist)) # 取并集
return recommend_list
主要流程就是:
# 代码解释与总结
1. 数据获取
getdata()函数从数据库中获取所有用户对职位的评分数据,并组织成字典格式:
- 外层字典以用户ID为键
- 内层字典以职位ID为键,评分为值
2. 相似度计算
`Euclid(user1, user2)`函数使用欧几里得距离计算两个用户的相似度:
- 找出两个用户都评价过的职位
- 计算这些职位评分的欧式距离
- 将距离转换为相似度(距离越小相似度越大)
3. 寻找相似用户
`top_simliar(user)`函数:
- 计算指定用户与其他所有用户的相似度
- 返回按相似度降序排列的用户列表及原始数据
4. 推荐职位
`recommend_user(user)`是核心推荐函数:
1. 获取与当前用户最相似的5个用户
2. 收集这些相似用户评价过的职位
3. 按评分排序取前10个职位作为推荐列表
4. 特殊处理:
- 如果用户有意向岗位(work),则将意向岗位相关职位加入推荐列表
- 如果推荐列表为空,则直接使用意向岗位筛选结果
2.8 admin后台管理
3 结尾:
由于代码量较多,我就不一一粘贴代码了,需要该项目的同学可以私信我,除此之外本人也写了许多类似的可视化和推荐系统的项目,有需要使用或者学习的同学可以添加我下方名片获取哦!