Re: 從零開始的【comic spider】《最簡單的實現》(上)

前言

一個最簡單的漫畫腳本最基本的功能是下載,可是在是下載以前還要作什麼?是找資源!那麼「搜索」功能也應該算在一個最簡單漫畫爬蟲的基本功能。因此,接下來圍繞這兩個功能看看從一零開始的 ic-comic-spiderhtml

ps:咱們用於爬取漫畫的站點肯定爲 chuixuenode

歡迎 star試用 yo!ios

正文

實現目標

# 下載漫畫全本
icsdr http://xxx

# 搜索漫畫
icsdr --search [comic name]
複製代碼

分析頁面

目錄頁面

咱們以 ONE-PUNCH MAN 的目錄頁爲例子。 目錄頁面的分析目標是:1. 獲取漫畫名;2. 獲取漫畫每章節的連接;git

審查元素,能夠找到漫畫名的節點:github

咱們能夠經過如下方式複製節點路徑方便待會獲取對應節點:npm

# 漫畫名的節點路徑
#intro_l > div.title > h1

# 目錄章節節點路徑
#play_0 ul > li > a
複製代碼

 

章節內漫畫圖片頁

圖片頁就不能直接經過審查元素獲取節點路徑獲取圖片的資源連接了。圖片的節點通常都不會直接在服務端渲染後,基本上都是在瀏覽器端渲染,所以咱們直接用axios請求到的html文本是瀏覽器渲染前的,言外之意就是裏面是沒有圖片的節點的!json

那麼,想要獲取圖片的資源連接咱們就須要直接從源碼中找圖片連接的組成規則,當前這個網站也是如此!axios

如今我打開ONE-PUNCH MAN第127話頁面,審查元素能夠看到解析後的圖片連接:api

根據這個渲染後的連接去源碼裏面找連接的組成方式:打開 控制檯 -> source,開始閱讀源碼找組合圖片連接的邏輯,此處省略十年!

而後,如下是我找到的組合規則,而且封裝成函數:promise

const imgOrigins = ['http://2.csc1998.com/', 'http://img.csc1998.com/'];
const viewidThreshold = 519253;
/** * 獲取圖片origin * @param { number } viewId */
function getImgOrigin(viewId) {
  let origin;
  if (viewId >= viewidThreshold) {
    origin = imgOrigins[0];
  } else {
    origin = imgOrigins[1];
  }
  return origin;
}

