淘宝抓取所有历史订单

思路

淘宝是我迄今为止遇到的反爬虫技术最厉害的一个网站,我估计在业界它也是处于顶尖水平的。这里我一共花了整整一天24个小时才有所小得。

这篇文章里,我采用手动登陆淘宝的方式获取登陆用的Cookie,我会在接下来所有请求里直接使用这个cookie,后面应该会再写文章讲如何抓这个Cookie以及机器识别验证码。

言归正传,要完成这个项目,先要弄清整个流程。我要抓的是“我的订单”下的商品信息,在登陆状态下,访问不同页数的订单页面会发送对应的XHR,接着服务器会返回一个包含商品信息的json文件,这个很容易找。但是当我用同样的Header和Query String Parameters构建一条请求时,这条请求会被怀疑为爬虫请求,淘宝会要求进行验证码认证。个人认为有两种思路解决这个验证码的问题:一个是找到是什么原因导致淘宝认为你是爬虫,从而找到绕过验证码的方法;另一个思路就是让爬虫能够通过验证码检测。经过很长时间的分析和尝试,绕过验证码的方法我没找到,验证码的流程却分析了出来。

验证码的流程

首先需要让页面进入验证码流程,这里只需要用一个简陋的爬虫访问一下订单页面就行了。接着在浏览器上刷新或者访问下一页订单,页面会弹出验证码,在输入正确的验证码之后,页面会成功跳转到下一页。使用浏览器工具将整个过程的报文都抓下来,逐条分析,如图,这里我把关键的几条报文按顺序标注了一下:

taobao-流程.jpg

我也把相关报文的Query String Parameters也截了下来,如下:

taobao-参数列表.jpg

分析整个流程是最困难的一步,如果有人也做这个项目,我个人建议还是先自己分析为好。整个流程大概是这样子的(序号也对应了报文的编号,上图中的数字也对应了各自的编号):

  1. 这条POST请求就是正常的订单信息请求,如果没有进入验证码流程,服务器应该直接返回商品信息json文件。如果进入了验证码流程,服务器会返回一个URL,你需要访问这个URL获取包含计算所有参数的js文件和验证码图片。
  2. 这条报文就是访问第1条报文里的URL,唯一一点区别是我这里加了style=mini参数,之后我会得到一个很小的验证码图片。这条报文会返回一个包含JavaScript的HTML页面,里面包含了所有之后需要的参数,分别是:identity, sessionid, type和action, event_submit_do_unique, smPolicy, smApp, smReturn, smCharset, smTag, captcha, smSign, ua, code。具体哪个是哪个我就不细说了,其实也不重要,之后只需要参照报文用起来就行了。这里说两个参数:code,这就是正确的验证码;ua,据说是淘宝根据一个很复杂的算法计算出来的,其变量还包含鼠标键盘的操作记录。
  3. ua.js很明显这是计算UA值用的,不过这里的Trick就是光有这个文件还不够,因为里面没有变量UA_opt的申明,你需要结合第2条响应里的一部分js代码在加上ua.js文件才能计算出最后需要的ua值。这里要非常感谢这位老哥的文章:ua.js中UA_Opt设定信息的重要性与来源分析,他分析了所有的js文件,非常厉害。
  4. 第4条报文使用了2里面获得的identity,sessionid和type,以及代表当前时间的t来获取验证码图片。我会将这个图片保存到本地,以便之后手动输入验证码。
  5. 第5条是用来判断你输入的验证码是否正确的,除了4里面的参数,还增加了code(你输入的验证码),_ksTS和callback在爬虾米音乐时遇到过,这里是一样的,前者是时间+一个数字,后者是上个数字+1,代表返回的json块的名字。如果验证码正确,服务器会返回SUCCESS消息,否则返回ERROR,你需要重新发送第4条报文继续验证。这里注意,如果你能保证你输入的验证码肯定是正确的,这条报文可以省略。
  6. 这条是最重要的一条,你需要用到上述所有的参数来向服务器请求smToken,这个Token只能用一次,它是你能继续访问订单页面的凭证(也是最重要的一个参数)。
  7. 第6条报文会返回一个URL,它其实就是要访问的订单页面,你可以获取smToken和smSign然后把它们加在第一条请求末尾,也可以直接访问这个URL。两者是一样的。

经过了这些步骤,你就可以继续访问订单页面了。这里再补充两张图作为辅助:

taobao-参数抓取.png

蓝框里就是上述第2条响应页面中我们需要抓取参数的地方,它们script里所以只能用正则抓取。注意第二个蓝框里获取ua的值时调用了getUA()方法,它在这个这个页面稍微上一点的地方:

taobao-参数ua.jpg

可以看到,ua的值其实是由一部分这里的代码+ua.js文件才能计算出来的。这里我是将这部分代码保存到了本地的html里,然后用selenium+PhantomJS模拟访问这个页面来获取ua值的,具体可以看之后的代码。这里不能使用execjs库,因为这里涉及到鼠标操作,这必须要在浏览器里完成,而不是一般的js代码。

