nodejs eggjs框架 爬蟲 readhub.me

eggTest

最近作了一款 高仿ReadHub小程序 微信小程序 canvas 自動適配 自動換行,保存圖片分享到朋友圈 gitee.com/richard1015…html

具體代碼已被開源,後續我會繼續更新,歡迎指正node

github.com/richard1015…mysql

gitee.com/richard1015…git

你可能會像我同樣,日常對科技圈發生的熱點新聞很感興趣。天天利用剛打開電腦的時候,又或者是工做間隙,瀏覽幾個更新及時的科技資訊網站。可是,科技圈天天發生的熱點新聞就那麼幾件。看來看去,新聞的重複度高,硬廣軟文還看了很多。不只浪費時間,還抓不住重點。github

ReadHub 經過爬蟲各類科技新聞 大數據過濾篩選 (我的猜測,大概是這一個意思),因此本身寫個爬蟲把數據爬到本身mysql數據庫中sql

代碼思路:chrome

經過網上各類調用 動態網站數據 爬蟲分爲兩種解決方案數據庫

1.模擬瀏覽器請求 使用 相應框架 好比:Selenium、PhantomJs。npm

2.精準分析頁面,找到對應請求接口,直接獲取api數據。json

優勢:性能高,使用方便。咱們直接獲取原數據接口(換句話說就是直接拿取網頁這一塊動態數據的API接口),確定會使用方便,而且改變的可能性也比較小。 缺點:缺點也是明顯的,如何獲取接口API?有些網站可能會考慮到數據的安全性,作各類限制、混淆等。這就須要看開發者我的的基本功了,進行各類分析了。 我我的在爬取ReadHub時,發現《熱門話題》 列表是 無混淆,因此找到請求規律,爬取成功 ,剩下 開發者資訊、科技動態、區塊鏈快訊、招聘行情 請求index被混淆,因此暫無成功。 我在本次採用第二種解決方案 chrome瀏覽器分析

1.使用chrome 調試工具 Mac 按 alt + cmd+ i Windows 按 F12 或者 右鍵檢查 或 審查元素 找到Network 選中 xhr模塊

預覽1
1.png

可經過圖片中看到 每次滾動加載數據時 都會有api請求數據, 咱們發現 下次觸發滾動加載時,的lastCursor的值 爲 上次請求的 數組中最後一個對象中的order值

因此咱們發現 只是的請求 url地址爲 api.readhub.me/topic?lastC… 中 的lastCursor 動態設置,便可完成抓取數據

預覽2
2.png

那麼接下來 咱們須要 創建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;
複製代碼

項目運行圖

預覽3
3.png
預覽4
4.png

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.
相關文章
相關標籤/搜索