├── .gitignore ├── README.md └── scraper-jd.py /.gitignore: -------------------------------------------------------------------------------- 1 | /venv/ 2 | /.idea/ 3 | /config.ini 4 | /jd-autobuy.iml 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JD_AutoBuy优化 2 | 3 | 非常感谢原作者 https://github.com/Adyzng/jd-autobuy 提供的代码 4 | 5 | ## 京东抢购 6 | Python爬虫,自动登录京东网站,查询商品库存,价格,显示购物车详情等。
7 | 可以指定时间抢购商品,自动购买下单,然后手动去京东付款就行。 8 | 9 | 10 | ## chang log 11 | 配置项在代码里都用 # config:标注过的,可以根据自己的浏览器的信息进行相关配置 12 | 13 | 更改为使用配置cookie的方式登录京东商城 14 | 15 | 增加了定时抢购的功能 16 | 17 | 18 | 19 | ## 运行环境 20 | Python 2.7 21 | 22 | 23 | ## 第三方库 24 | - [Requests][1]: 简单好用,功能强大的Http请求库 25 | - [beautifulsoup4][2]: HTML文档格式化及便签选择器 26 | 27 | 28 | 29 | ## 环境配置 30 | ``` Python 31 | pip install requests 32 | pip install beautifulsoup4 33 | ``` 34 | 35 | 36 | ## 使用帮助 37 | ``` cmd 38 | > python scraper-jd.py -h 39 | usage: scraper-jd.py [-h] [-u USERNAME] [-p PASSWORD] [-g GOOD] [-c COUNT] 40 | [-w WAIT] [-f] [-s] 41 | 42 | Simulate to login Jing Dong, and buy sepecified good 43 | 44 | optional arguments: 45 | -h, --help show this help message and exit 46 | -u USERNAME, --username USERNAME 47 | Jing Dong login user name 48 | -p PASSWORD, --password PASSWORD 49 | Jing Dong login user password 50 | -g GOOD, --good GOOD Jing Dong good ID 51 | -c COUNT, --count COUNT 52 | The count to buy 53 | -w WAIT, --wait WAIT Flush time interval, unit MS 54 | -f, --flush Continue flash if good out of stock 55 | -s, --submit Submit the order to Jing Dong 56 | ``` 57 | 58 | ## 实例输出 59 | ``` cmd 60 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 61 | Thu Mar 30 17:10:01 2017 > 请打开京东手机客户端,准备扫码登陆: 62 | 201 : 二维码未扫描 ,请扫描二维码 63 | 201 : 二维码未扫描 ,请扫描二维码 64 | 201 : 二维码未扫描 ,请扫描二维码 65 | 201 : 二维码未扫描 ,请扫描二维码 66 | 202 : 请手机客户端确认登录 67 | 200 : BADACIFYhf6fakfHvjiYTlwGzSp4EjFATN3Xw1ePR1hITtw0 68 | 登陆成功 69 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 70 | Thu Mar 30 17:10:28 2017 > 商品详情 71 | 编号:3133857 72 | 库存:现货 73 | 价格:6399.00 74 | 名称:Apple iPhone 7 Plus (A1661) 128G 黑色 移动联通电信4G手机 75 | 链接:http://cart.jd.com/gate.action?pid=3133857&pcount=1&ptype=1 76 | 商品已成功加入购物车! 77 | 购买数量:3133857 > 1 78 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 79 | Thu Mar 30 17:10:30 2017 > 购物车明细 80 | 购买 数量 价格 总价 商品 81 | Y 1 6399.00 6399.00 Apple iPhone 7 Plus (A1661) 128G 黑色 移动联通电信4G手机 82 | 总数: 1 83 | 总额: 6399.00 84 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 85 | Thu Mar 30 17:10:30 2017 > 订单详情 86 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 87 | ... 88 | ``` 89 | 90 | ## 注 91 | 代码仅供学习之用,京东网页不断变化,代码并不一定总是能正常运行。
92 | 如果您发现有Bug,Welcome to Pull Request. 93 | 94 | 95 | [1]: http://docs.python-requests.org 96 | [2]: https://www.crummy.com/software/BeautifulSoup 97 | -------------------------------------------------------------------------------- /scraper-jd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | JD online shopping helper tool 5 | ----------------------------------------------------- 6 | 7 | only support to login by QR code, 8 | username / password is not working now. 9 | 10 | """ 11 | 12 | import bs4 13 | import requests, requests.utils, pickle 14 | import requests.packages.urllib3 15 | 16 | requests.packages.urllib3.disable_warnings() 17 | 18 | import os 19 | import time 20 | import datetime 21 | import json 22 | import random 23 | 24 | import argparse 25 | # from selenium import webdriver 26 | 27 | 28 | import sys 29 | 30 | reload(sys) 31 | sys.setdefaultencoding('utf-8') 32 | 33 | # get function name 34 | FuncName = lambda n=0: sys._getframe(n + 1).f_code.co_name 35 | 36 | 37 | def tags_val(tag, key='', index=0): 38 | ''' 39 | return html tag list attribute @key @index 40 | if @key is empty, return tag content 41 | ''' 42 | if len(tag) == 0 or len(tag) <= index: 43 | return '' 44 | elif key: 45 | txt = tag[index].get(key) 46 | return txt.strip(' \t\r\n') if txt else '' 47 | else: 48 | txt = tag[index].text 49 | return txt.strip(' \t\r\n') if txt else '' 50 | 51 | 52 | def tag_val(tag, key=''): 53 | ''' 54 | return html tag attribute @key 55 | if @key is empty, return tag content 56 | ''' 57 | if tag is None: 58 | return '' 59 | elif key: 60 | txt = tag.get(key) 61 | return txt.strip(' \t\r\n') if txt else '' 62 | else: 63 | txt = tag.text 64 | return txt.strip(' \t\r\n') if txt else '' 65 | 66 | 67 | def get_session(): 68 | # 初始化session 69 | session = requests.session() 70 | # config:可以在订单结算页面打开调试模式,点击offline,然后点击提交,可以看到提交请求里有header信息 71 | session.headers = {"Accept": "application/json, text/javascript, */*; q=0.01", 72 | "Content-Type": "application/x-www-form-urlencoded", 73 | "Origin": "https://trade.jd.com", 74 | "Referer": "https://trade.jd.com/shopping/order/getOrderInfo.action", 75 | "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.3861.400 QQBrowser/10.7.4313.400", 76 | "X-Requested-With": "XMLHttpRequest"} 77 | 78 | # 获取cookies保存到session 79 | session.cookies = get_cookies() 80 | return session 81 | 82 | 83 | def get_cookies(): 84 | """解析cookies内容并添加到cookiesJar""" 85 | manual_cookies = {} 86 | # config:点击我的订单,在list.action请求header里就有cookie,全部复制过来 87 | cookies_String = "your cookie string" 88 | for item in cookies_String.split(';'): 89 | name, value = item.strip().split('=', 1) 90 | # 用=号分割,分割1次 91 | manual_cookies[name] = value 92 | # 为字典cookies添加内容 93 | cookiesJar = requests.utils.cookiejar_from_dict(manual_cookies, cookiejar=None, overwrite=True) 94 | return cookiesJar 95 | 96 | 97 | class JDWrapper(object): 98 | ''' 99 | This class used to simulate login JD 100 | ''' 101 | 102 | def __init__(self, usr_name=None, usr_pwd=None): 103 | # config:cookie info,eid和fp可以在订单结算页面打开调试模式,点击offline,然后点击提交,可以看到提交请求里有eip和fp 104 | self.eid = 'your eid' 105 | self.fp = 'your fp' 106 | 107 | self.usr_name = usr_name 108 | self.usr_pwd = usr_pwd 109 | 110 | self.sess = get_session() 111 | 112 | self.cookies = { 113 | 114 | } 115 | 116 | @staticmethod 117 | def response_status(resp): 118 | if resp.status_code != requests.codes.OK: 119 | print 'Status: %u, Url: %s' % (resp.status_code, resp.url) 120 | return False 121 | return True 122 | 123 | def good_stock(self, stock_id, good_count=1, area_id=None): 124 | ''' 125 | 33 : on sale, 126 | 34 : out of stock 127 | ''' 128 | # http://ss.jd.com/ss/areaStockState/mget?app=cart_pc&ch=1&skuNum=3180350,1&area=1,72,2799,0 129 | # response: {"3180350":{"a":"34","b":"1","c":"-1"}} 130 | # stock_url = 'http://ss.jd.com/ss/areaStockState/mget' 131 | 132 | # http://c0.3.cn/stocks?callback=jQuery2289454&type=getstocks&skuIds=3133811&area=1_72_2799_0&_=1490694504044 133 | # jQuery2289454({"3133811":{"StockState":33,"freshEdi":null,"skuState":1,"PopType":0,"sidDely":"40","channel":1,"StockStateName":"现货","rid":null,"rfg":0,"ArrivalDate":"","IsPurchase":true,"rn":-1}}) 134 | # jsonp or json both work 135 | stock_url = 'https://c0.3.cn/stocks' 136 | 137 | payload = { 138 | 'type': 'getstocks', 139 | 'skuIds': str(stock_id), 140 | 'area': area_id or '1_72_2799_0', # area change as needed 141 | } 142 | 143 | try: 144 | # get stock state 145 | resp = self.sess.get(stock_url, params=payload) 146 | if not self.response_status(resp): 147 | print u'获取商品库存失败' 148 | return (0, '') 149 | 150 | # return json 151 | resp.encoding = 'gbk' 152 | stock_info = json.loads(resp.text) 153 | stock_stat = int(stock_info[stock_id]['StockState']) 154 | stock_stat_name = stock_info[stock_id]['StockStateName'] 155 | 156 | # 33 : on sale, 34 : out of stock, 36: presell 157 | return stock_stat, stock_stat_name 158 | 159 | except Exception as e: 160 | print 'Stocks Exception:', e 161 | time.sleep(5) 162 | 163 | return (0, '') 164 | 165 | def good_detail(self, stock_id, area_id=None): 166 | # return good detail 167 | good_data = { 168 | 'id': stock_id, 169 | 'name': '', 170 | 'link': '', 171 | 'price': '', 172 | 'stock': '', 173 | 'stockName': '', 174 | } 175 | 176 | try: 177 | # shop page 178 | stock_link = 'http://item.jd.com/{0}.html'.format(stock_id) 179 | resp = self.sess.get(stock_link) 180 | 181 | # good page 182 | soup = bs4.BeautifulSoup(resp.text, "html.parser") 183 | 184 | # good name 185 | tags = soup.select('div#name h1') 186 | if len(tags) == 0: 187 | tags = soup.select('div.sku-name') 188 | good_data['name'] = tags_val(tags).strip(' \t\r\n') 189 | 190 | # cart link 191 | tags = soup.select('a#InitCartUrl') 192 | link = tags_val(tags, key='href') 193 | 194 | if link[:2] == '//': link = 'http:' + link 195 | good_data['link'] = link 196 | 197 | except Exception, e: 198 | print 'Exp {0} : {1}'.format(FuncName(), e) 199 | 200 | # good price 201 | good_data['price'] = self.good_price(stock_id) 202 | 203 | # good stock 204 | good_data['stock'], good_data['stockName'] = self.good_stock(stock_id=stock_id, area_id=area_id) 205 | # stock_str = u'有货' if good_data['stock'] == 33 else u'无货' 206 | 207 | print '+++++++++++++++++++++++++++++++++++++++++++++++++++++++' 208 | print u'{0} > 商品详情'.format(time.ctime()) 209 | print u'编号:{0}'.format(good_data['id']) 210 | print u'库存:{0}'.format(good_data['stockName']) 211 | print u'价格:{0}'.format(good_data['price']) 212 | print u'名称:{0}'.format(good_data['name']) 213 | # print u'链接:{0}'.format(good_data['link']) 214 | 215 | return good_data 216 | 217 | def good_price(self, stock_id): 218 | # get good price 219 | url = 'http://p.3.cn/prices/mgets' 220 | payload = { 221 | 'type': 1, 222 | 'pduid': int(time.time() * 1000), 223 | 'skuIds': 'J_' + stock_id, 224 | } 225 | 226 | price = '?' 227 | try: 228 | resp = self.sess.get(url, params=payload) 229 | resp_txt = resp.text.strip() 230 | # print resp_txt 231 | 232 | js = json.loads(resp_txt[1:-1]) 233 | # print u'价格', 'P: {0}, M: {1}'.format(js['p'], js['m']) 234 | price = js.get('p') 235 | 236 | except Exception, e: 237 | print 'Exp {0} : {1}'.format(FuncName(), e) 238 | 239 | return price 240 | 241 | def buy(self, options): 242 | # stock detail 243 | good_data = self.good_detail(options.good) 244 | 245 | # retry until stock not empty 246 | if good_data['stock'] != 33: 247 | # flush stock state 248 | while good_data['stock'] != 33 and options.flush: 249 | print u'<%s> <%s>' % (good_data['stockName'], good_data['name']) 250 | time.sleep(options.wait / 1000.0) 251 | good_data['stock'], good_data['stockName'] = self.good_stock(stock_id=options.good, 252 | area_id=options.area) 253 | 254 | 255 | # failed 256 | link = good_data['link'] 257 | if good_data['stock'] != 33 or link == '': 258 | # print u'stock {0}, link {1}'.format(good_data['stock'], link) 259 | return False 260 | 261 | try: 262 | # change buy count 263 | if options.count != 1: 264 | link = link.replace('pcount=1', 'pcount={0}'.format(options.count)) 265 | 266 | # add to cart 267 | resp = self.sess.get(link, cookies=self.cookies) 268 | soup = bs4.BeautifulSoup(resp.text, "html.parser") 269 | 270 | # tag if add to cart succeed 271 | tag = soup.select('h3.ftx-02') 272 | if tag is None: 273 | tag = soup.select('div.p-name a') 274 | 275 | if tag is None or len(tag) == 0: 276 | print u'添加到购物车失败' 277 | return False 278 | 279 | print '+++++++++++++++++++++++++++++++++++++++++++++++++++++++' 280 | print u'{0} > 购买详情'.format(time.ctime()) 281 | print u'链接:{0}'.format(link) 282 | print u'结果:{0}'.format(tags_val(tag)) 283 | 284 | except Exception, e: 285 | print 'Exp {0} : {1}'.format(FuncName(), e) 286 | else: 287 | self.cart_detail() 288 | return self.order_info(options.submit) 289 | 290 | return False 291 | 292 | def cart_detail(self): 293 | # list all goods detail in cart 294 | cart_url = 'https://cart.jd.com/cart.action' 295 | cart_header = u'购买 数量 价格 总价 商品' 296 | cart_format = u'{0:8}{1:8}{2:12}{3:12}{4}' 297 | 298 | try: 299 | resp = self.sess.get(cart_url, cookies=self.cookies) 300 | resp.encoding = 'utf-8' 301 | soup = bs4.BeautifulSoup(resp.text, "html.parser") 302 | 303 | print '+++++++++++++++++++++++++++++++++++++++++++++++++++++++' 304 | print u'{0} > 购物车明细'.format(time.ctime()) 305 | print cart_header 306 | 307 | for item in soup.select('div.item-form'): 308 | check = tags_val(item.select('div.cart-checkbox input'), key='checked') 309 | check = ' + ' if check else ' - ' 310 | count = tags_val(item.select('div.quantity-form input'), key='value') 311 | price = tags_val(item.select('div.p-price strong')) 312 | sums = tags_val(item.select('div.p-sum strong')) 313 | gname = tags_val(item.select('div.p-name a')) 314 | #: ¥字符解析出错, 输出忽略¥ 315 | print cart_format.format(check, count, price[1:], sums[1:], gname) 316 | 317 | t_count = tags_val(soup.select('div.amount-sum em')) 318 | t_value = tags_val(soup.select('span.sumPrice em')) 319 | print u'总数: {0}'.format(t_count) 320 | print u'总额: {0}'.format(t_value[1:]) 321 | 322 | except Exception, e: 323 | print 'Exp {0} : {1}'.format(FuncName(), e) 324 | 325 | def order_info(self, submit=False): 326 | # get order info detail, and submit order 327 | print '+++++++++++++++++++++++++++++++++++++++++++++++++++++++' 328 | print u'{0} > 订单详情'.format(time.ctime()) 329 | 330 | try: 331 | order_url = 'http://trade.jd.com/shopping/order/getOrderInfo.action' 332 | payload = { 333 | 'rid': str(int(time.time() * 1000)), 334 | } 335 | 336 | # get preorder page 337 | rs = self.sess.get(order_url, params=payload, cookies=self.cookies) 338 | soup = bs4.BeautifulSoup(rs.text, "html.parser") 339 | 340 | # order summary 341 | payment = tag_val(soup.find(id='sumPayPriceId')) 342 | detail = soup.find(class_='fc-consignee-info') 343 | 344 | if detail: 345 | snd_usr = tag_val(detail.find(id='sendMobile')) 346 | snd_add = tag_val(detail.find(id='sendAddr')) 347 | 348 | print u'应付款:{0}'.format(payment) 349 | print snd_usr 350 | print snd_add 351 | 352 | # just test, not real order 353 | if not submit: 354 | return False 355 | 356 | # config:order info,可以在订单结算页面打开调试模式,点击offline,然后点击提交,可以看到提交form data里有相关配置 357 | payload = { 358 | 'overseaPurchaseCookies': '', 359 | 'submitOrderParam.btSupport': '1', 360 | 'submitOrderParam.ignorePriceChange': '0', 361 | 'submitOrderParam.sopNotPutInvoice': 'false', 362 | 'submitOrderParam.trackID': 'TestTrackId', 363 | 'submitOrderParam.payPassword': 'Your encrypted password', 364 | 'submitOrderParam.eid': self.eid, 365 | 'submitOrderParam.fp': self.fp, 366 | 'presaleStockSign': 1, 367 | 'vendorRemarks': [{"venderId": "10240281", "remark": ""}], 368 | 'submitOrderParam.isBestCoupon': 1, 369 | 'submitOrderParam.jxj': 1 370 | } 371 | 372 | order_url = 'http://trade.jd.com/shopping/order/submitOrder.action?&presaleStockSign=1' 373 | rp = self.sess.post(order_url, params=payload, cookies=self.cookies) 374 | 375 | if rp.status_code == 200: 376 | js = json.loads(rp.text) 377 | if js['success'] == True: 378 | print u'下单成功!订单号:{0}'.format(js['orderId']) 379 | print u'请前往东京官方商城付款' 380 | return True 381 | else: 382 | print u'下单失败!<{0}: {1}>'.format(js['resultCode'], js['message']) 383 | if js['resultCode'] == '60017': 384 | # 60017: 您多次提交过快,请稍后再试 385 | time.sleep(1) 386 | else: 387 | print u'请求失败. StatusCode:', rp.status_code 388 | 389 | except Exception, e: 390 | print 'Exp {0} : {1}'.format(FuncName(), e) 391 | 392 | return False 393 | 394 | def login(self): 395 | for flag in range(1, 3): 396 | try: 397 | targetURL = 'https://order.jd.com/center/list.action' 398 | payload = { 399 | 'rid': str(int(time.time() * 1000)), 400 | } 401 | resp = self.sess.get( 402 | url=targetURL, params=payload, allow_redirects=False) 403 | if resp.status_code == requests.codes.OK: 404 | print '校验是否登录[成功]' 405 | return True 406 | else: 407 | print '校验是否登录[失败]' 408 | print '请重新输入cookie' 409 | time.sleep(1) 410 | continue 411 | except Exception as e: 412 | print u'第【%s】次失败请重新获取cookie', flag 413 | time.sleep(1) 414 | continue 415 | sys.exit(1) 416 | 417 | 418 | def main(options): 419 | jd = JDWrapper() 420 | 421 | jd.login() 422 | # config:在此处配置购买的时间,若购买时间小于当前时间,则执行购买操作 423 | time1 = datetime.datetime(2021, 3, 16, 0, 0, 0, 250) 424 | while time1 >= datetime.datetime.now(): 425 | print u'时间记录{0}'.format(datetime.datetime.now()) 426 | time.sleep(options.wait / 2000.0) 427 | while not jd.buy(options) and options.flush: 428 | time.sleep(options.wait / 1000.0) 429 | 430 | 431 | if __name__ == '__main__': 432 | # help message 433 | parser = argparse.ArgumentParser(description='Simulate to login Jing Dong, and buy sepecified good') 434 | # config:地址id,商品详情页,右键检查地址元素,data-localid就是area 435 | parser.add_argument('-a', '--area', 436 | help='Area string, like: 1_72_2799_0 for Beijing', default='02_1930_49324_49398') 437 | # config:商品id,需要进入商品详情页,地址栏的数字就是goodId 438 | parser.add_argument('-g', '--good', 439 | help='Jing Dong good ID', default='568021857460') 440 | # config:抢购数量 441 | parser.add_argument('-c', '--count', type=int, 442 | help='The count to buy', default=1) 443 | parser.add_argument('-w', '--wait', 444 | type=int, default=500, 445 | help='Flush time interval, unit MS') 446 | parser.add_argument('-f', '--flush', 447 | action='store_true', 448 | help='Continue flash if good out of stock') 449 | # config:default指定是否下单 450 | parser.add_argument('-s', '--submit', 451 | action='store_true', default='true', 452 | help='Submit the order to Jing Dong') 453 | 454 | options = parser.parse_args() 455 | print options 456 | 457 | main(options) 458 | --------------------------------------------------------------------------------