jd_spider_requests.py 20 KB

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