「nodejs + docker + github pages 」 定制自己的 「今日头条」

前言

在闲暇之余,我们经常会逛各种社区,逛掘金看技术软文,逛虎扑看今日赛事,逛头条看热门时事,逛 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 代码自行尝试。

完整代码地址

效果

如果觉得有帮助到你,你懂的~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容