10分鐘教你擼一個nodejs爬蟲系統

最近在搗鼓一個仿簡書的開源項目,從前端到後臺,一戰擼到底。就須要數據支持,最近mock數據,比較費勁。簡書的不少數據都是後臺渲染的,很難快速抓api請求數據,本人又比較懶,就想到用寫個簡易爬蟲系統。css

項目初始化

安裝nodejs,官網中文網。根據本身系統安裝,這裏跳過,表示你已經安裝了nodejs。html

選擇一款順手拉風的編輯器,用來寫代碼。推薦webstorm最近版。前端

webstorm建立一個工程,起一個喜歡的名字。建立一個package.json文件,webstorm快捷建立package.json很是簡單。仍是用命令行建立,打開Terminal,默認當前項目根目錄,npm init,一直下一步。node

能夠看這裏npm經常使用你應該懂的使用技巧jquery

主要技術棧

  • superagent 頁面數據下載git

  • cheerio 頁面數據解析github

這是2個npm包,咱們先下載在接着繼續,下載須要時間的。web

npm install superagent cheerio --save

接下啦簡單說說這2個是啥東西ajax

superagent 頁面數據下載

superagent是nodejs裏一個很是方便的客戶端請求代碼模塊,superagent是一個輕量級的,漸進式的ajax API,可讀性好,學習曲線低,內部依賴nodejs原生的請求API,適用於nodejs環境下。數據庫

請求方式

  • get (默認)

  • post

  • put

  • delete

  • head

語法:request(RequestType, RequestUrl).end(callback(err, res));

寫法:

request
    .get('/login')
    .end(function(err, res){
        // code
    });

設置Content-Type

  • application/json (默認)

  • form

  • json

  • png

  • xml

  • ...

設置方式:

1. 
request
    .get('/login')
    .set('Content-Type', 'application/json');
2. 
request
    .get('/login')
    .type('application/json');
3. 
request
    .get('/login')
    .accept('application/json');

以上三種方效果同樣。

設置參數

  • query

  • send

query

設置請求參數,能夠寫json對象或者字符串形式。

json對象{key,value}

能夠寫多組key,value

request
    .get('/login')
    .query({
        username: 'jiayi',
        password: '123456'
    });
字符串形式key=value

能夠寫多組key=value,須要用&隔開

request
    .get('/login')
    .query('username=jiayi&password=123456');

sned

設置請求參數,能夠寫json對象或者字符串形式。

json對象{key,value}

能夠寫多組key,value

request
    .get('/login')
    .sned({
        username: 'jiayi',
        password: '123456'
    });
字符串形式key=value

能夠寫多組key=value,須要用&隔開

request
    .get('/login')
    .sned('username=jiayi&password=123456');

上面兩種方式可使用在一塊兒

request
    .get('/login')
    .query({
        id: '100'
    })
    .sned({
          username: 'jiayi',
          password: '123456'
      });

響應屬性Response

Response text

Response.text包含未解析前的響應內容,通常只在mime類型可以匹配text/json、x-www-form-urlencoding的狀況下,默認爲nodejs客戶端提供,這是爲了節省內存,由於當響應以文件或者圖片大內容的狀況下影響性能。

Response header fields

Response.header包含解析以後的響應頭數據,鍵值都是node處理成小寫字母形式,好比res.header('content-length')。

Response Content-Type

Content-Type響應頭字段是一個特列,服務器提供res.type來訪問它,默認res.charset是空的,若是有的化,則自動填充,例如Content-Type值爲text/html;charset=utf8,則res.type爲text/html;res.charset爲utf8。

Response status

http響應規範

cheerio 頁面數據解析

cheerio是一個node的庫,能夠理解爲一個Node.js版本的jquery,用來從網頁中以 css selector取數據,使用方式和jquery基本相同。

  • 類似的語法:Cheerio 包括了 jQuery 核心的子集。Cheerio 從jQuery庫中去除了全部 DOM不一致性和瀏覽器尷尬的部分,揭示了它真正優雅的API。

  • 閃電般的塊:Cheerio 工做在一個很是簡單,一致的DOM模型之上。解析,操做,呈送都變得難以置信的高效。基礎的端到端的基準測試顯示Cheerio 大約比JSDOM快八倍(8x)。

  • 巨靈活: Cheerio 封裝了兼容的htmlparser。Cheerio 幾乎可以解析任何的 HTML 和 XML document。

