├── .gitattributes ├── Console Version ├── JDAutoBuyTool.py ├── Please fill out this document.txt ├── jdAutoBuyGood.log ├── requirements.txt └── test.py ├── GUI Version ├── autobuy.py ├── image │ ├── down.png │ ├── left.png │ └── right.png └── window.qss ├── LICENSE ├── README.md └── assets ├── 1580963259089.png ├── 1580963623908.png ├── 1581038258274.png ├── 1581218076866.png ├── 1581218537205.png ├── 1581249279630.png ├── 1581508416184.png ├── 1581508444771.png ├── 1581768204211.png ├── 1581768213048.png └── 1583151909197.png /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /Console Version/JDAutoBuyTool.py: -------------------------------------------------------------------------------- 1 | # -*- coding=utf-8 -*- 2 | import requests 3 | import logging 4 | import logging.handlers 5 | import time 6 | import json 7 | import sys 8 | import random 9 | from bs4 import BeautifulSoup 10 | import smtplib 11 | import re 12 | from email.mime.text import MIMEText 13 | from email.header import Header 14 | 15 | import traceback 16 | 17 | def setLogger(logFileName, logger): 18 | logger.setLevel(logging.INFO) 19 | formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s') 20 | 21 | console_handler = logging.StreamHandler() 22 | console_handler.setFormatter(formatter) 23 | logger.addHandler(console_handler) 24 | 25 | file_handler = logging.handlers.RotatingFileHandler( 26 | logFileName, maxBytes=10485760, backupCount=5, encoding="utf-8") 27 | file_handler.setFormatter(formatter) 28 | logger.addHandler(file_handler) 29 | 30 | 31 | def sendMail(url, isOrder, sendTo): 32 | if len(sendTo) == 0 or sendTo == '$$$$$$$@qq.com': 33 | return 34 | 35 | mailRe = re.compile('^\w{1,15}@\w{1,10}\.(com|cn|net)$') 36 | if not re.search(mailRe, sendTo): 37 | return 38 | 39 | sendFrom = '645064582@qq.com' 40 | 41 | smtp_server = 'smtp.qq.com' 42 | if isOrder: 43 | msg = MIMEText(url + ' 商品已下单, 请在尽快付款', 'plain', 'utf-8') 44 | else: 45 | msg = MIMEText(url + ' 商品下单失败.', 'plain', 'utf-8') 46 | 47 | msg['From'] = Header(sendFrom) 48 | msg['To'] = Header(sendTo) 49 | msg['Subject'] = Header('买到啦') 50 | 51 | server = smtplib.SMTP_SSL(host=smtp_server) 52 | server.connect(smtp_server, 465) 53 | server.login(sendFrom, 'nkrzicfjkzznbehi') 54 | server.sendmail(sendFrom, sendTo, msg.as_string()) 55 | server.quit() 56 | 57 | 58 | def sendError(sendTo): 59 | if len(sendTo) == 0 or sendTo == '$$$$$$$@qq.com': 60 | return 61 | 62 | mailRe = re.compile('^\w{1,15}@\w{1,10}\.(com|cn|net)$') 63 | if not re.search(mailRe, sendTo): 64 | return 65 | 66 | sendFrom = '645064582@qq.com' 67 | 68 | # 发信服务器 69 | smtp_server = 'smtp.qq.com' 70 | # 邮箱正文内容, 第一个参数为内容, 第二个参数为格式(plain 为纯文本), 第三个参数为编码 71 | msg = MIMEText('main()函数出错嘞!', 'plain', 'utf-8') 72 | 73 | # 邮件头信息 74 | msg['From'] = Header(sendFrom) 75 | msg['To'] = Header(sendTo) 76 | msg['Subject'] = Header('ERROR!') 77 | # 开启发信服务, 加密传输 78 | server = smtplib.SMTP_SSL(host=smtp_server) 79 | server.connect(smtp_server, 465) 80 | server.login(sendFrom, 'nkrzicfjkzznbehi') 81 | server.sendmail(sendFrom, sendTo, msg.as_string()) 82 | server.quit() 83 | 84 | 85 | def get_tag_value(tag, key='', index=0): 86 | if key: 87 | value = tag[index].get(key) 88 | else: 89 | value = tag[index].text 90 | return value.strip(' \t\r\n') 91 | 92 | 93 | def responseStatus(resp): 94 | if resp.status_code != requests.codes.OK: 95 | print('Status: %u, Url: %s' % (resp.status_code, resp.url)) 96 | return False 97 | return True 98 | def validateCookies(logger, session): 99 | for flag in range(1, 3): 100 | try: 101 | targetURL = 'https://order.jd.com/center/list.action' 102 | payload = { 103 | 'rid': str(int(time.time() * 1000)), 104 | } 105 | resp = session.get(url=targetURL, params=payload, allow_redirects=False) 106 | if resp.status_code == requests.codes.OK: 107 | logger.info('登录成功!') 108 | return True 109 | else: 110 | logger.info('第[%s]次请重新获取cookie...', flag) 111 | time.sleep(5) 112 | continue 113 | except Exception as e: 114 | logger.info('第[%s]次请重新获取cookie...', flag) 115 | time.sleep(5) 116 | continue 117 | 118 | 119 | def getUsername(logger, session): 120 | userName_Url = 'https://passport.jd.com/new/helloService.ashx?callback=jQuery339448&_=' + str( 121 | int(time.time() * 1000)) 122 | session.headers = { 123 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36", 124 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", 125 | "Referer": "https://order.jd.com/center/list.action", 126 | "Connection": "keep-alive" 127 | } 128 | resp = session.get(url=userName_Url, allow_redirects=True) 129 | resultText = resp.text 130 | resultText = resultText.replace('jQuery339448(', '') 131 | resultText = resultText.replace(')', '') 132 | usernameJson = json.loads(resultText) 133 | logger.info('登录账号名称: ' + usernameJson['nick']) 134 | 135 | 136 | def cancelSelectCartItem(session): 137 | url = "https://cart.jd.com/cancelAllItem.action" 138 | data = { 139 | 't': 0, 140 | 'outSkus': '', 141 | 'random': random.random() 142 | } 143 | resp = session.post(url, data=data) 144 | if resp.status_code != requests.codes.OK: 145 | print('Status: %u, Url: %s' % (resp.status_code, resp.url)) 146 | return False 147 | return True 148 | 149 | def cart_detail(session, logger, isOutput=False): 150 | url = 'https://cart.jd.com/cart.action' 151 | headers = { 152 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36", 153 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", 154 | "Referer": "https://order.jd.com/center/list.action", 155 | "Host": "cart.jd.com", 156 | "Connection": "keep-alive" 157 | } 158 | resp = session.get(url, headers=headers) 159 | soup = BeautifulSoup(resp.text, "html.parser") 160 | 161 | cartDetail = dict() 162 | for item in soup.find_all(class_='item-item'): 163 | sku_id = item['skuid'] # 商品id 164 | try: 165 | # 例如: ['increment', '8888', '100001071956', '1', '13', '0', '50067652554'] 166 | # ['increment', '8888', '100002404322', '2', '1', '0'] 167 | item_attr_list = item.find(class_='increment')['id'].split('_') 168 | p_type = item_attr_list[4] 169 | promo_id = target_id = item_attr_list[-1] if len(item_attr_list) == 7 else 0 170 | 171 | cartDetail[sku_id] = { 172 | 'name': get_tag_value(item.select('div.p-name a')), # 商品名称 173 | 'verder_id': item['venderid'], # 商家id 174 | 'count': int(item['num']), # 数量 175 | 'unit_price': get_tag_value(item.select('div.p-price strong'))[1:], # 单价 176 | 'total_price': get_tag_value(item.select('div.p-sum strong'))[1:], # 总价 177 | 'is_selected': 'item-selected' in item['class'], # 商品是否被勾选 178 | 'p_type': p_type, 179 | 'target_id': target_id, 180 | 'promo_id': promo_id 181 | } 182 | except Exception as e: 183 | logger.error("商品%s在购物车中的信息无法解析, 报错信息: %s, 该商品自动忽略", sku_id, e) 184 | 185 | if isOutput == True: 186 | logger.info('当前购物车信息: %s', cartDetail) 187 | return cartDetail 188 | 189 | def change_item_num_in_cart(sku_id, vender_id, num, p_type, target_id, promo_id, session): 190 | url = "https://cart.jd.com/changeNum.action" 191 | data = { 192 | 't': 0, 193 | 'venderId': vender_id, 194 | 'pid': sku_id, 195 | 'pcount': num, 196 | 'ptype': p_type, 197 | 'targetId': target_id, 198 | 'promoID': promo_id, 199 | 'outSkus': '', 200 | 'random': random.random(), 201 | # 'locationId' 202 | } 203 | session.headers = { 204 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36", 205 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", 206 | "Referer": "https://cart.jd.com/cart", 207 | "Connection": "keep-alive" 208 | } 209 | resp = session.post(url, data=data) 210 | return json.loads(resp.text)['sortedWebCartResult']['achieveSevenState'] == 2 211 | 212 | 213 | def addItemToCart(sku_id, session, logger): 214 | url = 'https://cart.jd.com/gate.action' 215 | payload = { 216 | 'pid': sku_id, 217 | 'pcount': 1, 218 | 'ptype': 1, 219 | } 220 | resp = session.get(url=url, params=payload) 221 | if 'https://cart.jd.com/cart.action' in resp.url: # 套装商品加入购物车后直接跳转到购物车页面 222 | result = True 223 | else: # 普通商品成功加入购物车后会跳转到提示 "商品已成功加入购物车!" 页面 224 | soup = BeautifulSoup(resp.text, "html.parser") 225 | result = bool(soup.select('h3.ftx-02')) # [

