简单新闻搜索引擎的实现
概述
文本主要介绍一个简单新闻搜索引擎项目的实现,主要包括四个部分:
- 新闻爬虫
- 使用elasticsearch存储数据
- 使用express搭建网站后端
- 使用vue搭建网站前端
项目github地址: https://github.com/joker-star-l/Simple-News-Search-Engine
新闻爬虫
本项目爬取的是 中国新闻网、 新华网和 光明网三家新闻网站的国际新闻板块。其中中国新闻网和新华网的新闻条目是动态加载的,需要找到动态请求的接口;光明网的新闻条目是静态的,直接解析页面即可。下面介绍具体的爬取流程。
中国新闻网
在浏览器开发者工具中的network栏,我们可以找到动态加载新闻条目的接口: https://channel.chinanews.com.cn/cns/cjs2/gj.shtml?pager=0&pagenum=9&t=8_58。
该接口有三个query参数,分别为pager、pagenum和t。经测试,pager表示页码数;pagenum表示本次请求需要的新闻条数;t表示请求时间,如果不携带参数t也能得到结果。
在该接口返回的结果中,去掉开头的“specialcnsdata = ”和结尾的“;newslist = specialcnsdata;”,中间部分为json风格的字符串,其结构如下。"docs"为存储了新闻条目信息的数组,数组中每个元素的“url”字段是我们需要的信息。
{
"docs":[
{
"pubtime":"", // 发布时间
"channel":"", // 新闻来源
"img_cns":"",
"id":"", // 新闻id
"title":"", // 新闻标题
"content":"", // 新闻内容(不全,只有前半部分)
"url":"", // 新闻url
"source_url":"", // 新闻url(跟url内容是重复的)
"galleryphoto":"", // 新闻图片url
"content_y":""
},
...
]
}
拿到了新闻的url数组之后,便可以遍历请求数组中的每一个url,获得具体的新闻页面信息,代码如下。
const request = require('request');
// 用于解析页面DOM元素的库,使用方法与jquery类似,使用css选择器语法筛选DOM元素
const cheerio = require('cheerio');
...
request(crawlerUtil.getUri(url), (err, res, body) => {
if (!err && res.statusCode === 200) {
const $ = cheerio.load(body)
let contentDOM = $("#cont_1_1_2 > div.left_zw > p")
// 获取新闻标题
let title = $('#cont_1_1_2 > h1').text().trim()
let content = ''
// 获取新闻内容
contentDOM.each((i, el) => {
content += $(el).text().trim() + '\n'
})
let abstract = content.slice(0, Math.min(100, content.length)).replace(/\n/g, ' ') + '...'
// 获取新闻发布时间
let time = getTime($("#cont_1_1_2 > div.left-time > div").text().trim())
// 获取新闻来源
let origin = $("#cont_1_1_2 > div.left-time > div").text().trim().slice(21, -4)
...
}
...
})
新华网
类似地,我们可以在浏览器开发者工具中的network栏找到新华网动态加载新闻条目的接口: http://www.news.cn/worldpro/ds_8d5294ed513c4779af6242a3623aa27b.json。
该接口返回的格式为json,“datasource”为存储了新闻条目信息的数组。与中国新闻网不同的是,该接口没有参数,返回的新闻条数固定为1000。“datasource”中每个元素的结构如下(无关属性不在此列出),“publishUrl”字段是我们需要的信息。
{
"title":"", // 新闻标题
"content":"", // 新闻内容(都是空字符串)
"publishUrl":"", // 新闻url
"publishTime":"", // 发布时间
...
}
类似地,我们可以遍历请求数组中的每一个url,获得具体的新闻页面信息,使用cheerio库来解析页面,此处不再赘述。
光明网
与上面两个网站不同的是,光明网的新闻条目是静态的,比如第一页新闻所在页面的url为 https://world.gmw.cn/node_4661.htm,第二页为 /node_4661_2.htm,第三页为 /node_4661_3.htm,以此类推,总共有10个页面。
我们可以使用cheerio库来拿到页面上所有新闻条目的url,请求并解析具体新闻页面的工作与之前类似,不再赘述。
使用elasticsearch存储数据
Elasticsearch 是一个分布式的免费开源搜索和分析引擎,适用于包括文本、数字、地理空间、结构化和非结构化数据等在内的所有类型的数据。Elasticsearch 以其简单的 REST 风格 API、分布式特性、速度和可扩展性而闻名。
建立连接
在node.js中,我们可以用以下方式建立一个与elasticsearch连接的客户端。
// 引入elasticsearch模块
const es = require('elasticsearch');
const url = 'http://127.0.0.1:9200'
// 创建与配置客户端
const client = new es.Client({
host: url
})
存储结构
在elasticsearch中建立索引存储新闻数据,具体结构如下。
{
"properties": {
"title": { // 新闻标题
"type": "text", // 类型:text(类型为text的字段会被分词)
"analyzer": "ik_max_word" // 分词器:ik分词器的max_word模式
},
"abstract": { // 新闻摘要
"type": "text",
"analyzer": "ik_max_word"
},
"content": { // 新闻内容
"type": "text",
"analyzer": "ik_max_word"
},
"time": { // 发布时间
"type": "date",
"format": "yyyy-MM-dd HH:mm"
},
"origin": { // 新闻来源
"type": "keyword"
},
"url": { // 新闻url
"type": "keyword"
},
"md5": { // 新闻内容经过md5加密后的结果
"type": "keyword"
}
}
}
在elasticsearch中,类型为“text”的字段会被分词,这里对“title”、“abstract”和“content”三个字段进行分词。“md5”为新闻内容被md5算法加密后的结果,用于去重。在爬虫爬取新闻的过程中,很有可能会出现一篇新闻被多次爬取的情况,而且同一篇新闻很有可能发布在不同的网站上,因此不能根据url来去重,而是要根据新闻内容来去重。但新闻的内容普遍较长,比较起来效率较低,使用md5算法加密得到较短的字符串进行比较能够很好地解决这个问题。
添加数据
本项目中,数据添加过程分为以下2个步骤:
- 判重:计算新闻的md5,判断elasticsearch中是否包含该值;
- 插入:如果不包含,则插入数据。
对应的添加函数如下。
function insertOne(data, callback=() => {}) {
const dataMd5 = md5(data.abstract)
client.search({
index,
type,
body: {
query: {
match: {md5: dataMd5}
}
}
})
.then(res => {
let dataList = res['hits']['hits']
if (dataList.length === 0) {
data.md5 = dataMd5
client.index({
index,
type,
body: data
})
.then(res => callback(null, res))
}
})
.catch(err => console.log(err.message))
}
查询数据
本项目中,搜索过程分为以下3个步骤:
- 分词:对搜索内容进行分词;
- 匹配:匹配要求为新闻标题或者内容包含搜索内容分词的60%以上;
- 排序:对匹配到的新闻首先根据elasticsearch计算出的相关性分数降序排序,分数相同的则根据时间降序排序。
对应的查询条件如下。
str = ''
analyzer = 'ik_max_word'
query: {
bool: {
should: [
{match: {title: {
query: str, // 查询内容
analyzer, // 分词器
minimum_should_match: '60%'
}}},
{match: {content: {
query: str, // 查询内容
analyzer, // 分词器
minimum_should_match: '60%'
}}}
],
minimum_should_match: 1
}
},
sort: [
{_score: 'desc'},
{time: 'desc'}
]
关于elasticsearch计算相关性分数的算法,在5.0版本以前使用的是TF/IDF算法,5.0版本以后改为了BM25算法,具体细节可以参考这篇博客。
使用express搭建网站后端
本项目后端较为简单,使用express框架快速搭建完成。
接口
1. 查询接口
GET /search
query参数:words
根据words查询相关新闻
2. 时间热度分析接口
GET /analyze
query参数:words
对words进行最近一周的时间热度分析
统一返回格式
规定标准的json数据返回格式。
{
"code": 200,
"msg": "success",
"data": {}
}
统一异常处理
使用异常中间件进行统一的异常处理。
app.use((err, req, res, next) => {
console.log('error: ' + err.message);
res.send({
code: 400,
msg: err.message
})
})
使用vue搭建网站前端
前端使用vue框架搭建,配合element-ui组件库以及echarts完成。
路由结构
export default new VueRouter({
routes: [
{
path: '/',
redirect: {
path: '/search'
}
},
{
path: '/search', // 查询页面
component: SearchPage
},
{
path: '/news-list/:words/:random', // 查询结果页面
component: NewsListPage
}
]
})
查询页面
查询结果页面
显示查询结果,进行了分页处理,每页显示10条新闻,点击新闻标题即可跳转到相应的新闻页面。
提供“时间”和“相关性”两种排序方式,默认按照“相关性”排序。
时间热度分析
显示时间热度分析图,横坐标为日期,纵坐标为相关的新闻条数。