├── .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 |
--------------------------------------------------------------------------------