「nodejs + docker + github pages 」 定製自己的 「今日頭條」

null仔發表於2019-11-21

前言

在閒暇之餘,我們經常會逛各種社群,逛掘金看技術軟文,逛虎撲看今日賽事,逛頭條看熱門時事,逛 91……

每個社群都有各種各樣的資訊,但有時我們只想看某個社群的某些資訊。那我們能不能將這些社群裡我們想要的資訊做一下整合 定製成自己的“今日頭條”呢?

思路

每天定時抓取 資訊的標題和連結 整合後釋出到自己的網站 這樣每天只要開啟自己的網站就可以看到屬於自己的今日頭條啦~

  • 抓取資訊 puppeteer
  • 定時任務 node-schedule
  • 部署 docker + github pages

我的今日頭條

  • 掘金社群 前端熱門文章
  • 今日頭條 熱門時事
  • 虎撲社群 nba 賽事
  • QQ 音樂 熱門音樂

ok,開擼...

專案初始化

npm init -y
複製程式碼
today's hot
│   README.md
└───html
│   │   index.html  // 網站入口,用於部署github pages
└───resource
│   │   index.json  // 資訊資料,爬取存放檔案
└───tasks           // 任務佇列
│   │   index.js
│   │   juejin.js
│   │   top.js
│   │   nba.js
│   │   music.js
│   │   jianshu.js
└───tools          //  工具類
    │   index.js
│   index.js       //  工程入口
│   package.json
複製程式碼

抓取資訊

抓取資訊 我使用的是 puppeteer,它是 Google Chrome 團隊官方的一個工具,提供了一些 API 來控制 chrome!(一聽就很刺激。)

npm i puppeteer --save
複製程式碼

我們先寫一個簡單的 demo 來了解一些 puppeteer 的基本 api.

const puppeteer = require("puppeteer");

const task = async () => {
  // 開啟chrome瀏覽器
  const browser = await puppeteer.launch({
    // 關閉無頭模式,方便檢視
    headless: false
  });
  // 新建頁面
  const page = await browser.newPage();
  // 跳轉到掘金
  await page.goto("https://juejin.im");
  // 截圖儲存
  await page.screenshot({
    path: "./juejin.png"
  });
};
task();
複製程式碼

juejin

ok~我們趁陰明站長不在的時候,來掘金"拿點"東西~

掘金的前端熱門文章是我比較關注的模組,我們來"拿"這個模組的資訊.

const puppeteer = require("puppeteer");

const task = async () => {
  // 開啟chrome瀏覽器
  const browser = await puppeteer.launch({
    headless: false
  });
  // 新建頁面
  const page = await browser.newPage();
  // 跳轉到掘金
  await page.goto("https://juejin.im");
  // 選單導航對應的類名
  const navSelector = ".view-nav .nav-item";
  // 前端選單
  const navType = "前端";
  // 等待選單載入完成...
  await page.waitFor(navSelector);
  // 選單導航名稱
  const navList = await page.$$eval(navSelector, ele =>
    ele.map(el => el.innerText)
  ); // [ '推薦', '後端', '前端', 'Android', 'iOS', '人工智慧', '開發工具', '程式碼人生', '閱讀' ]
  // 找出選單中前端模組對應的索引
  const webNavIndex = navList.findIndex(item => item === navType);
  // 點選前端模組並等待頁面跳轉完成
  await Promise.all([
    page.waitForNavigation(),
    page.click(`${navSelector}:nth-child(${webNavIndex + 1})`)
  ]);
  // 截圖儲存
  await page.screenshot({
    path: "./juejin-web.png"
  });
};
task();
複製程式碼

juejin

上圖可以看到,我們已經跳轉到了前端模組.

接下來,我們只要找出文章列表對應的類名就可以對它進行爬取.

const puppeteer = require("puppeteer");

const task = async () => {
  // 開啟chrome瀏覽器
  const browser = await puppeteer.launch({
    headless: false
  });
  // 新建頁面
  const page = await browser.newPage();
  // 跳轉到掘金
  await page.goto("https://juejin.im");
  // 選單導航選擇器
  const navSelector = ".view-nav .nav-item";
  // 文章列表選擇器
  const listSelector = ".entry-list .item a.title";
  // 選單類別
  const navType = "前端";
  await page.waitFor(navSelector);
  // 導航列表
  const navList = await page.$$eval(navSelector, ele =>
    ele.map(el => el.innerText)
  );
  // 前端導航索引
  const webNavIndex = navList.findIndex(item => item === navType);
  await Promise.all([
    page.waitForNavigation(),
    page.click(`${navSelector}:nth-child(${webNavIndex + 1})`)
  ]);
  // 等待文章列表選擇器載入完成
  await page.waitForSelector(listSelector, {
    timeout: 5000
  });
  // 通過選擇器找到對應列表項的標題和連結
  const res = await page.$$eval(listSelector, ele =>
    ele.map(el => ({
      url: el.href,
      text: el.innerText
    }))
  );
  // [ { url: 'https://juejin.im/post/5dd55512f265da47a807cc06',
  //   text: 'if 我是前端Leader,怎麼走出小微前端團隊的圍牆?' },
  // { url: 'https://juejin.im/post/5dd49a45e51d45400206a655',
  //   text: 'Koa還是那個Koa,但是Nodejs已經不再是那個Nodejs' },
  // { url: 'https://juejin.im/post/5dd4b991e51d450818244c30',
  //   text: 'WebSocket 原理淺析與實現簡單聊天' },...
};
task();
複製程式碼