这里插一点题外话,在做爬虫时经常会碰到一些token值的计算,我以为直接猜测其算法是下下下策。因为目前的web架构决定了要计算这类值只能在浏览器端(为什么?),而对应的算法也得在本地,这类算法可以是公开的算法,比如之前遇到的AES和RSA,也可以是自建的算法,比如这里的ua和之前遇到的歌曲下载地址的解密算法。通常情况下,这类算法会包含在js文件里。而网站能做的无非就是将算法隐藏的深一点。因此,从一个高层角度来看这个问题的话,理论上只要看懂所有报文,肯定能找到计算这类值得地方。

我觉得爬虫反爬虫和信息安全攻防在某种程度上有些类似,两者最大的漏洞其实还是人类的行为,在尽量不影响用户体验的情况下,增加图片识别,文字识别或者手机邮箱验证,这将大大增加爬虫的难度。

再插一点题外话的题外话,之前我提到的有的js文件采用了很难记的变量名,我之前以为这是反爬虫的手段,现在我才知道这么做其实是为了压缩js文件达到减少带宽的目的。不过这也确实增加了阅读js文件的难度。

当然这些都是我个人的有点想法,不知道对不对,也可能随着我继续学习下会有改变。

代码

代码的流程就是上述的流程,也有相应的注释标注。

taobao.py

import requests
import re
import json
import time
from random import choice
from bs4 import BeautifulSoup
from prettytable import PrettyTable
from selenium import webdriver

import Configure

header = {}
header['user-agent'] =  choice(Configure.FakeUserAgents)
header['referer'] = 'https://buyertrade.taobao.com/trade/itemlist/list_bought_items.htm'

cookies = {}
cookiestr = '''
            (Cookies)
            '''

for cookie in cookiestr.split(';'):
    name,value=cookie.strip().split('=',1)  
    cookies[name]=value

def getOnePageOrderHistory(pageNum, newURL=None):
    url = "https://buyertrade.taobao.com/trade/itemlist/asyncBought.htm"
    payload = {
        'action':'itemlist/BoughtQueryAction',
        'event_submit_do_query':1,
        '_input_charset':'utf8'
    }
    formdata = {
        'pageNum':pageNum,
        'pageSize':15,
        'prePageNo':pageNum-1
    }

    # 验证码通过后,新的URL后面会带Token值
    # 带着这个值才能访问成功,并且访问下个页面不再需要验证码
    # newURL就是通过验证后的新URL
    if newURL:
        url = newURL

    try:
        response = requests.post(url, headers=header, params=payload, data=formdata, cookies=cookies)
        content = None

        if response.status_code == requests.codes.ok:
            content = response.text
            
    except Exception as e:
            print (e)

    # 成功直接获取订单,失败进入验证码流程
    data = json.loads(content)
    if data.get('mainOrders'):
        getOrderDetails(data.get('mainOrders'))
    else:
        passCodeCheck(data.get('url'), pageNum)

# 打印订单信息
def getOrderDetails(data):
    table = PrettyTable()
    table.field_names = ["ID", "卖家", "名称", "订单创建时间", "价格", "状态"]

    for order in data:
        tmp = []
        #id = 
        tmp.append(order.get('id'))
        #shopName
        tmp.append(order.get('seller').get('shopName'))
        #title
        tmp.append(order.get('subOrders')[0].get('itemInfo').get('title'))
        #createTime
        tmp.append(order.get('orderInfo').get('createTime'))
        #actualFee
        tmp.append(order.get('payInfo').get('actualFee'))
        #text
        tmp.append(order.get('statusInfo').get('text'))

        table.add_row(tmp)

    print (table)

