likes
comments
collection
share

eggjs+jieba对文章进行分词实现词云效果

作者站长头像
站长
· 阅读数 5

先来看看类似效果图

eggjs+jieba对文章进行分词实现词云效果

表结构:

分词收集表 eggjs+jieba对文章进行分词实现词云效果

分词统计表 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,分词和词云的结合还是挺有意思的,可以当业务场景的案例积累