eggTest
最近做了一款 高仿ReadHub小程式 微信小程式 canvas 自動適配 自動換行,儲存圖片分享到朋友圈 gitee.com/richard1015…
具體程式碼已被開源,後續我會繼續更新,歡迎指正
你可能會像我一樣,平常對科技圈發生的熱點新聞很感興趣。每天利用剛開啟電腦的時候,又或者是工作間隙,瀏覽幾個更新及時的科技資訊網站。但是,科技圈每天發生的熱點新聞就那麼幾件。看來看去,新聞的重複度高,硬廣軟文還看了不少。不僅浪費時間,還抓不住重點。
ReadHub 通過爬蟲各種科技新聞 大資料過濾篩選 (個人猜想,大概是這一個意思),所以自己寫個爬蟲把資料爬到自己mysql資料庫中
程式碼思路:
通過網上各種呼叫 動態網站資料 爬蟲分為兩種解決方案
1.模擬瀏覽器請求 使用 相應框架 比如:Selenium、PhantomJs。
2.精準分析頁面,找到對應請求介面,直接獲取api資料。
優點:效能高,使用方便。我們直接獲取原資料介面(換句話說就是直接拿取網頁這一塊動態資料的API介面),肯定會使用方便,並且改變的可能性也比較小。 缺點:缺點也是明顯的,如何獲取介面API?有些網站可能會考慮到資料的安全性,做各種限制、混淆等。這就需要看開發者個人的基本功了,進行各種分析了。 我個人在爬取ReadHub時,發現《熱門話題》 列表是 無混淆,所以找到請求規律,爬取成功 ,剩下 開發者資訊、科技動態、區塊鏈快訊、招聘行情 請求index被混淆,所以暫無成功。 我在本次採用第二種解決方案 chrome瀏覽器分析
1.使用chrome 除錯工具 Mac 按 alt + cmd+ i Windows 按 F12 或者 右鍵檢查 或 審查元素 找到Network 選中 xhr模組
可通過圖片中看到 每次滾動載入資料時 都會有api請求資料, 我們發現 下次觸發滾動載入時,的lastCursor的值 為 上次請求的 陣列中最後一個物件中的order值
所以我們發現 只是的請求 url地址為 api.readhub.me/topic?lastC… 中 的lastCursor 動態設定,即可完成抓取資料
那麼接下來 我們需要 建立mysql資料庫
CREATE DATABASE `news` /*!40100 DEFAULT CHARACTER SET utf8 COLLATE utf8_bin */;
CREATE TABLE `news` (
`id` varchar(11) COLLATE utf8_bin NOT NULL,
`order` double NOT NULL,
`title` varchar(200) COLLATE utf8_bin NOT NULL,
`jsonstr` json DEFAULT NULL,
`createdAt` varchar(255) COLLATE utf8_bin DEFAULT NULL,
`updatedAt` varchar(255) COLLATE utf8_bin DEFAULT NULL,
`insertTime` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
複製程式碼
然後就是編寫 nodejs 中程式碼邏輯 我在下面的抓取衝採用 eggjs 框架中的 egg-mysql 進行連線資料庫 eggjs.org/zh-cn/tutor…
使用定時任務來 執行爬取資料
1.news.service 中程式碼實現
// app/service/news.js
const Service = require('egg').Service;
class NewsService extends Service {
async list(pageIndex = '', pageSize = '20') {
try {
// read config
const { serverUrl } = this.config.readhub;
// 熱門話題
const topic = `${serverUrl}topic?lastCursor=${pageIndex}&pageSize=${pageSize}`;
// use build-in http client to GET hacker-news api
const result = await this.ctx.curl(topic,
{
dataType: 'json',
}
);
if (result.status === 200) {
return result.data;
}
return [];
} catch (error) {
this.logger.error(error);
return [];
}
}
async saveDB(list) {
try {
const newsClient = this.app.mysql.get("news");
list.data.forEach(item => {
// 插入
newsClient.insert('news', {
id: item.id,
order: item.order,
title: item.title,
jsonstr: JSON.stringify(item),
createdAt: new Date(item.createdAt).getTime(),
updatedAt: new Date(item.updatedAt).getTime(),
}).then(result => {
// 判斷更新成功
const updateSuccess = result.affectedRows === 1;
this.logger.info(item.id + " > " + updateSuccess);
}).catch(error => {
//入庫失敗錯誤機制觸發
this.app.cache.errorNum += 1;
})
});
} catch (error) {
this.logger.error(error);
}
}
}
module.exports = NewsService;
複製程式碼
2.定時任務程式碼實現
update_cache.js
const Subscription = require('egg').Subscription;
class UpdateCache extends Subscription {
// 通過 schedule 屬性來設定定時任務的執行間隔等配置
static get schedule() {
return {
interval: '5s', // 隔單位 m 分 、 s 秒、 ms 毫秒
type: 'all', // 指定所有的 worker 都需要執行
immediate: true, //配置了該引數為 true 時,這個定時任務會在應用啟動並 ready 後立刻執行一次這個定時任務。
disable: false//配置該引數為 true 時,這個定時任務不會被啟動。
};
}
// subscribe 是真正定時任務執行時被執行的函式
async subscribe() {
let ctx = this.ctx;
ctx.logger.info('update cache errorNum = ' + ctx.app.cache.errorNum);
// errorNum 當錯誤數量 > 50時 停止抓取資料
if (ctx.app.cache.errorNum > 50) {
ctx.logger.info('errorNum > 50 stop ');
return;
}
ctx.logger.info('update cache begin ! currentLastCursor = ' + ctx.app.cache.lastCursor);
const pageIndex = ctx.app.cache.lastCursor || '';
const pageSize = '20';
const newsList = await ctx.service.news.list(pageIndex == 1 ? '' : pageIndex, pageSize);
if (newsList.data.length == 0) {
//沒有資料時錯誤機制觸發
this.app.cache.errorNum += 1;
ctx.logger.info('no data stop ! currentLastCursor = ' + ctx.app.cache.lastCursor);
} else {
ctx.service.news.saveDB(newsList)
ctx.app.cache.lastCursor = newsList.data[newsList.data.length - 1].order;
ctx.logger.info('update cache end ! currentLastCursor set = ' + ctx.app.cache.lastCursor);
}
}
}
module.exports = UpdateCache;
複製程式碼
update_cache_init.js
const Subscription = require('egg').Subscription;
class UpdateCacheInit extends Subscription {
// 通過 schedule 屬性來設定定時任務的執行間隔等配置
static get schedule() {
return {
interval: 60 * 24 + 'm', // 隔單位 m 分 、 s 秒、 ms 毫秒
type: 'all', // 指定所有的 worker 都需要執行
immediate: true, //配置了該引數為 true 時,這個定時任務會在應用啟動並 ready 後立刻執行一次這個定時任務。
disable: false//配置該引數為 true 時,這個定時任務不會被啟動。
};
}
// subscribe 是真正定時任務執行時被執行的函式
async subscribe() {
let ctx = this.ctx;
ctx.logger.info('update cache init');
if (ctx.app.cache.errorNum > 50) {
//初始化內建快取
ctx.app.cache = {
lastCursor: '',
errorNum: 0 //錯誤數量
};
}
}
}
module.exports = UpdateCacheInit;
複製程式碼
專案執行圖
QuickStart
see egg docs for more detail.
Development
$ npm i
$ npm run dev
$ open http://localhost:7001/
複製程式碼
Deploy
$ npm start
$ npm stop
複製程式碼
npm scripts
- Use
npm run lint
to check code style. - Use
npm test
to run unit test. - Use
npm run autod
to auto detect dependencies upgrade, see autod for more detail.