淘宝抓取所有历史订单
思路
淘宝是我迄今为止遇到的反爬虫技术最厉害的一个网站,我估计在业界它也是处于顶尖水平的。这里我一共花了整整一天24个小时才有所小得。
这篇文章里,我采用手动登陆淘宝的方式获取登陆用的Cookie,我会在接下来所有请求里直接使用这个cookie,后面应该会再写文章讲如何抓这个Cookie以及机器识别验证码。
言归正传,要完成这个项目,先要弄清整个流程。我要抓的是“我的订单”下的商品信息,在登陆状态下,访问不同页数的订单页面会发送对应的XHR,接着服务器会返回一个包含商品信息的json文件,这个很容易找。但是当我用同样的Header和Query String Parameters构建一条请求时,这条请求会被怀疑为爬虫请求,淘宝会要求进行验证码认证。个人认为有两种思路解决这个验证码的问题:一个是找到是什么原因导致淘宝认为你是爬虫,从而找到绕过验证码的方法;另一个思路就是让爬虫能够通过验证码检测。经过很长时间的分析和尝试,绕过验证码的方法我没找到,验证码的流程却分析了出来。
验证码的流程
首先需要让页面进入验证码流程,这里只需要用一个简陋的爬虫访问一下订单页面就行了。接着在浏览器上刷新或者访问下一页订单,页面会弹出验证码,在输入正确的验证码之后,页面会成功跳转到下一页。使用浏览器工具将整个过程的报文都抓下来,逐条分析,如图,这里我把关键的几条报文按顺序标注了一下:
taobao-流程.jpg我也把相关报文的Query String Parameters也截了下来,如下:
taobao-参数列表.jpg分析整个流程是最困难的一步,如果有人也做这个项目,我个人建议还是先自己分析为好。整个流程大概是这样子的(序号也对应了报文的编号,上图中的数字也对应了各自的编号):
- 这条POST请求就是正常的订单信息请求,如果没有进入验证码流程,服务器应该直接返回商品信息json文件。如果进入了验证码流程,服务器会返回一个URL,你需要访问这个URL获取包含计算所有参数的js文件和验证码图片。
- 这条报文就是访问第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,据说是淘宝根据一个很复杂的算法计算出来的,其变量还包含鼠标键盘的操作记录。 - ua.js很明显这是计算UA值用的,不过这里的Trick就是光有这个文件还不够,因为里面没有变量UA_opt的申明,你需要结合第2条响应里的一部分js代码在加上ua.js文件才能计算出最后需要的ua值。这里要非常感谢这位老哥的文章:ua.js中UA_Opt设定信息的重要性与来源分析,他分析了所有的js文件,非常厉害。
- 第4条报文使用了2里面获得的identity,sessionid和type,以及代表当前时间的t来获取验证码图片。我会将这个图片保存到本地,以便之后手动输入验证码。
- 第5条是用来判断你输入的验证码是否正确的,除了4里面的参数,还增加了code(你输入的验证码),_ksTS和callback在爬虾米音乐时遇到过,这里是一样的,前者是时间+一个数字,后者是上个数字+1,代表返回的json块的名字。如果验证码正确,服务器会返回SUCCESS消息,否则返回ERROR,你需要重新发送第4条报文继续验证。这里注意,如果你能保证你输入的验证码肯定是正确的,这条报文可以省略。
- 这条是最重要的一条,你需要用到上述所有的参数来向服务器请求smToken,这个Token只能用一次,它是你能继续访问订单页面的凭证(也是最重要的一个参数)。
- 第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