Python爬蟲:煎蛋網圖片URL解密處理

轉自:https://yukunweb.com/2018/5/jiandan-encryption-processing/?page=1 俞坤的博客

最近一直有朋友問我改版的煎蛋網妹子圖怎麼爬,因爲他們花費精力結果抓了一整個文件夾的防盜圖。我之前在很久以前的一篇博客說過,對於這種js處理的網頁,要想抓取到網頁上看到的數據,大致有三種方法:

  • Selenium結合瀏覽器驅動,直接獲取加載js後的頁面,解析數據。這種方法最爲簡單粗暴,不過速度會慢一點,處理煎蛋這樣的網頁有點大材小用;
  • 直接使用python執行js文件,幸運的是PyV8庫很符合要求,不過PyV8似乎不支持python3,python3可以使用PyExecJS庫。這種方法也很簡單,不過如果執行的js文件依賴pquery庫的話,比較麻煩;
  • 用python模擬js加密方式,拿到加密處理後的數據,這種方法就是本篇主要討論的內容,優點是依賴少速度快,缺點是如果煎蛋加密方式改了,需要跟着改。

分析網頁

post34_1

首先打開審查元素看他的實際響應內容,可以看到img標籤中實際src屬性的值是一個固定的值//img.jandan.net/img/blank.gif。而onload屬性指向的是一個js的jandan_load_img()函數,this參數大多數情況是指的當前標籤。後面接着span標籤包含的一串hash值。

目前我們找到了jandan_load_img()函數,接着就是確定包含這個函數的js文件。方法很簡單,在每一個返回的js響應中,去搜索。

post34_2

目前的js文件是經過壓縮混淆過的,我們可以複製放到線上解壓工具裏解壓。如果用的是Chrome,可以找到source文件,點擊如下圖標紅框的按鈕:

post34_3

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語言翻譯過來就可以了。

抓取思路

  1. 請求網頁拿到html;
  2. 從網頁中解析出所有img標籤後span標籤包含的hash值;
  3. 正則匹配到html中對應js文件url,請求得到js代碼;
  4. 正則匹配到js中jandan_load_img()傳遞給解密函數的常量;
  5. 將hash值和常量傳遞給解密函數,返回對應圖片url;
  6. 下載圖片。

翻譯js函數

對面上面的步驟,我不做多餘描述,主要說一下對js代碼的翻譯,後面會放整理後的代碼地址。

如果要翻譯上面的js代碼,只需要搞清楚代碼中調用的函數對應python什麼函數就可以了,沒有什麼難點。

md5()是對md5.jshex_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)

這次不知道爲什麼煎蛋的解密變簡單了.......

最後十分感謝原作者的分享,一下子學會了不少。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章