jd_spider_requests.py 20 KB

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