iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 17
0

定義目標

身為一個熱血工程師,看漫畫肯定是平常愛好,而網路上其實有很多漫畫資源,除了追漫畫外,也很常去回味一些經典漫畫。不過網路上的漫畫資源都是用網頁呈現,體感不是很流暢,有時候遇到塞車或開圖慢,整個看漫畫的悠閒就不見了,是不是能把整本漫畫下載下來,我們就能夠慢慢的回味。

ps. 這次 demo 我們使用動漫伊甸園來做演示。


實際探訪

打開網址,印入眼簾的是一堆漫畫目錄,我們就隨便挑一本來探訪。點了漫畫之後,可以看到漫畫目錄,我們需要將所有目錄列表裡面的漫畫都抓下來。再進一步觀察,隔目錄列表裡面的每個集數,都有這個集數的 ID 類似 296977,點擊其中一個目錄後會進到漫畫頁面,同時發現這一集的網址 http://dmeden.net/comichtml/296977/1.html?s=8&d=0 是由這個目錄 ID 所組合而成的。

進到這集的漫畫頁,左上角有頁碼,這樣就能夠知道這集有幾張圖片。然後點下一頁,發現網址有變化,是用第幾頁當成是 html 檔名來傳遞。

接下來我們來觀察一下圖片網址,雖然在 dev tool 的 elements tab 可以看到圖片的網址,但是在 network request 裡面卻看不到圖片網址,可見這個內容肯定是 js render 出來的,那麼待會我們做研究的時候,這部分應該就會是技術點。


分解研究

我們若要抓到整本漫畫,那麼會拆解成以下三個步驟:

  1. 取得漫畫所有集數 ID
  2. 取得這集有幾張圖片
  3. 取得圖片

取得漫畫所有集數 ID

用 postman 測試一下是否能夠順利抓到頁面,然後再用 selector 來選擇 .l_s 看看,那麼就確定漫畫集數沒有問題。

取得這集有幾張圖片

用 postman 測試一下是否能夠順利抓到頁面,然後再用 selector 來選擇 #iPageHtm a 看看,那麼就確定圖片張數沒有問題。

取得圖片

剛剛探訪時就已經得知這部分是由 js render 出來的,這時候我們就要發揮偵探的精神,去把產生圖片的方法找出來。首先我們先針對已知條件做個搜尋,目前已知圖片 ID 是 img1021,那我們就來搜尋這個字串看看。

發現找到另一個結果在 view.js,點開發現,他是屬於 getCuImg(),然後這個 function 感覺就只是確認有 id 然後回傳而已,所以我們再網上追搜尋看看 getCuImg

接著在 view.js 裡面出現另外兩個使用 getCuImg 的地方,其中一個在 window_onload function 裡面。我們可以嘗試著設 breakpoint 來去觀察看看裡面的變化。發現 arrDS 是分流伺服器的選擇,cuImg.src = sCuDomian + unsuan(cuImg.name); 這行才是關鍵,了解他是透過一個叫做 unsuan function 來解譯圖片位置,而加密過的字串就是圖片的 name。

function window_onload()
{
    if(document.getElementById("btnPagePrv").disabled)
        document.getElementById("btnPagePrv2").disabled = true;
    if(document.getElementById("btnPageNext").disabled)
        document.getElementById("btnPageNext2").disabled = true;
    
    
    arrDS = document.getElementById("hdDomain").value.split("|");
    cuDomainNo = getUrlPar("d");
    if(cuDomainNo=="") cuDomainNo="0";
    var sCuDomian = getDomain(parseInt(cuDomainNo))
    
    document.getElementById("spSl").innerHTML = dfSL(cuDomainNo);
    
    cuImg = getCuImg();
    cuImg.src = sCuDomian + unsuan(cuImg.name);
	cuImg.style.cursor = "hand";
    //var evt = window.event;
    cuImg.onmousedown = function(evt){drag(evt);};
}

接下來我們要來取得關鍵的 unsuan function,我們直接搜尋就能得到 unsuan 的 definition,來觀察一下這個 function,試試看能否順利解密取得圖片位置。

基本上我們可以直接拿來用,不過我們要改寫一下 su = location.hostname.toLowerCase(); 這段,因為雖然都是 js,但在前端和後端的環境不同,所提供的變數也不同,location 就是前端獨有的變數,我們可以直接將他的數值給一個固定的結果,所以程式會改寫如下,在測試一次解譯圖片位置,確認沒問題就能進入實作了。


實作程式碼

unsuan function

將我們剛剛研究的 unsuan function 加進來。

function unsuan(s) {
    sw = "jmmh.net|dmeden.com|dmeden.net";
    su = "dmeden.net";
    b = false;
    for (i = 0; i < sw.split("|").length; i++) {
        if (su.indexOf(sw.split("|")[i]) > -1) {
            b = true;
            break
        }
    }
    if (!b)
        return "";
    x = s.substring(s.length - 1);
    w = "abcdefghijklmnopqrstuvwxyz";
    xi = w.indexOf(x) + 1;
    sk = s.substring(s.length - xi - 12, s.length - xi - 1);
    s = s.substring(0, s.length - xi - 12);
    k = sk.substring(0, sk.length - 1);
    f = sk.substring(sk.length - 1);
    for (i = 0; i < k.length; i++) {
        eval("s=s.replace(/" + k.substring(i, i + 1) + "/g,'" + i + "')")
    }
    ss = s.split(f);
    s = "";
    for (i = 0; i < ss.length; i++) {
        s += String.fromCharCode(ss[i])
    }
    return s
}

getIndexs function

