《花100塊做個摸魚小網站! 》第八篇—增加詞雲元件和搜尋元件

sum墨發表於2024-10-22

⭐️基礎連結導航⭐️

伺服器 → ☁️ 阿里雲活動地址

看樣例 → 🐟 摸魚小網站地址

學程式碼 → 💻 原始碼庫地址

一、前言

大家好呀,我是summo,最近小網站崩潰了幾天,原因一個是SSL證書到期,二個是免費的RDS也到期了,而我正邊學習邊找工作中,就沒有顧得上修,不好意思哈(PS:八股文好難背,演算法好難刷)。

小網站的內容和元件也不少了,今天我們繼續來豐富的它的功能,讓它看起來更美觀和有用。今天會增加詞雲元件和搜尋元件,並且還會將網站的內容排列一下,難度不高,但是更有意思。我們先從詞雲元件開始做。

二、詞雲元件

不同機構的熱搜有一樣也有不一樣的,詞雲元件的作用是將熱搜標題進行分詞和計數,統計出最高頻率的熱搜,方便大家快速瞭解最熱的熱搜內容是什麼。

1. 結巴分詞器

jieba是一個分詞器,可以實現智慧拆詞,最早是提供了python包,後來由花瓣(huaban)開發出了java版本。
原始碼連線:https://github.com/huaban/jieba-analysis

(1) maven依賴

<!-- jieba分詞器 -->
<dependency>
  <groupId>com.huaban</groupId>
  <artifactId>jieba-analysis</artifactId>
  <version>1.0.2</version>
</dependency>

(2) 寫一個Demo試試分詞器

Demo如下:

package com.summo.sbmy.web.controller;

import com.google.common.collect.Lists;
import com.huaban.analysis.jieba.JiebaSegmenter;

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class WordCloudTest {

    public static void main(String[] args) {
        List<String> titleList = Lists.newArrayList(
                "《花100塊做個摸魚小網站! 》第七篇—誰訪問了我們的網站?",
                "《花100塊做個摸魚小網站! 》第六篇—將小網站部署到雲伺服器上",
                "《花100塊做個摸魚小網站! 》第五篇—透過xxl-job定時獲取熱搜資料",
                "《花100塊做個摸魚小網站! 》第四篇—前端應用搭建和完成第一個熱搜元件",
                "《花100塊做個摸魚小網站! 》第三篇—熱搜表結構設計和熱搜資料儲存",
                "《花100塊做個摸魚小網站! 》第二篇—後端應用搭建和完成第一個爬蟲",
                "《花100塊做個摸魚小網站! 》第一篇—買雲伺服器和初始化環境",
                "《花100塊做個摸魚小網站! · 序》靈感來源");
        JiebaSegmenter segmenter = new JiebaSegmenter();
        Map<String, Integer> wordCount = new HashMap<>();
        Iterator<String> var4 = titleList.iterator();

        while (var4.hasNext()) {
            String title = var4.next();
            List<String> words = segmenter.sentenceProcess(title.trim());
            Iterator<String> var7 = words.iterator();

            while (var7.hasNext()) {
                String word = var7.next();
                wordCount.put(word, wordCount.getOrDefault(word, 0) + 1);
            }
        }
        wordCount.forEach((word, count) -> {
            System.out.println("word->" + word + ";count->" + count);
        });
    }

}

執行結果如下:

從結果上看,句子已經被分成多個詞語,並且統計出了次數,但是還出現了很多無意義的詞語,比如“的”、“和”、“了”這些,這樣的詞語被稱為停用詞,一般這樣的詞要過濾掉。我們可以去網上搜尋常見的停用詞,然後在設定權重的時候把它給剔除掉。我使用的停用詞庫已經提交到了程式碼庫中,大家可以直接取用。

(3) 熱搜標題分詞介面

WordCloudController.java

package com.summo.sbmy.web.controller;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Sets;
import com.huaban.analysis.jieba.JiebaSegmenter;
import com.summo.sbmy.cache.hotSearch.HotSearchCacheManager;
import com.summo.sbmy.cache.sys.SysConfigCacheManager;
import com.summo.sbmy.common.model.dto.HotSearchDTO;
import com.summo.sbmy.common.model.dto.WordCloudDTO;
import com.summo.sbmy.common.result.ResultModel;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.*;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/hotSearch/wordCloud")
public class WordCloudController {

    private static Set<String> STOP_WORDS;
    private static JSONArray WEIGHT_WORDS_ARRAY;

    @RequestMapping("/queryWordCloud")
    public ResultModel<List<WordCloudDTO>> queryWordCloud(@RequestParam(required = true) Integer topN) {
        List<HotSearchDTO> hotSearchDTOS = gatherHotSearchData();
        List<String> titleList = hotSearchDTOS.stream().map(HotSearchDTO::getHotSearchTitle).collect(Collectors.toList());
        return ResultModel.success(findTopFrequentNouns(titleList, topN));
    }

