疫情期间,口罩成了市面上的硬通货,简直就是“一罩难求”,不过在某些电商平台上有预约抢购的医用外科口罩,抢得到抢不到就要看个人运气了,但是我坚信科技改变生活,就用Python写了个抢购的脚本,可以自动预约,到点自动抢购JD。
ps:仅供技术交流
思路:
实现步骤:
4. 发现jd的抢购规则
找一个也需要预约抢购的商品,通过Fiddler抓包,找出目标接口
这里推荐去抓包一下N95的抢购,因为价格稍微有点高,所以一般都可以手动抢,这样就可以抓包分析出接口地址了。
5. 编码
根据找到的接口地址的规律,开始写代码
代码实现:
假设我们现在要抢购这一个
商品信息url:https://item.jd.com/100011412340.html
访问并抓包,分析数据包:
import requests,re,pickle
import time,datetime
import json
import sys,os
import logging
import logging.handlers
import random
from dateutil.parser import parse
LOG_FILENAME = '/kz.log'
logger = logging.getLogger()
def set_logger():
logger.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s')
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
file_handler = logging.handlers.RotatingFileHandler(
LOG_FILENAME, maxBytes=10485760, backupCount=5, encoding="utf-8")
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
set_logger()
class JDSpider:
'''
登陆模块作者 zstu-lly
参考 https://github.com/zstu-lly/JD_Robot
'''
def __init__(self):
# init url related
self.home = 'https://passport.jd.com/new/login.aspx'
self.login = 'https://passport.jd.com/uc/loginService'
self.imag = 'https://authcode.jd.com/verify/image'
self.auth = 'https://passport.jd.com/uc/showAuthCode'
self.goodsurl = ["https://item.jd.com/100011551632.html"]
self.sess = requests.Session()
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36',
'ContentType': 'text/html; charset=utf-8',
'Accept-Encoding': 'gzip, deflate, sdch',
'Accept-Language': 'zh-CN,zh;q=0.8',
'Connection': 'keep-alive',
}
self.cookies = {
}
self.address = {}
def checkLogin(self):
# 恢复之前保存的cookie
checkUrl = 'https://passport.jd.com/uc/qrCodeTicketValidation'
try:
logger.info('+++++++++++++++++++++++++++++++++++++++++++++++++++++++')
logger.info(f'{time.ctime()} > 检查登录状态中... ')
with open('cookie', 'rb') as f:
cookies = requests.utils.cookiejar_from_dict(pickle.load(f))
response = self.sess.get(checkUrl, cookies=cookies)
print(response.text)
if response.status_code != requests.codes.OK:
logger.info('登录过期, 请重新登录!')
return False
else:
logger.info('登陆状态正常')
self.cookies.update(dict(cookies))
return True
except Exception as e:
logger.info(e)
return False
def login_by_QR(self):
# jd login by QR code
try:
logger.info('+++++++++++++++++++++++++++++++++++++++++++++++++++++++')
logger.info(f'{time.ctime()} > 请打开京东手机客户端,准备扫码登录:')
urls = (
'https://passport.jd.com/new/login.aspx',
'https://qr.m.jd.com/show',
'https://qr.m.jd.com/check',
'https://passport.jd.com/uc/qrCodeTicketValidation'
)
# step 1: open login page
response = self.sess.get(
urls[0],
headers=self.headers
)
if response.status_code != requests.codes.OK:
logger.info(f"获取登录页失败:{response.status_code}")
return False
# update cookies
self.cookies.update(response.cookies)
# step 2: get QR image
response = self.sess.get(
urls[1],
headers=self.headers,
cookies=self.cookies,
params={
'appid': 133,
'size': 147,
't': int(time.time() * 1000),
}
)
if response.status_code != requests.codes.OK:
logger.info(f"获取二维码失败:{response.status_code}")
return False
# update cookies
self.cookies.update(response.cookies)
# save QR code
image_file = 'qr.png'
with open(image_file, 'wb') as f:
for chunk in response.iter_content(chunk_size=1024):
f.write(chunk)
# scan QR code with phone
if os.name == "nt":
# for windows
os.system('start ' + image_file)
else:
if os.uname()[0] == "Linux":
# for linux platform
os.system("eog " + image_file)
else:
# for Mac platform
os.system("open " + image_file)
# step 3: check scan result 京东上也是不断去发送check请求来判断是否扫码的
self.headers['Host'] = 'qr.m.jd.com'
self.headers['Referer'] = 'https://passport.jd.com/new/login.aspx'
# check if QR code scanned
qr_ticket = None
retry_times = 100 # 尝试100次
while retry_times:
retry_times -= 1
response = self.sess.get(
urls[2],
headers=self.headers,
cookies=self.cookies,
params={
'callback': 'jQuery%d' % random.randint(1000000, 9999999),
'appid': 133,
'token': self.cookies['wlfstk_smdl'],
'_': int(time.time() * 1000)
}
)
if response.status_code != requests.codes.OK:
continue
rs = json.loads(re.search(r'{.*?}', response.text, re.S).group())
if rs['code'] == 200:
logger.info(f"{rs['code']} : {rs['ticket']}")
qr_ticket = rs['ticket']
break
else:
logger.info(f"{rs['code']} : {rs['msg']}")
time.sleep(3)
if not qr_ticket:
logger.info("二维码登录失败")
return False
# step 4: validate scan result
# must have
self.headers['Host'] = 'passport.jd.com'
self.headers['Referer'] = 'https://passport.jd.com/new/login.aspx'
response = requests.get(
urls[3],
headers=self.headers,
cookies=self.cookies,
params={'t': qr_ticket},
)
if response.status_code != requests.codes.OK:
print(f"二维码登录校验失败:{response.status_code}")
return False
# 京东有时候会认为当前登录有危险,需要手动验证
# url: https://safe.jd.com/dangerousVerify/index.action?username=...
res = json.loads(response.text)
if not response.headers.get('p3p'):
if 'url' in res:
logger.info(f"需要手动安全验证: {res['url']}")
return False
else:
logger.info(res)
logger.info('登录失败!!')
return False
# login succeed
self.headers['P3P'] = response.headers.get('P3P')
self.cookies.update(response.cookies)
self.sess.cookies = response.cookies
# 保存cookie
with open('cookie', 'wb') as f:
pickle.dump(self.cookies, f)
return True
except Exception as e:
logger.info(e)
raise
def opengoods(self):
"""
预约
"""
for url in self.goodsurl:
goodsno = url.split("/")[-1][:-5]
yuyue = "https://yushou.jd.com/youshouinfo.action?callback=fetchJSON&sku={}&_={}".format(goodsno,int(time.time()*1000))
self.headers["Referer"]=url
res = self.sess.get(yuyue,headers=self.headers)
resjson = json.loads(res.text[10:-2])
logger.info(resjson)
yuyueinfo = resjson["info"] # 预约状态
yuyuestarttime = parse(resjson["yueStime"]) # 预约开始时间
yuyueendtime = resjson["yueEtime"] # 预约结束时间
buytime = resjson["qiangStime"] # 开始抢购时间
# url = ""
infourl = "https:"+resjson["url"] # 预约的url
# self.yuyue(infourl) # 预约的地址
# self.ko(goodsno) # 获取抢购链接
# self.getaddress(goodsno) # 获取收货地址
# self.submit(goodsno) # 提交订单
# url = "https://marathon.jd.com/seckill/seckill.action?skuId=100011521400&num=1&rid=1583546499"
# self.yuyue(url)
self.buytime = parse(buytime)
if yuyueinfo == "预约未开始":
pass # 等待开始预约了就开始预约
while True:
timenow = parse(time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())))
if timenow > yuyuestarttime:
infourl = "https:"+resjson["url"] # 预约的url
self.headers["Referer"]=yuyue # 换个头
if self.yuyue(infourl):
logger.info("预约成功,等待开抢,开始抢购时间{}".format(buytime))
self.kogoods(goodsno)
break
time.sleep(60)
elif yuyueinfo == "预约进行中":
while True:
timenow = parse(time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())))
if timenow > yuyuestarttime:
infourl = "https:"+resjson["url"] # 预约的url
self.headers["Referer"]=yuyue # 换个头
if self.yuyue(infourl):
logger.info("预约成功,等待开抢,开始抢购时间{}".format(buytime))
self.kogoods(goodsno)
break
time.sleep(60)
elif yuyueinfo == "预约结束抢购未开始":
logger.info("预约成功,等待开抢,开始抢购时间{}".format(buytime))
self.kogoods(goodsno)
# self.kogoods(goodsno)
def kogoods(self,goodsno):
"""
等待抢购
"""
# for i in range(50):
# logger.info("第{}次抢购".format(str(i)))
# self.getaddress(goodsno) # 获取收货地址
# if self.submit(goodsno): # 提交订单
# return
# time.sleep(0.5)
# i+=1
while True:
timenow = parse(time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())))
logger.info((self.buytime - timenow).seconds)
if (self.buytime - timenow).seconds >120:
time.sleep(60)
continue
elif (self.buytime - timenow).seconds >60:
time.sleep(30)
continue
elif (self.buytime - timenow).seconds >30:
time.sleep(5)
continue
elif (self.buytime - timenow).seconds <=10:
# if timenow == self.buytime:
self.getaddress(goodsno) # 获取收货地址
for i in range(50):
logger.info("第{}次抢购".format(str(i)))
if self.submit(goodsno): # 提交订单
return
time.sleep(0.5)
i+=1
return
def yuyue(self,url):
"""
预约
"""
res = self.sess.get(url,headers = self.headers,cookies = self.cookies)
if res.status_code == 200:
# with open ("a{}.txt".format(str(int(time.time()))),"w") as f:
# f.write(res.text)
# print(url)
return True # 预约成功
def ko(self,goodsnoid):
"""
抢购
"""
url= "https://itemko.jd.com/itemShowBtn?callback=jQuery1288178&skuId={}&from=pc&_={}".format(goodsnoid,int(time.time()*1000))
res = self.sess.get(url,headers = self.headers,cookies = self.cookies)
logger.info(res.text)
resjson = json.loads(res.text[14:-2])
logger.info(resjson)
kourl = "https:"+resjson["url"] # 抢购地址
res = self.sess.get(kourl,headers = self.headers,cookies = self.cookies)
logger.info(res.text)
def getaddress(self,goodsno):
"""
获取订单地址
"""
url = "https://marathon.jd.com/seckillnew/orderService/pc/init.action"
data = {
"sku":goodsno,
"num":1,
"isModifyAddress":False
}
logger.info("开始获取订单信息")
res = self.sess.post(url,data=data,headers = self.headers,cookies = self.cookies)
logger.info(res.text)
self.address = json.loads(res.text)
def submit(self,goodsno):
"""
提交订单
"""
url = "https://marathon.jd.com/seckillnew/orderService/pc/submitOrder.action?skuId={}".format(goodsno) # 提交订单id
logger.info("尝试提交订单")
plyaload={
"skuId":goodsno,
"num":1,
"addressId": self.address["addressList"][0]["id"],
"yuShou":True,
"isModifyAddress":False,
"name":self.address["addressList"][0]["name"],
"provinceId":self.address["addressList"][0]["provinceId"],
"cityId":self.address["addressList"][0]["cityId"],
"countyId":self.address["addressList"][0]["countyId"],
"townId":self.address["addressList"][0]["townId"],
"addressDetail":self.address["addressList"][0]["addressDetail"],
"mobile":self.address["addressList"][0]["mobile"],
"mobileKey":self.address["addressList"][0]["mobileKey"],
"email":self.address["addressList"][0]["email"],
"postCode":self.address["addressList"][0]["postCode"],
"invoiceTitle":self.address["invoiceInfo"]["invoiceTitle"],
"invoiceCompanyName":self.address["invoiceInfo"]["invoiceCompany"],
"invoiceContent":self.address["invoiceInfo"]["invoiceContentType"],
"invoiceTaxpayerNO":self.address["invoiceInfo"]["invoiceCode"],
"invoiceEmail":self.address["invoiceInfo"]["invoiceEmail"],
"invoicePhone":self.address["invoiceInfo"]["invoicePhone"],
"invoicePhoneKey":self.address["invoiceInfo"]["invoicePhoneKey"],
"invoice":True,
"password":"",
"codTimeType":3,
"paymentType":4,
"areaCode":'',
"overseas":0,
"phone":"",
"eid":"xxx",
"fp":"xxx",
"token":self.address["token"],
"pru":""
}
res = self.sess.post(url,data=plyaload,headers = self.headers,cookies = self.cookies)
logger.info("抢购结果:"+res.text)
if "成功" in res.text:
return True
else:
return False
jd = JDSpider()
cookiesstatus = jd.checkLogin()
if not cookiesstatus:
cookiesstatus = jd.login_by_QR()
logger.info("登陆成功")
else:
# loginstatus = jd.login_by_QR()
jd.opengoods()
if cookiesstatus:
jd.opengoods()