之前在魚C論壇的時候,看到很多人都在用Python寫爬蟲爬煎蛋網的妹子圖,當時我也寫過,爬了很多的妹子圖片。後來煎蛋網把妹子圖的網頁改進了,對圖片的地址進行了加密,所以論壇裡面的人經常有人問怎麼請求的頁面沒有連結。這篇文章就來說一下煎蛋網OOXX妹子圖的連結擷取方式。
首先說明一下,之前煎蛋網之所以增加了反爬蟲機制,應該就是因為有太多的人去爬他們的網站了。爬蟲頻繁的訪問網站會給網站帶來壓力,所以,建議大家寫爬蟲簡單的運行成功就適可而止,不要過分地去爬別人的東西。 爬蟲思路分析 圖片下載流程圖
首先,用一張簡單的流程圖(非規範流程圖格式)來展示一下爬取簡單網的妹子圖的整個流程:
流程圖解讀
1、爬取煎蛋網的妹子圖,我們首先要開啟任意一個妹子圖的頁面,比如 http://jandan.net/ooxx/page-44#comments 然後,我們需要請求這個頁面,擷取2個關鍵的資訊(後續會說明資訊的具體作用),其中第一個資訊是每個妹子圖片的 hash 值,這個是後續用來解密產生圖片地址的關鍵資訊。
2、在頁面中除了提取到圖片的 hash 之外,還有提取到當前頁的一個關鍵的js檔案的地址,這個js檔案中包含了一個同樣是用來產生圖片地址的關鍵參數,要得到這個參數,必須去請求這個JS地址,當時妹子圖的每個頁面的js地址是不同的,所以需要從頁面中提取。
3、得到了圖片的 hash 和 js 中的關鍵參數之後,可以根據js 中提供的解密方式,得到圖片的連結,這個解密方式後續用Python代碼和js代碼的參照來說明。
4、有了圖片連結,下載圖片就不多說了,後續會有第二篇文章,來使用多線程+多進程的方式下載圖片。 頁面分析 網頁原始碼解讀
我們可以開啟一個妹子圖的頁面,還是最開始的 http://jandan.net/ooxx/page-44#comments 為例,然後查看原始碼(注意,不是審查元素),可以看到本應該放圖片地址的地方並沒有圖片地址,而是類似於下面的代碼:
<p><img src="//img.jandan.net/img/blank.gif" onload="jandan_load_img(this)" /><span class="img-hash">ece8ozWUT/VGGxW1hlbITPgE0XMZ9Y/yWpCi5Rz5F/h2uSWgxwV6IQl6DAeuFiT9mH2ep3CETLlpwyD+kU0YHpsHPLnY6LMHyIQo6sTu9/UdY5k+Vjt3EQ</span></p>
從這個代碼可以看出來,圖片地址被一個js函數代替了,也就是說圖片地址是由這個jandan_load_img(this)函數來擷取並載入的,所以,現在的關鍵是,需要到JS檔案中尋找這個函數的意義。 js檔案解讀
通過在每個js檔案中搜尋jandan_load_img,最後可以在一個地址類似於 http://cdn.jandan.net/static/min/1d694f08895d377af4835a24f06090d0.29100001.js 的檔案中找到這個函數的定義,將壓縮的JS代碼格式化查看,可以看到具體的定義如下片段:
function jandan_load_img(b) { var d = $(b); var f = d.next("span.img-hash"); var e = f.text(); f.remove(); var c = f_Qa8je29JONvWCrmeT1AJocgAtaiNWkcN(e, "agC37Is2vpAYzkFI9WVObFDN5bcFn1Px");
這段代碼的意思很容易看懂,首先它提取了當前標籤下css為img-hash的span標籤的文本,也就是我們最開始說的圖片的 hash 值,然後把這個值和一個字串參數(每個頁面的這個參數是變動的,這個頁面是 agC37Is2vpAYzkFI9WVObFDN5bcFn1Px)一起傳遞到另外一個函數f_Qa8je29JONvWCrmeT1AJocgAtaiNWkcN中,所以我們還要去查看這個函數的意義才行,這個函數就是用來產生圖片連結的函數了。 f_ 函數的解讀
可以在js中尋找這個f_函數的定義,可以看到有兩個,但是沒關係,根據代碼從上到下執行的規律,我們只需要看比較靠後的那個就行了,完整的內容如下:
var f_Qa8je29JONvWCrmeT1AJocgAtaiNWkcN = function(m, r, d) { var e = "DECODE"; var r = r ? r : ""; var d = d ? d : 0; var q = 4; r = md5(r); var o = md5(r.substr(0, 16)); var n = md5(r.substr(16, 16)); if (q) { if (e == "DECODE") { var l = m.substr(0, q) } } else { var l = "" } var c = o + md5(o + l); var k; if (e == "DECODE") { m = m.substr(q); k = base64_decode(m) } var h = new Array(256); for (var g = 0; g < 256; g++) { h[g] = g } var b = new Array(); for (var g = 0; g < 256; g++) { b[g] = c.charCodeAt(g % c.length) } for (var f = g = 0; g < 256; g++) { f = (f + h[g] + b[g]) % 256; tmp = h[g]; h[g] = h[f]; h[f] = tmp } var t = ""; k = k.split(""); for (var p = f = g = 0; g < k.length; g++) { p = (p + 1) % 256; f = (f + h[p]) % 256; tmp = h[p]; h[p] = h[f]; h[f] = tmp; t += chr(ord(k[g]) ^ (h[(h[p] + h[f]) % 256])) } if (e == "DECODE") { if ((t.substr(0, 10) == 0 || t.substr(0, 10) - time() > 0) && t.substr(10, 16) == md5(t.substr(26) + n).substr(0, 16)) { t = t.substr(26) } else { t = "" } } return t};
這個函數需要傳遞3個參數,第一個參數是圖片的 hash值,第二個參數就是在jandan_load_img函數中看到的一個字串,第三個參數其實沒用,因為在jandan_load_img函數中根本沒有傳入。我們只需要按照JS代碼的意思把這個函數改寫成 Python 代碼就行了。 Python改寫函數
使用Python將f_函數改寫之後應該是這樣的:
def get_imgurl(m, r='', d=0): '''解密擷取圖片連結''' e = "DECODE" q = 4 r = _md5(r) o = _md5(r[0:0 + 16]) n = _md5(r[16:16 + 16]) l = m[0:q] c = o + _md5(o + l) m = m[q:] k = _base64_decode(m) h = list(range(256)) b = [ord(c[g % len(c)]) for g in range(256)] f = 0 for g in range(0, 256): f = (f + h[g] + b[g]) % 256 tmp = h[g] h[g] = h[f] h[f] = tmp t = "" p, f = 0, 0 for g in range(0, len(k)): p = (p + 1) % 256 f = (f + h[p]) % 256 tmp = h[p] h[p] = h[f] h[f] = tmp t += chr(k[g] ^ (h[(h[p] + h[f]) % 256])) t = t[26:] return t
這個函數需要用到另外兩個函數,第一個是MD5加密的函數,這個函數對應的是JS中這樣的段落:
var o = md5(r.substr(0, 16));
js的substr()函數其實就是Python裡面的切片的用法,稍微查看一下定義就能懂,不解釋。
MD5加密轉化成Python版本如下:
def _md5(value): '''md5加密''' m = hashlib.md5() m.update(value.encode('utf-8')) return m.hexdigest()
然後還有一個bash64的解碼函數,這個函數在js中的這一個段用到了:
k = base64_decode(m)
使用Python的時候需要注意,如果直接使用Python的base64.b64decode的話會報錯,具體的報錯內容是:
binascii.Error: Incorrect padding
所以在將資料進行解碼之前先要處理一下,具體的函數是:
def _base64_decode(data): '''bash64解碼,要注意原字串長度報錯問題''' missing_padding = 4 - len(data) % 4 if missing_padding: data += '=' * missing_padding return base64.b64decode(data)
到這裡,擷取圖片連結的函數就完成了,主要就是使用3個函數。
我們可以傳入兩個從網頁中複製到的參數到這個函數中測試一下:
m = 'ece8ozWUT/VGGxW1hlbITPgE0XMZ9Y/yWpCi5Rz5F/h2uSWgxwV6IQl6DAeuFiT9mH2ep3CETLlpwyD+kU0YHpsHPLnY6LMHyIQo6sTu9/UdY5k+Vjt3EQ'r = 'HpRB2OSft5RhlSyZaXV8xYpvEAgDThcA'print(get_imgurl(m,r))
可以看到如下輸出:
//ww3.sinaimg.cn/mw600/0073ob6Pgy1fpet9wku7dj30hs0qljuz.jpg
注意:這裡的r參數是從每個頁面中的js中複製的,每個頁面的js地址是變動的,這個參數也是變動的。 擷取hash和js地址
之前說過,hash值是擷取圖片地址的關鍵參數,而另外的參數在js檔案中,並且這個js檔案每個頁面不同,所以現在來提取這兩個關鍵參數。 批量擷取hash
擷取圖片的hash值很方便,我們可以使用 BeautifulSoup 的方法即可,具體的程式碼片段:
def get_urls(url): '''擷取一個頁面的所有圖片的連結''' headers = { 'user-agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0', 'Host': 'jandan.net' } html = requests.get(url, headers=headers).text js_url = 'http:' + re.findall('<script src="(//cdn.jandan.net/static/min/[\w\d]+\.\d+\.js)"></script>', html)[-1] _r = get_r(js_url) soup = BeautifulSoup(html, 'lxml') tags = soup.select('.img-hash') for tag in tags: img_hash = tag.text img_url = get_imgurl(img_hash,_r) print(img_url)
提取圖片hash 的代碼是這一句:
soup = BeautifulSoup(html, 'lxml') tags = soup.select('.img-hash') for tag in tags: img_hash = tag.text
擷取js中關鍵字符串
而擷取js地址的方式是使用的Regex:
js_url = 'http:' + re.findall('<script src="(//cdn.jandan.net/static/min/[\w\d]+\.\d+\.js)"></script>', html)[-1]
這裡要注意,因為正則提取的是一個列表,所以最後需要取列表中的一個連結,經過查看,我發現有的頁面有兩個這種JS檔案,有一個是被注釋掉了,所以都要使用最後一個,這個的表達方式是清單索引中使用[-1]取最後一個。
得到js地址之後需要請求,然後找到關鍵字符串,具體可以寫成一個函數:
def get_r(js_url): '''擷取關鍵字符串''' js = requests.get(js_url).text _r = re.findall('c=f_[\w\d]+\(e,"(.*?)"\)', js)[0] return _r
完整代碼
下面就是擷取一個頁面的全部的圖片連結的完整代碼:
# -*- coding: utf-8 -*-import requestsfrom bs4 import BeautifulSoupimport hashlibimport reimport base64def _md5(value): '''md5加密''' m = hashlib.md5() m.update(value.encode('utf-8')) return m.hexdigest()def _base64_decode(data): '''bash64解碼,要注意原字串長度報錯問題''' missing_padding = 4 - len(data) % 4 if missing_padding: data += '=' * missing_padding return base64.b64decode(data)def get_imgurl(m, r='', d=0): '''解密擷取圖片連結''' e = "DECODE" q = 4 r = _md5(r) o = _md5(r[0:0 + 16]) n = _md5(r[16:16 + 16]) l = m[0:q] c = o + _md5(o + l) m = m[q:] k = _base64_decode(m) h = list(range(256)) b = [ord(c[g % len(c)]) for g in range(256)] f = 0 for g in range(0, 256): f = (f + h[g] + b[g]) % 256 tmp = h[g] h[g] = h[f] h[f] = tmp t = "" p, f = 0, 0 for g in range(0, len(k)): p = (p + 1) % 256 f = (f + h[p]) % 256 tmp = h[p] h[p] = h[f] h[f] = tmp t += chr(k[g] ^ (h[(h[p] + h[f]) % 256])) t = t[26:] return tdef get_r(js_url): '''擷取關鍵字符串''' js = requests.get(js_url).text _r = re.findall('c=f_[\w\d]+\(e,"(.*?)"\)', js)[0] return _rdef get_urls(url): '''擷取一個頁面的所有圖片的連結''' headers = { 'user-agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0', 'Host': 'jandan.net' } html = requests.get(url, headers=headers).text js_url = 'http:' + re.findall('<script src="(//cdn.jandan.net/static/min/[\w\d]+\.\d+\.js)"></script>', html)[-1] _r = get_r(js_url) soup = BeautifulSoup(html, 'lxml') tags = soup.select('.img-hash') for tag in tags: img_hash = tag.text img_url = get_imgurl(img_hash,_r) print(img_url)if __name__ == '__main__': get_urls('http://jandan.net/ooxx/page-44')
運行上面的代碼,可以列印出這個頁面的所有圖片連結,部分連結如下:
//ww3.sinaimg.cn/mw600/0073ob6Pgy1fpet9wku7dj30hs0qljuz.jpg//ww3.sinaimg.cn/mw600/0073tLPGgy1fpet9mszjwj30hs0g1jsv.jpg//ww3.sinaimg.cn/mw600/0073ob6Pgy1fpesskkgobj31jk1jkk5b.jpg//wx3.sinaimg.cn/mw600/006XfbArly1fpesq2jn1vj30j60svaz3.jpg//wx3.sinaimg.cn/mw600/6967abd2gy1fpenoyobrcj20u03d0b2d.jpg//wx3.sinaimg.cn/mw600/6967abd2gy1fpenp38v9uj20u03zkhdy.jpg
總結:到這裡為止,提取煎蛋網妹子圖的圖片連結的方式其實已經給出來了,下一篇會接著講通過多線程+多進程的方式下載圖片。
原文首發:http://www.tendcode.com/article/jiandan-meizi-spider/