def passCodeCheck(referer_url, pageNum):
    # 在url中插入style=mini获取包含后续要用到的所有参数的页面
    url = referer_url.replace("?", "?style=mini&")

    try:
        response = requests.post(url, headers=header, cookies=cookies)
        content = None

        if response.status_code == requests.codes.ok:
            content = response.text
            
    except Exception as e:
        print (e)

    # 获取identity, sessionid和type
    pattern = re.compile(
        'new Checkcode\({.*?identity: \'(.*?)\''
        '.*?sessionid: \'(.*?)\''
        '.*?type: \'(.*?)\'.*?}\)', re.S)
    data = pattern.findall(content)
    
    m_identity = data[0][0]
    m_sessionid = data[0][1]
    m_type = data[0][2]

    # 获取action, m_event_submit_do_unique, m_smPolicy
    # m_smApp, m_smReturn, m_smCharset, smTag
    # captcha和smSign
    pattern = re.compile(
        'data: {'
        '.*?action: \'(.*?)\''
        '.*?event_submit_do_unique: \'(.*?)\''
        '.*?smPolicy: \'(.*?)\''
        '.*?smApp: \'(.*?)\''
        '.*?smReturn: \'(.*?)\''
        '.*?smCharset: \'(.*?)\''
        '.*?smTag: \'(.*?)\''
        '.*?captcha: \'(.*?)\''
        '.*?smSign: \'(.*?)\',', re.S)
    data = pattern.findall(content)
    
    m_action = data[0][0]
    m_event_submit_do_unique = data[0][1]
    m_smPolicy = data[0][2]
    m_smApp = data[0][3]
    m_smReturn = data[0][4]
    m_smCharset = data[0][5]
    m_smTag = data[0][6]
    m_captcha = data[0][7]
    m_smSign = data[0][8]

    # 处理验证码
    res = False
    m_code = ""
    while res == False:
        res, m_code = checkCode(m_identity, m_sessionid, m_type, url)

    # 构建URL,获取最后的Token
    murl = "https://sec.taobao.com/query.htm"

    mheader = {}
    mheader['user-agent'] =  choice(Configure.FakeUserAgents)
    mheader['referer'] = url

    mpayload = {
        'action':m_action,
        'event_submit_do_unique':m_event_submit_do_unique,
        'smPolicy':m_smPolicy,
        'smApp':m_smApp,
        'smReturn':m_smReturn,
        'smCharset':m_smCharset,
        'smTag':m_smTag,
        'captcha':m_captcha,
        'smSign':m_smSign,
        'ua':getUA(), # 获取最新的UA
        'identity':m_identity,
        'code':m_code,
        '_ksTS':'{0:d}_39'.format(int(time.time()*1000)),
        'callback':'jsonp40'
    }

    try:
        response = requests.get(murl, headers=mheader, params=mpayload, cookies=cookies)
        content = None
        
        if response.status_code == requests.codes.ok:
            content = response.text
            
    except Exception as e:
        print (e)

    pattern = re.compile('{(.*?)}', re.S)
    data = pattern.findall(content)
    jsond = json.loads('{'+data[0]+'}')

    # 这个json文件里包含了最后访问用的URL
    murl = jsond.get('url')
    getOnePageOrderHistory(pageNum, murl)


def checkCode(m_identity, m_sessionid, m_type, url):
    # 获取验证码的图片
    murl = "https://pin.aliyun.com/get_img"

    mheader = {}
    mheader['user-agent'] =  choice(Configure.FakeUserAgents)
    mheader['referer'] = url

    mpayload = {
        'identity':m_identity,
        'sessionid':m_sessionid,
        'type':m_type,
        't':int(time.time()*1000)
    }

    try:
        response = requests.get(murl, headers=mheader, params=mpayload, cookies=cookies)
        content = None
        
        if response.status_code == requests.codes.ok:
            content = response.content
            
    except Exception as e:
        print (e)

    # 将验证码图片写入本地
    with open("codeimg.jpg","wb") as file:
        file.write(content)

    # 输入并验证验证码
    code = input("请输入验证码:")

    murl = "https://pin.aliyun.com/check_img"

    mpayload = {
        'identity':m_identity,
        'sessionid':m_sessionid,
        'type':m_type,
        'code':code,
        '_ksTS': '{0:d}_29'.format(int(time.time()*1000)),
        'callback':'jsonp30',
        'delflag':0
    }

    try:
        response = requests.get(murl, headers=mheader, params=mpayload, cookies=cookies)
        content = None
        
        if response.status_code == requests.codes.ok:
            content = response.text
            
    except Exception as e:
        print (e)

    # 检测是否成功
    # 这里要返回这个验证码,后面会用到
    pattern = re.compile("SUCCESS",re.S)
    data = pattern.findall(content)

    if data:
        return True, code
    else:
        return False, code

def getUA():
    # 利用PhantomJS模拟浏览器行为
    # 访问本地的js文件来获取UA
    driver = webdriver.PhantomJS()
    driver.get("file:///D:/OneDrive/Documents/Python%E5%92%8C%E6%95%B0%E6%8D%AE%E6%8C%96%E6%8E%98/code/taobao/ua.html")
    content = driver.find_element_by_tag_name('p').text
    driver.close()

    return content
    

if __name__ == '__main__':
    for i in range(2,25):
        getOnePageOrderHistory(i)
        print ("抓取第{0:d}页。".format(i))
        time.sleep(2)

ua.html

这个代码如果用浏览器访问,会在页面里生成最新的ua,之后再用selenium抓下来就可以了。

<html>
<head>
<script>
var UA_Opt=new Object;
var ua="";
UA_Opt.LogVal="ua";
UA_Opt.MaxMCLog=6;
UA_Opt.MaxMPLog=5;
UA_Opt.MaxKSLog=5;
UA_Opt.Token=new Date().getTime()+":"+Math.random();
UA_Opt.SendMethod=8;
UA_Opt.Flag=12430;
function getUA(){
    var tmp = ua;
    try {
        UA_Opt.Token= new Date().getTime()+":"+Math.random();
        UA_Opt.reload();
    }
    catch(err){}
        return tmp;
}
</script>
<script src="https://uaction.alicdn.com/js/ua.js"></script>
<script>

ua:getUA()
document.write("<p>"+ua+"</p>");
</script>
</head>
<body>

</body>
<html>

最终效果图

这里还有个尴尬的地方,我直接抓20个页面的时候一次都没有进入验证码流程,而一页一页访问的时候就很容易进入。图片是我好不容易进入了一次验证码流程后抓下来的,可以看到第一张表格上方有输入验证码。

因为我这两年在国外,淘宝上买的东西全是dota饰品,见笑了。

taobao-结果.jpg
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270