商品已成功加入购物车!

] 226 | 227 | if result: 228 | logger.info('%s 已成功加入购物车', sku_id) 229 | else: 230 | logger.error('%s 添加到购物车失败', sku_id) 231 | 232 | 233 | def get_checkout_page_detail(session, logger): 234 | url = 'http://trade.jd.com/shopping/order/getOrderInfo.action' 235 | # url = 'https://cart.jd.com/gotoOrder.action' 236 | payload = { 237 | 'rid': str(int(time.time() * 1000)), 238 | } 239 | headers = { 240 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36", 241 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", 242 | "Referer": "https://cart.jd.com/cart.action", 243 | "Connection": "keep-alive", 244 | 'Host': 'trade.jd.com', 245 | } 246 | try: 247 | resp = session.get(url=url, params=payload, headers=headers) 248 | if not responseStatus(resp): 249 | logger.error('获取订单结算页信息失败') 250 | return '' 251 | 252 | soup = BeautifulSoup(resp.text, "html.parser") 253 | risk_control = get_tag_value(soup.select('input#riskControl'), 'value') 254 | 255 | order_detail = { 256 | 'address': soup.find('span', id='sendAddr').text[5:], # remove '寄送至: ' from the begin 257 | 'receiver': soup.find('span', id='sendMobile').text[4:], # remove '收件人:' from the begin 258 | 'total_price': soup.find('span', id='sumPayPriceId').text[1:], # remove '¥' from the begin 259 | 'items': [] 260 | } 261 | 262 | logger.info("下单信息: %s", order_detail) 263 | return order_detail 264 | except requests.exceptions.RequestException as e: 265 | logger.error('订单结算页面获取异常: %s' % e) 266 | except Exception as e: 267 | logger.error('下单页面数据解析异常: %s', e) 268 | return None 269 | 270 | 271 | def submit_order(risk_control, session, logger, payment_pwd): 272 | url = 'https://trade.jd.com/shopping/order/submitOrder.action' 273 | # js function of submit order is included in https://trade.jd.com/shopping/misc/js/order.js?r=2018070403091 274 | 275 | # overseaPurchaseCookies: 276 | # vendorRemarks: [] 277 | # submitOrderParam.sopNotPutInvoice: false 278 | # submitOrderParam.trackID: TestTrackId 279 | # submitOrderParam.ignorePriceChange: 0 280 | # submitOrderParam.btSupport: 0 281 | # riskControl: 282 | # submitOrderParam.isBestCoupon: 1 283 | # submitOrderParam.jxj: 1 284 | # submitOrderParam.trackId: 285 | 286 | data = { 287 | 'overseaPurchaseCookies': '', 288 | 'vendorRemarks': '[]', 289 | 'submitOrderParam.sopNotPutInvoice': 'false', 290 | 'submitOrderParam.trackID': 'TestTrackId', 291 | 'submitOrderParam.ignorePriceChange': '0', 292 | 'submitOrderParam.btSupport': '0', 293 | 'riskControl': risk_control, 294 | 'submitOrderParam.isBestCoupon': 1, 295 | 'submitOrderParam.jxj': 1, 296 | 'submitOrderParam.trackId': '9643cbd55bbbe103eef18a213e069eb0', # Todo: need to get trackId 297 | # 'submitOrderParam.eid': eid, 298 | # 'submitOrderParam.fp': fp, 299 | 'submitOrderParam.needCheck': 1, 300 | } 301 | 302 | def encrypt_payment_pwd(paymentPwd): 303 | return ''.join(['u3' + x for x in paymentPwd]) 304 | 305 | if len(payment_pwd) > 0: 306 | data['submitOrderParam.payPassword'] = encrypt_payment_pwd(payment_pwd) 307 | 308 | headers = { 309 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36", 310 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", 311 | "Referer": "http://trade.jd.com/shopping/order/getOrderInfo.action", 312 | "Connection": "keep-alive", 313 | 'Host': 'trade.jd.com', 314 | } 315 | 316 | try: 317 | resp = session.post(url=url, data=data, headers=headers) 318 | resp_json = json.loads(resp.text) 319 | 320 | if resp_json.get('success'): 321 | logger.info('订单提交成功! 订单号: %s', resp_json.get('orderId')) 322 | return True 323 | else: 324 | message, result_code = resp_json.get('message'), resp_json.get('resultCode') 325 | if result_code == 0: 326 | # self._save_invoice() 327 | message = message + '(下单商品可能为第三方商品, 将切换为普通发票进行尝试)' 328 | elif result_code == 60077: 329 | message = message + '(可能是购物车为空或未勾选购物车中商品)' 330 | elif result_code == 60123: 331 | message = message + '(未配置支付密码)' 332 | logger.info('订单提交失败, 错误码: %s, 返回信息: %s', result_code, message) 333 | logger.info(resp_json) 334 | return False 335 | except Exception as e: 336 | logger.error(e) 337 | return False 338 | 339 | 340 | def item_removed(sku_id): 341 | headers = { 342 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36", 343 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", 344 | "Referer": "http://trade.jd.com/shopping/order/getOrderInfo.action", 345 | "Connection": "keep-alive", 346 | 'Host': 'trade.jd.com', 347 | } 348 | url = 'https://item.jd.com/{}.html'.format(sku_id) 349 | page = requests.get(url=url, headers=headers) 350 | return '该商品已下柜' not in page.text 351 | 352 | 353 | def buyGood(sku_id, session, logger, payment_pwd): 354 | for count in range(1, 5): 355 | logger.info('第[%s/%s]次尝试提交订单', count, 5) 356 | cancelSelectCartItem(session) 357 | cart = cart_detail(session, logger) 358 | if sku_id not in cart: 359 | addItemToCart(sku_id, session, logger) 360 | cart_detail(session, logger, True) 361 | 362 | risk_control = get_checkout_page_detail(session, logger) 363 | if len(risk_control) > 0: 364 | if submit_order(risk_control, session, logger, payment_pwd): 365 | return True 366 | logger.info('等待%ss', 3) 367 | time.sleep(3) 368 | else: 369 | logger.info('执行结束, 提交订单失败!') 370 | return False 371 | 372 | def main(sendTo, cookies_String, url): 373 | session = requests.session() 374 | session.headers = { 375 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/531.36", 376 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", 377 | "Connection": "keep-alive" 378 | } 379 | 380 | logFileName = 'jdAutoBuyGood.log' 381 | logger = logging.getLogger() 382 | setLogger(logFileName, logger) 383 | 384 | manual_cookies = {} 385 | for item in cookies_String.split(';'): 386 | # 用=号分割. 387 | name, value = item.strip().split('=', 1) 388 | manual_cookies[name] = value 389 | 390 | # print(manual_cookies) 391 | cookiesJar = requests.utils.cookiejar_from_dict(manual_cookies, cookiejar=None, overwrite=True) 392 | session.cookies = cookiesJar 393 | 394 | payment_pwd = '' 395 | flag = 1 396 | while (1): 397 | try: 398 | if flag == 1: 399 | validateCookies(logger, session) 400 | getUsername(logger, session) 401 | checkSession = requests.Session() 402 | checkSession.headers = { 403 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36", 404 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", 405 | "Connection": "keep-alive" 406 | } 407 | logger.info('第' + str(flag) + '次 查询...') 408 | flag += 1 409 | for i in url: 410 | # 商品url 411 | skuId = i.split('skuId=')[1].split('&')[0] 412 | skuidUrl = 'https://item.jd.com/' + skuId + '.html' 413 | response = checkSession.get(i) 414 | if (response.text.find('无货') > 0): 415 | logger.info('[%s]商品无货.', skuId) 416 | else: 417 | if item_removed(skuId): 418 | logger.info('[%s]商品有货啦! 马上下单...', skuId) 419 | if buyGood(skuId, session, logger, payment_pwd): 420 | sendMail(skuidUrl, True, sendTo) 421 | sys.exit(1) 422 | else: 423 | sendMail(skuidUrl, False, sendTo) 424 | sys.exit(1) 425 | else: 426 | logger.info('[%s]商品有货, 但已下架.', skuId) 427 | time.sleep(5) 428 | if flag % 20 == 0: 429 | logger.info('校验是否还在登录...') 430 | validateCookies(logger, session) 431 | except Exception as e: 432 | print(traceback.format_exc()) 433 | time.sleep(10) 434 | 435 | 436 | if __name__ == '__main__': 437 | sendTo = '' 438 | cookies_String = '' 439 | fp = open("./Please fill out this document.txt", 'r', encoding='utf-8') 440 | cont = fp.read() 441 | pattern = re.compile("'(.*)'") 442 | contRe = pattern.findall(cont) 443 | if len(contRe) >= 2: 444 | sendTo = contRe[0] 445 | cookies_String = contRe[1] 446 | print("Please confirm the email address: %s" % sendTo) 447 | if len(cookies_String) == 0: 448 | print("ERROR: Missing cookie.") 449 | 450 | contRe = contRe[2:] 451 | 452 | try: 453 | main(sendTo, cookies_String, contRe) 454 | except Exception: 455 | sendError(sendTo) -------------------------------------------------------------------------------- /Console Version/Please fill out this document.txt: -------------------------------------------------------------------------------- 1 | 请注意所有讯息在单引号内填写 2 | 您接收下单成功讯息的邮箱(可不填): '$$$$$$$@qq.com' 3 | 4 | 您的登录cookie(请按照README.md内指南获取): 5 | '' 6 | 7 | 您需要自动购买的商品URL(请按照README.md内指南获取): 8 | 'https://c0.3.cn/stock?skuId=1835967&area=16_1362_44319_51500&venderId=1000084390&buyNum=1&choseSuitSkuIds=&cat=9192,12190,1517&extraParam={%22originid%22:%221%22}&fqsp=0&pdpin=jd_77a8935fb872d&pduid=526700225&ch=1&callback=jQuery1535213' 9 | 'https://c0.3.cn/stock?skuId=1835968&area=16_1362_44319_51500&venderId=1000084390&buyNum=1&choseSuitSkuIds=&cat=9192,12190,1517&extraParam={%22originid%22:%221%22}&fqsp=0&pdpin=jd_77a8935fb872d&pduid=526700225&ch=1&callback=jQuery366840' 10 | 'https://c0.3.cn/stock?skuId=1336984&area=16_1362_44319_51500&venderId=1000078145&buyNum=1&choseSuitSkuIds=&cat=9192,12190,1517&extraParam={%22originid%22:%221%22}&fqsp=0&pdpin=jd_77a8935fb872d&pduid=526700225&ch=1&callback=jQuery94700' 11 | 'https://c0.3.cn/stock?skuId=65466451629&area=16_1362_44319_51500&venderId=127922&buyNum=1&choseSuitSkuIds=&cat=9855,9858,9924&extraParam={%22originid%22:%221%22}&fqsp=0&pdpin=jd_77a8935fb872d&pduid=526700225&ch=1&callback=jQuery8432226' 12 | 'https://c0.3.cn/stock?skuId=7498169&area=16_1362_44319_51500&venderId=1000128491&buyNum=1&choseSuitSkuIds=&cat=9855,9858,9924&extraParam={%22originid%22:%221%22}&fqsp=0&pdpin=jd_77a8935fb872d&pduid=526700225&ch=1&callback=jQuery7323892' 13 | 'https://c0.3.cn/stock?skuId=7263128&area=16_1362_44319_51500&venderId=1000128491&buyNum=1&choseSuitSkuIds=&cat=9855,9858,9924&extraParam={%22originid%22:%221%22}&fqsp=0&pdpin=jd_77a8935fb872d&pduid=526700225&ch=1&callback=jQuery9621467' 14 | 'https://c0.3.cn/stock?skuId=4061438&area=16_1362_44319_51500&venderId=1000005584&buyNum=1&choseSuitSkuIds=&cat=9192,12190,1517&extraParam={%22originid%22:%221%22}&fqsp=0&pdpin=jd_77a8935fb872d&pduid=526700225&ch=1&callback=jQuery620656' 15 | 'https://c0.3.cn/stock?skuId=65421329578&area=16_1362_44319_51500&venderId=593210&buyNum=1&choseSuitSkuIds=&cat=9192,12190,1517&extraParam={%22originid%22:%221%22}&fqsp=0&pdpin=jd_77a8935fb872d&pduid=526700225&ch=1&callback=jQuery4952468' 16 | 'https://c0.3.cn/stock?skuId=100005678825&area=16_1362_44319_51500&venderId=1000090691&buyNum=1&choseSuitSkuIds=&cat=9855,9858,9924&extraParam={%22originid%22:%221%22}&fqsp=0&pdpin=jd_77a8935fb872d&pduid=526700225&ch=1&callback=jQuery2192795' 17 | 'https://c0.3.cn/stock?skuId=100005294853&area=16_1362_44319_51500&venderId=1000090691&buyNum=1&choseSuitSkuIds=&cat=9855,9858,9924&extraParam={%22originid%22:%221%22}&fqsp=0&pdpin=jd_77a8935fb872d&pduid=526700225&ch=1&callback=jQuery8766944' 18 | 'https://c0.3.cn/stock?skuId=45923412989&area=16_1362_44319_51500&venderId=10066244&buyNum=1&choseSuitSkuIds=&cat=9192,12190,1517&extraParam={%22originid%22:%221%22}&fqsp=0&pdpin=jd_77a8935fb872d&pduid=526700225&ch=1&callback=jQuery8072251' 19 | 'https://c0.3.cn/stock?skuId=62830056100&area=16_1362_44319_51500&venderId=10066244&buyNum=1&choseSuitSkuIds=&cat=9192,12190,1517&extraParam={%22originid%22:%221%22}&fqsp=0&pdpin=jd_77a8935fb872d&pduid=526700225&ch=1&callback=jQuery599501' 20 | 'https://c0.3.cn/stock?skuId=45006657879&area=16_1362_44319_51500&venderId=10066244&buyNum=1&choseSuitSkuIds=&cat=9192,12190,1517&extraParam={%22originid%22:%221%22}&fqsp=0&pdpin=jd_77a8935fb872d&pduid=526700225&ch=1&callback=jQuery6257903' -------------------------------------------------------------------------------- /Console Version/jdAutoBuyGood.log: -------------------------------------------------------------------------------- 1 | 2020-02-06 13:01:10,916 INFO: 登录成功! 2 | 2020-02-06 13:01:11,168 INFO: 登录账号名称: jd_180519iya 3 | 2020-02-06 13:01:11,173 INFO: 第1次 查询... 4 | 2020-02-06 13:01:11,389 INFO: [1835967]商品无货. 5 | 2020-02-06 13:01:11,583 INFO: [1835968]商品无货. 6 | 2020-02-07 18:13:10,251 INFO: 登录成功! 7 | 2020-02-07 18:13:10,463 INFO: 登录账号名称: jd_180519iya 8 | 2020-02-07 18:13:10,467 INFO: 第1次 查询... 9 | 2020-02-07 18:13:10,699 INFO: [1835967]商品无货. 10 | 2020-02-07 18:13:10,947 INFO: [1835968]商品无货. 11 | -------------------------------------------------------------------------------- /Console Version/requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.22.0 2 | beautifulsoup4==4.8.2 3 | -------------------------------------------------------------------------------- /Console Version/test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ''' 4 | 【简介】 5 | PyQt5中QButton例子 6 | ''' 7 | 8 | import sys 9 | from PyQt5.QtCore import * 10 | from PyQt5.QtGui import * 11 | from PyQt5.QtWidgets import * 12 | 13 | class Form(QDialog): 14 | def __init__(self, parent=None): 15 | super(Form, self).__init__(parent) 16 | layout = QVBoxLayout() #设置垂直布局 17 | 18 | self.btn1 = QPushButton("Button1") 19 | # setChenkable():设置按钮是否已经被选中,true表示按钮将被保持已点击和释放状态 20 | self.btn1.setCheckable(True) 21 | # toggle():在按钮之间进行切换 22 | self.btn1.toggle() 23 | # 通过lambda方式来传递额外的参数btn1,将clicked信号发送给槽函数whichbtn() 24 | self.btn1.clicked.connect(lambda:self.whichbtn(self.btn1) ) 25 | self.btn1.clicked.connect(self.btnstate) 26 | layout.addWidget(self.btn1) 27 | 28 | self.btn2 = QPushButton('image') 29 | self.btn2.setIcon(QIcon(QPixmap("./images/python.png"))) 30 | self.btn2.clicked.connect(lambda:self.whichbtn(self.btn2) ) 31 | layout.addWidget(self.btn2) 32 | self.setLayout(layout) 33 | 34 | self.btn3 = QPushButton("Disabled") 35 | # setEnabled():设置按钮是否可用,false时按钮变成不可用状态,点击它不会发送信号 36 | self.btn3.setEnabled(False) 37 | layout.addWidget(self.btn3) 38 | 39 | self.btn4= QPushButton("&Download") 40 | self.btn4.setDefault(True) #设置按钮的默认状态 41 | self.btn4.clicked.connect(lambda:self.whichbtn(self.btn4)) 42 | layout.addWidget(self.btn4) 43 | self.setWindowTitle("Button demo") 44 | 45 | def btnstate(self): 46 | if self.btn1.isChecked(): #返回按钮的状态 47 | print("button pressed" ) 48 | 49 | else: 50 | print("button released" ) 51 | 52 | def whichbtn(self,btn): 53 | print("clicked button is " + btn.text() ) 54 | 55 | if __name__ == '__main__': 56 | app = QApplication(sys.argv) 57 | btnDemo = Form() 58 | btnDemo.show() 59 | sys.exit(app.exec_()) -------------------------------------------------------------------------------- /GUI Version/autobuy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'qsswindow.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.14.1 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | from PyQt5 import QtCore, QtGui, QtWidgets 10 | from PyQt5.QtWidgets import QApplication 11 | from PyQt5.QtCore import QTimer 12 | 13 | import sys 14 | 15 | import argparse 16 | import os 17 | import pickle 18 | import random 19 | import time 20 | import json 21 | import requests 22 | import re 23 | import logging 24 | import logging.handlers 25 | from bs4 import BeautifulSoup 26 | import smtplib 27 | from email.mime.text import MIMEText 28 | from email.header import Header 29 | 30 | class Ui_QSSWindow(object): 31 | def setupUi(self, QSSWindow): 32 | QSSWindow.setObjectName("QSSWindow") 33 | QSSWindow.resize(950, 450) 34 | self.centralwidget = QtWidgets.QWidget(QSSWindow) 35 | self.centralwidget.setObjectName("centralwidget") 36 | self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.centralwidget) 37 | self.verticalLayout_3.setObjectName("verticalLayout_3") 38 | self.tabWidget = QtWidgets.QTabWidget(self.centralwidget) 39 | self.tabWidget.setObjectName("tabWidget") 40 | self.tab = QtWidgets.QWidget() 41 | self.tab.setObjectName("tab") 42 | 43 | self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.tab) 44 | self.verticalLayout_2.setObjectName("verticalLayout_2") 45 | self.groupBox = QtWidgets.QGroupBox(self.tab) 46 | self.groupBox.setObjectName("groupBox") 47 | self.verticalLayout = QtWidgets.QVBoxLayout(self.groupBox) 48 | self.verticalLayout.setObjectName("verticalLayout") 49 | self.goods = QtWidgets.QHBoxLayout() 50 | self.goods.setObjectName("goods") 51 | self.labelGoods = QtWidgets.QLabel(self.groupBox) 52 | self.labelGoods.setObjectName("labelGoods") 53 | self.goods.addWidget(self.labelGoods) 54 | self.inputGoods = QtWidgets.QLineEdit(self.groupBox) 55 | self.inputGoods.setObjectName("inputGoods") 56 | self.goods.addWidget(self.inputGoods) 57 | self.verticalLayout.addLayout(self.goods) 58 | self.area = QtWidgets.QHBoxLayout() 59 | self.area.setObjectName("area") 60 | self.labelArea = QtWidgets.QLabel(self.groupBox) 61 | self.labelArea.setObjectName("labelArea") 62 | self.area.addWidget(self.labelArea) 63 | self.inputArea = QtWidgets.QLineEdit(self.groupBox) 64 | self.inputArea.setObjectName("inputArea") 65 | self.area.addWidget(self.inputArea) 66 | 67 | self.labelMail = QtWidgets.QLabel(self.groupBox) 68 | self.labelMail.setObjectName("labelMail") 69 | self.area.addWidget(self.labelMail) 70 | self.inputMail = QtWidgets.QLineEdit(self.groupBox) 71 | self.inputMail.setObjectName("inputMail") 72 | self.area.addWidget(self.inputMail) 73 | 74 | self.labelNum = QtWidgets.QLabel(self.groupBox) 75 | self.labelNum.setObjectName("labelNum") 76 | self.area.addWidget(self.labelNum) 77 | self.comboBox = QtWidgets.QComboBox(self.groupBox) 78 | self.comboBox.setObjectName("comboBox") 79 | self.area.addWidget(self.comboBox) 80 | 81 | self.checkBox = QtWidgets.QCheckBox(self.groupBox) 82 | self.checkBox.setObjectName("checkBox") 83 | self.area.addWidget(self.checkBox) 84 | 85 | self.verticalLayout.addLayout(self.area) 86 | self.speedLayout = QtWidgets.QHBoxLayout() 87 | self.speedLayout.setObjectName("speedLayout") 88 | self.horizontalSlider = QtWidgets.QSlider(self.groupBox) 89 | self.horizontalSlider.setMaximum(100) 90 | self.horizontalSlider.setProperty("value", 50) 91 | self.horizontalSlider.setOrientation(QtCore.Qt.Horizontal) 92 | self.horizontalSlider.setTickInterval(0) 93 | self.horizontalSlider.setObjectName("horizontalSlider") 94 | self.speedLayout.addWidget(self.horizontalSlider) 95 | self.progressBar = QtWidgets.QProgressBar(self.groupBox) 96 | self.progressBar.setProperty("value", 24) 97 | self.progressBar.setObjectName("progressBar") 98 | self.speedLayout.addWidget(self.progressBar) 99 | 100 | self.verticalLayout.addLayout(self.speedLayout) 101 | self.textEdit = QtWidgets.QTextEdit(self.groupBox) 102 | self.textEdit.setObjectName("textEdit") 103 | self.verticalLayout.addWidget(self.textEdit) 104 | self.buttonLayout = QtWidgets.QHBoxLayout() 105 | self.buttonLayout.setObjectName("buttonLayout") 106 | self.loginBtn = QtWidgets.QPushButton(self.groupBox) 107 | self.loginBtn.setStyleSheet("") 108 | self.loginBtn.setObjectName("loginBtn") 109 | self.buttonLayout.addWidget(self.loginBtn) 110 | self.startBtn = QtWidgets.QPushButton(self.groupBox) 111 | self.startBtn.setStyleSheet("") 112 | self.startBtn.setObjectName("startBtn") 113 | self.buttonLayout.addWidget(self.startBtn) 114 | self.stopBtn = QtWidgets.QPushButton(self.groupBox) 115 | self.stopBtn.setStyleSheet("") 116 | self.stopBtn.setObjectName("stopBtn") 117 | self.buttonLayout.addWidget(self.stopBtn) 118 | self.verticalLayout.addLayout(self.buttonLayout) 119 | self.verticalLayout_2.addWidget(self.groupBox) 120 | self.tabWidget.addTab(self.tab, "") 121 | self.tab_1 = QtWidgets.QWidget() 122 | self.tab_1.setObjectName("tab_1") 123 | 124 | self.labelAboutMe = QtWidgets.QLabel(self.tab_1) 125 | # self.labelAboutMe.setGeometry(QtCore.QRect(50, 30, 280, 180)) 126 | self.labelAboutMe.setObjectName("labelAboutMe") 127 | 128 | self.tabWidget.addTab(self.tab_1, "") 129 | self.verticalLayout_3.addWidget(self.tabWidget) 130 | QSSWindow.setCentralWidget(self.centralwidget) 131 | self.statusbar = QtWidgets.QStatusBar(QSSWindow) 132 | self.statusbar.setObjectName("statusbar") 133 | QSSWindow.setStatusBar(self.statusbar) 134 | self.menubar = QtWidgets.QMenuBar(QSSWindow) 135 | self.menubar.setGeometry(QtCore.QRect(0, 0, 680, 25)) 136 | self.menubar.setObjectName("menubar") 137 | self.file = QtWidgets.QMenu(self.menubar) 138 | self.file.setObjectName("file") 139 | self.edit = QtWidgets.QMenu(self.menubar) 140 | self.edit.setObjectName("edit") 141 | QSSWindow.setMenuBar(self.menubar) 142 | self.file.addSeparator() 143 | self.menubar.addAction(self.file.menuAction()) 144 | self.menubar.addAction(self.edit.menuAction()) 145 | 146 | self.retranslateUi(QSSWindow) 147 | self.tabWidget.setCurrentIndex(5) 148 | QtCore.QMetaObject.connectSlotsByName(QSSWindow) 149 | 150 | 151 | 152 | 153 | 154 | def retranslateUi(self, QSSWindow): 155 | _translate = QtCore.QCoreApplication.translate 156 | QSSWindow.setWindowTitle(_translate("QSSWindow", "MainWindow")) 157 | self.tabWidget.setToolTip(_translate("QSSWindow", "


