jd_spider_requests.py 22 KB

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