前言
这个系统主要实现了以下功能:
- 爬虫:数据爬取及分词
- 后端:数据库全文模糊搜索、高频词获取
- 前端:输入拼音缩写或文字后匹配输入建议、搜索、列表分页、高亮关键词、相关度排序及时间排序、深色模式及浅色模式切换
爬虫: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!