2025年3月25日 星期二 甲辰(龙)年 月廿四 设为首页 加入收藏
rss
您当前的位置:首页 > 计算机 > 编程开发 > Python

【Python】京东抢购脚本

时间:09-11来源:作者:点击数:75

1 问题背景

编写脚本监听商品库存,一旦查询到货源便开始尝试自动下单。

2 设计思路

京东对于商品的抢购主要分为两种:

  • 预约抢购:到点开放购买,和普通商品下单流程一致;
  • 秒杀商品:单独的抢购接口和下单流程。

当然本次针对的预约抢购类或无货订购类,即整体下单流程和购买普通商品时一样:

登录账号 → 进入购物车 → 选择抢购商品 → 点击去结算 → 点击提交订单 → 选择付款方式并付款

3 具体实现

采用京东 WEB 端接口实现我们的脚本程序。

于是经过对京东网页下单流程的分析,将我们的脚本程序分为四个模块:

  • 账号登录模块
  • 库存监听模块
  • 购物车管理模块
  • 订单管理模块
3.1 账号登录

由于使用账号密码时有验证码限制,此处采用扫码登录方式绕过。

本次主要针对京东登录页进行抓包分析,找到几个有用接口.

获取登录二维码
  • def getQRcode(self):
  • url = 'https://qr.m.jd.com/show'
  • payload = {
  • 'appid': 133,
  • 'size': 147,
  • 't': str(int(time.time() * 1000)),
  • }
  • headers = {
  • 'User-Agent': self.userAgent,
  • 'Referer': 'https://passport.jd.com/new/login.aspx',
  • }
  • resp = self.sess.get(url=url, headers=headers, params=payload)
  • if not self.respStatus(resp):
  • return None
  • return resp.content
获取Ticket
  • def getQRcodeTicket(self):
  • url = 'https://qr.m.jd.com/check'
  • payload = {
  • 'appid': '133',
  • 'callback': 'jQuery{}'.format(random.randint(1000000, 9999999)),
  • 'token': self.sess.cookies.get('wlfstk_smdl'),
  • '_': str(int(time.time() * 1000)),
  • }
  • headers = {
  • 'User-Agent': self.userAgent,
  • 'Referer': 'https://passport.jd.com/new/login.aspx',
  • }
  • resp = self.sess.get(url=url, headers=headers, params=payload)
  • if not self.respStatus(resp):
  • return False
  • respJson = self.parseJson(resp.text)
  • if respJson['code'] != 200:
  • return None
  • else:
  • return respJson['ticket']
验证 Ticket
  • def validateQRcodeTicket(self, ticket):
  • url = 'https://passport.jd.com/uc/qrCodeTicketValidation'
  • headers = {
  • 'User-Agent': self.userAgent,
  • 'Referer': 'https://passport.jd.com/uc/login?ltype=logout',
  • }
  • resp = self.sess.get(url=url, headers=headers, params={'t': ticket})
  • if not self.respStatus(resp):
  • return False
  • respJson = json.loads(resp.text)
  • if respJson['returnCode'] == 0:
  • return True
  • else:
  • return False

此时验证 Ticket 有效后使用 pickle 库将程序会话中的 cookie 保存到本地以便下次使用。

3.2 库存监听

库存监听较为简单,分析商品详情页,获取店铺ID以及商品分类属性

获取商品详情信息
  • def getItemDetail(self, skuId):
  • url = 'https://item.jd.com/{}.html'.format(skuId)
  • page = requests.get(url=url, headers=self.headers)
  • html = etree.HTML(page.text)
  • vender = html.xpath(
  • '//div[@class="follow J-follow-shop"]/@data-vid')[0]
  • cat = html.xpath('//a[@clstag="shangpin|keycount|product|mbNav-3"]/@href')[
  • 0].replace('//list.jd.com/list.html?cat=', '')
  • if not vender or not cat:
  • raise Exception('获取商品信息失败,请检查SKU是否正确')
  • detail = dict(catId=cat, venderId=vender)
  • return detail