再來撰寫一個取得所有集數的 function,輸入參數為漫畫網址和一個 callback 接收所有集數 ID。

function getIndexs(commic, callback) {
  request(commic, (err, res, body) => {
    var $ = cheerio.load(body)
    var indexs = $('.l_s').map((index, obj) => {
      return {
        id: $(obj).attr('href').match(/ID=(\d+)&/)[1],
        title: $(obj).text()
      }
    }).get()
    callback(indexs)
  })
}

getPages function

接下來撰寫取得這集有幾張圖片的 function,輸入參數為這個集數的 ID 和一個 callback 接收圖片張數。

function getPages(index, callback) {
  request(`http://dmeden.net/comichtml/${index.id}/1.html?s=8`, (err, res, body) => {
    var $ = cheerio.load(body)
    var pages = $('#iPageHtm a').map((index, obj) => {
      return $(obj).text()
    }).get()
    callback(pages);
  })
}

getImage function

接下來撰寫取得圖片網址的 function,接收參數集數 ID、第幾張圖片,和一個 callback 接收圖片網址,這邊再把剛剛準備好的 unsuan 一起加進來解析。

function getImage(index, page, callback) {
  var page = page
  var index = index
  request(`http://dmeden.net/comichtml/${index.id}/${page}.html?s=8`, (err, res, body) => {
    if(err){
        callback('')
    }else{
      var $ = cheerio.load(body)
      callback('http://100.94201314.net/dm08/' + unsuan($('img').attr('name')))
    }
  })
}

組合積木

首先我們先 call getIndexs 取得所有集數,然後用 async.map 去 call getPages 取得每一集的圖片張數,然後再次用 async.map 去 call getImage 取得該圖片網址,這樣就能順利取得這本漫畫的圖片 url。

getIndexs('http://dmeden.net/comicinfo/31497.html', (indexs) => {
  async.map(indexs, (index, callback) => {
    getPages(index, (pages) => {
      async.map(pages, (page, callback) => {
        getImage(index, page, (image) => {
          callback(null, image)
        })
      }, (err, results) => {
        callback(null, [].concat.apply([], results))
      })
    })
  }, (err, results) => {
    [].concat.apply([], results).forEach(item => console.log(item))
  })
})

完整程式碼

const request = require('request');
const cheerio = require('cheerio');
const async = require('async');

getIndexs('http://dmeden.net/comicinfo/31497.html', (indexs) => {
  async.map(indexs, (index, callback) => {
    getPages(index, (pages) => {
      async.map(pages, (page, callback) => {
        getImage(index, page, (image) => {
          callback(null, image)
        })
      }, (err, results) => {
        callback(null, [].concat.apply([], results))
      })
    })
  }, (err, results) => {
    [].concat.apply([], results).forEach(item => console.log(item))
  })
})



function getImage(index, page, callback) {
  var page = page
  var index = index
  request(`http://dmeden.net/comichtml/${index.id}/${page}.html?s=8`, (err, res, body) => {
    if(err){
        callback('')
    }else{
      var $ = cheerio.load(body)
      callback('http://100.94201314.net/dm08/' + unsuan($('img').attr('name')))
    }
  })
}

function getPages(index, callback) {
  request(`http://dmeden.net/comichtml/${index.id}/1.html?s=8`, (err, res, body) => {
    var $ = cheerio.load(body)
    var pages = $('#iPageHtm a').map((index, obj) => {
      return $(obj).text()
    }).get()
    callback(pages);
  })
}

function getIndexs(commic, callback) {
  request(commic, (err, res, body) => {
    var $ = cheerio.load(body)
    var indexs = $('.l_s').map((index, obj) => {
      return {
        id: $(obj).attr('href').match(/ID=(\d+)&/)[1],
        title: $(obj).text()
      }
    }).get()
    callback(indexs)
  })
}



function unsuan(s) {
  sw = "jmmh.net|dmeden.com|dmeden.net";
  su = 'dmeden.net';
  b = false;
  for (i = 0; i < sw.split("|").length; i++) {
    if (su.indexOf(sw.split("|")[i]) > -1) {
      b = true;
      break
    }
  }
  if (!b)
    return "";
  x = s.substring(s.length - 1);
  w = "abcdefghijklmnopqrstuvwxyz";
  xi = w.indexOf(x) + 1;
  sk = s.substring(s.length - xi - 12, s.length - xi - 1);
  s = s.substring(0, s.length - xi - 12);
  k = sk.substring(0, sk.length - 1);
  f = sk.substring(sk.length - 1);
  for (i = 0; i < k.length; i++) {
    eval("s=s.replace(/" + k.substring(i, i + 1) + "/g,'" + i + "')")
  }
  ss = s.split(f);
  s = "";
  for (i = 0; i < ss.length; i++) {
    s += String.fromCharCode(ss[i])
  }
  return s
}


衍伸應用

好的,我要鄭重的申明我並不鼓勵盜版,所以取得連結之後,請不要下載到自己的電腦裡面,如果你這麼做的話那就是犯法了!

基本上對岸有很多這類的 resource,而每個網站多多少少都有機制去防止別人輕易地取得圖片原始檔。但說句實在話的,在前端 browser 能看到的,基本上就代表程式碼已經在使用者端了,能做的只是盡可能讓使用者解析困難,而這種解譯過程對我來說是很好玩的一部分。


上一篇
2017/12/19 23:46:00 原始數據統計 (json)
下一篇
ibon 上傳文件
系列文
爬蟲始終來自於墮性34

1 則留言

0
pjchender
iT邦新手 5 級 ‧ 2018-01-05 15:13:54

這篇含金量好高啊!!

我要留言

立即登入留言