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

前言

在閑暇之余挤牛,我們經(jīng)常會(huì)逛各種社區(qū)莹痢,逛掘金看技術(shù)軟文,逛虎撲看今日賽事墓赴,逛頭條看熱門時(shí)事竞膳,逛 91……

每個(gè)社區(qū)都有各種各樣的資訊,但有時(shí)我們只想看某個(gè)社區(qū)的某些資訊诫硕。那我們能不能將這些社區(qū)里我們想要的信息做一下整合 定制成自己的“今日頭條”呢坦辟?

思路

每天定時(shí)抓取 資訊的標(biāo)題和鏈接 整合后發(fā)布到自己的網(wǎng)站 這樣每天只要打開自己的網(wǎng)站就可以看到屬于自己的今日頭條啦~

  • 抓取資訊 puppeteer
  • 定時(shí)任務(wù) node-schedule
  • 部署 docker + github pages

我的今日頭條

  • 掘金社區(qū) 前端熱門文章
  • 今日頭條 熱門時(shí)事
  • 虎撲社區(qū) nba 賽事
  • QQ 音樂 熱門音樂

ok,開擼...

項(xiàng)目初始化

npm init -y
today's hot
│   README.md
└───html
│   │   index.html  // 網(wǎng)站入口,用于部署github pages
└───resource
│   │   index.json  // 資訊數(shù)據(jù),爬取存放文件
└───tasks           // 任務(wù)隊(duì)列
│   │   index.js
│   │   juejin.js
│   │   top.js
│   │   nba.js
│   │   music.js
│   │   jianshu.js
└───tools          //  工具類
    │   index.js
│   index.js       //  工程入口
│   package.json

抓取資訊

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

npm i puppeteer --save

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

const puppeteer = require("puppeteer");

const task = async () => {
  // 打開chrome瀏覽器
  const browser = await puppeteer.launch({
    // 關(guān)閉無頭模式,方便查看
    headless: false
  });
  // 新建頁面
  const page = await browser.newPage();
  // 跳轉(zhuǎn)到掘金
  await page.goto("https://juejin.im");
  // 截屏保存
  await page.screenshot({
    path: "./juejin.png"
  });
};
task();
juejin

ok~我們趁陰明站長不在的時(shí)候,來掘金"拿點(diǎn)"東西~

掘金的前端熱門文章是我比較關(guān)注的模塊,我們來"拿"這個(gè)模塊的資訊.

const puppeteer = require("puppeteer");

const task = async () => {
  // 打開chrome瀏覽器
  const browser = await puppeteer.launch({
    headless: false
  });
  // 新建頁面
  const page = await browser.newPage();
  // 跳轉(zhuǎn)到掘金
  await page.goto("https://juejin.im");
  // 菜單導(dǎo)航對(duì)應(yīng)的類名
  const navSelector = ".view-nav .nav-item";
  // 前端菜單
  const navType = "前端";
  // 等待菜單加載完成...
  await page.waitFor(navSelector);
  // 菜單導(dǎo)航名稱
  const navList = await page.$$eval(navSelector, ele =>
    ele.map(el => el.innerText)
  ); // [ '推薦', '后端', '前端', 'Android', 'iOS', '人工智能', '開發(fā)工具', '代碼人生', '閱讀' ]
  // 找出菜單中前端模塊對(duì)應(yīng)的索引
  const webNavIndex = navList.findIndex(item => item === navType);
  // 點(diǎn)擊前端模塊并等待頁面跳轉(zhuǎn)完成
  await Promise.all([
    page.waitForNavigation(),
    page.click(`${navSelector}:nth-child(${webNavIndex + 1})`)
  ]);
  // 截屏保存
  await page.screenshot({
    path: "./juejin-web.png"
  });
};
task();
juejin

上圖可以看到,我們已經(jīng)跳轉(zhuǎn)到了前端模塊.

接下來,我們只要找出文章列表對(duì)應(yīng)的類名就可以對(duì)它進(jìn)行爬取.

const puppeteer = require("puppeteer");

const task = async () => {
  // 打開chrome瀏覽器
  const browser = await puppeteer.launch({
    headless: false
  });
  // 新建頁面
  const page = await browser.newPage();
  // 跳轉(zhuǎn)到掘金
  await page.goto("https://juejin.im");
  // 菜單導(dǎo)航選擇器
  const navSelector = ".view-nav .nav-item";
  // 文章列表選擇器
  const listSelector = ".entry-list .item a.title";
  // 菜單類別
  const navType = "前端";
  await page.waitFor(navSelector);
  // 導(dǎo)航列表
  const navList = await page.$$eval(navSelector, ele =>
    ele.map(el => el.innerText)
  );
  // 前端導(dǎo)航索引
  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
  });
  // 通過選擇器找到對(duì)應(yīng)列表項(xiàng)的標(biāo)題和鏈接
  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章办,怎么走出小微前端團(tuán)隊(duì)的圍墻?' },
  // { url: 'https://juejin.im/post/5dd49a45e51d45400206a655',
  //   text: 'Koa還是那個(gè)Koa锉走,但是Nodejs已經(jīng)不再是那個(gè)Nodejs' },
  // { url: 'https://juejin.im/post/5dd4b991e51d450818244c30',
  //   text: 'WebSocket 原理淺析與實(shí)現(xiàn)簡單聊天' },...
};
task();

