eggjs+jieba对文章进行分词实现词云效果
先来看看类似效果图
表结构:
分词收集表
分词统计表
一,对文章进行分词
本来想使用 nodejieba库的,但看到很多人在安装的时候都会趟坑,这里我使用的是@node-rs/jieba库,安装比较简单,是基于rust重写的,官网说分词速度更快,但没有nodejieba提供的那么多方法,这里也只用了获取文章的前20个高频词,安装命令如下:
npm i @node-rs/jieba
jieba初始化,支持关键词命中,在初始化时传入
const fs = require('fs');
const path = require('path');
const jieba = require('@node-rs/jieba'); // 引入jieba库
let stopWordsObj = {} // 停词
// 初始化
async function jiebaInit() {
// 获取模块中元数据的排序数据
const keyWords = fs.readFileSync(path.resolve(__dirname, '../public/jieba_keywords.txt'))
// jieba.load()
// jieba.loadDict(fs.readFileSync(...))
// jieba.loadTFIDFDict(fs.readFileSync(...))
try {
console.log('jieba初始化')
jieba.loadDict(keyWords) // jieba初始化
} catch (error) {
console.log(error)
}
// 停词,过滤没意义的词
const stopWords = fs.readFileSync(path.resolve(__dirname, '../public/jieba_stopWords.txt'), 'utf8').replace(/\r/g, "").split('\n');
stopWordsObj = new Set(stopWords)
console.log('获取停词')
}
查询数据,并进行分词,返回前20个高频词写入分词收集表(文章为维度),和每天的统计表(每天为维度),一天会有多篇文章生成,统计表会合并累加当天文章的所有分词,取前50个高频词和count序列化记录下来
// 一天的所有文章分词
async function findDataAndCollectWord(ctx, createdTimeAry,keyWordsNum = 20) {
try {
const dayCountObj = {}
// 查询文章数据
const resParams = {
raw: true,
order: [
['created_at']
],
where: {
created_at: {
[Op.between]: createdTimeAry // [startTime, endTime]
}
}
};
// 查询文字来源数据
const resourceList = await ctx.model[targetModel].findAll(resParams)
if (resourceList.length === 0) return // 没有内容,停止执行
for (let j = 0; j < resourceList.length; j++) { // 遍历每篇文章
const resourceItem = resource[j]
// jieba方法返回最高词频的热词,优先返回命中的关键词
const contWords = jieba.extract(title, keyWordsNum) // keyWordsNum 分词数量 20
for (let k = 0; k < contWords.length; k++) {
const word = contWords[k].keyword
if (stopWordsObj.has(word)) { // 停词判断
// console.log('过滤词语:', word)
continue
}
if (!isNaN(word)) { // 过滤单纯数字
continue
}
// 数据入库
const wordParams = {
word,
targetId,
targetType
};
await ctx.model.xxCollect.create(wordParams);
if (dayCountObj[word]) { // 统计每个词出现的数量 dayCountObj.word = 1
dayCountObj[word].count++
} else {
dayCountObj[word] = {
type: keyWordsObj[word] ? 'keyWords': 'normal', // 标记是否是关键词
count: 1
}
}
}
}
// 统计每天最高热词,并从高到低排序 保存另一张统计表,记录每天的最高热词,列表查询直接查询该表返回
const dataObj = {}
const { itemcounts, itemJson } = sortObjBycount(dayCountObj, keyWordsNum) // 对象排序并取keyWordsNum =20个
dataObj[`dataCounts`] = itemcounts
dataObj[`dataJson`] = JSON.stringify(itemJson)
if (Object.keys(dataObj).length > 0) { // 没有数据不创建
await createCollectCounts(ctx, 'day', targetType, createdTimeAry, dataObj)
}
} catch (error) {
ctx.logger.error(error);
}
}
每天的文章分词--定时任务schedule
module.exports = {
schedule: {
cron: '00 55 23 * * ?', // 每天23:55:00开始执行
type: 'worker',
// immediate: true, //立刻执行一次
},
async task(ctx) {
console.log('开始执行查询内容分词')
const startTime = dayjs().format('YYYY-MM-DD 00:00:00');
const endTime = dayjs().format('YYYY-MM-DD 23:59:59');
// 执行分词内容查询
await findDataAndCollectWord(ctx, [startTime, endTime])
}
};
二,查询范围内的分词统计数据返回给前端
先查询范围内的统计表数据,把里面的分词数据重新合并累加,得出该时间段的热词排序后返回
// 获取分词列表接口
async wordCollectCountList() {
const { ctx, app, service } = this;
const reqBody = ctx.request.body;
ctx.valid({
collectTime: { // 日期范围:['2023-12-1', '2023-12-12']
type: 'array',
itemType: "string",
rule: {},
min: 2,
required: true
},
isKeyWord: { type: 'string', required: false }, // '1' : '0' 是否只返回业务定下的关键词
keyWordsNum: { type: 'number', required: false }, // 返回关键词数量
}, reqBody);
const { collectTime, isKeyWord, keyWordsNum } = reqBody;
const [startDate, endDate] = collectTime
const days = dayjs(endDate).diff(startDate, 'day') + 1 // 获取两个日期相差天数
if (days > 366) {
ctx.error(4001, '日期范围不能超过366天');
return;
}
const _startTime = dayjs(startDate).format('YYYY-MM-DD 00:00:00')
const _endTime = dayjs(endDate).format('YYYY-MM-DD 23:59:59')
const tableQuery = {
order: [
['countTime', 'DESC']
],
raw: true,
where: {
type: 'day',
countTime: {
[Op.between]: [_startTime, _endTime]
}
}
};
try {
let weekJsonObj = null // json大对象,合并count相加
// 查询每天的分词数据
const dayCollect = await ctx.model.xxCollectCounts.findAll(tableQuery)
// 统计每天最高热词
for (let j = 0; j < dayCollect.length; j++) {
const oneDayItem = dayCollect[j]
const oneJson = JSON.parse(oneDayItem[`dataJson`])
if (weekJsonObj) {
for (const key in oneJson) {
if (weekJsonObj.hasOwnProperty(key)) { // 这些时间段内的热词合并 加一
weekJsonObj[key].count += oneJson[key].count
} else {
weekJsonObj[key] = oneJson[key]
}
}
} else {
weekJsonObj = oneJson
}
}
// 没有数据返回
if (!weekJsonObj) {
ctx.success({
counts: 0,
list: []
});
return
}
const { itemcounts, itemAry } = sortObjBycount(weekJsonObj, keyWordsNum || 50) // 合并后的对象排序并取20个
// 组装数据
const dataAry = itemAry.map(item => {
return {
name: item.key,
value: item.count,
type: item.type // 是否关键词
}
})
const res = {
counts: itemcounts,
list: dataAry
};
ctx.success(res);
} catch (error) {
ctx.logger.error(error);
}
}
三,前端基于echarts-wordcloud实现自定义形状绘制词云图
echarts-wordcloud是基于echarts的一个词云库,支持自定义形状,这里要先安装echarts包,再安装echarts-wordcloud的包
npm install echarts
npm install echarts-wordcloud
下面是echarts-wordcloud的使用和配置 echarts-wordcloud官方github地址
// 添加一个div容器
<div id="cloudWord" ref="cloudWord"></div>
// 然后项目引入包
import * as echarts from 'echarts';
import 'echarts-wordcloud';
// 加载纯黑色背景的形状图,例如:云图,心形等,注意不能纯白色背景,否则在火狐展示不出来
var maskImage = new Image();
// 重点:图片的base64码, 可以在阿里iconfont图标库下载图片,再把图片转成base64,注意检查火狐效果,不行就找UI给吧
maskImage.src ='data:image/png;base64,...'
// vue里的配置项和初始化
data() {
return {
cloudECharts: null,
cloudOption: {
title: {
x: 'center',
text: 'xxx热词统计',
textStyle: {
fontSize: 19,
fontWeight: '600'
}
},
backgroundColor: '#fff',
series: [{
type: 'wordCloud',
sizeRange: [15, 70], // 用来调整字的大小范围
rotationRange: [0, 0], // 每个词旋转的角度范围和旋转的步进
rotationStep: 40,
gridSize: 10, // 用来调整词之间的距离
// 可用的形状有(circle)圆形(默认)、(cardioid)心形,(diamond)菱形,(triangle-forward)三角形向前,(triangle)三角形,(pentagon)五边形和(star)星形。*/
shape: 'circle',
left: 'center', //位置的配置
top: 'center',
// width: '600px', // 如果不是原比例的大小,会影响形状展示
// height: '80%',
drawOutOfBound: false, // 允许词太大的时候,超出画布的范围
layoutAnimation: true, // 布局的时候是否有动画
maskImage:maskImage, // 自定义形状图片,此处添加图片的base64格式
// 如果字体太大而无法显示文本,
// 是否收缩文本。如果将其设置为false,则文本将不渲染。如果设置为true,则文本将被缩小。
// 从echarts-wordcloud@2.1.0开始支持此选项
shrinkToFit: true,
textStyle: {
// 颜色可以用一个函数来返回字符串,这里是随机色
color: function (v) {
return 'rgb(' + [
Math.round(Math.random() * 180),
Math.round(Math.random() * 180),
Math.round(Math.random() * 180)
].join(',') + ')';
},
fontFamily: 'sans-serif',
fontWeight: '550'
},
// emphasis: { // hover上去的效果
// focus: 'self',
// textStyle: {
// textShadowBlur: 10,
// // textShadowColor: '#333'
// }
// },
//data格式是一个数组对象, [{ name: '', value: 10 }]
data: []
}]
},
}
},
mounted() {
//随着屏幕大小调节图表
window.addEventListener("resize", () => {
this.cloudECharts && this.cloudECharts.resize();
});
},
methods:{
getList(){
// axios请求上面的 wordCollectCountList接口 获取词云数据,这里就不写了
const cloudData = res || [] // [{ name: '', value: 10 }]
// 渲染词云
if (this.cloudECharts) {
this.cloudECharts.dispose(); // 切换数据时需先销毁
}
if (cloudData && cloudData.length > 0) {
this.cloudOption.series[0].data = cloudData
this.cloudECharts = echarts.init(this.$refs.cloudWord)
this.cloudECharts.setOption(this.cloudOption)
}
}
}
总结
1,关键词需要足够多,足够凝练,以便于命中文章分词展示,否则会分出很多看起来无意义的词,这是要在技术落地前和业务反复确认的点 2,词云图如果没有足够多的词填充,会显得比较空,没有铺满的效果好看 3,分词和词云的结合还是挺有意思的,可以当业务场景的案例积累
转载自:https://juejin.cn/post/7329801250706227210