")) 158 | self.tabWidget.setWhatsThis(_translate("QSSWindow", "


")) 159 | self.groupBox.setTitle(_translate("QSSWindow", "一定能尽快战胜疫情, 中国加油!")) 160 | self.labelGoods.setText(_translate("QSSWindow", "请输入商品ID(以逗号间隔):")) 161 | self.labelArea.setText(_translate("QSSWindow", "请输入收件地区编码:")) 162 | self.labelMail.setText(_translate("QSSWindow", "接受讯息邮箱:")) 163 | self.labelNum.setText(_translate("QSSWindow", "购买数量:")) 164 | self.loginBtn.setText(_translate("QSSWindow", "扫码登录")) 165 | self.startBtn.setText(_translate("QSSWindow", "开始监控(可自动登录)")) 166 | self.stopBtn.setText(_translate("QSSWindow", "停止监控")) 167 | self.checkBox.setText(_translate("QSSWindow", "是否自动忽略下架商品")) 168 | 169 | 170 | self.labelAboutMe.setText(_translate("QSSWindow", '

使用指南

请点击这里跳转

战疫情, 加油!

欢迎在GitHub上加星. 谢谢!

Tips: 登录一次之后本地会保存登录信息, 重启软件之后仍然可以记住账号登录信息

只需点击"开始监控"就可以自动登录, 不必重复扫码哦

