身為一個熱血工程師,看漫畫肯定是平常愛好,而網路上其實有很多漫畫資源,除了追漫畫外,也很常去回味一些經典漫畫。不過網路上的漫畫資源都是用網頁呈現,體感不是很流暢,有時候遇到塞車或開圖慢,整個看漫畫的悠閒就不見了,是不是能把整本漫畫下載下來,我們就能夠慢慢的回味。
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 出來的,那麼待會我們做研究的時候,這部分應該就會是技術點。
我們若要抓到整本漫畫,那麼會拆解成以下三個步驟:
用 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 加進來。
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
}
再來撰寫一個取得所有集數的 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)
})
}
接下來撰寫取得這集有幾張圖片的 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);
})
}
接下來撰寫取得圖片網址的 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 能看到的,基本上就代表程式碼已經在使用者端了,能做的只是盡可能讓使用者解析困難,而這種解譯過程對我來說是很好玩的一部分。