jjzjj

python+vue2+nodejs 搜索引擎课设 SCAU数信学院本科生通知检索(附代码)

我先润了 2023-08-18 原文

前言

这个系统主要实现了以下功能:

  1. 爬虫:数据爬取及分词
  2. 后端:数据库全文模糊搜索、高频词获取
  3. 前端:输入拼音缩写或文字后匹配输入建议、搜索、列表分页、高亮关键词、相关度排序及时间排序、深色模式及浅色模式切换

爬虫:python
后端:nodejs
前端:vue2

这个课设从代码到报告一共用了三天,ddl生死时速,所以很多地方实现得并不好,比如等我都交上去了突然发现没做关键词搜索历史,等我去做机器学习实验的时候才发现关键词自动补全用的是FP-growth算法。不过我确实尽力了,因为老师宽容,加上疫情不考试由课设决定成绩,这门课拿了满绩点,混子人生中第一个满绩点呜呜呜。

爬虫

数据爬取

新闻爬取是参考爬取华农数信院官网的新闻,并且发送到邮箱

改动在于:

  1. 爬取的字段变为标题、发布时间、描述
  2. 将发送到邮箱改为保存到数据库
  3. 爬取所有新闻而非单页新闻

需要注意的地方是我爬取的时候最后一页是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!

有关python+vue2+nodejs 搜索引擎课设 SCAU数信学院本科生通知检索(附代码)的更多相关文章

  1. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  2. Python 相当于 Perl/Ruby ||= - 2

    这个问题在这里已经有了答案:关闭10年前。PossibleDuplicate:Pythonconditionalassignmentoperator对于这样一个简单的问题表示歉意,但是谷歌搜索||=并不是很有帮助;)Python中是否有与Ruby和Perl中的||=语句等效的语句?例如:foo="hey"foo||="what"#assignfooifit'sundefined#fooisstill"hey"bar||="yeah"#baris"yeah"另外,类似这样的东西的通用术语是什么?条件分配是我的第一个猜测,但Wikipediapage跟我想的不太一样。

  3. java - 什么相当于 ruby​​ 的 rack 或 python 的 Java wsgi? - 2

    什么是ruby​​的rack或python的Java的wsgi?还有一个路由库。 最佳答案 来自Python标准PEP333:Bycontrast,althoughJavahasjustasmanywebapplicationframeworksavailable,Java's"servlet"APImakesitpossibleforapplicationswrittenwithanyJavawebapplicationframeworktoruninanywebserverthatsupportstheservletAPI.ht

  4. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  5. python - 如何读取 MIDI 文件、更改其乐器并将其写回? - 2

    我想解析一个已经存在的.mid文件,改变它的乐器,例如从“acousticgrandpiano”到“violin”,然后将它保存回去或作为另一个.mid文件。根据我在文档中看到的内容,该乐器通过program_change或patch_change指令进行了更改,但我找不到任何在已经存在的MIDI文件中执行此操作的库.他们似乎都只支持从头开始创建的MIDI文件。 最佳答案 MIDIpackage会为您完成此操作,但具体方法取决于midi文件的原始内容。一个MIDI文件由一个或多个音轨组成,每个音轨是十六个channel中任何一个上的

  6. 「Python|Selenium|场景案例」如何定位iframe中的元素? - 2

    本文主要介绍在使用Selenium进行自动化测试或者任务时,对于使用了iframe的页面,如何定位iframe中的元素文章目录场景描述解决方案具体代码场景描述当我们在使用Selenium进行自动化测试的时候,可能会遇到一些界面或者窗体是使用HTML的iframe标签进行承载的。对于iframe中的标签,如果直接查找是无法找到的,会抛出没有找到元素的异常。比如近在咫尺的例子就是,CSDN的登录窗体就是使用的iframe,大家可以尝试通过F12开发者模式查看到的tag_name,class_name,id或者xpath来定位中的页面元素,会抛出NoSuchElementException异常。解决

  7. 计算机毕业设计ssm+vue基本微信小程序的小学生兴趣延时班预约小程序 - 2

    项目介绍随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱小学生兴趣延时班预约小程序的设计与开发被用户普遍使用,为方便用户能够可以随时进行小学生兴趣延时班预约小程序的设计与开发的数据信息管理,特开发了小程序的设计与开发的管理系统。小学生兴趣延时班预约小程序的设计与开发的开发利用现有的成熟技术参考,以源代码为模板,分析功能调整与小学生兴趣延时班预约小程序的设计与开发的实际需求相结合,讨论了小学生兴趣延时班预约小程序的设计与开发的使用。开发环境开发说明:前端使用微信微信小程序开发工具:后端使用ssm:VU

  8. python ffmpeg 使用 pyav 转换 一组图像 到 视频 - 2

    2022/8/4更新支持加入水印水印必须包含透明图像,并且水印图像大小要等于原图像的大小pythonconvert_image_to_video.py-f30-mwatermark.pngim_dirout.mkv2022/6/21更新让命令行参数更加易用新的命令行使用方法pythonconvert_image_to_video.py-f30im_dirout.mkvFFMPEG命令行转换一组JPG图像到视频时,是将这组图像视为MJPG流。我需要转换一组PNG图像到视频,FFMPEG就不认了。pyav内置了ffmpeg库,不需要系统带有ffmpeg工具因此我使用ffmpeg的python包装p

  9. Python 刷Leetcode题库,顺带学英语单词(31) - 2

    ValidPalindromeGivenastring,determineifitisapalindrome,consideringonlyalphanumericcharactersandignoringcases. [#125]Example:"Aman,aplan,acanal:Panama"isapalindrome."raceacar"isnotapalindrome.Haveyouconsiderthatthestringmightbeempty?Thisisagoodquestiontoaskduringaninterview.Forthepurposeofthisproblem

  10. python - 是否可以使用 Ruby 或 Python 禁用 anchor /引用来发出有效的 YAML? - 2

    是否可以在PyYAML或Ruby的Psych引擎中禁用创建anchor和引用(并有效地显式列出冗余数据)?也许我在网上搜索时遗漏了一些东西,但在Psych中似乎没有太多可用的选项,而且我也无法确定PyYAML是否允许这样做.基本原理是我必须序列化一些数据并将其以可读的形式传递给一个不是真正的技术同事进行手动验证。有些数据是多余的,但我需要以最明确的方式列出它们以提高可读性(anchor和引用是提高效率的好概念,但不是人类可读性)。Ruby和Python是我选择的工具,但如果有其他一些相当简单的方法来“展开”YAML文档,它可能就可以了。 最佳答案

随机推荐