須要先loading一個須要加載html文檔,後面就能夠jQuery同樣使用操做頁面了。

const cheerio = require('cheerio');
const $ = cheerio.load('<ul id="fruits">...</ul>');
$('#fruits').addClass('newClass');

基本全部選擇器基本和jQuery同樣,就不一一列舉。具體怎麼使用看官網

上面已經基本把咱們要用到東西有了基本的瞭解了,咱們用到比較簡單,接下來就開始寫代碼了,爬數據了哦。

抓取首頁文章列表20條數據

根目錄建立一個app.js文件。

實現思路步驟

  1. 引入依賴

  2. 定義一個地址

  3. 發起請求

  4. 頁面數據解析

  5. 分析頁面數據

  6. 生成數據

1. 引入依賴:

const superagent = require('superagent');
const cheerio = require('cheerio');

2. 定義一個地址

const reptileUrl = "http://www.jianshu.com/";

3. 發起請求

superagent.get(reptileUrl).end(function (err, res) {
    // 拋錯攔截
     if(err){
         return throw Error(err);
     }
    // 等待 code
});

這個時候咱們會向簡書首頁發一個請求,只要不拋錯,走if,那麼就能夠繼續往下看了。

4. 頁面數據解析

superagent.get(reptileUrl).end(function (err, res) {
    // 拋錯攔截
     if(err){
         return throw Error(err);
     }
   /**
   * res.text 包含未解析前的響應內容
   * 咱們經過cheerio的load方法解析整個文檔,就是html頁面全部內容,能夠經過console.log($.html());在控制檯查看
   */
   let $ = cheerio.load(res.text);
});

註釋已經說明這行代碼的意思,就不在說明了。就下了就比較難了。

5. 分析頁面數據

你需在瀏覽器打開簡書官網,簡書是後臺渲染部分可見的數據,後續數據是經過ajax請求,使用js填充。咱們爬數據,通常只能爬到後臺渲染的部分,js渲染的是爬不到,若是ajax,你能夠直接去爬api接口,那個往後再說。

言歸正傳,簡書首頁文章列表,默認會加載20條數據,這個已經夠我用了,你每次刷新,若是有更新就會更新,最新的永遠在最上面。

這20條數據存在頁面一個類叫.note-list的ul裏面,每條數據就是一個li,ul父級有一個id叫list-container,學過html的都知道id是惟一,保證不出錯,我選擇id往下查找。

$('#list-container .note-list li')

上面就是cheerio幫咱們獲取到說有須要的文章列表的li,是否是和jq寫同樣。我要獲取li裏面內容就須要遍歷 Element.each(function(i, elem) {}) 也是和jq同樣

$('#list-container .note-list li').each(function(i, elem) {
   // 拿到當前li標籤下全部的內容,開始幹活了
});

以上都比較簡單,複雜的是下面的,數據結構。咱們須要怎麼拼裝數據,我大體看了一下頁面,根據經驗總結了一個結構,還算靠譜。

{
     id:  每條文章id
    slug:每條文章訪問的id (加密的id)
    title: 標題
    abstract: 描述
    thumbnails: 縮略圖 (若是文章有圖,就會抓第一張,若是沒有圖就沒有這個字段)
   collection_tag:文集分類標籤
   reads_count: 閱讀計數
   comments_count: 評論計數
   likes_count:喜歡計數
   author: {    做者信息
      id:沒有找到
      slug: 每一個用戶訪問的id (加密的id)
      avatar:會員頭像
      nickname:會員暱稱(註冊填的那個)
      sharedTime:發佈日期
   }
}

基本數據結構有了,先定義一個數組data,來存放拼裝的數據,留給後面使用。

隨便截取一條文章數據