ok,我們已經成功拿到了掘金前端熱門文章的內容,趁站長還沒來,趕緊溜~其他網站也是一樣的方法,這裡就不囉嗦了~

我們拿到了資訊,接下來對它進行儲存。

儲存資訊

因為只是玩具級別的 demo,這裡就不用資料庫了,簡單的用 json 進行儲存。

// resource/index.json
{
  "data": []
}
複製程式碼

我們基於 nodejs fs 檔案操作模組,簡單封裝讀寫方法。

// tools/index.js
const fs = require("fs");
const fileServer = {
  // 寫檔案
  write(path, text) {
    fs.writeFileSync(path, text);
  },
  // 讀檔案
  read(path) {
    return fs.readFileSync(path);
  }
};
複製程式碼

接下來,我們只要在每次獲取完資訊,將內容寫進檔案就好了

const { fileServer } = require("./tools");
const path = require("path");
const task = () => {
  // 獲取資訊任務
  const getMsgTask = Promise.all(tasks());
  getMsgTask.then(res => {
    // 讀取json
    const { data } = JSON.parse(
      fileServer.read(path.join(resourcePath, "./index.json")).toString()
    );
    // ... 此處省略對資訊 格式化內容
    const text = msgHandle(res);
    // 寫入資訊
    fileServer.write(
      path.join(resourcePath, "./index.json"),
      JSON.stringify({
        data: [
          {
            date: now,
            text
          },
          ...data
        ]
      })
    );
  });
};
複製程式碼

儲存完資訊,我們只要請求這個檔案,將它渲染出來就好了~

// html/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>今日資訊</title>
    <script src="https://cdn.bootcss.com/marked/0.7.0/marked.min.js"></script>
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
  </head>
  <body>
    <div id="content"></div>
  </body>
  <script>
    (function() {
        $.ajax({
          url: "http://localhost:8888/index.json",
          dataType: "json",
          success(data) {
            const content = data.data.reduce((a, b) => a + b.text, "");
            // 資訊我使用的是markdown進行儲存,所以用marked進行轉換
            $("#content").html(marked(content));
          }
        });
    })();
  </script>
</html>
複製程式碼

定時任務

定時任務使用的是node-schedule,非常簡單易用的一個 nodejs 庫。

// 每日18時定時任務
function crontab() {
  schedule.scheduleJob(`00 00 18 * * *`, mainTask);
}
// 任務
function mainTask(){...}
複製程式碼

部署

部署我採用的是 docker + github pages 。

docker 部署這裡有兩個要注意的地方

  1. 時區問題:docker 時區是 UTC,和北京時間差了 8 小時,會導致我們的定時任務時間失準.

  2. docker 和 puppeteer chorium 源問題 ...

# Dockerfile

FROM node:10-slim
# 建立專案程式碼的目錄
RUN mkdir -p /workspace

# 指定RUN、CMD與ENTRYPOINT命令的工作目錄
WORKDIR /workspace

# 複製宿主機當前路徑下所有檔案到docker的工作目錄
COPY . /workspace
# 清除npm快取檔案
RUN npm cache clean --force && npm cache verify
# 如果設定為true,則當執行package scripts時禁止UID/GID互相切換
# RUN npm config set unsafe-perm true

RUN npm config set registry "https://registry.npm.taobao.org"

RUN npm install -g pm2@latest
# Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others)
# Note: this installs the necessary libs to make the bundled version of Chromium that Puppeteer
# installs, work. 此處有牆...
# https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
  && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
  && apt-get update \
  && apt-get install -y google-chrome-unstable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf \
  --no-install-recommends \
  && rm -rf /var/lib/apt/lists/*

# 只安裝package.json dependencies
RUN npm install --production

RUN npm i puppeteer
# 設定時區
RUN rm -rf /etc/localtime && ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

EXPOSE 8888

CMD [ "pm2-docker", "start", "pm2.json" ]

複製程式碼

構建映象 shell

# build.sh
docker build -t today-hot .
複製程式碼

啟動容器 shell

# run.sh
curPath=`cd $(dirname $0);pwd -P`
docker run --name todayHot -d -v $curPath:/workspace -p 8888:8888 today-hot
複製程式碼

接下來只要把 html 檔案部署到網站上即可,我們這裡使用 github-pages ,免費的靜態網站託管平臺~

npm install gh-pages --save
複製程式碼

在 package.json 定義 scripts

  "scripts": {
    "deploy": "gh-pages -d html"
  }

  npm run deploy 將前端資源推送到github上,然後通過 xxx.github.io/xxx  就可以訪問了
複製程式碼

結語

本文主要講解的是思路,具體程式碼如下,爬蟲 服務並沒有部署到伺服器,大家可以 download 程式碼自行嘗試。

完整程式碼地址

效果

如果覺得有幫助到你,你懂的~

相關文章