美文网首页爬虫
淘宝抓取所有历史订单

淘宝抓取所有历史订单

作者: 小温侯 | 来源:发表于2018-07-21 22:55 被阅读644次

    思路

    淘宝是我迄今为止遇到的反爬虫技术最厉害的一个网站,我估计在业界它也是处于顶尖水平的。这里我一共花了整整一天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

    相关文章

      网友评论

      • 知识学者:感觉爬虫好无聊,做了一个月不到,再也不想做了,就是找规律,分析网页。。。
        拿到string就好办了,什么query string,from date都是对数据库操作。 还有find判断关键字等等,:grin: 我是渣渣,现在java,js做的就是上次爬取的左边的导航栏,tree,每一次click查询数据库,给东西。。。
        小温侯:@东风冷雪 啊?你想表达什么

      本文标题:淘宝抓取所有历史订单

      本文链接:https://www.haomeiwen.com/subject/gfvkmftx.html