')) 171 | self.labelAboutMe.setOpenExternalLinks(True) 172 | 173 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), _translate("QSSWindow", "Console")) 174 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_1), _translate("QSSWindow", "指南")) 175 | 176 | self.file.setTitle(_translate("QSSWindow", "文件")) 177 | self.edit.setTitle(_translate("QSSWindow", "编辑")) 178 | 179 | 180 | 181 | class Autobuy(QtWidgets.QMainWindow, Ui_QSSWindow): 182 | def __init__(self): 183 | super().__init__() 184 | self.sess = requests.Session() 185 | self.sess.headers = { 186 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36", 187 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", 188 | "Referer": "https://order.jd.com/center/list.action", 189 | "Connection": "keep-alive" 190 | } 191 | 192 | self.headers = { 193 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/531.36", 194 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", 195 | "Referer": "https://cart.jd.com/cart.action", 196 | "Connection": "keep-alive" 197 | } 198 | 199 | self.speed = 5000 200 | self.isMonitorSoldout = True 201 | # self.isLogin = False 202 | self.cookiesString = '' 203 | self.cookies = {} 204 | self.skuidString = '' 205 | self.skuid = [] 206 | self.cont = 1 207 | self.timer = QTimer(self) 208 | self.logger = logging.getLogger() 209 | self.loadQSS() 210 | self.setupUi(self) 211 | self.connectSign() 212 | self.initData() 213 | self.show() 214 | 215 | 216 | def loadQSS(self): 217 | file = 'window.qss' 218 | with open(file, 'rt', encoding='utf8') as f: 219 | styleSheet = f.read() 220 | self.setStyleSheet(styleSheet) 221 | f.close() 222 | 223 | def setLogger(self, logFileName='jdAutoBuyGood.log'): 224 | self.logger.setLevel(logging.INFO) 225 | formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s') 226 | 227 | console_handler = logging.StreamHandler() 228 | console_handler.setFormatter(formatter) 229 | self.logger.addHandler(console_handler) 230 | 231 | file_handler = logging.handlers.RotatingFileHandler( 232 | logFileName, maxBytes=10485760, backupCount=5, encoding="utf-8") 233 | file_handler.setFormatter(formatter) 234 | self.logger.addHandler(file_handler) 235 | 236 | def initData(self): 237 | self.inputGoods.setText("65466451629,65437208345,7498169,7498165,1835968,7263128,7498167,17449572304,37934196731,100001086804,56657322838,56657322841,100005294853,1938795,15595191653,15595191654,45923412989,1835967,1336984,65466451629,7498169,7263128,4061438,65421329578,100005678825,100005294853,45923412989,62830056100,45006657879") 238 | self.inputArea.setText("16_1362_44319_51500") 239 | self.inputMail.setText("$$$$$$$@qq.com") 240 | self.comboBox.addItems(['1', '2', '3']) 241 | self.horizontalSlider.setValue(50) 242 | self.horizontalSlider.setMaximum(100) 243 | self.horizontalSlider.setMinimum(0) 244 | self.setProgressBar() 245 | 246 | self.setLogger() 247 | 248 | if len(self.cookiesString) != 0: 249 | manual_cookies = {} 250 | for item in self.cookiesString.split(';'): 251 | # 用=号分割. 252 | name, value = item.strip().split('=', 1) 253 | manual_cookies[name] = value 254 | self.cookies = requests.utils.cookiejar_from_dict(manual_cookies, cookiejar=None, overwrite=True) 255 | self.sess.cookies = self.cookies 256 | 257 | def connectSign(self): 258 | self.horizontalSlider.valueChanged.connect(self.setProgressBar) 259 | self.horizontalSlider.valueChanged[int].connect(self.slvaluechange) 260 | 261 | self.loginBtn.clicked.connect(self.loginByQR) 262 | self.startBtn.clicked.connect(self.monitorConnect) 263 | self.stopBtn.clicked.connect(self.stopConnect) 264 | 265 | def stopConnect(self): 266 | self.updateStateText("已停止监控.") 267 | self.timer.stop() 268 | 269 | def stopNow(self): 270 | self.updateStateText("已停止监控.") 271 | self.timer.stop() 272 | 273 | def slvaluechange(self, val): 274 | # self.updateStateText("设置抢购速度: %d" % val) 275 | if val > 85: 276 | self.speed = 500 277 | elif val > 60: 278 | self.speed = 9000 - val * 100 279 | else: 280 | self.speed = 10000 - val * 100 281 | 282 | def setProgressBar(self): 283 | self.progressBar.setValue(self.horizontalSlider.value()) 284 | 285 | def checkLogin(self): 286 | self.updateStateText('正在验证登录状态...') 287 | for flag in range(1, 3): 288 | try: 289 | targetURL = 'https://order.jd.com/center/list.action' 290 | payload = { 291 | 'rid': str(int(time.time() * 1000)), 292 | } 293 | resp = self.sess.get(url=targetURL, params=payload, allow_redirects=False) 294 | if resp.status_code == requests.codes.OK: 295 | self.updateStateText('登录成功!') 296 | return True 297 | else: 298 | self.updateStateText('第 %s 次再尝试验证cookie...' % flag) 299 | self.updateStateText('正在尝试从历史讯息中恢复...') 300 | with open('cookie', 'rb') as f: 301 | cookies = requests.utils.cookiejar_from_dict(pickle.load(f)) 302 | # print(cookies) 303 | self.sess.cookies = cookies 304 | continue 305 | except Exception as e: 306 | self.updateStateText(str(e)) 307 | self.updateStateText('第 %s 次再尝试验证cookie...' % flag) 308 | continue 309 | self.updateStateText('请登录!') 310 | return False 311 | 312 | def loginByQR(self): 313 | # jd login by QR code 314 | try: 315 | self.updateStateText('请您打开京东手机客户端或微信扫一扫, 准备扫码登录') 316 | 317 | urls = ( 318 | 'https://passport.jd.com/new/login.aspx', 319 | 'https://qr.m.jd.com/show', 320 | 'https://qr.m.jd.com/check', 321 | 'https://passport.jd.com/uc/qrCodeTicketValidation' 322 | ) 323 | # step 1: open login page 324 | response = self.sess.get( 325 | urls[0], 326 | headers=self.headers 327 | ) 328 | if response.status_code != requests.codes.OK: 329 | self.updateStateText(f'获取登录页失败: {response.status_code}') 330 | # self.isLogin = False 331 | return False 332 | # update cookies 333 | self.cookies.update(response.cookies) 334 | self.sess.cookies = response.cookies 335 | 336 | # step 2: get QR image 337 | response = self.sess.get( 338 | urls[1], 339 | headers=self.headers, 340 | cookies=self.cookies, 341 | params={ 342 | 'appid': 133, 343 | 'size': 147, 344 | 't': int(time.time() * 1000), 345 | } 346 | ) 347 | if response.status_code != requests.codes.OK: 348 | self.updateStateText(f'获取二维码失败: {response.status_code}') 349 | # self.isLogin = False 350 | return False 351 | 352 | # update cookies 353 | self.cookies.update(response.cookies) 354 | self.sess.cookies = response.cookies 355 | 356 | # save QR code 357 | image_file = 'qr.png' 358 | with open(image_file, 'wb') as f: 359 | for chunk in response.iter_content(chunk_size=1024): 360 | f.write(chunk) 361 | 362 | # scan QR code with phone 363 | if os.name == "nt": 364 | # for windows 365 | os.system('start ' + image_file) 366 | else: 367 | if os.uname()[0] == "Linux": 368 | # for linux platform 369 | os.system("eog " + image_file) 370 | else: 371 | # for Mac platform 372 | os.system("open " + image_file) 373 | 374 | # step 3: check scan result 375 | self.headers['Host'] = 'qr.m.jd.com' 376 | self.headers['Referer'] = 'https://passport.jd.com/new/login.aspx' 377 | 378 | # check if QR code scanned 379 | qr_ticket = None 380 | retry_times = 1000 # 尝试1000次 381 | while retry_times: 382 | retry_times -= 1 383 | response = self.sess.get( 384 | urls[2], 385 | headers=self.headers, 386 | cookies=self.cookies, 387 | params={ 388 | 'callback': 'jQuery%d' % random.randint(1000000, 9999999), 389 | 'appid': 133, 390 | 'token': self.cookies['wlfstk_smdl'], 391 | '_': int(time.time() * 1000) 392 | } 393 | ) 394 | if response.status_code != requests.codes.OK: 395 | continue 396 | rs = json.loads(re.search(r'{.*?}', response.text, re.S).group()) 397 | if rs['code'] == 200: 398 | self.updateStateText(f"{rs['ticket']}(Response Code: {rs['code']})") 399 | qr_ticket = rs['ticket'] 400 | break 401 | else: 402 | self.updateStateText(f"{rs['msg']}(Response Code: {rs['code']})") 403 | time.sleep(2) 404 | 405 | if not qr_ticket: 406 | self.updateStateText("ERROR: 二维码登录失败.") 407 | # self.isLogin = False 408 | return False 409 | 410 | # step 4: validate scan result 411 | self.headers['Host'] = 'passport.jd.com' 412 | self.headers['Referer'] = 'https://passport.jd.com/new/login.aspx' 413 | response = requests.get( 414 | urls[3], 415 | headers=self.headers, 416 | cookies=self.cookies, 417 | params={'t': qr_ticket}, 418 | ) 419 | if response.status_code != requests.codes.OK: 420 | self.updateStateText(f"二维码登录校验失败: {response.status_code}") 421 | # self.isLogin = False 422 | return False 423 | 424 | # 京东有时会认为当前登录有危险, 需要手动验证 425 | # url: https://safe.jd.com/dangerousVerify/index.action?username=... 426 | res = json.loads(response.text) 427 | if not response.headers.get('p3p'): 428 | if 'url' in res: 429 | self.updateStateText(f"请进行手动安全验证: {res['url']}") 430 | # self.isLogin = False 431 | return False 432 | else: 433 | self.updateStateText('登录失败, ERROR message: ' + res) 434 | # self.isLogin = False 435 | return False 436 | 437 | # login succeed 438 | self.headers['P3P'] = response.headers.get('P3P') 439 | self.cookies.update(response.cookies) 440 | self.sess.cookies = response.cookies 441 | 442 | 443 | # save cookie 444 | with open('cookie', 'wb') as f: 445 | pickle.dump(self.cookies, f) 446 | 447 | self.updateStateText("登录成功!") 448 | # self.isLogin = True 449 | self.getUsername() 450 | return True 451 | 452 | except Exception as e: 453 | # self.isLogin = False 454 | self.updateStateText('ERROR message: ' + str(e)) 455 | raise 456 | 457 | def getUsername(self): 458 | userName_Url = 'https://passport.jd.com/new/helloService.ashx?callback=jQuery339448&_=' + str( 459 | int(time.time() * 1000)) 460 | 461 | resp = self.sess.get(url=userName_Url, allow_redirects=True) 462 | resultText = resp.text 463 | resultText = resultText.replace('jQuery339448(', '') 464 | resultText = resultText.replace(')', '') 465 | usernameJson = json.loads(resultText) 466 | self.updateStateText('账号名称: ' + usernameJson['nick']) 467 | 468 | def checkStock(self): 469 | url = 'https://c0.3.cn/stocks' 470 | 471 | callback = 'jQuery' + str(random.randint(1000000, 9999999)) 472 | 473 | headers = { 474 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/531.36", 475 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", 476 | "Referer": "https://cart.jd.com/cart.action", 477 | "Connection": "keep-alive", 478 | } 479 | 480 | payload = { 481 | 'type': 'getstocks', 482 | 'skuIds': self.skuidString, 483 | 'area': self.areaID, 484 | 'callback': callback, 485 | '_': int(time.time() * 1000), 486 | } 487 | resp = self.sess.get(url=url, params=payload, headers=headers) 488 | respText = resp.text.replace(callback + '(', '').replace(')', '') 489 | respJson = json.loads(respText) 490 | inStockSkuid = [] 491 | nohasSkuid = [] 492 | abnormalSkuid = [] 493 | 494 | soldOut = 0 495 | alloca = 0 496 | for i in self.skuid: 497 | try: 498 | if respJson[i]['StockStateName'] == '无货': 499 | nohasSkuid.append(i) 500 | soldOut += 1 501 | elif respJson[i]['StockStateName'] == '可配货': 502 | nohasSkuid.append(i) 503 | alloca += 1 504 | else: 505 | inStockSkuid.append(i) 506 | 507 | except Exception as e: 508 | abnormalSkuid.append(i) 509 | 510 | if soldOut != 0: 511 | self.updateStateText('监控的 %d 个商品无货.' % soldOut) 512 | if alloca != 0: 513 | self.updateStateText('监控的 %d 个商品所在地区暂无货, 未来可能配货.' % alloca) 514 | if len(abnormalSkuid) > 0: 515 | self.updateStateText('WARNING: %s 编号商品查询异常.' % ','.join(abnormalSkuid)) 516 | return inStockSkuid 517 | 518 | def isSoldOut(self, sku_id): 519 | headers = { 520 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/531.36", 521 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", 522 | "Referer": "http://trade.jd.com/shopping/order/getOrderInfo.action", 523 | "Connection": "keep-alive", 524 | 'Host': 'item.jd.com', 525 | } 526 | url = 'https://item.jd.com/{}.html'.format(sku_id) 527 | page = requests.get(url=url, headers=headers) 528 | return '该商品已下柜' not in page.text 529 | 530 | def selectAll(self): 531 | url = "https://cart.jd.com/selectAllItem.action" 532 | data = { 533 | 't': 0, 534 | 'outSkus': '', 535 | 'random': random.random() 536 | } 537 | resp = self.sess.post(url, data=data) 538 | if resp.status_code != requests.codes.OK: 539 | self.updateStateText('全选购物车商品出错! status_code: %u, URL: %s' % (resp.status_code, resp.url)) 540 | return False 541 | self.updateStateText('全选购物车商品成功.') 542 | return True 543 | 544 | def cart_detail(self, isOutput=False): 545 | url = 'https://cart.jd.com/cart.action' 546 | resp = self.sess.get(url, headers=self.headers) 547 | soup = BeautifulSoup(resp.text, "html.parser") 548 | 549 | cartDetail = dict() 550 | for item in soup.find_all(class_='item-item'): 551 | sku_id = item['skuid'] # 商品id 552 | try: 553 | # 例如: ['increment', '8888', '100001071956', '1', '13', '0', '50067652554'] 554 | # ['increment', '8888', '100002404322', '2', '1', '0'] 555 | item_attr_list = item.find(class_='increment')['id'].split('_') 556 | p_type = item_attr_list[4] 557 | promo_id = target_id = item_attr_list[-1] if len(item_attr_list) == 7 else 0 558 | 559 | def get_tag_value(tag, key='', index=0): 560 | if key: 561 | value = tag[index].get(key) 562 | else: 563 | value = tag[index].text 564 | return value.strip(' \t\r\n') 565 | 566 | cartDetail[sku_id] = { 567 | 'name': get_tag_value(item.select('div.p-name a')), # 商品名称 568 | 'verder_id': item['venderid'], # 商家id 569 | 'count': int(item['num']), # 数量 570 | 'unit_price': get_tag_value(item.select('div.p-price strong'))[1:], # 单价 571 | 'total_price': get_tag_value(item.select('div.p-sum strong'))[1:], # 总价 572 | 'is_selected': 'item-selected' in item['class'], # 商品是否被勾选 573 | 'p_type': p_type, 574 | 'target_id': target_id, 575 | 'promo_id': promo_id 576 | } 577 | except Exception as e: 578 | self.updateStateText("ERROR: 商品%s在购物车中的信息无法解析, 报错信息: %s, 该商品自动忽略.", sku_id, e) 579 | 580 | if isOutput == True: 581 | self.updateStateText('当前购物车信息: %s.' % cartDetail) 582 | return cartDetail 583 | 584 | def addItemToCart(self, sku_id): 585 | url = 'https://cart.jd.com/gate.action' 586 | addNum = self.comboBox.currentIndex() + 1 587 | payload = { 588 | 'pid': sku_id, 589 | 'pcount': addNum, 590 | 'ptype': 1 591 | } 592 | resp = self.sess.get(url=url, params=payload) 593 | if 'https://cart.jd.com/cart.action' in resp.url: # 套装商品加入购物车后直接跳转到购物车页面 594 | result = True 595 | else: # 普通商品成功加入购物车后会跳转到提示 "商品已成功加入购物车!" 页面 596 | soup = BeautifulSoup(resp.text, "html.parser") 597 | result = bool(soup.select('h3.ftx-02')) # [