查询库存
  • def getItemStock(self, skuId, num, areaId):
  • item = self.itemDetails.get(skuId)
  • if not item:
  • return False
  • url = 'https://c0.3.cn/stock'
  • payload = {
  • 'skuId': skuId,
  • 'buyNum': num,
  • 'area': areaId,
  • 'ch': 1,
  • '_': str(int(time.time() * 1000)),
  • 'callback': 'jQuery{}'.format(random.randint(1000000, 9999999)),
  • # get error stock state without this param
  • 'extraParam': '{"originid":"1"}',
  • # get 403 Forbidden without this param (obtained from the detail page)
  • 'cat': item.get('catId'),
  • # return seller information with this param (can't be ignored)
  • 'venderId': item.get('venderId')
  • }
  • headers = {
  • 'User-Agent': self.userAgent,
  • 'Referer': 'https://item.jd.com/{}.html'.format(skuId),
  • }
  • respText = ''
  • try:
  • respText = requests.get(
  • url=url, params=payload, headers=headers, timeout=self.timeout).text
  • respJson = self.parseJson(respText)
  • stockInfo = respJson.get('stock')
  • skuState = stockInfo.get('skuState') # 商品是否上架
  • # 商品库存状态:33 -- 现货 0,34 -- 无货 36 -- 采购中 40 -- 可配货
  • stockState = stockInfo.get('StockState')
  • return skuState == 1 and stockState in (33, 40)
3.3 购物车操作

无货商品加入到购物车我们是无法通过页面操作的,我们这边可以使用其他有货商品进行尝试,主要查看购物车的增删改查接口:

取消所有选中商品
  • def uncheckCartAll(self):
  • """ 取消所有选中商品
  • return 购物车信息
  • """
  • url = 'https://api.m.jd.com/api'
  • headers = {
  • 'User-Agent': self.userAgent,
  • 'Content-Type': 'application/x-www-form-urlencoded',
  • 'origin': 'https://cart.jd.com',
  • 'referer': 'https://cart.jd.com'
  • }
  • data = {
  • 'functionId': 'pcCart_jc_cartUnCheckAll',
  • 'appid': 'JDC_mall_cart',
  • 'body': '{"serInfo":{"area":"","user-key":""}}',
  • 'loginType': 3
  • }
  • resp = self.sess.post(url=url, headers=headers, data=data)
  • # return self.respStatus(resp) and resp.json()['success']
  • return resp
加入购入车
  • def addCartSku(self, skuId, skuNum):
  • """ 加入购入车
  • skuId 商品sku
  • skuNum 购买数量
  • retrun 是否成功
  • """
  • url = 'https://api.m.jd.com/api'
  • headers = {
  • 'User-Agent': self.userAgent,
  • 'Content-Type': 'application/x-www-form-urlencoded',
  • 'origin': 'https://cart.jd.com',
  • 'referer': 'https://cart.jd.com'
  • }
  • data = {
  • 'functionId': 'pcCart_jc_cartAdd',
  • 'appid': 'JDC_mall_cart',
  • 'body': '{\"operations\":[{\"carttype\":1,\"TheSkus\":[{\"Id\":\"' + skuId + '\",\"num\":' + str(skuNum) + '}]}]}',
  • 'loginType': 3
  • }
  • resp = self.sess.post(url=url, headers=headers, data=data)
  • return self.respStatus(resp) and resp.json()['success']
修改购物车商品数量
  • def changeCartSkuCount(self, skuId, skuUid, skuNum, areaId):
  • """ 修改购物车商品数量
  • skuId 商品sku
  • skuUid 商品用户关系
  • skuNum 购买数量
  • retrun 是否成功
  • """
  • url = 'https://api.m.jd.com/api'
  • headers = {
  • 'User-Agent': self.userAgent,
  • 'Content-Type': 'application/x-www-form-urlencoded',
  • 'origin': 'https://cart.jd.com',
  • 'referer': 'https://cart.jd.com'
  • }
  • body = '{\"operations\":[{\"TheSkus\":[{\"Id\":\"'+skuId+'\",\"num\":'+str(
  • skuNum)+',\"skuUuid\":\"'+skuUid+'\",\"useUuid\":false}]}],\"serInfo\":{\"area\":\"'+areaId+'\"}}'
  • data = {
  • 'functionId': 'pcCart_jc_changeSkuNum',
  • 'appid': 'JDC_mall_cart',
  • 'body': body,
  • 'loginType': 3
  • }
  • resp = self.sess.post(url=url, headers=headers, data=data)
  • return self.respStatus(resp) and resp.json()['success']

