前言
在閑暇之余挤牛,我們經(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();
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();
上圖可以看到,我們已經(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è)要注意的地方
時(shí)區(qū)問題:docker 時(shí)區(qū)是 UTC,和北京時(shí)間差了 8 小時(shí),會(huì)導(dǎo)致我們的定時(shí)任務(wù)時(shí)間失準(zhǔn).
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 代碼自行嘗試。
如果覺得有幫助到你,你懂的~