商品已成功加入购物车!

] 598 | 599 | if result: 600 | self.updateStateText('%s 编号商品已成功加入购物车, 数量: %d.' % (sku_id, addNum)) 601 | else: 602 | self.updateStateText('ERROR: %s 编号商品添加到购物车失败.' % sku_id) 603 | 604 | def responseStatus(self, resp): 605 | if resp.status_code != requests.codes.OK: 606 | self.updateStateText('Status: %u, Url: %s' % (resp.status_code, resp.url)) 607 | return False 608 | return True 609 | 610 | def getCheckoutPageDetail(self): 611 | url = 'http://trade.jd.com/shopping/order/getOrderInfo.action' 612 | # url = 'https://cart.jd.com/gotoOrder.action' 613 | payload = { 614 | 'rid': str(int(time.time() * 1000)), 615 | } 616 | headers = { 617 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36", 618 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", 619 | "Referer": "https://cart.jd.com/cart.action", 620 | "Connection": "keep-alive", 621 | 'Host': 'trade.jd.com', 622 | } 623 | try: 624 | resp = self.sess.get(url=url, params=payload, headers=headers) 625 | 626 | if not self.responseStatus(resp): 627 | self.updateStateText('ERROR: 获取订单结算页信息失败.') 628 | return None 629 | soup = BeautifulSoup(resp.text, "html.parser") 630 | 631 | order_detail = { 632 | 'address': soup.find('span', id='sendAddr').text[5:], 633 | 'receiver': soup.find('span', id='sendMobile').text[4:], 634 | 'total_price': soup.find('span', id='sumPayPriceId').text[1:], 635 | 'items': [] 636 | } 637 | 638 | self.updateStateText("下单信息: %s" % order_detail) 639 | return order_detail 640 | except requests.exceptions.RequestException as e: 641 | self.updateStateText('订单结算页面获取异常, ERROR message: %s.' % e) 642 | except Exception as e: 643 | self.updateStateText('下单页面数据解析异常, ERROR message: %s.' % e) 644 | return None 645 | 646 | def submit_order(self, risk_control, payment_pwd): 647 | url = 'https://trade.jd.com/shopping/order/submitOrder.action' 648 | # js function of submit order is included in https://trade.jd.com/shopping/misc/js/order.js?r=2018070403091 649 | 650 | # overseaPurchaseCookies: 651 | # vendorRemarks: [] 652 | # submitOrderParam.sopNotPutInvoice: false 653 | # submitOrderParam.trackID: TestTrackId 654 | # submitOrderParam.ignorePriceChange: 0 655 | # submitOrderParam.btSupport: 0 656 | # riskControl: 657 | # submitOrderParam.isBestCoupon: 1 658 | # submitOrderParam.jxj: 1 659 | # submitOrderParam.trackId: 660 | 661 | data = { 662 | 'overseaPurchaseCookies': '', 663 | 'vendorRemarks': '[]', 664 | 'submitOrderParam.sopNotPutInvoice': 'false', 665 | 'submitOrderParam.trackID': 'TestTrackId', 666 | 'submitOrderParam.ignorePriceChange': '0', 667 | 'submitOrderParam.btSupport': '0', 668 | 'riskControl': risk_control, 669 | 'submitOrderParam.isBestCoupon': 1, 670 | 'submitOrderParam.jxj': 1, 671 | 'submitOrderParam.trackId': '9643cbd55bbbe103eef18a213e069eb0', # Todo: need to get trackId 672 | # 'submitOrderParam.eid': eid, 673 | # 'submitOrderParam.fp': fp, 674 | 'submitOrderParam.needCheck': 1, 675 | } 676 | 677 | def encrypt_payment_pwd(paymentPwd): 678 | return ''.join(['u3' + x for x in paymentPwd]) 679 | 680 | if len(payment_pwd) > 0: 681 | data['submitOrderParam.payPassword'] = encrypt_payment_pwd(payment_pwd) 682 | 683 | headers = { 684 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36", 685 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", 686 | "Referer": "http://trade.jd.com/shopping/order/getOrderInfo.action", 687 | "Connection": "keep-alive", 688 | 'Host': 'trade.jd.com', 689 | } 690 | 691 | try: 692 | resp = self.sess.post(url=url, data=data, headers=headers) 693 | resp_json = json.loads(resp.text) 694 | 695 | if resp_json.get('success'): 696 | self.updateStateText('订单提交成功! 订单号: %s' % resp_json.get('orderId')) 697 | return True 698 | else: 699 | message, result_code = resp_json.get('message'), resp_json.get('resultCode') 700 | if result_code == 0: 701 | # self._save_invoice() 702 | message = message + '(下单商品可能为第三方商品, 将切换为普通发票进行尝试)' 703 | elif result_code == 60077: 704 | message = message + '(可能是购物车为空或未勾选购物车中商品)' 705 | elif result_code == 60123: 706 | message = message + '(未配置支付密码)' 707 | self.updateStateText('订单提交失败, 错误码: %s, 返回信息: %s.' % (result_code, message)) 708 | self.updateStateText(resp_json) 709 | return False 710 | except Exception as e: 711 | self.updateStateText("ERROR: " + str(e)) 712 | return False 713 | 714 | def cancelSelectCartItem(self): 715 | url = "https://cart.jd.com/cancelAllItem.action" 716 | data = { 717 | 't': 0, 718 | 'outSkus': '', 719 | 'random': random.random() 720 | } 721 | resp = self.sess.post(url, data=data) 722 | if resp.status_code != requests.codes.OK: 723 | self.updateStateText('cancelSelectCartItem() function WARNING: %u, Url: %s' % (resp.status_code, resp.url)) 724 | return False 725 | return True 726 | 727 | def buyGoods(self, sku_id): 728 | for i in range(1, 2): 729 | self.updateStateText('第 %d/%d 次尝试提交订单...' % (i, 2)) 730 | self.cancelSelectCartItem() 731 | cart = self.cart_detail() 732 | if sku_id not in cart: 733 | self.addItemToCart(sku_id) 734 | self.cart_detail(True) 735 | 736 | risk_control = self.getCheckoutPageDetail() 737 | if risk_control != None: 738 | if self.submit_order(risk_control, ''): 739 | return True 740 | time.sleep(1) 741 | else: 742 | self.updateStateText('执行结束,提交订单失败.') 743 | return False 744 | 745 | def monitorConnect(self): 746 | if self.checkLogin() == False: 747 | return False 748 | self.skuidString = self.inputGoods.text() 749 | pattern = re.compile(",|,") 750 | self.skuid = pattern.split(self.skuidString) 751 | 752 | self.areaID = self.inputArea.text() 753 | 754 | self.timer.timeout.connect(self.monitorMain) 755 | self.updateStateText("当前轮询速度为 %f 秒/次." % (self.speed / 1000)) 756 | if self.checkBox.isChecked(): 757 | self.isMonitorSoldout = False 758 | self.updateStateText('当前模式将为您自动忽略并删除下架商品.') 759 | else: 760 | self.isMonitorSoldout = True 761 | self.updateStateText('当前模式将为您保持监控下架商品, 若其上架则立即抢购.') 762 | self.timer.start(self.speed) # 设置计时间隔并启动 763 | return True 764 | 765 | def sendMail(self, url, isOrder): 766 | sendTo = self.inputMail.text() 767 | if len(sendTo) == 0 or sendTo == '$$$$$$$@qq.com': 768 | return 769 | 770 | mailRe = re.compile('^\w{1,15}@\w{1,10}\.(com|cn|net)$') 771 | if not re.search(mailRe, sendTo): 772 | return 773 | 774 | sendFrom = '645064582@qq.com' 775 | 776 | smtp_server = 'smtp.qq.com' 777 | if isOrder: 778 | msg = MIMEText('您抢购的 ' + url + ' 商品已下单, 请在尽快付款.', 'plain', 'utf-8') 779 | else: 780 | msg = MIMEText('您抢购的 ' + url + ' 商品下单失败.', 'plain', 'utf-8') 781 | 782 | msg['From'] = Header(sendFrom) 783 | msg['To'] = Header(sendTo) 784 | msg['Subject'] = Header('京东商品自动购买程序提示讯息') 785 | 786 | server = smtplib.SMTP_SSL(host=smtp_server) 787 | server.connect(smtp_server, 465) 788 | server.login(sendFrom, 'nkrzicfjkzznbehi') 789 | server.sendmail(sendFrom, sendTo, msg.as_string()) 790 | server.quit() 791 | 792 | def removeItem(self): 793 | url = "https://cart.jd.com/batchRemoveSkusFromCart.action" 794 | data = { 795 | 't': 0, 796 | 'null': '', 797 | 'outSkus': '', 798 | 'random': random.random(), 799 | 'locationId': '19-1607-4773-0' 800 | } 801 | headers = { 802 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.37", 803 | "Accept": "application/json, text/javascript, */*; q=0.01", 804 | "Referer": "https://cart.jd.com/cart.action", 805 | "Host": "cart.jd.com", 806 | "Content-Type": "application/x-www-form-urlencoded", 807 | "Accept-Encoding": "gzip, deflate, br", 808 | "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7", 809 | "Origin": "https://cart.jd.com", 810 | "Connection": "keep-alive" 811 | } 812 | 813 | resp = self.sess.post(url, data=data, headers=headers) 814 | if resp.status_code != requests.codes.OK: 815 | self.updateStateText('清空购物车勾选商品出错. status_code: %u, URL: %s.' % (resp.status_code, resp.url)) 816 | return False 817 | 818 | self.updateStateText('清空购物车勾选商品成功!') 819 | return True 820 | 821 | def monitorMain(self): 822 | try: 823 | checkSession = requests.Session() 824 | checkSession.headers = { 825 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/531.36", 826 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", 827 | "Connection": "keep-alive" 828 | } 829 | self.updateStateText('第 ' + str(self.cont) + ' 次查询:') 830 | self.cont += 1 831 | inStockSkuid = self.checkStock() 832 | soldOutNum = 0 833 | for skuId in inStockSkuid: 834 | if self.isSoldOut(skuId): 835 | self.updateStateText('%s 编号商品有货啦! 马上下单.' % skuId) 836 | skuidUrl = 'https://item.jd.com/' + skuId + '.html' 837 | if self.buyGoods(skuId): 838 | self.sendMail(skuidUrl, True) 839 | else: 840 | self.sendMail(skuidUrl, False) 841 | self.stopNow() 842 | 843 | else: 844 | if self.isMonitorSoldout == False: 845 | self.updateStateText('%s 编号商品已下架.' % skuId) 846 | self.skuid.remove(skuId) 847 | idBeg = self.skuidString.find(str(skuId)) 848 | idEnd = idBeg + len(str(skuId)) 849 | self.skuidString = self.skuidString[0:idBeg] + self.skuidString[idEnd + 1:] 850 | self.inputGoods.setText(self.skuidString) 851 | self.updateStateText('已将 %s 编号的下架商品清除, 并更新了商品编号输入框.' % skuId) 852 | self.selectAll() 853 | self.removeItem() 854 | else: 855 | soldOutNum += 1 856 | if soldOutNum != 0: 857 | self.updateStateText('监控的 %d 个商品已下架, 但当前模式保持监控.' % soldOutNum) 858 | 859 | if self.cont % 300 == 0: 860 | self.checkLogin() 861 | except Exception as e: 862 | import traceback 863 | self.updateStateText(traceback.format_exc()) 864 | 865 | def updateStateText(self, stateText): 866 | # print(stateText) 867 | self.logger.info(stateText) 868 | self.textEdit.moveCursor(QtGui.QTextCursor.End) 869 | self.textEdit.insertPlainText(f'{time.ctime()} > ' + stateText + '\n') 870 | 871 | def main(): 872 | app = QApplication(sys.argv) 873 | test = Autobuy() 874 | sys.exit(app.exec_()) 875 | 876 | 877 | 878 | if __name__ == '__main__': 879 | main() -------------------------------------------------------------------------------- /GUI Version/image/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyikaii/auto-buy-Python-tool/5aa039dcefb7efce34f8d8174d4410860fadbf4e/GUI Version/image/down.png -------------------------------------------------------------------------------- /GUI Version/image/left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyikaii/auto-buy-Python-tool/5aa039dcefb7efce34f8d8174d4410860fadbf4e/GUI Version/image/left.png -------------------------------------------------------------------------------- /GUI Version/image/right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyikaii/auto-buy-Python-tool/5aa039dcefb7efce34f8d8174d4410860fadbf4e/GUI Version/image/right.png -------------------------------------------------------------------------------- /GUI Version/window.qss: -------------------------------------------------------------------------------- 1 | /* 2 | Notice: 3 | 01 注释只能用多行注释,单行注释会出错 4 | */ 5 | /*******定义字体,背景颜色*********/ 6 | *{ 7 | font: "微软雅黑"; 8 | color: rgb(220, 220, 220); 9 | background-color: rgb(68, 68, 68); 10 | border-radius: 2px; 11 | padding: 3px; 12 | margin: 2px; 13 | } 14 | /****************定义边框*****************/ 15 | QLineEdit, QPushButton, QTimeEdit, 16 | QSpinBox, QDoubleSpinBox, QComboBox, 17 | QDateEdit, QDateTimeEdit, QGroupBox, 18 | QTextEdit, QTabWidget { 19 | border:1px solid rgb(36, 36, 36); 20 | } 21 | /*************菜单下拉按钮*****************/ 22 | QMenu::item { 23 | background-color: transparent; 24 | } 25 | /********菜单下拉按钮(被选择时)***********/ 26 | QMenu::item:selected { 27 | background-color: rgb(100, 100, 100); 28 | } 29 | /****************定义菜单栏***************/ 30 | QMenuBar { 31 | border-radius: 0; 32 | background-color: rgb(72, 72, 72); 33 | border-bottom: 1px solid rgb(36, 36, 36); 34 | } 35 | /************菜单栏按钮(被选择时)********/ 36 | QMenuBar::item:selected { 37 | background-color: rgb(100, 100, 100); 38 | } 39 | /************菜单栏按钮(鼠标悬停)********/ 40 | QMenu:hover { 41 | background-color: rgb(100, 100, 100); 42 | } 43 | /*********按钮(渐变:突出显示)***********/ 44 | QPushButton { 45 | border-style: inset; 46 | background-color: qlineargradient(spread:pad, x1:0.5, y1:0, x2:0.5, y2:1, stop:0 rgba(72, 72, 72, 255), stop:1 rgba(56, 56, 56, 255)); 47 | border:1px solid rgb(36, 36, 36); 48 | border-radius: 6px; 49 | min-width: 5em; 50 | min-height: 1em; 51 | padding: 6px 6px; 52 | margin: 5px 53 | } 54 | /**************按钮(鼠标悬停)**********/ 55 | QPushButton:hover { 56 | background-color: rgb(91, 91, 91); 57 | } 58 | /**************按钮(按下)*************/ 59 | QPushButton:pressed { 60 | background-color: rgb(68, 68, 68); 61 | } 62 | QSpinBox::up-button, QTimeEdit::up-button, 63 | QDateEdit::up-button, QDateTimeEdit::up-button, 64 | QDoubleSpinBox::up-button { 65 | subcontrol-position: right; 66 | border: 0px; 67 | } 68 | QSpinBox::down-button, QTimeEdit::down-button, 69 | QDateEdit::down-button, QDateTimeEdit::down-button, 70 | QDoubleSpinBox::down-button { 71 | subcontrol-position: left; 72 | border: 0px; 73 | } 74 | QSpinBox::up-arrow, QTimeEdit::up-arrow, 75 | QDateEdit::up-arrow, QDateTimeEdit::up-arrow, 76 | QDoubleSpinBox::up-arrow { 77 | image: url(image/right.png); 78 | } 79 | QSpinBox::down-arrow, QTimeEdit::down-arrow, 80 | QDateEdit::down-arrow, QDateTimeEdit::down-arrow, 81 | QDoubleSpinBox::down-arrow { 82 | image: url(image/left.png); 83 | } 84 | /*********定义Slider槽的形态**************/ 85 | QSlider::groove:horizontal { 86 | height: 6px; 87 | border-radius: 3px; 88 | } 89 | /*********定义Slider柄的形态**************/ 90 | QSlider::handle:horizontal { 91 | background-color: rgb(68, 68, 68); 92 | border-radius: 5px; 93 | border: 2px solid rgb(36, 36, 36); 94 | height: 8px; 95 | width: 8px; 96 | margin: -3px -3px; 97 | } 98 | /*********定义handle右边的背景**************/ 99 | QSlider::add-page:horizontal{ 100 | background: rgb(72, 72, 72); 101 | } 102 | /*********定义handle左边的背景**************/ 103 | QSlider::sub-page:horizontal{ 104 | background: rgb(36, 36, 36); 105 | } 106 | /****************定义进度条****************/ 107 | QProgressBar { 108 | border: 2px solid grey; 109 | border-radius: 5px; 110 | text-align: center; 111 | height: 12px; 112 | } 113 | /*************定义进度条背景**************/ 114 | QProgressBar::chunk { 115 | background-color: rgb(128, 128, 128); 116 | width: 8px; 117 | margin: 0.5px; 118 | } 119 | 120 | QTabWidget::pane { /* The tab widget frame */ 121 | border: 1px solid rgb(36, 36, 36); 122 | } 123 | QTabWidget::tab-bar { 124 | border: 1px solid rgb(36, 36, 36); 125 | } 126 | QTabBar::tab { 127 | background: qlineargradient(spread:pad, x1:0.5, y1:0, x2:0.5, y2:1, stop:0 rgba(100, 100, 100, 255), stop:1 rgba(82, 82, 82, 255)); 128 | border: 1px solid rgb(36, 36, 36); 129 | border-bottom-color: #C2C7CB; /* same as the pane color */ 130 | min-width: 12ex; 131 | min-height: 6ex; 132 | padding: 2px 4px; 133 | } 134 | QTabBar::tab:selected, QTabBar::tab:hover { 135 | border-top: 2px solid red; 136 | padding-top: 3px; 137 | background: qlineargradient(spread:pad, x1:0.5, y1:0, x2:0.5, y2:1, stop:0 rgba(72, 72, 72, 255), stop:1 rgba(56, 56, 56, 255)); 138 | } 139 | /*************QComboBox******************/ 140 | QComboBox::drop-down { 141 | subcontrol-origin: padding; 142 | subcontrol-position: top right; 143 | width: 20px; 144 | border: 0px; 145 | } 146 | QComboBox::down-arrow { 147 | image: url(image/down.png); 148 | } 149 | QComboBox::down-arrow:on { /* shift the arrow when popup is open */ 150 | top: 1px; 151 | left: 1px; 152 | } 153 | /*********下拉列表的item背景***************/ 154 | QComboBox QAbstractItemView { 155 | border: 0px; 156 | selection-background-color: rgb(100, 100, 100); 157 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Yikai Zhang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # auto-buy-Python-tool 2 | 图形界面, 电脑小白也会用, 下载可直接运行! 京东自动购买口罩 实时抢购口罩 工具, 抗击疫情 中国加油! 3 | 4 | `4.0` :fire: 5 | 6 | 点击这里 下载`JDAutoBuy-release.zip`, 解压后可**直接运行!** 7 | 8 | ### 欢迎加星 9 | 10 | > 修复了商品下架后的问题, 更新了交互界面; 修复了可配货商品的判断, 更新了数量调整接口, 更新了是否监控下架商品选项 11 | 12 | :star2: 13 | 14 | 15 | 16 | 17 | 18 | ## 使用指南 19 | 20 | ### :notebook_with_decorative_cover: Tips: 登录一次之后本地会保存登录信息, 重启软件(注意重启之后也行)之后仍然可以记住账号登录信息, 重启之后只需点击"开始监控"就可以登录! 不必重复扫码! 21 | 22 | 运行界面**如下图**: 23 | 24 | ![interface](./assets/1581218076866.png) 25 | 26 | Update at 2020-3-2: Continuously monitor goods removed from JD.![monitorSoldOutGoods](./assets/1583151909197.png) 27 | 28 | Update at 2020-2-15: quantity can be modified![quantity](./assets/1581768213048.png) 29 | 30 | ### 填写方式: 31 | 32 | Tips: 软件启动时带有标准填写格式的默认值, 请留意. 33 | 34 | + **输入商品ID:** 比如`URL`为: https://item.jd.com/1835967.html 的商品ID为**1835967**. 35 | + **输入收件地区编码**: 使用Chrome浏览器(如果是其他浏览器请用同样方式打开**开发者工具**)登录京东并访问商品页, 选择派送地址后按`F12`查找`stock?`开头的`URL`讯息, 如下图: ![AreaID](./assets/1581218537205.png) 36 | + **接受讯息邮箱**: 您的接受讯息邮箱. 37 | + **滑动条**: 控制监控时查询的速度(频率). 38 | + **购买数量**: 调整一次购买数量. 39 | + **是否自动忽略下架商品**: 未打勾 - 下架商品上架时立即抢购, 打勾 - 自动忽略下架商品并删除. 40 | 41 | 42 | 43 | 以下选项您可以不看也会使用: 44 | 45 | **扫码登录**: 如果京东手机客户端无法扫码, 请用**微信**扫一扫试一下. 46 | 47 | **开始监控**: 如果**未登录点击此按钮**, 您之前登录成功过则会读取历史登录记录尝试登录, 否则会提示您先扫码登录. 48 | 49 | 以下是有货时购买的界面, 祝您成功. 50 | 51 | ![success](./assets/1581508416184.png) 52 | 53 | 更新后`2.0`运行界面: 54 | 55 | ![updateRun](./assets/1581508444771.png) 56 | 57 | Update at 2020-2-15 20:06:14: 最近比较难抢了, 因为京东一些商品是预约完再抢购, 预约完之后即被放入购物车, 导致监控有货无货较困难. 见谅. 58 | 59 | --- 60 | 61 | ## 以下是控制台版本程序的说明, 如果您使用图形界面版本, 可以不看 62 | 63 | **获取登录*cookie***: Chrome 登录京东后`F12`, 如下图. 64 | 65 | ![1580963259089](./assets/1580963259089.png) 66 | 67 | 68 | 69 | **获取商品URL**: 登录并访问商品页, 选择派送地址后查找**stock?**开头的URL讯息, 如下图. 70 | 71 | ![1580963623908](./assets/1580963623908.png) 72 | 73 | 请注意商品`URL`需要是您的地址编号, 如TXT文件中`area=`参数. 74 | 75 | 76 | 77 | **以上两步**获得的讯息填入`Please fill out this document.txt`文件. 78 | 79 | ```shell 80 | pip install -r requirements.txt 81 | python JDAutoBuyTool.py 82 | ``` 83 | 84 | 85 | 86 | **运行环境**: 87 | 88 | `Windows 10, Python 3` 89 | 90 | **运行示例**: 91 | 92 | ![1581038258274](./assets/1581038258274.png) 93 | 94 | --- 95 | 96 | 感谢[cycz](https://github.com/cycz)大佬, 参考了部分代码. 97 | 98 | 心情复杂, 也不懂抢口罩工具能不能帮上忙, **一定能尽快战胜疫情! 中国加油!** :star2: -------------------------------------------------------------------------------- /assets/1580963259089.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyikaii/auto-buy-Python-tool/5aa039dcefb7efce34f8d8174d4410860fadbf4e/assets/1580963259089.png -------------------------------------------------------------------------------- /assets/1580963623908.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyikaii/auto-buy-Python-tool/5aa039dcefb7efce34f8d8174d4410860fadbf4e/assets/1580963623908.png -------------------------------------------------------------------------------- /assets/1581038258274.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyikaii/auto-buy-Python-tool/5aa039dcefb7efce34f8d8174d4410860fadbf4e/assets/1581038258274.png -------------------------------------------------------------------------------- /assets/1581218076866.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyikaii/auto-buy-Python-tool/5aa039dcefb7efce34f8d8174d4410860fadbf4e/assets/1581218076866.png -------------------------------------------------------------------------------- /assets/1581218537205.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyikaii/auto-buy-Python-tool/5aa039dcefb7efce34f8d8174d4410860fadbf4e/assets/1581218537205.png -------------------------------------------------------------------------------- /assets/1581249279630.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyikaii/auto-buy-Python-tool/5aa039dcefb7efce34f8d8174d4410860fadbf4e/assets/1581249279630.png -------------------------------------------------------------------------------- /assets/1581508416184.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyikaii/auto-buy-Python-tool/5aa039dcefb7efce34f8d8174d4410860fadbf4e/assets/1581508416184.png -------------------------------------------------------------------------------- /assets/1581508444771.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyikaii/auto-buy-Python-tool/5aa039dcefb7efce34f8d8174d4410860fadbf4e/assets/1581508444771.png -------------------------------------------------------------------------------- /assets/1581768204211.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyikaii/auto-buy-Python-tool/5aa039dcefb7efce34f8d8174d4410860fadbf4e/assets/1581768204211.png -------------------------------------------------------------------------------- /assets/1581768213048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyikaii/auto-buy-Python-tool/5aa039dcefb7efce34f8d8174d4410860fadbf4e/assets/1581768213048.png -------------------------------------------------------------------------------- /assets/1583151909197.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyikaii/auto-buy-Python-tool/5aa039dcefb7efce34f8d8174d4410860fadbf4e/assets/1583151909197.png --------------------------------------------------------------------------------