以上是我们一次购买需要用到的最少接口,为了不破坏账户购物车中已有数据,采用以下步骤准备好购物车:

  • 取消全部勾选(返回购物车信息);
  • 已在购物车则修改商品数量;
  • 不在购物车则加入购物车。
3.4 订单操作

当我们准备好购物车之后(选中购买商品以及调整购买数量),就可以进行下一步订单相关操作:

获取结算单
  • def getCheckoutPage(self):
  • """获取订单结算页面信息
  • :return: 结算信息 dict
  • """
  • url = 'http://trade.jd.com/shopping/order/getOrderInfo.action'
  • # url = 'https://cart.jd.com/gotoOrder.action'
  • payload = {
  • 'rid': str(int(time.time() * 1000)),
  • }
  • headers = {
  • 'User-Agent': self.userAgent,
  • 'Referer': 'https://cart.jd.com/cart',
  • }
提交订单
  • def submitOrder(self):
  • """提交订单
  • :return: True/False 订单提交结果
  • """
  • url = 'https://trade.jd.com/shopping/order/submitOrder.action'
  • # js function of submit order is included in https://trade.jd.com/shopping/misc/js/order.js?r=2018070403091
  • data = {
  • 'overseaPurchaseCookies': '',
  • 'vendorRemarks': '[]',
  • 'submitOrderParam.sopNotPutInvoice': 'false',
  • 'submitOrderParam.trackID': 'TestTrackId',
  • 'submitOrderParam.ignorePriceChange': '0',
  • 'submitOrderParam.btSupport': '0',
  • 'riskControl': self.risk_control,
  • 'submitOrderParam.isBestCoupon': 1,
  • 'submitOrderParam.jxj': 1,
  • 'submitOrderParam.trackId': self.track_id,
  • 'submitOrderParam.eid': self.eid,
  • 'submitOrderParam.fp': self.fp,
  • 'submitOrderParam.needCheck': 1,
  • }
3.5 其他模块
3.5.1 微信通知

抢购成功增加微信通知

  • def send_wechat(message, desp, sckey):
  • """
  • server酱微信通知
  • :param message:
  • :param desp: 发送内容
  • :param sckey: 密钥
  • :return:
  • """
  • if not message.strip():
  • logger.error('Text of message is empty!')
  • return
  • now_time = str(datetime.datetime.now())
  • desp = '[{0}]'.format(now_time) if not desp else '{0} [{1}]'.format(desp, now_time)
  • try:
  • resp = requests.get(
  • 'https://sc.ftqq.com/{}.send?text={}&desp={}'.format(sckey, message, desp)
  • )
  • resp_json = json.loads(resp.text)
  • if resp_json['data']['errno'] == 0:
  • logger.info('Message sent successfully [text: %s, desp: %s]', message, desp)
  • else:
  • logger.error('Fail to send message, reason: %s', resp.text)
  • except requests.exceptions.RequestException as req_error:
  • logger.error('Request error: %s', req_error)
  • except Exception as e:
  • logger.error('Fail to send message [text: %s, desp: %s]: %s', message, desp, e)
3.5.2 日志模块
  • #!/usr/bin/env python
  • # -*- encoding=utf8 -*-
  • import logging
  • import logging.handlers
  • import os
  • from time import strftime
  • LOG_FILENAME = strftime("logs\jd-buyer_%Y_%m_%d_%H.log")
  • logger = logging.getLogger()
  • def set_logger():
  • path = os.path.dirname(os.getcwd()+ '\\logs\\') # 判断日志目录
  • if not os.path.exists(path):
  • os.makedirs(path)
  • 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()
3.5.3 打包成APP模块

可以参考pyinstaller或者auto-py-to-exe用法

3.5.4 参数模块
  • {
  • "skuId": "10060659038775",
  • "areaId": "1_2901_4135",
  • "count": 9,
  • "stockInterval": 9,
  • "submitInterval": 0,
  • "buyTime": "",
  • "password": ""
  • }
  • [messenger]
  • # 使用了Server酱的推送服务
  • # 如果想开启下单成功后消息推送,则将 enable 设置为 true,默认为 false 不开启推送
  • # 开启消息推送必须填入 sckey,如何获取请参考 http://sc.ftqq.com/3.version。感谢Server酱~
  • enable = true
  • sckey = ""
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门