ok,我們已經(jīng)成功拿到了掘金前端熱門文章的內(nèi)容,趁站長還沒來,趕緊溜~其他網(wǎng)站也是一樣的方法,這里就不啰嗦了~

我們拿到了資訊,接下來對(duì)它進(jìn)行保存滨彻。

保存資訊

因?yàn)橹皇峭婢呒?jí)別的 demo,這里就不用數(shù)據(jù)庫了,簡單的用 json 進(jìn)行保存。

// 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);
  }
};

接下來,我們只要在每次獲取完資訊,將內(nèi)容寫進(jìn)文件就好了

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

保存完資訊,我們只要請求這個(gè)文件,將它渲染出來就好了~

// 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進(jìn)行保存,所以用marked進(jìn)行轉(zhuǎn)換
            $("#content").html(marked(content));
          }
        });
    })();
  </script>
</html>

定時(shí)任務(wù)

定時(shí)任務(wù)使用的是node-schedule,非常簡單易用的一個(gè) nodejs 庫亭饵。

// 每日18時(shí)定時(shí)任務(wù)
function crontab() {
  schedule.scheduleJob(`00 00 18 * * *`, mainTask);
}
// 任務(wù)
function mainTask(){...}

部署

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

docker 部署這里有兩個(gè)要注意的地方

  1. 時(shí)區(qū)問題:docker 時(shí)區(qū)是 UTC,和北京時(shí)間差了 8 小時(shí),會(huì)導(dǎo)致我們的定時(shí)任務(wù)時(shí)間失準(zhǔn).

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

# Dockerfile

FROM node:10-slim
# 創(chuàng)建項(xiàng)目代碼的目錄
RUN mkdir -p /workspace

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

# 復(fù)制宿主機(jī)當(dāng)前路徑下所有文件到docker的工作目錄
COPY . /workspace
# 清除npm緩存文件
RUN npm cache clean --force && npm cache verify
# 如果設(shè)置為true辜羊,則當(dāng)運(yùn)行package scripts時(shí)禁止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
# 設(shè)置時(shí)區(qū)
RUN rm -rf /etc/localtime && ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

EXPOSE 8888

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

構(gòu)建鏡像 shell

# build.sh
docker build -t today-hot .

啟動(dòng)容器 shell

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

接下來只要把 html 文件部署到網(wǎng)站上即可,我們這里使用 github-pages ,免費(fèi)的靜態(tài)網(wǎng)站托管平臺(tái)~

npm install gh-pages --save

在 package.json 定義 scripts

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

  npm run deploy 將前端資源推送到github上,然后通過 xxx.github.io/xxx  就可以訪問了

結(jié)語

本文主要講解的是思路,具體代碼如下,爬蟲 服務(wù)并沒有部署到服務(wù)器,大家可以 download 代碼自行嘗試。

完整代碼地址

效果

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末词顾,一起剝皮案震驚了整個(gè)濱河市八秃,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌计技,老刑警劉巖喜德,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異垮媒,居然都是意外死亡舍悯,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門睡雇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來萌衬,“玉大人,你說我怎么就攤上這事它抱★踉ィ” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵观蓄,是天一觀的道長混移。 經(jīng)常有香客問我,道長侮穿,這世上最難降的妖魔是什么歌径? 我笑而不...
    開封第一講書人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮亲茅,結(jié)果婚禮上回铛,老公的妹妹穿的比我還像新娘。我一直安慰自己克锣,他們只是感情好茵肃,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著袭祟,像睡著了一般验残。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上巾乳,一...
    開封第一講書人閱讀 51,737評(píng)論 1 305
  • 那天胚膊,我揣著相機(jī)與錄音故俐,去河邊找鬼。 笑死紊婉,一個(gè)胖子當(dāng)著我的面吹牛药版,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播喻犁,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼槽片,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了肢础?” 一聲冷哼從身側(cè)響起还栓,我...
    開封第一講書人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎传轰,沒想到半個(gè)月后剩盒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡慨蛙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年辽聊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片期贫。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡跟匆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出通砍,到底是詐尸還是另有隱情玛臂,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布封孙,位于F島的核電站迹冤,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏虎忌。R本人自食惡果不足惜泡徙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望呐籽。 院中可真熱鬧锋勺,春花似錦蚀瘸、人聲如沸狡蝶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽贪惹。三九已至,卻和暖如春寂嘉,著一層夾襖步出監(jiān)牢的瞬間奏瞬,已是汗流浹背枫绅。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留硼端,地道東北人并淋。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像珍昨,于是被迫代替她去往敵國和親县耽。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容