<li id="note-12732916" data-note-id="12732916" class="have-img">
    <a class="wrap-img" href="/p/b0ea2ac2d5c4" target="_blank">
      <img src="//upload-images.jianshu.io/upload_images/1996705-7e00331b8f3dbc5d.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/375/h/300" alt="300" />
    </a>
  <div class="content">
    <div class="author">
      <a class="avatar" target="_blank" href="/u/652fbdd1e7b3">
        <img src="//upload.jianshu.io/users/upload_avatars/1996705/738ba2908445?imageMogr2/auto-orient/strip|imageView2/1/w/96/h/96" alt="96" />
</a>      <div class="name">
        <a class="blue-link" target="_blank" href="/u/652fbdd1e7b3">xxx</a>
        <span class="time" data-shared-at="2017-05-24T08:05:12+08:00"></span>
      </div>
    </div>
    <a class="title" target="_blank" href="/p/b0ea2ac2d5c4">xxxxxxx</a>
    <p class="abstract">
     xxxxxxxxx...
    </p>
    <div class="meta">
        <a class="collection-tag" target="_blank" href="/c/8c92f845cd4d">xxxx</a>
      <a target="_blank" href="/p/b0ea2ac2d5c4">
        <i class="iconfont ic-list-read"></i> 414
</a>        <a target="_blank" href="/p/b0ea2ac2d5c4#comments">
          <i class="iconfont ic-list-comments"></i> 2
</a>      <span><i class="iconfont ic-list-like"></i> 16</span>
        <span><i class="iconfont ic-list-money"></i> 1</span>
    </div>
  </div>
</li>

咱們就拿定義的數據結構和實際的頁面dom去一一比對,去獲取咱們想要的數據。

id: 每條文章id

li上有一個 data-note-id="12732916"這個東西就是文章的id,
怎麼獲取:$(elem).attr('data-note-id'),這樣就完事了

slug:每條文章訪問的id (加密的id)

若是你點文章標題,或者帶縮略圖的位置,都會跳轉一個新頁面 http://www.jianshu.com/p/xxxxxx 這樣的格式。標題是一個a連接,連接上有一個href屬性,裏面有一段 /p/xxxxxx 這樣的 /p/是文章詳情一個標識,xxxxxx是標識哪片文章。而咱們slug就是這個xxxxxx,就須要處理一下。$(elem).find('.title').attr('href').replace(//p//, ""),這樣就能夠獲得xxxxxx了。

title: 標題

這個簡單,$(elem).find('.title').text()就行了。

abstract: 描述

這個簡單,$(elem).find('.abstract').text()就行了。

thumbnails: 縮略圖 (若是文章有圖,就會抓第一張,若是沒有圖就沒有這個字段)

這個存在.wrap-img這a標籤裏面img裏,若是沒有就不顯示,$(elem).find('.wrap-img img').attr('src'),若是取不到就是一個undefined,那正合我意。

下面4個都在.meta的div裏面 (我沒有去打賞的數據,由於我不須要這個數據)

collection_tag:文集分類標籤

有對應的class,$(elem).find('.collection-tag').text()

reads_count: 閱讀計數

這個就比較麻煩了,它的結構是這樣的

<a target="_blank" href="/p/b0ea2ac2d5c4">
        <i class="iconfont ic-list-read"></i> 414
</a>

還要有一個字體圖標的class可使用,否則還真很差玩,那須要怎麼獲取了,$(elem).find('.ic-list-read').parent().text(),先去查找這個字體圖標i標籤,而後去找它的父級a標籤,獲取裏面text文本,標籤就不被獲取了,只剩下數字。

接下來2個同樣處理的。

comments_count: 評論計數

$(elem).find('.ic-list-comments').parent().text()

likes_count:喜歡計數

$(elem).find('.ic-list-like').parent().text()

接來就是會員信息,所有都在.author這個div裏面

id:沒有找到

slug: 每一個用戶訪問的id (加密的id)

