python+vue2+nodejs 搜索引擎课设 SCAU数信学院本科生通知检索(附源码)
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
前言
这个系统主要实现了以下功能
- 爬虫数据爬取及分词
- 后端数据库全文模糊搜索、高频词获取
- 前端输入拼音缩写或文字后匹配输入建议、搜索、列表分页、高亮关键词、相关度排序及时间排序、深色模式及浅色模式切换
爬虫python
后端nodejs
前端vue2
这个课设从代码到报告一共用了三天ddl生死时速所以很多地方实现得并不好比如等我都交上去了突然发现没做关键词搜索历史等我去做机器学习实验的时候才发现关键词自动补全用的是FP-growth算法。不过我确实尽力了因为老师宽容加上疫情不考试由课设决定成绩这门课拿了满绩点混子人生中第一个满绩点呜呜呜。
爬虫
数据爬取
新闻爬取是参考爬取华农数信院官网的新闻并且发送到邮箱。
改动在于
- 爬取的字段变为标题、发布时间、描述
- 将发送到邮箱改为保存到数据库
- 爬取所有新闻而非单页新闻
需要注意的地方是我爬取的时候最后一页是65页只有5条新闻这部分需要按照实际情况自己改动。
from bs4 import BeautifulSoup
import requests
from peewee import*
datalist = []
def main():
for i in range(1, 66):
getdata(i)
print("已获取数据")
# print(datalist)
savedata(datalist)
print("已保存数据")
def getdata(page):
headers = {
'User - Agent': 'Mozilla / 5.0(Windows NT 10.0; Win64; x64) AppleWebKit / 537.36(KHTML, like Gecko) Chrome / 89.0.4389.90 Safari / 537.36 Edg / 89.0.774.54'
}
num = 20
# 最后一页不足20条
if(page == 65):
num = 5
page = str(page)
# 发送请求获取页面的html
info = requests.get(url='https://info.scau.edu.cn/3772/list'+page+'.htm', headers=headers)
info.encoding = 'utf-8'
# 用BeautifulSoup解析html
html = BeautifulSoup(info.text, 'html.parser')
# print(html)
# result = []
# 循环拿对应的新闻的标题、日期和链接
for i in range(0, num):
title = html.select('.title')[i].text
date1 = html.select('.date')[i].text
desc = html.select('.desc')[i].find('a').text
# a = targetUrl.get('href')
# prefixUrl = 'https://info.scau.edu.cn'
# targetUrl = prefixUrl + a
# print(targetUrl)
datalist.append({
"title": title,
"date": date1,
"desc": desc
})
print(page)
# return result
# print(result)
def savedata(data):
db = MySQLDatabase('scauInfo', host='127.0.0.1', user='mysql用户名', passwd='mysql密码')
db.connect()
class BaseModel(Model):
class Meta:
database = db # 每一个继承BaseModel类的子类都是连接db表
class Info(BaseModel):
title = CharField()
date = CharField()
desc = CharField()
Info.create_table()
i = 0
length = len(data)
# print(data[0])
# print(data[0]['title'])
while i < length:
Info.create(title=data[i]['title'], date=data[i]['date'], desc=data[i]['desc'])
i += 1
if __name__ == "__main__": # 当程序执行时
# 调用函数
main()
分词
主要目的就是计算在文章中出现的高频词存放在关键词列表中用户搜索的时候可以补全。
关键词补全功能用户输入拼音缩写或部分关键字时需要出现模糊匹配的关键词供用户选择比如用户输入“jxj”或“奖”时下拉框需要弹出关键词“奖学金”。
问题来了如何获得"奖学金"这个词当时的想法有两个
- 搜集用户输入的关键词比如“奖学金”被搜索五次以上就算高频搜索词将其纳入关键词列表存进数据库用户搜索时再将其从数据库中select出来
- 但是时间短测试数据有限所以又想到从文章中搜集出现的高频词汇存储前120个高频词补充进关键词列表所以才有分词这个步骤
上面是载入停用词表后用结巴分词的结果问题有两个 - 即使用停用词表筛出了很多没意义的词但筛选结果还是有很多没意义的词:“我院”“学年”等等所以人工将一些词补充进停用词表
- 分词不准确比如“蓝桥杯”和“线上赛”被分开了解决方法是人工观察结果自定义一个分词表
from peewee import*
import jieba
import re
title = []
# date = []
desc = []
words = []
no_stop_words = []
new_a = {}
db = MySQLDatabase('scauInfo', host='127.0.0.1', user='mysql用户名', passwd='mysql密码')
db.connect()
class BaseModel(Model):
class Meta:
database = db # 每一个继承BaseModel类的子类都是连接db表
def main():
getSentence()
splitSentence()
useStopWord()
getTimes()
def getSentence():
class Info(BaseModel):
title = CharField()
date = CharField()
desc = CharField()
datas = Info.select()
for data in datas:
title.append(data.title)
# date.append(data.date)
desc.append(data.desc)
def splitSentence():
jieba.load_userdict('D:\xxx路径\specialWords.txt')
for sentence1 in title:
# 使用jieba进行分词,使用精确模式
devision_words1 = jieba.cut(sentence1, cut_all=False)
# 将分词后的结果转化为列表然后添加到分词列表中
words.extend(list(devision_words1))
for sentence2 in desc:
# 使用jieba进行分词,使用精确模式
devision_words2 = jieba.cut(sentence2, cut_all=False)
# 将分词后的结果转化为列表然后添加到分词列表中
words.extend(list(devision_words2))
# print(words)
def useStopWord():
stop_path = r"D:\xxx路径\stopWord.txt" # 停用词表的位置
stop_list = []
for line in open(stop_path, 'r', encoding='utf-8').readlines():
stop_list.append(line.strip())
for word in words: # 使用分词后的结果然后用空格进行分割,得到每个分词
if word not in stop_list: # 如果这个分词不在停用词表中并且不是换行或者制表符就将其加入到最后的字符串中,然后加一个空格
word = re.sub(r'\d', "", word) # 去除单词中的数字
word = re.sub(r'\s', "", word) # 去除单词中的空格
word = re.sub(r'\W', "", word) # 去除单词中的字母
if word:
if(len(word) > 1):
no_stop_words.append(word)
def getTimes():
# 统计每一个单词的出现次数使用字典的形式进行统计
result = {}
for word in no_stop_words:
res = result.get(word, 0)
if res == 0:
result[word] = 1
else:
result[word] = result[word] + 1
result = sorted(result.items(), key=lambda kv: (kv[1], kv[0]), reverse=True)
result = dict(result)
for i, (k, v) in enumerate(result.items()):
new_a[k] = v
if i == 119:
saveWords(list(new_a.keys()))
# print(new_a.keys())
break
new_a.clear()
def saveWords(data):
class highFreWords (BaseModel):
word = CharField()
highFreWords.create_table()
i = 0
length = len(data)
while i < length:
highFreWords.create(word=data[i])
i += 1
# def getAllFrequency:
# # 统计词频,使用上一问得到的字典
# cum2 = {}
# sum = 0
# new_a.clear()
# for i, (k, v) in enumerate(fre2.items()):
# sum = sum + v
# new_a[k] = sum
# cum2 = new_a.copy()
#
# def getEveryFrequency:
# # 使用字典得到的累计词频结果
# for i, (k, v) in enumerate(cum2.items()):
# new_a[k] = v
# if i == 9:
# print(new_a)
# break
if __name__ == "__main__": # 当程序执行时
# 调用函数
main()
数据库结构
如果照上面代码运行数据库结构应该是这样的
后端
用nodejs写了两个接口
- 全文模糊搜索接口
- 获取文章高频关键词及用户搜索高频词接口
全文模糊搜索接口接收前端传来的参数关键词keyWords、排序方式sortType、当前页数curPage。
(1) 当关键词keyWords为“%%”时表示用户未进行搜索或输入关键词为空此时应当返回按时间降序排列的所有通知写出筛选语句用“date desc”控制按照时间降序返回数据当时间相同时根据id正序返回数据。向数据库发起查询请求如果出错则将错误抛出。获取数据总条数按照每页20条的规则用splice方法及前端传来的curPage对数据进行切分。最后向前端返回表示处理成功的200状态码、消息提示、切分好的数据及数据总条数。
(2) 当关键词keyWords不为“%%”时表示用户输入了关键词此时先用nodejieba将关键词进行分词如“奖学金公示”会被划分为“奖学金”和“公示”接着用slice().join()将数组转为用“|”连接的字符串。先从userInput表中筛选此关键词是否存在在表中如果不存在将关键词插入表中times字段赋值为1表明此关键词被搜索过一次否则更新表将对应的times字段值加1表明此关键词被搜索次数增加一次。当sortType为0时表示按照相关度返回模糊匹配的数据用正则表达式将标题、日期、描述中字段含有关键词的部分找到(即模糊搜索)再从其中计算它们的出现次数作为keyweight按照出现次数降序排序获取数据当sortType为1时表示按照时间降序返回模糊匹配的数据用正则表达式将标题、日期、描述中字段含有关键词的部分找到按照时间字段降序排序获取数据。获取数据总条数按照每页20条的规则用splice方法及前端传来的curPage对数据进行切分。最后向前端返回表示处理成功的200状态码、消息提示、切分好的数据及数据总条数。
获取文章高频关键词及用户搜索高频词接口:
(1) 获取文章高频关键词写出筛选语句获取highFreWords表中所有高频关键词获取失败则抛出错误。用map方法遍历数据项将其中的”word”属性转为”value”属性。
(2) 获取用户搜索高频词写出筛选语句获取userInput表中times字段值大于5的关键词获取失败则抛出错误。用map方法及replace方法遍历数据项将其中的”word”属性转为”value”属性“|”分隔符转换为空格。
向前端返回表示处理成功的200状态码、消息提示、转化完成的数据。
const express = require("express")
const app = express()
const mysql = require("mysql")
var bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())
const { load, cut } = require('@node-rs/jieba')
load()
app.listen(3000, () => {
console.log("服务器开启3000端口...")
})
// 创建与数据库的连接
var connection = mysql.createConnection({
host: '127.0.0.1',
user: "用户名",
password: '密码',
database: "scauInfo",
port: '3306'
})
connection.connect((err) => {
if (err) throw err
console.log("连接成功")
})
// 全表模糊查询
app.post('/search', function (req, res) {
let keyWords = req.body.keyWords
let sortType = req.body.sortType
let curPage = Number(req.body.curPage)
if (keyWords == '%%') {
let selectSQL = "select * from Info order by date desc, id"
connection.query(selectSQL, function (err, rows, fields) {
if (err) throw err
let total = rows.length
let pageSize = 20
let data = rows.splice((curPage - 1) * pageSize, pageSize)
res.send({ status: 200, message: 'get searchList', data: data, total: total })
})
}
else {
let tmp = cut(keyWords, false)
splitKeyWords = tmp.slice(1, tmp.length - 1).join("|")
let sql1 = "select * from userInput where words = '" + splitKeyWords + "'"
connection.query(sql1, function (err, rows, fields) {
if (err) throw err
if (rows.length == 0) {
sql2 = "insert into userInput(words,times) values('" + splitKeyWords + "',1)"
connection.query(sql2, function (err, rows, fields) {
if (err) throw err
})
return
}
let sql3 = "update userInput set times = times + 1 where words = '" + splitKeyWords + "'"
connection.query(sql3, function (err, rows, fields) {
if (err) throw err
})
})
// console.log('keywords', splitKeyWords)
let selectSQL = ""
if (sortType == 0) {
selectSQL = "SELECT *,((IF( title REGEXP '" + splitKeyWords + "', 1, 0))+(IF( date REGEXP '" + splitKeyWords + "', 1, 0)) + (IF( `desc` REGEXP '" + splitKeyWords + "', 1, 0))) AS keyweight FROM Info WHERE CONCAT_WS(' ', title, date, `desc`) REGEXP '" + splitKeyWords + "' ORDER BY keyweight DESC"
} else {
selectSQL = "select * from Info where title REGEXP '" + splitKeyWords + "' or date REGEXP '" + splitKeyWords + "' or `desc` REGEXP '" + splitKeyWords + "' order by date desc, id"
}
console.log("selectSQL", selectSQL)
connection.query(selectSQL, function (err, rows, fields) {
if (err) throw err
if (rows.length == 0) {
res.send({ status: 404, message: '暂无搜索结果' })
return
}
let total = rows.length
let pageSize = 20
let data = rows.splice((curPage - 1) * pageSize, pageSize)
res.send({ status: 200, message: 'get searchList', data: data, total: total, splitKeyWords: splitKeyWords })
})
}
})
app.get('/getSuggestWords', function (req, res) {
let selectSQL1 = "select word from highFreWords"
connection.query(selectSQL1, function (err, rows, fields) {
if (err) throw err
let data1 = rows.map((item) => {
return {
value: item['word']
}
})
let selectSQL2 = "select words from userInput where times > 5"
connection.query(selectSQL2, function (err, rows, fields) {
if (err) throw err
let data2 = rows.map((item) => {
return {
value: item['words'].replace('|', ' ')
}
})
data1 = data1.concat(data2)
res.send({ status: 200, message: 'get suggestWords', data: data1 })
})
})
})
前端
组件库用的是elementui
输入拼音缩写或文字后匹配输入建议功能
组件使用autocomplete拼音匹配用的库是pinyin-match实现关键点是模糊匹配
搜索功能
当用户点击输入建议的下拉框列表项或搜索按钮时由于要请求接口返回新数据先将控制加载图标显示的变量设为true并重置分页列表中当前页数为1使用正则表达式去除输入关键词中的空格将处理好的关键词发送给后端由后端对数据库进行全文模糊搜索返回筛选出的通知列表给前端再由前端接收搜索结果列表并更新展示在结果页。
列表分页功能
分页组件是el-pagination。设定分页模式、每页展示的通知列表条数初始页码及列表总数。列表总数通过调用搜索接口由后端返回通知列表总数获得。通过current-change事件监听用户翻页更新当前页码后调用搜索接口获取更新后的通知列表并显示在页面上。
高亮关键词功能
当搜索接口返回经过分词处理的关键词列表后用正则表达式将其从段落中选择出来为避免搜索结果不区分大小写使用函数形式及模板字符串将关键词字段替换为html语句为其加上红色的css属性使用v-for遍历通知列表使用v-html解析html代码将标题日期描述中出现的关键字展示出来。
相关度排序及时间排序功能
当用户未进行搜索通知列表默认按时间排序用v-show隐藏el-select组件当用户进行搜索并且结果已经展示在页面时提供排序功能显示el-select组件下拉框中绑定选项列表列表数组的每一项由value和label组成的对象构成。用户可以选择相关度排序或时间排序label当监听到用户选择的选项有变动时获取用户选项值中label对应的value并调用搜索接口将用户选项value传递给后端规定传递的值value为0时为按相关度排序当传递的值value为1时按时间排序。前端将后端返回的已排序好的结果展示在页面。
深色模式及浅色模式切换功能
首先用v-deep更改组件样式隐藏未选择项设置其颜色为透明设置active-color及inactive-color作为切换选择器的颜色。安装scss预处理器后在assets文件夹下建立_themes.scss用于配置不同的主体配色方案将对应主体的颜色变量集合存放在$themes中。在assets文件夹下建立_handle.scss用于操作主题变量用@mixin定义可重复使用的样式遍历主题将局部变量提升为全局变量再用插值表达式判断html中data-theme的属性值。声明一个根据key获取颜色的方法用@include引用混合样式。在页面的vue文件下的style中先引入对应的_handle.scss文件并根据需求在对应地方引入对应的混入器。使用el-switch组件为用户提供一个开关通过监听用户的操作如用户打开开关此时值为true将表示主题的变量设置为light并给页面节点设置data-theme为light的属性实现浅色模式切换反之亦然。
效果展示
输入拼音缩写匹配输入建议
输入文字匹配输入建议
关键字高亮
按时间排序
** 按相关性排序**
列表分页
日间(浅色)模式
夜间(深色)模式
最后
项目已上传至搜索引擎课设 SCAU数信学院本科生通知检索欢迎star