jd_spider_requests.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. import random
  2. import sys
  3. import time
  4. import requests
  5. import functools
  6. import json
  7. import os
  8. import pickle
  9. from lxml import etree
  10. from jd_logger import logger
  11. from timer import Timer
  12. from util import parse_json, send_wechat, get_session, response_status, save_image, open_image
  13. from config import global_config
  14. from concurrent.futures import ProcessPoolExecutor
  15. from exception import SKException
  16. class JdSeckill(object):
  17. def __init__(self):
  18. # 初始化信息
  19. self.session = get_session()
  20. self.sku_id = global_config.getRaw('config', 'sku_id')
  21. self.seckill_num = 2
  22. self.seckill_init_info = dict()
  23. self.seckill_url = dict()
  24. self.seckill_order_data = dict()
  25. self.timers = Timer()
  26. self.default_user_agent = global_config.getRaw('config', 'DEFAULT_USER_AGENT')
  27. self.headers = {'User-Agent': self.default_user_agent}
  28. self.is_login = False
  29. self.nick_name = None
  30. self._load_cookies()
  31. def reserve(self):
  32. """
  33. 预约
  34. """
  35. self.__reserve()
  36. def seckill(self):
  37. """
  38. 抢购
  39. """
  40. self.__seckill()
  41. def wait_some_time(self):
  42. time.sleep(random.randint(100, 300) / 1000)
  43. def seckill_by_proc_pool(self, work_count=5):
  44. """
  45. 多进程进行抢购
  46. work_count:进程数量
  47. """
  48. with ProcessPoolExecutor(work_count) as pool:
  49. for i in range(work_count):
  50. pool.submit(self.seckill)
  51. def __reserve(self):
  52. """
  53. 预约
  54. """
  55. self._validate_cookies()
  56. while True:
  57. try:
  58. self.make_reserve()
  59. except Exception as e:
  60. logger.info('预约发生异常!', e)
  61. self.wait_some_time()
  62. def __seckill(self):
  63. """
  64. 抢购
  65. """
  66. self._validate_cookies()
  67. while True:
  68. try:
  69. self.request_seckill_url()
  70. while True:
  71. self.request_seckill_checkout_page()
  72. self.submit_seckill_order()
  73. except Exception as e:
  74. logger.info('抢购发生异常,稍后继续执行!', e)
  75. self.wait_some_time()
  76. def _load_cookies(self):
  77. cookies_file = ''
  78. if os.path.exists('./cookies'):
  79. for name in os.listdir('./cookies'):
  80. if name.endswith('.cookies'):
  81. cookies_file = './cookies/{0}'.format(name)
  82. break
  83. if cookies_file == '':
  84. return
  85. with open(cookies_file, 'rb') as f:
  86. local_cookies = pickle.load(f)
  87. self.session.cookies.update(local_cookies)
  88. self.is_login = self._validate_cookies()
  89. def _save_cookies(self):
  90. cookies_file = './cookies/{0}.cookies'.format(self.nick_name)
  91. directory = os.path.dirname(cookies_file)
  92. if not os.path.exists(directory):
  93. os.makedirs(directory)
  94. with open(cookies_file, 'wb') as f:
  95. pickle.dump(self.session.cookies, f)
  96. def _validate_cookies(self):
  97. """验证cookies是否有效(是否登陆)
  98. 通过访问用户订单列表页进行判断:若未登录,将会重定向到登陆页面。
  99. :return: cookies是否有效 True/False
  100. """
  101. url = 'https://order.jd.com/center/list.action'
  102. payload = {
  103. 'rid': str(int(time.time() * 1000)),
  104. }
  105. try:
  106. resp = self.session.get(url=url, params=payload, allow_redirects=False)
  107. if resp.status_code == requests.codes.OK:
  108. return True
  109. except Exception as e:
  110. logger.error(e)
  111. self.session = get_session()
  112. return False
  113. def _get_login_page(self):
  114. url = "https://passport.jd.com/new/login.aspx"
  115. page = self.session.get(url, headers=self.headers)
  116. return page
  117. def _get_QRcode(self):
  118. url = 'https://qr.m.jd.com/show'
  119. payload = {
  120. 'appid': 133,
  121. 'size': 147,
  122. 't': str(int(time.time() * 1000)),
  123. }
  124. headers = {
  125. 'User-Agent': self.default_user_agent,
  126. 'Referer': 'https://passport.jd.com/new/login.aspx',
  127. }
  128. resp = self.session.get(url=url, headers=headers, params=payload)
  129. if not response_status(resp):
  130. logger.info('获取二维码失败')
  131. return False
  132. QRCode_file = 'QRcode.png'
  133. save_image(resp, QRCode_file)
  134. logger.info('二维码获取成功,请打开京东APP扫描')
  135. open_image(QRCode_file)
  136. return True
  137. def check_login(func):
  138. """用户登陆态校验装饰器。若用户未登陆,则调用扫码登陆"""
  139. @functools.wraps(func)
  140. def new_func(self, *args, **kwargs):
  141. if not self.is_login:
  142. logger.info("{0} 需登陆后调用,开始扫码登陆".format(func.__name__))
  143. self.login_by_QRcode()
  144. return func(self, *args, **kwargs)
  145. return new_func
  146. def _get_QRcode_ticket(self):
  147. url = 'https://qr.m.jd.com/check'
  148. payload = {
  149. 'appid': '133',
  150. 'callback': 'jQuery{}'.format(random.randint(1000000, 9999999)),
  151. 'token': self.session.cookies.get('wlfstk_smdl'),
  152. '_': str(int(time.time() * 1000)),
  153. }
  154. headers = {
  155. 'User-Agent': self.default_user_agent,
  156. 'Referer': 'https://passport.jd.com/new/login.aspx',
  157. }
  158. resp = self.session.get(url=url, headers=headers, params=payload)
  159. if not response_status(resp):
  160. logger.error('获取二维码扫描结果异常')
  161. return False
  162. resp_json = parse_json(resp.text)
  163. if resp_json['code'] != 200:
  164. logger.info('Code: %s, Message: %s', resp_json['code'], resp_json['msg'])
  165. return None
  166. else:
  167. logger.info('已完成手机客户端确认')
  168. return resp_json['ticket']
  169. def _validate_QRcode_ticket(self, ticket):
  170. url = 'https://passport.jd.com/uc/qrCodeTicketValidation'
  171. headers = {
  172. 'User-Agent': self.default_user_agent,
  173. 'Referer': 'https://passport.jd.com/uc/login?ltype=logout',
  174. }
  175. resp = self.session.get(url=url, headers=headers, params={'t': ticket})
  176. if not response_status(resp):
  177. return False
  178. resp_json = json.loads(resp.text)
  179. if resp_json['returnCode'] == 0:
  180. return True
  181. else:
  182. logger.info(resp_json)
  183. return False
  184. def login_by_QRcode(self):
  185. """二维码登陆
  186. :return:
  187. """
  188. if self.is_login:
  189. logger.info('登录成功')
  190. return
  191. self._get_login_page()
  192. # download QR code
  193. if not self._get_QRcode():
  194. raise SKException('二维码下载失败')
  195. # get QR code ticket
  196. ticket = None
  197. retry_times = 85
  198. for _ in range(retry_times):
  199. ticket = self._get_QRcode_ticket()
  200. if ticket:
  201. break
  202. time.sleep(2)
  203. else:
  204. raise SKException('二维码过期,请重新获取扫描')
  205. # validate QR code ticket
  206. if not self._validate_QRcode_ticket(ticket):
  207. raise SKException('二维码信息校验失败')
  208. logger.info('二维码登录成功')
  209. self.is_login = True
  210. self.nick_name = self.get_username()
  211. self._save_cookies()
  212. def make_reserve(self):
  213. """商品预约"""
  214. logger.info('商品名称:{}'.format(self.get_sku_title()))
  215. url = 'https://yushou.jd.com/youshouinfo.action?'
  216. payload = {
  217. 'callback': 'fetchJSON',
  218. 'sku': self.sku_id,
  219. '_': str(int(time.time() * 1000)),
  220. }
  221. headers = {
  222. 'User-Agent': self.default_user_agent,
  223. 'Referer': 'https://item.jd.com/{}.html'.format(self.sku_id),
  224. }
  225. resp = self.session.get(url=url, params=payload, headers=headers)
  226. resp_json = parse_json(resp.text)
  227. reserve_url = resp_json.get('url')
  228. self.timers.start()
  229. while True:
  230. try:
  231. self.session.get(url='https:' + reserve_url)
  232. logger.info('预约成功,已获得抢购资格 / 您已成功预约过了,无需重复预约')
  233. if global_config.getRaw('messenger', 'enable') == 'true':
  234. success_message = "预约成功,已获得抢购资格 / 您已成功预约过了,无需重复预约"
  235. send_wechat(success_message)
  236. break
  237. except Exception as e:
  238. logger.error('预约失败正在重试...')
  239. def get_username(self):
  240. """获取用户信息"""
  241. url = 'https://passport.jd.com/user/petName/getUserInfoForMiniJd.action'
  242. payload = {
  243. 'callback': 'jQuery'.format(random.randint(1000000, 9999999)),
  244. '_': str(int(time.time() * 1000)),
  245. }
  246. headers = {
  247. 'User-Agent': self.default_user_agent,
  248. 'Referer': 'https://order.jd.com/center/list.action',
  249. }
  250. resp = self.session.get(url=url, params=payload, headers=headers)
  251. try_count = 5
  252. while not resp.text.startswith("jQuery"):
  253. try_count = try_count - 1
  254. if try_count > 0:
  255. resp = self.session.get(url=url, params=payload, headers=headers)
  256. else:
  257. break
  258. self.wait_some_time()
  259. # 响应中包含了许多用户信息,现在在其中返回昵称
  260. # jQuery2381773({"imgUrl":"//storage.360buyimg.com/i.imageUpload/xxx.jpg","lastLoginTime":"","nickName":"xxx","plusStatus":"0","realName":"xxx","userLevel":x,"userScoreVO":{"accountScore":xx,"activityScore":xx,"consumptionScore":xxxxx,"default":false,"financeScore":xxx,"pin":"xxx","riskScore":x,"totalScore":xxxxx}})
  261. return parse_json(resp.text).get('nickName')
  262. def get_sku_title(self):
  263. """获取商品名称"""
  264. url = 'https://item.jd.com/{}.html'.format(global_config.getRaw('config', 'sku_id'))
  265. resp = self.session.get(url).content
  266. x_data = etree.HTML(resp)
  267. sku_title = x_data.xpath('/html/head/title/text()')
  268. return sku_title[0]
  269. def get_seckill_url(self):
  270. """获取商品的抢购链接
  271. 点击"抢购"按钮后,会有两次302跳转,最后到达订单结算页面
  272. 这里返回第一次跳转后的页面url,作为商品的抢购链接
  273. :return: 商品的抢购链接
  274. """
  275. url = 'https://itemko.jd.com/itemShowBtn'
  276. payload = {
  277. 'callback': 'jQuery{}'.format(random.randint(1000000, 9999999)),
  278. 'skuId': self.sku_id,
  279. 'from': 'pc',
  280. '_': str(int(time.time() * 1000)),
  281. }
  282. headers = {
  283. 'User-Agent': self.default_user_agent,
  284. 'Host': 'itemko.jd.com',
  285. 'Referer': 'https://item.jd.com/{}.html'.format(self.sku_id),
  286. }
  287. while True:
  288. resp = self.session.get(url=url, headers=headers, params=payload)
  289. resp_json = parse_json(resp.text)
  290. if resp_json.get('url'):
  291. # https://divide.jd.com/user_routing?skuId=8654289&sn=c3f4ececd8461f0e4d7267e96a91e0e0&from=pc
  292. router_url = 'https:' + resp_json.get('url')
  293. # https://marathon.jd.com/captcha.html?skuId=8654289&sn=c3f4ececd8461f0e4d7267e96a91e0e0&from=pc
  294. seckill_url = router_url.replace(
  295. 'divide', 'marathon').replace(
  296. 'user_routing', 'captcha.html')
  297. logger.info("抢购链接获取成功: %s", seckill_url)
  298. return seckill_url
  299. else:
  300. logger.info("抢购链接获取失败,稍后自动重试")
  301. self.wait_some_time()
  302. def request_seckill_url(self):
  303. """访问商品的抢购链接(用于设置cookie等"""
  304. logger.info('用户:{}'.format(self.get_username()))
  305. logger.info('商品名称:{}'.format(self.get_sku_title()))
  306. self.timers.start()
  307. self.seckill_url[self.sku_id] = self.get_seckill_url()
  308. logger.info('访问商品的抢购连接...')
  309. headers = {
  310. 'User-Agent': self.default_user_agent,
  311. 'Host': 'marathon.jd.com',
  312. 'Referer': 'https://item.jd.com/{}.html'.format(self.sku_id),
  313. }
  314. self.session.get(
  315. url=self.seckill_url.get(
  316. self.sku_id),
  317. headers=headers,
  318. allow_redirects=False)
  319. def request_seckill_checkout_page(self):
  320. """访问抢购订单结算页面"""
  321. logger.info('访问抢购订单结算页面...')
  322. url = 'https://marathon.jd.com/seckill/seckill.action'
  323. payload = {
  324. 'skuId': self.sku_id,
  325. 'num': self.seckill_num,
  326. 'rid': int(time.time())
  327. }
  328. headers = {
  329. 'User-Agent': self.default_user_agent,
  330. 'Host': 'marathon.jd.com',
  331. 'Referer': 'https://item.jd.com/{}.html'.format(self.sku_id),
  332. }
  333. self.session.get(url=url, params=payload, headers=headers, allow_redirects=False)
  334. def _get_seckill_init_info(self):
  335. """获取秒杀初始化信息(包括:地址,发票,token)
  336. :return: 初始化信息组成的dict
  337. """
  338. logger.info('获取秒杀初始化信息...')
  339. url = 'https://marathon.jd.com/seckillnew/orderService/pc/init.action'
  340. data = {
  341. 'sku': self.sku_id,
  342. 'num': self.seckill_num,
  343. 'isModifyAddress': 'false',
  344. }
  345. headers = {
  346. 'User-Agent': self.default_user_agent,
  347. 'Host': 'marathon.jd.com',
  348. }
  349. resp = self.session.post(url=url, data=data, headers=headers)
  350. return parse_json(resp.text)
  351. def _get_seckill_order_data(self):
  352. """生成提交抢购订单所需的请求体参数
  353. :return: 请求体参数组成的dict
  354. """
  355. logger.info('生成提交抢购订单所需参数...')
  356. # 获取用户秒杀初始化信息
  357. self.seckill_init_info[self.sku_id] = self._get_seckill_init_info()
  358. init_info = self.seckill_init_info.get(self.sku_id)
  359. default_address = init_info['addressList'][0] # 默认地址dict
  360. invoice_info = init_info.get('invoiceInfo', {}) # 默认发票信息dict, 有可能不返回
  361. token = init_info['token']
  362. data = {
  363. 'skuId': self.sku_id,
  364. 'num': self.seckill_num,
  365. 'addressId': default_address['id'],
  366. 'yuShou': 'true',
  367. 'isModifyAddress': 'false',
  368. 'name': default_address['name'],
  369. 'provinceId': default_address['provinceId'],
  370. 'cityId': default_address['cityId'],
  371. 'countyId': default_address['countyId'],
  372. 'townId': default_address['townId'],
  373. 'addressDetail': default_address['addressDetail'],
  374. 'mobile': default_address['mobile'],
  375. 'mobileKey': default_address['mobileKey'],
  376. 'email': default_address.get('email', ''),
  377. 'postCode': '',
  378. 'invoiceTitle': invoice_info.get('invoiceTitle', -1),
  379. 'invoiceCompanyName': '',
  380. 'invoiceContent': invoice_info.get('invoiceContentType', 1),
  381. 'invoiceTaxpayerNO': '',
  382. 'invoiceEmail': '',
  383. 'invoicePhone': invoice_info.get('invoicePhone', ''),
  384. 'invoicePhoneKey': invoice_info.get('invoicePhoneKey', ''),
  385. 'invoice': 'true' if invoice_info else 'false',
  386. 'password': '',
  387. 'codTimeType': 3,
  388. 'paymentType': 4,
  389. 'areaCode': '',
  390. 'overseas': 0,
  391. 'phone': '',
  392. 'eid': global_config.getRaw('config', 'eid'),
  393. 'fp': global_config.getRaw('config', 'fp'),
  394. 'token': token,
  395. 'pru': ''
  396. }
  397. return data
  398. def submit_seckill_order(self):
  399. """提交抢购(秒杀)订单
  400. :return: 抢购结果 True/False
  401. """
  402. url = 'https://marathon.jd.com/seckillnew/orderService/pc/submitOrder.action'
  403. payload = {
  404. 'skuId': self.sku_id,
  405. }
  406. self.seckill_order_data[self.sku_id] = self._get_seckill_order_data()
  407. logger.info('提交抢购订单...')
  408. headers = {
  409. 'User-Agent': self.default_user_agent,
  410. 'Host': 'marathon.jd.com',
  411. 'Referer': 'https://marathon.jd.com/seckill/seckill.action?skuId={0}&num={1}&rid={2}'.format(
  412. self.sku_id, self.seckill_num, int(time.time())),
  413. }
  414. resp = self.session.post(
  415. url=url,
  416. params=payload,
  417. data=self.seckill_order_data.get(
  418. self.sku_id),
  419. headers=headers)
  420. resp_json = parse_json(resp.text)
  421. # 返回信息
  422. # 抢购失败:
  423. # {'errorMessage': '很遗憾没有抢到,再接再厉哦。', 'orderId': 0, 'resultCode': 60074, 'skuId': 0, 'success': False}
  424. # {'errorMessage': '抱歉,您提交过快,请稍后再提交订单!', 'orderId': 0, 'resultCode': 60017, 'skuId': 0, 'success': False}
  425. # {'errorMessage': '系统正在开小差,请重试~~', 'orderId': 0, 'resultCode': 90013, 'skuId': 0, 'success': False}
  426. # 抢购成功:
  427. # {"appUrl":"xxxxx","orderId":820227xxxxx,"pcUrl":"xxxxx","resultCode":0,"skuId":0,"success":true,"totalMoney":"xxxxx"}
  428. if resp_json.get('success'):
  429. order_id = resp_json.get('orderId')
  430. total_money = resp_json.get('totalMoney')
  431. pay_url = 'https:' + resp_json.get('pcUrl')
  432. logger.info(
  433. '抢购成功,订单号:{}, 总价:{}, 电脑端付款链接:{}'.format(order_id,total_money,pay_url)
  434. )
  435. if global_config.getRaw('messenger', 'enable') == 'true':
  436. success_message = "抢购成功,订单号:{}, 总价:{}, 电脑端付款链接:{}".format(order_id, total_money, pay_url)
  437. send_wechat(success_message)
  438. return True
  439. else:
  440. logger.info('抢购失败,返回信息:{}'.format(resp_json))
  441. if global_config.getRaw('messenger', 'enable') == 'true':
  442. error_message = '抢购失败,返回信息:{}'.format(resp_json)
  443. send_wechat(error_message)
  444. return False