這個處理方式和文章slug同樣,$(elem).find('.avatar').attr('href').replace(//u//, ""),惟一不一樣的須要吧p換成u。

avatar:會員頭像

$(elem).find('.avatar img').attr('src')

nickname:會員暱稱(註冊填的那個)

暱稱存在一個叫.blue-link標籤裏面,$(elem).find('.blue-link').text()

sharedTime:發佈日期

這個發佈日期,你看到頁面是個性化時間,xx小時前啥的,若是直接取就是一個坑爹的事了,在.time的span上有一個data-shared-at="2017-05-24T08:05:12+08:00"這個纔是正真的時間,你會發現它一上來是空的,是js來格式化的。$(elem).find('.time').attr('data-shared-at')

以上就是全部字段來源的。接下來要說一個坑爹的事,text()獲取出來的,有回車符/n和空格符/s。因此須要寫一個方法把它們去掉。

function replaceText(text){
    return text.replace(/\n/g, "").replace(/\s/g, "");
}

組裝起來的數據代碼:

let data = [];
// 下面就是和jQuery同樣獲取元素,遍歷,組裝咱們須要數據,添加到數組裏面
$('#list-container .note-list li').each(function(i, elem) {
    let _this = $(elem);
    data.push({
       id: _this.attr('data-note-id'),
       slug: _this.find('.title').attr('href').replace(/\/p\//, ""),
       author: {
           slug: _this.find('.avatar').attr('href').replace(/\/u\//, ""),
           avatar: _this.find('.avatar img').attr('src'),
           nickname: replaceText(_this.find('.blue-link').text()),
           sharedTime: _this.find('.time').attr('data-shared-at')
       },
       title: replaceText(_this.find('.title').text()),
       abstract: replaceText(_this.find('.abstract').text()),
       thumbnails: _this.find('.wrap-img img').attr('src'),
       collection_tag: replaceText(_this.find('.collection-tag').text()),
       reads_count: replaceText(_this.find('.ic-list-read').parent().text()) * 1,
       comments_count: replaceText(_this.find('.ic-list-comments').parent().text()) * 1,
       likes_count: replaceText(_this.find('.ic-list-like').parent().text()) * 1
   });
});

let _this = $(elem); 先把$(elem);存到一個變量裏面,jq寫習慣了。

有幾個*1是吧數字字符串轉成數字,js小技巧,不解釋。

6. 生成數據

數據已經能夠獲取了,都存在data這個數據裏面,如今是20條數據,咱們理想的數據,那麼放在node裏面,咱們仍是拿不到,怎麼辦,一個存在數據庫(尚未弄到哪裏,我都尚未想好怎麼建數據庫表設計),一個就存在本地json文件。

那就存在本地json文件。nodejs是一個服務端語言,就說能夠訪問本地磁盤,添加文件和訪問文件。須要引入nodejs內置的包fs。

const fs = require('fs');

它的其餘用法不解釋了,只說一個建立一個文件,而且在裏面寫內容

這是寫文件的方法:

fs.writeFile(filename,data,[options],callback); 
/**
 * filename, 必選參數,文件名
 * data, 寫入的數據,能夠字符或一個Buffer對象
 * [options],flag 默認‘2’,mode(權限) 默認‘0o666’,encoding 默認‘utf8’
 * callback  回調函數,回調函數只包含錯誤信息參數(err),在寫入失敗時返回。
 */

咱們須要這樣來寫了:

// 寫入數據, 文件不存在會自動建立
fs.writeFile(__dirname + '/data/article.json', JSON.stringify({
    status: 0,
    data: data
}), function (err) {
    if (err) throw err;
    console.log('寫入完成');
});

注意事項

  1. 我方便管理數據,放在data文件夾,若是你也是這樣,記得必定先要在根目錄建一個data文件夾否則就會報錯

  2. 默認utf-8編碼;

  3. 寫json文件必定要JSON.stringify()處理,否則就是[object Object]這貨了。

  4. 若是是文件名能夠直接article.json會自動生成到當前項目根目錄裏,若是要放到某個文件裏,例如data,必定要加上__dirname + '/data/article.json'。千萬不能寫成3. 若是是文件名能夠直接article.json會自動生成到當前項目根目錄裏,若是要放到某個文件裏,例如data,必定要加上__dirname + '/data/article.json'。千萬不能寫成'/data/article.json'否則就會拋錯,找不到文件夾,由於文件夾在你所在的項目的盤符裏。例如G:/data/article.json。

以上基本就完成一個列表頁面的抓取。看下完整代碼:

/**
 * 獲取依賴
 * @type {*}
 */
const superagent = require('superagent');
const cheerio = require('cheerio');
const fs = require('fs');
/**
 * 定義請求地址
 * @type {*}
 */
const reptileUrl = "http://www.jianshu.com/";
/**
 * 處理空格和回車
 * @param text
 * @returns {string}
 */
function replaceText(text) {
  return text.replace(/\n/g, "").replace(/\s/g, "");
}
/**
 * 核心業務
 * 發請求,解析數據,生成數據
 */
superagent.get(reptileUrl).end(function (err, res) {
    // 拋錯攔截
    if (err) {
        return throw Error(err);
    }
    // 解析數據
    let $ = cheerio.load(res.text);
    /**
     * 存放數據容器
     * @type {Array}
     */
    let data = [];
    // 獲取數據
    $('#list-container .note-list li').each(function (i, elem) {
        let _this = $(elem);
        data.push({
            id: _this.attr('data-note-id'),
            slug: _this.find('.title').attr('href').replace(/\/p\//, ""),
            author: {
                slug: _this.find('.avatar').attr('href').replace(/\/u\//, ""),
                avatar: _this.find('.avatar img').attr('src'),
                nickname: replaceText(_this.find('.blue-link').text()),
                sharedTime: _this.find('.time').attr('data-shared-at')
            },
            title: replaceText(_this.find('.title').text()),
            abstract: replaceText(_this.find('.abstract').text()),
            thumbnails: _this.find('.wrap-img img').attr('src'),
            collection_tag: replaceText(_this.find('.collection-tag').text()),
            reads_count: replaceText(_this.find('.ic-list-read').parent().text()) * 1,
            comments_count: replaceText(_this.find('.ic-list-comments').parent().text()) * 1,
            likes_count: replaceText(_this.find('.ic-list-like').parent().text()) * 1
        });
    });
   // 生成數據
    // 寫入數據, 文件不存在會自動建立
    fs.writeFile(__dirname + '/data/article.json', JSON.stringify({
        status: 0,
        data: data
    }), function (err) {
        if (err) throw err;
        console.log('寫入完成');
    });
});

一個簡書首頁文章列表的爬蟲就大工告成了,運行代碼,打開Terminal運行node app.js或者node app都行。或者在package.json的scripts對象下添加一個"dev": "node app.js",而後用webstorm的npm面板運行。

有文章列表就有對應的詳情頁面,後面繼續講解怎麼爬詳情。

抓取首頁文章列表對應的20條詳情數據

有了上面抓取文章列表的經驗,接下來就好辦多了,完事開頭難。

實現思路步驟

  1. 引入依賴

  2. 定義一個地址

  3. 發起請求

  4. 頁面數據解析

  5. 分析頁面數據

  6. 生成數據

1. 引入依賴

這個就不用引入,在一個文件裏面,由於比較簡單的,代碼很少,懶得分文件寫。導入導出模塊麻煩,人懶就這樣的。

但咱們須要寫一個函數,來處理爬詳情的方法。

function getArticle(item){
   // 等待code
}

2. 定義一個地址

注意這個地址,是有規律的,不是隨便的地址,隨便點開一篇文章就能夠看到地址欄,http://www.jianshu.com/p/xxxxxx, 咱們定義的reptileUrl = "http://www.jianshu.com/";那麼就須要拼地址了,還記得xxxxxx咱們存在哪裏嗎,存在slug裏面。請求地址:reptileUrl + 'p/' + item.slug

3. 發起請求

superagent.get(reptileUrl + 'p/' + item.slug).end(function (err, res) {
    // 拋錯攔截
     if(err){
         return throw Error(err);
     }
});

你懂的

4. 頁面數據解析

superagent.get(reptileUrl + 'p/' + item.slug).end(function (err, res) {
    // 拋錯攔截
     if(err){
         return throw Error(err);
     }
   /**
   * res.text 包含未解析前的響應內容
   * 咱們經過cheerio的load方法解析整個文檔,就是html頁面全部內容,能夠經過console.log($.html());在控制檯查看
   */
   let $ = cheerio.load(res.text);
});

5. 分析頁面數據

你可能會按上面的方法,打開一個頁面,而後就去獲取標籤上面的class,id。我開始也在這個上面遇到一個坑,頁面上有閱讀 ,評論 ,喜歡 這三個數據,我一開始覺得都是直接load頁面就有數據,在獲取時候,並無數據,是一個空。我就奇怪,而後我就按了幾回f5刷新,發現問題了,這幾個數據的是頁面加載完成之後才顯示出來的,那麼就是說這個有多是js渲染填充的。那就說明的我寫的代碼沒有錯。

有問題要解決呀,若是是js渲染,要麼會有網絡加載,刷新幾回,沒有這個數據,那就只能存在頁面裏,寫的內聯的script標籤裏面了,右鍵查看源碼,ctrl+f搜索,把閱讀 ,評論 ,喜歡的數字,隨便挑一個,找到了最底部data-name="page-data"的script標籤裏面,有一個json對象,裏面有些字段,和我文章列表定義很像,就是這個。有了這個就好辦了,省的我去截取一大堆操做。

解析script數據

let note = JSON.parse($('script[data-name=page-data]').text());

script裏面數據

{"user_signed_in":false,"locale":"zh-CN","os":"windows","read_mode":"day","read_font":"font2","note_show":{"is_author":false,"is_following_author":false,"is_liked_note":false,"uuid":"7219e299-034d-4051-b995-a6a4344038ef"},"note":{"id":12741121,"slug":"b746f17a8d90","user_id":6126137,"notebook_id":12749292,"commentable":true,"likes_count":59,"views_count":2092,"public_wordage":1300,"comments_count":29,"author":{"total_wordage":37289,"followers_count":221,"total_likes_count":639}}}

把script裏面內容都獲取出來,而後用 JSON方法,字符串轉對象。

接下來依舊是要定義數據結構:

article: {   文章信息
     id:  文章id
     slug:  每條文章訪問的id (加密的id)
    title: 標題
    content: 正文(記得要帶html標籤的)
    publishTime: 更新時間
     wordage: 字數
     views_count: 閱讀計數
    comments_count: 評論計數
    likes_count: 喜歡計數
},
author: {
    id: 用戶id
   slug: 每一個用戶訪問的id (加密的id)
   avatar: 會員頭像
   nickname: 會員暱稱(註冊填的那個)
   signature: 會員暱稱簽名
   total_wordage: 總字數
   followers_count: 總關注計數
   total_likes_count: 總喜歡計數
}

還要專題分類和評論列表我沒有累出來,有興趣能夠本身去看看怎麼爬出來。它們是單獨api接口,數據結構就不須要了。

由於有了note 這個對象不少數據都簡單了,仍是一個一個說明來源

article 文章信息

id: 文章id

主要信息都存在note.note裏面,文章id就是note.note.id,

slug: 每條文章訪問的id (加密的id)

note.note.slug

title: 標題
全部的正文都存在.post下的.article裏,那麼獲取title就是$('div.post').find('.article .title').text()

content: 正文(記得要帶html標籤的)

注意正文不是獲取text文本是要獲取html標籤,須要用到html來獲取而不是text,$('div.post').find('.article .show-content').html() 返回都是轉義字符。到時候前端須要處理就會顯示了。雖然咱們看不懂,瀏覽器看得懂就好了。

publishTime: 更新時間

這時間直接顯示出來了,不是個性化時間,直接取就行了$('div.post').find('.article .publish-time').text()

wordage: 字數

這個是一個標籤裏面<字數 1230>這樣的,咱們確定不能要這樣的,須要吧數字提取出來,$('div.post').find('.article .wordage').text().match(/d+/g)[0]*1 用正則獲取數字字符串,而後轉成數字。

views_count: 閱讀計數

note.note.views_count

comments_count: 評論計數

note.note.comments_count

likes_count: 喜歡計數

note.note.likes_count

author 用戶信息

id: 用戶id

前面的文章列表咱們並無拿到用戶id,note.note發現了一個user_id,反正不論是不是先存了再說,別空着,note.note.user_id

slug: 每一個用戶訪問的id (加密的id)

文章列表怎麼獲取,這個就怎麼獲取$('div.post').find('.avatar').attr('href').replace(//u//, "")

avatar: 會員頭像

$('div.post').find('.avatar img').attr('src')

nickname: 會員暱稱(註冊填的那個)

$('div.post').find('.author .name a').text()

signature: 會員暱稱簽名

這個簽名在上面位置了,就在文章正文下面,評論和打賞上面,有個很大關注按鈕那個灰色框框裏面,最早一段文字。$('div.post').find('.signature').text()

total_wordage: 總字數

note.note.author.total_wordage

followers_count: 總關注計數

note.note.author.followers_count

total_likes_count: 總喜歡計數

note.note.author.total_likes_count

有些字段命名就是從note.note這個json對象裏面獲取的,一開始我也不知道取什麼名字。

最終拼接的數據

/**
         * 存放數據容器
         * @type {Array}
         */
        let data = {
            article: {
                id: note.note.id,
                slug: note.note.slug,
                title: replaceText($post.find('.article .title').text()),
                content: replaceText($post.find('.article .show-content').html()),
                publishTime: replaceText($post.find('.article .publish-time').text()),
                wordage: $post.find('.article .wordage').text().match(/\d+/g)[0]*1,
                views_count: note.note.views_count,
                comments_count: note.note.comments_count,
                likes_count: note.note.likes_count
            },
            author: {
                id: note.note.user_id,
                slug: $post.find('.avatar').attr('href').replace(/\/u\//, ""),
                avatar: $post.find('.avatar img').attr('src'),
                nickname: replaceText($post.find('.author .name a').text()),
                signature: replaceText($post.find('.signature').text()),
                total_wordage: note.note.author.total_wordage,
                followers_count: note.note.author.followers_count,
                total_likes_count: note.note.author.total_likes_count
            }
        };

6. 生成數據

和列表生成數據基本同樣,有一個區別。文件須要加一個標識,article_+ item.slug(文章訪問的id)

// 寫入數據, 文件不存在會自動建立
        fs.writeFile(__dirname + '/data/article_' + item.slug + '.json', JSON.stringify({
            status: 0,
            data: data
        }), function (err) {
            if (err) throw err;
            console.log('寫入完成');
        });

基本就擼完了,看獲取詳情的完整代碼:

function getArticle(item) {
// 拼接請求地址
  let url = reptileUrl + '/p/' + item.slug;
   /**
 * 核心業務
 * 發請求,解析數據,生成數據
 */
    superagent.get(url).end(function (err, res) {
        // 拋錯攔截
    if (err) {
        return throw Error(err);
    }
      // 解析數據
        let $ = cheerio.load(res.text);
    // 獲取容器,存放在變量裏,方便獲取
        let $post = $('div.post');
    // 獲取script裏的json數據
        let note = JSON.parse($('script[data-name=page-data]').text());
        /**
         * 存放數據容器
         * @type {Array}
         */
        let data = {
            article: {
                id: note.note.id,
                slug: note.note.slug,
                title: replaceText($post.find('.article .title').text()),
                content: replaceText($post.find('.article .show-content').html()),
                publishTime: replaceText($post.find('.article .publish-time').text()),
                wordage: $post.find('.article .wordage').text().match(/\d+/g)[0]*1,
                views_count: note.note.views_count,
                comments_count: note.note.comments_count,
                likes_count: note.note.likes_count
            },
            author: {
                id: note.note.user_id,
                slug: $post.find('.avatar').attr('href').replace(/\/u\//, ""),
                avatar: $post.find('.avatar img').attr('src'),
                nickname: replaceText($post.find('.author .name a').text()),
                signature: replaceText($post.find('.signature').text()),
                total_wordage: note.note.author.total_wordage,
                followers_count: note.note.author.followers_count,
                total_likes_count: note.note.author.total_likes_count
            }
        };
       // 生成數據
        // 寫入數據, 文件不存在會自動建立
        fs.writeFile(__dirname + '/data/article_' + item.slug + '.json', JSON.stringify({
            status: 0,
            data: data
        }), function (err) {
            if (err) throw err;
            console.log('寫入完成');
        });
    });
}

你確定要問了,在哪裏調用了,
在上面獲取文章列表的請求end裏面底部隨便找個位置加上:

data.forEach(function (item) {
        getArticle(item);
    });

運行,你就會在data文件夾裏看到21個json文件。源文件,歡迎指正Bug。

相關文章
相關標籤/搜索