    /**
     * 獲取停用詞
     *
     * @return
     */
    private List<HotSearchDTO> gatherHotSearchData() {
        String stopWordsStr = SysConfigCacheManager.getConfigByGroupCodeAndKey("WordCloud", "StopWords");
        STOP_WORDS = Sets.newHashSet(stopWordsStr.split(","));
        WEIGHT_WORDS_ARRAY = JSONArray.parseArray(SysConfigCacheManager.getConfigByGroupCodeAndKey("WordCloud", "WeightWords"));
        List<HotSearchDTO> hotSearchDTOS = new ArrayList<>();
        HotSearchCacheManager.CACHE_MAP.forEach((key, detail) -> {
            hotSearchDTOS.addAll(detail.getHotSearchDTOList());
        });
        return hotSearchDTOS;
    }

    /**
     * 分詞
     *
     * @param titleList 標題列表
     * @param topN      擷取指定長度的熱詞大小
     * @return
     */
    public static List findTopFrequentNouns(List<String> titleList, int topN) {
        JiebaSegmenter segmenter = new JiebaSegmenter();
        Map<String, Integer> wordCount = new HashMap<>();
        Iterator<String> var4 = titleList.iterator();

        while (var4.hasNext()) {
            String title = var4.next();
            List<String> words = segmenter.sentenceProcess(title.trim());
            Iterator<String> var7 = words.iterator();

            while (var7.hasNext()) {
                String word = var7.next();
                wordCount.put(word, wordCount.getOrDefault(word, 0) + 1);
            }
        }

        return wordCount.entrySet().stream()
                //停用詞過濾
                .filter(entry -> !STOP_WORDS.contains(entry.getKey()))
                //構建物件
                .map(entry -> WordCloudDTO.builder().word(entry.getKey()).rate(entry.getValue()).build())
                //權重替換
                .map(wordCloudDTO -> {
                    if (CollectionUtils.isEmpty(WEIGHT_WORDS_ARRAY)) {
                        return wordCloudDTO;
                    } else {
                        WEIGHT_WORDS_ARRAY.forEach(weightedWord -> {
                            JSONObject tempObject = (JSONObject) weightedWord;
                            if (wordCloudDTO.getWord().equals(tempObject.getString("originWord"))) {
                                wordCloudDTO.setWord(tempObject.getString("targetWord"));
                                if (tempObject.containsKey("weight")) {
                                    wordCloudDTO.setRate(tempObject.getIntValue("weight"));
                                }
                            }
                        });
                        return wordCloudDTO;
                    }
                })
                //按出現頻率進行排序
                .sorted(Comparator.comparing(WordCloudDTO::getRate).reversed())
                //擷取前topN的資料
                .limit(topN)
                .collect(Collectors.toList());
    }

}

這裡我加了一個權重替換的邏輯,因為我發現分詞器對於有些熱詞的解析有問題。比如前段時間很火的熱搜“黑神話-悟空”,但在中文裡面“黑神話”並不是一個詞語,所以結巴在分詞的時候只能識別“神話”這個詞。為了解決這樣的問題,我就加了一個手動替換的邏輯。

2. 前端元件

(1) vue-wordcloud元件

元件官方文件連結如下:https://www.npmjs.com/package/vue-wordcloud

npm引入指令如下:cnpm install vue-wordcloud

(2) 元件程式碼

WordCloud.vue

<template>
  <el-card class="word-cloud-card">
    <wordcloud
      class="word-cloud"
      :data="words"
      nameKey="name"
      valueKey="value"
      :wordPadding="2"
      :fontSize="[10,50]"
      :showTooltip="true"
      :wordClick="wordClickHandler"
    />
  </el-card>
</template>

<script>
import wordcloud from "vue-wordcloud";
import apiService from "@/config/apiService.js";

export default {
  name: "app",
  components: {
    wordcloud,
  },
  methods: {
    wordClickHandler(name, value, vm) {
      console.log("wordClickHandler", name, value, vm);
    },
  },
  data() {
    return {
      words: [],
    };
  },
  created() {
    apiService
      .get("/hotSearch/wordCloud/queryWordCloud?topN=100")
      .then((res) => {
        this.words = res.data.data.map((item) => ({
          value: item.rate,
          name: item.word,
        }));
      })
      .catch((error) => {
        // 處理錯誤情況
        console.error(error);
      });
  },
};
</script>
<style scoped>
.word-cloud-card {
  padding: 0% !important;
  max-height: 300px;
  margin-top: 10px;
}
.word-cloud {
  max-height: 300px;
}
>>> .el-card__body {
  padding: 0;
}
</style>

元件使用起來很容易,效果也還不錯,但是造成了一個小BUG,用完這個元件後會導致小網站底部出現一個留白,現在都不知道怎麼解決。

三、重新佈局和搜尋元件

1. 重新佈局

由於小網站的元件越來越多,整體的佈局也需要重新設計一下,目前大概的佈局如下:

佈局使用的也是ElementUI自帶的佈局元件:

<el-container>
  <el-header> ... </el-header>
  <el-main> ... </el-main>
  <el-footer> ... </<el-footer>
</el-container>

2. 搜尋元件

搜尋元件使用的是<el-autocomplete>,使用方法看API文件就可以了。元件不難,唯一要注意的是搜尋出來的結果內容是可能會重複的,所以我們需要對結果加一個來源標識。
這裡需要使用一個slot組裝一個自定義元件,效果像這樣:

元件程式碼如下:

<template slot-scope="{ item }">
  <div style="display: flex; justify-content: space-between">
    <span style="max-width: 280px;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;">
      {{ item.label }}
    </span>
    <span style="max-width: 80px; color: #8492a6; font-size: 13px; white-space: nowrap; " >
      <img :src="getResourceInfo(item.resource).icon" style="width: 16px; height: 16px; vertical-align: middle"/>
        {{ getResourceInfo(item.resource).title }}
    </span>
  </div>
</template>

具體的邏輯可以去看我的原始碼,我這裡就不貼整個程式碼了。

四、小結一下

這些小元件並不是一開始就想好要做的,大部分都是我突然靈機一動想起來才做的。可能有些東西看起來並不是那麼有用,但是看著小網站的內容不斷豐富起來感覺非常不錯。這段時間我已經把全部的原始碼都提交到Gitee上了,但是還沒來得及review,所以後面我除了分享怎麼做元件外,還會跟大家分享我這4個月來遇到的一些BUG和問題,以及為什麼我的程式碼要這樣寫。

番外:頭條熱搜爬蟲

1. 爬蟲方案評估

頭條的熱搜介面返回的一串JSON格式資料,這就很簡單了,省的我們去解析dom,訪問連結是:[https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc)

2. 網頁解析程式碼

ToutiaoHotSearchJob.java

package com.summo.sbmy.job.toutiao;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import com.summo.sbmy.common.model.dto.HotSearchDetailDTO;
import com.summo.sbmy.dao.entity.SbmyHotSearchDO;
import com.summo.sbmy.service.SbmyHotSearchService;
import com.summo.sbmy.service.convert.HotSearchConvert;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.apache.commons.collections4.CollectionUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

import static com.summo.sbmy.cache.hotSearch.HotSearchCacheManager.CACHE_MAP;
import static com.summo.sbmy.common.enums.HotSearchEnum.TOUTIAO;

/**
 * @author summo
 * @version ToutiaoHotSearchJob.java, 1.0.0
 * @description  頭條熱搜Java爬蟲程式碼
 * @date 2024年08月09
 */
@Component
@Slf4j
public class ToutiaoHotSearchJob {

    @Autowired
    private SbmyHotSearchService sbmyHotSearchService;

    @XxlJob("toutiaoHotSearchJob")
    public ReturnT<String> hotSearch(String param) throws IOException {
        log.info(" 頭條熱搜爬蟲任務開始");
        try {
            //查詢今日頭條熱搜資料
            OkHttpClient client = new OkHttpClient().newBuilder().build();
            Request request = new Request.Builder().url(
                    "https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc").method("GET", null).build();
            Response response = client.newCall(request).execute();
            JSONObject jsonObject = JSONObject.parseObject(response.body().string());
            JSONArray array = jsonObject.getJSONArray("data");
            List<SbmyHotSearchDO> sbmyHotSearchDOList = Lists.newArrayList();
            for (int i = 0, len = array.size(); i < len; i++) {
                //獲取知乎熱搜資訊
                JSONObject object = (JSONObject)array.get(i);
                //構建熱搜資訊榜
                SbmyHotSearchDO sbmyHotSearchDO = SbmyHotSearchDO.builder().hotSearchResource(
                        TOUTIAO.getCode()).build();
                //設定知乎三方ID
                sbmyHotSearchDO.setHotSearchId(object.getString("ClusterIdStr"));
                //設定文章連線
                sbmyHotSearchDO.setHotSearchUrl(object.getString("Url"));
                //設定文章標題
                sbmyHotSearchDO.setHotSearchTitle(object.getString("Title"));
                //設定熱搜熱度
                sbmyHotSearchDO.setHotSearchHeat(object.getString("HotValue"));
                //按順序排名
                sbmyHotSearchDO.setHotSearchOrder(i + 1);
                sbmyHotSearchDOList.add(sbmyHotSearchDO);
            }
            if (CollectionUtils.isEmpty(sbmyHotSearchDOList)) {
                return ReturnT.SUCCESS;
            }
            //資料加到快取中
            CACHE_MAP.put(TOUTIAO.getCode(), HotSearchDetailDTO.builder()
                    //熱搜資料
                    .hotSearchDTOList(sbmyHotSearchDOList.stream().map(HotSearchConvert::toDTOWhenQuery).collect(Collectors.toList()))
                    //更新時間
                    .updateTime(Calendar.getInstance().getTime()).build());
            //資料持久化
            sbmyHotSearchService.saveCache2DB(sbmyHotSearchDOList);
            log.info(" 頭條熱搜爬蟲任務結束");
        } catch (IOException e) {
            log.error("獲取頭條資料異常", e);
        }
        return ReturnT.SUCCESS;
    }

    @PostConstruct
    public void init() {
        // 啟動執行爬蟲一次
        try {
            hotSearch(null);
        } catch (IOException e) {
            log.error("啟動爬蟲指令碼失敗",e);
        }
    }
}

相關文章