轉自:https://yukunweb.com/2018/5/jiandan-encryption-processing/?page=1 俞坤的博客
最近一直有朋友問我改版的煎蛋網妹子圖怎麼爬,因爲他們花費精力結果抓了一整個文件夾的防盜圖。我之前在很久以前的一篇博客說過,對於這種js處理的網頁,要想抓取到網頁上看到的數據,大致有三種方法:
- Selenium結合瀏覽器驅動,直接獲取加載js後的頁面,解析數據。這種方法最爲簡單粗暴,不過速度會慢一點,處理煎蛋這樣的網頁有點大材小用;
- 直接使用python執行js文件,幸運的是PyV8庫很符合要求,不過PyV8似乎不支持python3,python3可以使用PyExecJS庫。這種方法也很簡單,不過如果執行的js文件依賴pquery庫的話,比較麻煩;
- 用python模擬js加密方式,拿到加密處理後的數據,這種方法就是本篇主要討論的內容,優點是依賴少速度快,缺點是如果煎蛋加密方式改了,需要跟着改。
分析網頁
首先打開審查元素看他的實際響應內容,可以看到img標籤中實際src
屬性的值是一個固定的值//img.jandan.net/img/blank.gif
。而onload屬性指向的是一個js的jandan_load_img()
函數,this
參數大多數情況是指的當前標籤。後面接着span標籤包含的一串hash值。
目前我們找到了jandan_load_img()
函數,接着就是確定包含這個函數的js文件。方法很簡單,在每一個返回的js響應中,去搜索。
目前的js文件是經過壓縮混淆過的,我們可以複製放到線上解壓工具裏解壓。如果用的是Chrome,可以找到source文件,點擊如下圖標紅框的按鈕:
jandan_load_img()
函數內容爲
function jandan_load_img(b) {
var d = $(b);
var f = d.next("span.img-hash");
var e = f.text();
f.remove();
var c = jdXFKzuIDxRVqKYQfswJ5elNfow1x0JrJH(e, "zE4N6eHuAQP8vkQPb0wcuEcWnLzHYVhy");
var a = $('<a href="' + c.replace(/(\/\/\w+\.sinaimg\.cn\/)(\w+)(\/.+\.(gif|jpg|jpeg))/, "$1large$3") + '" target="_blank" class="view_img_link">[查看原圖]</a>');
d.before(a);
d.before("<br>");
d.removeAttr("onload");
d.attr("src", location.protocol + c.replace(/(\/\/\w+\.sinaimg\.cn\/)(\w+)(\/.+\.gif)/, "$1thumb180$3"));
...
}
可以看到js文件依賴PQuery庫,它拿到img標籤,接着拿到後面的span標籤內容,然後將它和一個常量傳給一串字符的這個函數,拿到這個一串字符的函數返回的內容,放到img和a標籤中。
我們在當前js文件搜索是否有這個jdXFKzuIDxRVqKYQfswJ5elNfow1x0JrJH()
函數,如果不出意外的話,可以搜索到2個函數。我們選擇後面一個,內容如下:
var jdXFKzuIDxRVqKYQfswJ5elNfow1x0JrJH = 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
};
拿到這段返回正確url的js代碼,我們只要用python語言翻譯過來就可以了。
抓取思路
- 請求網頁拿到html;
- 從網頁中解析出所有img標籤後span標籤包含的hash值;
- 正則匹配到html中對應js文件url,請求得到js代碼;
- 正則匹配到js中
jandan_load_img()
傳遞給解密函數的常量; - 將hash值和常量傳遞給解密函數,返回對應圖片url;
- 下載圖片。
翻譯js函數
對面上面的步驟,我不做多餘描述,主要說一下對js代碼的翻譯,後面會放整理後的代碼地址。
如果要翻譯上面的js代碼,只需要搞清楚代碼中調用的函數對應python什麼函數就可以了,沒有什麼難點。
md5()
是對md5.js
的hex_md5()
加密方式的封裝:
function md5(a) {
return hex_md5(a)
}
這個方法就相當於python的hashlib庫提供的md5摘要加密算法。感興趣的朋友自行了解,由於後面多次調用此方法,我們對它進行一個封裝:
def md5(str):
md5 = hashlib.md5()
md5.update(str.encode('utf-8'))
return md5.hexdigest()
js中的r.substr(0, 16)
調用的是stringObject.substr(start,length)
函數。substr()
方法可在字符串中抽取從start
下標開始的指定length
數目的字符。這裏注意的是length
指的是長度,不是結束的下標,也就是它可以翻譯爲:
r[0:16]
r[16:32] # 注意不是r[16:16]
base64_decode(m)
函數是對js的原生Base64編碼api的封裝:
function base64_decode(a) {
return window.atob(a)
}
大致使用如:
var str = 'javascript'
window.btoa(str) //轉碼結果 "amF2YXNjcmlwdA=="
window.atob("amF2YXNjcmlwdA==") //解碼結果 "javascript"
Base64是一種用64個字符來表示任意二進制數據的方法。幸運的是python內置的有base64庫,可以直接進行base64的編解碼。我們也對他進行一個封裝,方便閱讀。這裏有一個要注意的是,因爲base64是把3個字節變爲4個字節,base64編碼的長度必須是4的倍數,所以對於不是4的倍數的字符,需要加上=把base64字符串的長度變爲4的倍數。
def decode_base64(data):
return base64.b64decode(data + (4 - len(data) % 4) * '=')
var h = new Array(256);
翻譯過來就是h = list(range(256))
c.charCodeAt(g % c.length)
調用的是js的stringObject.charCodeAt(index)
函數,返回指定位置的字符的Unicode
編碼。這個返回值是0 - 65535
之間的整數。這個方法相當於python的ord()
函數。這行翻譯過來就是:
b[g] = ord(c[g % len(c)])
k = k.split("");
是將字符串k
分解成單獨的字符列表,在python中字符串本身也是一個可迭代對象,忽略就好。
chr()
和ord()
和python內置的chr()
、ord()
函數類似,我們直接調用對於函數,運行則回報錯:ord() expected string of length 1, but int found
。因爲python中的ord()
函數參數是一個str
類型的參數,而k[g]
實際上是一個字節,我們就不用調用ord()
函數就可以了。即:
t += chr(k[g] ^ (h[(h[p] + h[f]) % 256]))
最後
原作者代碼鏈接:https://github.com/Blackyukun/Jiandan
加密方式一直在變,可能會出錯,我寫的時候按下面程序就能運行。
import re
import os
import hashlib
import base64
import requests
from lxml import etree
from requests import ConnectionError
def get_html(url):
"""請求頁面,返回響應"""
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
'(KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'
}
try:
resp = requests.get(url, headers=headers)
if resp.status_code == 200:
return resp
return None
except ConnectionError:
print('Error.')
return None
def get_js_file():
"""
正則匹配加密 js url
"""
base_url = 'http://jandan.net/ooxx/'
html = get_html(base_url).text
js_url = ''
try:
pattern = r'.*<script\ssrc=\"\/\/(cdn.jandan.net\/static\/min.*?)\"><\/script>.*'
result = re.findall(pattern, html)
js_url = "http://" + result[len(result) - 1]
except Exception as e:
print(e)
js = get_html(js_url).text
return js
def get_salt(js):
"""正則匹配 js 中的加密常量"""
pattern = r'jandan_load_img.*?var c.*?"(.*?)"'
salt = re.findall(pattern, js, re.S)[0]
# print(salt)
return salt
def all_img_hash(page_url):
"""請求頁面,返回頁面中所有圖片 hash"""
html = get_html(page_url).text
doc = etree.HTML(html)
img_hash = doc.xpath('//span[@class="img-hash"]/text()')
# print(img_hash)
return img_hash
def init_md5(str):
"""封裝 md5"""
md5 = hashlib.md5()
md5.update(str.encode('utf-8'))
return md5.hexdigest()
def decode_base64(data):
"""封裝 base64"""
return base64.b64decode(data + (4 - len(data) % 4) * '=')
def simulation_js(img_hash, salt):
"""
翻譯 js 加密方式
:param img_hash: 圖片 hash
:param salt: 加密常量
:return: 圖片 url
"""
# r = salt if salt else ''
# d = 0
# q = 4
# r = init_md5(r)
# o = init_md5(r[:16])
# n = init_md5(r[16:32])
# if q:
# l = img_hash[:q]
# else: l = ''
#
# c = o + init_md5(o + l)
# img_hash = img_hash[q:]
# k = decode_base64(img_hash)
#
# h = list(range(256))
# b = list(range(256))
# for g in range(256):
# b[g] = ord(c[g % len(c)])
# f = 0
# for g in range(256):
# f = (f + h[g] + b[g]) % 256
# h[g], h[f] = h[f], h[g]
#
# t = ''
# p = f = 0
# for g in range(len(k)):
# p = (p + 1) % 256
# f = (f + h[p]) % 256
# h[p], h[f] = h[f], h[p]
# t += chr(k[g] ^ (h[(h[p] + h[f]) % 256]))
# t = t[26:]
t = decode_base64(img_hash)
# print(t)
return t
def parse_hash(salt, page_url):
img_hash = all_img_hash(page_url)
# print(img_hash)
for i in img_hash:
yield simulation_js(i, salt)
def download_img(dir_path, img_url):
"""下載"""
filename = img_url[-14:]
# print(img_url)
img_content = get_html(img_url).content
if not os.path.exists(dir_path):
os.mkdir(dir_path)
try:
with open(os.path.join(dir_path, filename), 'wb') as f:
f.write(img_content)
return True
except Exception as e:
print(e)
return False
def main(dir_path, page=1):
js = get_js_file()
salt = get_salt(js)
base_url = 'http://jandan.net/ooxx/'
for i in range(page+1):
page_url = base_url + 'page-{}/'.format(58-i)
# print(page_url)
# //wx1.sinaimg.cn/large/672f3952gy1g0u2mqymhyj20u00u0dja.jpg 正確結果
for img_url in parse_hash(salt, page_url):
# img_url = 'w'+str(img_url, encoding = "utf-8")
img_url = str(img_url, encoding = "utf-8")
print(img_url)
r = download_img(dir_path, 'http:' + img_url)
if r: print('success')
if __name__ == '__main__':
dir_path = 'E:/jiandan/'
main(dir_path)
這次不知道爲什麼煎蛋的解密變簡單了.......
最後十分感謝原作者的分享,一下子學會了不少。