首頁 > 軟體

教你用Python寫一個京東自動下單搶購指令碼

2023-03-23 22:02:39

1 問題背景

經過無數次搶購失敗後,發現商家會不定時的放出少量貨源,目測每次會有幾臺。如果我們編寫一個指令碼程式24小時不間斷監聽商品庫存,一旦查詢到貨源便開始嘗試自動下單,這樣就可以極大提高我們的成功概率。

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 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 有效後使用 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,
    }

4 完整程式碼

專案完整原始碼請看:https://github.com/zas023/JdBuyer

目前完成度較高,可以直接使用:

並提供 Windows 和 macOS 兩種使用者端;支援下單後微信通知觸達。

5 總結

指令碼自動化操作確實可以實現搶購商品,相比手動操作有較高的下單成功率,但僅靠上述程式碼想與某些專業搶購的伺服器進行比較還是相去甚遠。

到此這篇關於教你用Python寫一個京東自動下單搶購指令碼的文章就介紹到這了,更多相關Python京東自動搶購指令碼內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com