/** * 獲取圖片連接的後半部分 * @param { string } _doc html文本 */
function getImgPaths(_doc) {
  const photosr = [];
  const packedMatcheds = _doc.match(/packed=\"(.*)\"\;/);
  const packed = packedMatcheds && packedMatcheds[1];
  if (packed) {
    const temp = Buffer.from(packed, 'base64').toString();
    eval(eval(temp.slice(4)));
    photosr.shift();
  } else {
    const photosrMatched = _doc.match(/photosr\[.+\] =\".*";/);
    if (photosrMatched) {
        eval(photosrMatched[0]);
        photosr.shift();
    }
  }

  return photosr;
}

/** * 組合img的origin和 paths * @param { string } _origin * @param { string[] } _paths */
function generateImgSrcs(_origin, _paths) {
    const srcs = _paths.map((imgPath) => {
        return url.resolve(_origin, imgPath);
    });
    return srcs;
}
複製代碼

 

搜索的分析

首先是先找到搜索的api是什麼!


控制檯 -> Network,而後進行搜索,你就會發現這個搜索的api(大部分的站點也是這麼找)

那麼搜索的api便是:

http://www.chuixue.net/e/search/?searchget=1&show=title,player,playadmin,pinyin&keyboard=[keywork]
複製代碼

 

接下來就是分析結果列表


搜索結果的節點路徑

#dmList > ul > li > dl > dt > a
複製代碼

這樣就能夠拿到搜索到的資源了!

 

 

實現下載

須要用到的npm包:

  • axios:請求資源(獲取頁面html和下載漫畫圖片流)

  • cheerio:將html文本轉化成dom,方便提取目標資源連接

    主要用到的幾個方法是:

    1. .text( [textString] ):獲取節點文本
    2. .attr([string]):獲取節點屬性
    3. .html([string]):獲取節點html文本
    4. .eq([number]):獲取節點列表中的其中之一
  • iconv-lite: 用於轉碼請求回來的html文本,主要是爲了解決中文亂碼問題

    這裏補充說明如何知道html文本的編碼。這裏有兩個方法:

    1. 控制檯 -> Element:

    1. 控制檯 -> Console, 打印document.characterSet

獲取目錄資源

獲取目錄的dom對象


  1. axios請求目錄頁面,獲取html文本;
  2. iconv-lite將html文本轉碼成utf-8解決中文亂碼;
  3. 將html文本轉化成cheerio的dom對象;
/** * 獲取目錄dom對象 * @param {*} _url */
function getCatalogDom(_url) {
  const reqOptions = {
    method: 'GET',
    url: _url,
    responseType: 'arraybuffer'
  };
  return axios.request(reqOptions).then((resp) => {
    const data = iconv.decode(resp.data, 'GBK');
    const doc = cheerio.load(data);
    return doc;
  }).catch((err) => {
    console.log(err);
  });
}
複製代碼

 

獲取漫畫名字


漫畫名字節點路徑咱們已經知道,使用剛剛拿到的dom對象,使用cheerio的.text()獲取名字。

/** * 獲取漫畫名字 * @param { string } _doc */
function getComicName(_doc) {
  const selectorPath = '#intro_l > div.title > h1';
  return _doc(selectorPath).text();
}
複製代碼

 

獲取章節列表資源


使用上面找到的節點路徑便可獲取到章節的節點列表,而後使用.eq遍歷配合.attr方法便可獲取到章節的名字和連接。

/** * 獲取章節列表資源 * @param { object } _doc * @param { string } _catalogUrl */
function getComicChaptes(_doc, _catalogUrl) {
  const { URL } = url;
  const origin = (new URL(_catalogUrl)).origin;
  const selectorPath = '#play_0 ul > li > a';
  const chapterEles = _doc(selectorPath);
  const list = [];
  for (let i = 0, len = chapterEles.length; i < len; i+=1) {
    const ele = chapterEles.eq(i);
    list.push({
      title: ele.attr('title'),
      url: url.resolve(origin, ele.attr('href'))
    })
  }
  return list;
}
複製代碼

 

封裝一下以上函數


/** * 獲取目錄資源 * @param { string } _catalogUrl */
function parseCatalog(_catalogUrl) {
  return getCatalogDom(_catalogUrl)
    .then((_doc) => {
      const comicName = getComicName(_doc);
      const catalogs = getComicChaptes(_doc, _catalogUrl);
      return {
        comicName,
        catalogs
      };
    });
}
複製代碼

而後咱們只需調用這個parseCatalog函數便可獲取到漫畫的目錄資源

 

 

獲取漫畫圖片資源

解析完目錄以後,就是要獲取到每一個章節的圖片支資源連接,這個也是站點極力想要保護的部分,或多或少都會作一些防盜鏈的措施,好比chuixue就把圖片資源放列表用base64加密,而後將解密邏輯隱藏在其餘文件,這樣直接抓取漫畫頁就不能很容易地拿到圖片連接。

上面我已經找到組合圖片連接的邏輯,如今咱們首先要作的是把html文本爬回來,而後正則匹配到咱們須要的資源;

/** * 獲取章節的圖片資源連接 * @param { string } _chapterUrl */
function getImageSrcList(_chapterUrl) {
  const imgSrcList = [];
  const reqOptions = {
    method: 'GET',
    url: _chapterUrl
  };
  return axios.request(reqOptions).then((resp) => {
    const doc = resp.data || '';
    const viewId = doc.match(/var viewid = \"(.*)\"\;/)[1];
    const origin = getImgOrigin(viewId);
    const imgPaths = getImgPaths(doc);
    const imgSrcList = generateImgSrcs(origin, imgPaths);
    return imgSrcList;
  })
}
複製代碼

如今咱們就獲取第127話的圖片資源:

(function __main() {
  parseCatalog(config.catalog)
    .then((_data) => {
      // 最後一章節(127話)
      const lastedChapter = _data.catalogs.pop();
      getImageSrcList(lastedChapter.url).then((imgSrcList) => {
        console.log(imgSrcList);
      });
    });
})();
複製代碼

 

 

下載資源

以上咱們已經實現了一部分邏輯,已經能夠拿到圖片的資源連接,接下來咱們只須要將圖片下載回來就ok!

下面用遞歸實現對第127話漫畫圖片的下載(固然你也能夠直接遍歷,並在遍歷的過程當中直接發請求,讓全部的下載一塊兒併發,可是啊,請求得過於頻繁怕不是會被禁了你ip?!)。

(function _download(index) {
  const imgSrc = imgSrcList[index];
  if (imgSrc) {
    console.log('download:', imgSrc);
    const format = imgSrc.split('.').pop();
    const picIndex = index + 1;
    const fileName = picIndex + '.' + format;
    const savePath = path.join(process.cwd(), fileName);
    downloadPic(imgSrc, savePath).then(() => {
      _download(++index);
    }).catch((error) => {
      console.log(error.message);
    });
  } else {
    console.log('finish chapter!');
  }
})(0);
複製代碼

下載部分到此就基本結束,接下咱們在實現搜索的邏輯,而後就能夠進入優化的階段~

 

 

搜索邏輯的實現

  1. axios請求搜索的api,獲取搜索到的html文本
  2. 解析html文本,獲取到搜索的結果
/** * 站內搜索漫畫 * @param { string } keyword */
function search(_keyword) {
  console.log('search: ' + _keyword);
  const selectorPath = '#dmList > ul > li > dl > dt > a';
  const keyword = encodeText(_keyword);
  const origin = 'http://www.chuixue.net/';
  const searchUrl = 'http://www.chuixue.net/e/search/?searchget=1&show=title,player,playadmin,pinyin&keyboard=' + keyword;
  const reqOptions = {
    method: 'GET',
    url: searchUrl,
    responseType: 'arraybuffer'
  };
  const promise = axios.request(reqOptions).then((resp) => {
    const html = iconv.decode(resp.data, 'GBK');
    const doc = cheerio.load(html);
    const eles = doc(selectorPath);
    let searchList = [];
    for (let i = 0, len = eles.length; i < len; i += 1) {
      const ele = eles.eq(i);
      const name = ele.attr('title');
      const src = url.resolve(origin, ele.attr('href'));
      searchList.push({ name, src });
    }
    searchList = searchList.filter((item) => {
      return item.name;
    });
    return searchList;
  });

  return promise;
}
複製代碼

看上面的實現邏輯,你在函數的第三行你會發現這麼一個函數encodeText,這個不是語言自帶的函數,而是本身封裝的,這個轉碼函數是個什麼東西???

/** * 轉碼搜索關鍵字 * @param { string } _text */
function encodeText(_text) {
  const buf = iconv.encode(_text, 'GBK');
  const hex = buf.toString('hex');
  const chars = hex.split('');
  let text = '';
  for (let i = 0, len = chars.length; i < len; i += 2) {
    const char = chars.slice(i, i + 2).join('');
    text += `%${char.toUpperCase()}`;
  }
  return text;
}
複製代碼

你會發現轉碼後的結果和urlencode的結果很像,但其實不是~,你打開控制檯的Network你會發現

搜索的關鍵字確實是被轉碼了,可是又不是被urlencode了,可是你用encode試下你就會發現:

並不同~

這是爲何?

假如你對此感興趣你去看看這篇文章備用文章連接)。你去看這部分搜索的源碼你會發現,代碼並無作任何的轉碼操做,僅僅只是提交了表單!言外之意這個轉碼的過程是瀏覽器作的~

迴歸正題,搜索的實現結果:

 

 

優化腳本

回一下咱們的實現目標:

# 下載漫畫全本
icsdr http://xxx

# 搜索漫畫
icsdr --search [comic name]
複製代碼

作成命令行執行~

這時候咱們就須要commander這個庫!

#!/usr/bin/env node
 
const spider = require('./index');
const program = require('commander');

const config = {
  isSearch: false,
  isDownload: true,
  comicName: '',
  downloadLink: ''
};

program
  .version('0.1.0')
  .option('-s, --search [comic name]', 'Search Comic', function(comicName) {
    config.isSearch = true;
    config.isDownload = false;
    config.comicName = comicName;
  })
  .parse(process.argv);

(function __main() {
  if (config.isDownload) {
    if (!config.downloadLink) {
      config.downloadLink = process.argv.pop();
    }
    spider.download(config.downloadLink);
  } else {
    spider.search(config.comicName);
  }
})();
複製代碼

此時此刻你已經能夠使用命令行執行腳本:

# 下載漫畫
node icsdr.js [catalog link]

# 搜索漫畫
node icsdr.js -s [comic name]
複製代碼

已經很接近咱們的目標,這個時候咱們還須要修改一下咱們的package.json(若是沒有就npm init)

下面關鍵且必須的部分:

"name": "icsdr",
...
// 指明腳本執行文件
"bin": {
    "icsdr": "./icsdr.js"
},
複製代碼

你想要直接使用icsdr命令,有好幾個方法懂得本身然懂。

  • 寫好發佈到npm源上,而後全局安裝就好
  • 本地調試能夠使用npm link
  • 笨點能夠直接使用路徑全局安裝:npm i [absolute-path/relative-path] -g

而後你就能夠:

固然這裏爲了演示只是下載了第127話!

本次代碼例子已經上傳到個人github,能夠自行食用

歡迎star個人ic-comic-spider

小結

  1. axios獲取html文本;
  2. cheerio轉化html文本cheerio的dom和正則匹配獲取所需資源;
  3. axios獲取圖片流資源,fs保存文件;
  4. commander將腳本命令行化;

以上簡單說了一下comic spider的基本實現,下一篇將基於這個「基本實現」擴展一下下載相關的功能!

相關文章
相關標籤/搜索