mirror of https://github.com/FanbeiFan/JD-SHOPPER
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
381 lines
14 KiB
381 lines
14 KiB
3 years ago
|
import random
|
||
|
import time
|
||
|
import functools
|
||
|
from lxml import etree
|
||
|
from logger import logger
|
||
|
from login import SpiderSession, QrLogin
|
||
|
from message import sendMessage
|
||
|
from timer import Timer
|
||
|
from config import global_config
|
||
|
from concurrent.futures import ProcessPoolExecutor
|
||
|
from exception import SKException
|
||
|
from util import (
|
||
|
parse_json,
|
||
|
wait_some_time,
|
||
|
)
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
class Seckiller(object):
|
||
|
def __init__(self):
|
||
|
self.spider_session = SpiderSession()
|
||
|
self.spider_session.load_cookies_from_local()
|
||
|
|
||
|
self.qrlogin = QrLogin(self.spider_session)
|
||
|
|
||
|
# 初始化信息
|
||
|
self.sku_id = global_config.getRaw('config', 'sku_id')
|
||
|
self.seckill_num = eval(global_config.getRaw('settings', 'buy_amount'))
|
||
|
self.seckill_init_info = dict()
|
||
|
self.seckill_url = dict()
|
||
|
self.seckill_order_data = dict()
|
||
|
self.timers = Timer()
|
||
|
|
||
|
self.session = self.spider_session.get_session()
|
||
|
self.user_agent = self.spider_session.user_agent
|
||
|
self.nick_name = None
|
||
|
|
||
|
def login_by_qrcode(self):
|
||
|
"""
|
||
|
二维码登陆
|
||
|
:return:
|
||
|
"""
|
||
|
if self.qrlogin.is_login:
|
||
|
logger.info('登录成功')
|
||
|
return
|
||
|
|
||
|
self.qrlogin.login_by_qrcode()
|
||
|
|
||
|
if self.qrlogin.is_login:
|
||
|
self.nick_name = self.get_username()
|
||
|
self.spider_session.save_cookies_to_local(self.nick_name)
|
||
|
else:
|
||
|
raise SKException("二维码登录失败!")
|
||
|
|
||
|
def check_login(func):
|
||
|
"""
|
||
|
用户登陆态校验装饰器。若用户未登陆,则调用扫码登陆
|
||
|
"""
|
||
|
@functools.wraps(func)
|
||
|
def new_func(self, *args, **kwargs):
|
||
|
if not self.qrlogin.is_login:
|
||
|
logger.info("{0} 需登陆后调用,开始扫码登陆".format(func.__name__))
|
||
|
self.login_by_qrcode()
|
||
|
return func(self, *args, **kwargs)
|
||
|
return new_func
|
||
|
|
||
|
@check_login
|
||
|
def reserve(self):
|
||
|
"""
|
||
|
预约
|
||
|
"""
|
||
|
self._reserve()
|
||
|
|
||
|
@check_login
|
||
|
def seckill(self):
|
||
|
"""
|
||
|
抢购
|
||
|
"""
|
||
|
self._seckill()
|
||
|
|
||
|
@check_login
|
||
|
def seckill_by_proc_pool(self):
|
||
|
"""
|
||
|
多进程进行抢购
|
||
|
work_count:进程数量
|
||
|
"""
|
||
|
work_count = eval(global_config.getRaw('settings', 'work_count'))
|
||
|
with ProcessPoolExecutor(work_count) as pool:
|
||
|
for i in range(work_count):
|
||
|
pool.submit(self.seckill)
|
||
|
|
||
|
def _reserve(self):
|
||
|
"""
|
||
|
预约
|
||
|
"""
|
||
|
while True:
|
||
|
try:
|
||
|
self.make_reserve()
|
||
|
break
|
||
|
except Exception as e:
|
||
|
logger.info('预约发生异常!', e)
|
||
|
wait_some_time()
|
||
|
|
||
|
def _seckill(self):
|
||
|
"""
|
||
|
抢购
|
||
|
"""
|
||
|
while True:
|
||
|
try:
|
||
|
self.request_seckill_url()
|
||
|
while True:
|
||
|
self.request_seckill_checkout_page()
|
||
|
self.submit_seckill_order()
|
||
|
except Exception as e:
|
||
|
logger.info('抢购发生异常,稍后继续执行!', e)
|
||
|
wait_some_time()
|
||
|
|
||
|
|
||
|
def make_reserve(self):
|
||
|
"""商品预约"""
|
||
|
logger.info('商品名称:{}'.format(self.get_sku_title()[:40]+" ......"))
|
||
|
url = 'https://yushou.jd.com/youshouinfo.action?'
|
||
|
payload = {
|
||
|
'callback': 'fetchJSON',
|
||
|
'sku': self.sku_id,
|
||
|
'_': str(int(time.time() * 1000)),
|
||
|
}
|
||
|
headers = {
|
||
|
'User-Agent': self.user_agent,
|
||
|
'Referer': 'https://item.jd.com/{}.html'.format(self.sku_id),
|
||
|
}
|
||
|
resp = self.session.get(url=url, params=payload, headers=headers)
|
||
|
resp_json = parse_json(resp.text)
|
||
|
reserve_url = resp_json.get('url')
|
||
|
self.timers.start()
|
||
|
while True:
|
||
|
try:
|
||
|
self.session.get(url='https:' + reserve_url)
|
||
|
logger.info('预约成功,已获得抢购资格 / 您已成功预约过了,无需重复预约')
|
||
|
if global_config.getRaw('messenger', 'enable') == 'true':
|
||
|
success_message = "预约成功,已获得抢购资格 / 您已成功预约过了,无需重复预约"
|
||
|
sendMessage(success_message)
|
||
|
break
|
||
|
except Exception as e:
|
||
|
logger.error('预约失败正在重试...')
|
||
|
|
||
|
def get_username(self):
|
||
|
"""获取用户信息"""
|
||
|
url = 'https://passport.jd.com/user/petName/getUserInfoForMiniJd.action'
|
||
|
payload = {
|
||
|
'callback': 'jQuery{}'.format(random.randint(1000000, 9999999)),
|
||
|
'_': str(int(time.time() * 1000)),
|
||
|
}
|
||
|
headers = {
|
||
|
'User-Agent': self.user_agent,
|
||
|
'Referer': 'https://order.jd.com/center/list.action',
|
||
|
}
|
||
|
|
||
|
resp = self.session.get(url=url, params=payload, headers=headers)
|
||
|
|
||
|
try_count = 5
|
||
|
while not resp.text.startswith("jQuery"):
|
||
|
try_count = try_count - 1
|
||
|
if try_count > 0:
|
||
|
resp = self.session.get(url=url, params=payload, headers=headers)
|
||
|
else:
|
||
|
break
|
||
|
wait_some_time()
|
||
|
# 响应中包含了许多用户信息,现在在其中返回昵称
|
||
|
# 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}})
|
||
|
return parse_json(resp.text).get('nickName')
|
||
|
|
||
|
def get_sku_title(self):
|
||
|
"""获取商品名称"""
|
||
|
url = 'https://item.jd.com/{}.html'.format(global_config.getRaw('config', 'sku_id'))
|
||
|
resp = self.session.get(url).content
|
||
|
x_data = etree.HTML(resp)
|
||
|
sku_title = x_data.xpath('/html/head/title/text()')
|
||
|
return sku_title[0]
|
||
|
|
||
|
def get_seckill_url(self):
|
||
|
"""获取商品的抢购链接
|
||
|
点击"抢购"按钮后,会有两次302跳转,最后到达订单结算页面
|
||
|
这里返回第一次跳转后的页面url,作为商品的抢购链接
|
||
|
:return: 商品的抢购链接
|
||
|
"""
|
||
|
url = 'https://itemko.jd.com/itemShowBtn'
|
||
|
payload = {
|
||
|
'callback': 'jQuery{}'.format(random.randint(1000000, 9999999)),
|
||
|
'skuId': self.sku_id,
|
||
|
'from': 'pc',
|
||
|
'_': str(int(time.time() * 1000)),
|
||
|
}
|
||
|
headers = {
|
||
|
'User-Agent': self.user_agent,
|
||
|
'Host': 'itemko.jd.com',
|
||
|
'Referer': 'https://item.jd.com/{}.html'.format(self.sku_id),
|
||
|
}
|
||
|
while True:
|
||
|
resp = self.session.get(url=url, headers=headers, params=payload)
|
||
|
resp_json = parse_json(resp.text)
|
||
|
if resp_json.get('url'):
|
||
|
# https://divide.jd.com/user_routing?skuId=8654289&sn=c3f4ececd8461f0e4d7267e96a91e0e0&from=pc
|
||
|
router_url = 'https:' + resp_json.get('url')
|
||
|
# https://marathon.jd.com/captcha.html?skuId=8654289&sn=c3f4ececd8461f0e4d7267e96a91e0e0&from=pc
|
||
|
seckill_url = router_url.replace(
|
||
|
'divide', 'marathon').replace(
|
||
|
'user_routing', 'captcha.html')
|
||
|
logger.info("抢购链接获取成功: %s", seckill_url)
|
||
|
return seckill_url
|
||
|
else:
|
||
|
logger.info("抢购链接获取失败,稍后自动重试")
|
||
|
wait_some_time()
|
||
|
|
||
|
def request_seckill_url(self):
|
||
|
"""访问商品的抢购链接(用于设置cookie等"""
|
||
|
logger.info('用户:{}'.format(self.get_username()))
|
||
|
logger.info('商品名称:{}'.format(self.get_sku_title()))
|
||
|
self.timers.start()
|
||
|
self.seckill_url[self.sku_id] = self.get_seckill_url()
|
||
|
logger.info('访问商品的抢购连接...')
|
||
|
headers = {
|
||
|
'User-Agent': self.user_agent,
|
||
|
'Host': 'marathon.jd.com',
|
||
|
'Referer': 'https://item.jd.com/{}.html'.format(self.sku_id),
|
||
|
}
|
||
|
self.session.get(
|
||
|
url=self.seckill_url.get(
|
||
|
self.sku_id),
|
||
|
headers=headers,
|
||
|
allow_redirects=False)
|
||
|
|
||
|
def request_seckill_checkout_page(self):
|
||
|
"""访问抢购订单结算页面"""
|
||
|
logger.info('访问抢购订单结算页面...')
|
||
|
url = 'https://marathon.jd.com/seckill/seckill.action'
|
||
|
payload = {
|
||
|
'skuId': self.sku_id,
|
||
|
'num': self.seckill_num,
|
||
|
'rid': int(time.time())
|
||
|
}
|
||
|
headers = {
|
||
|
'User-Agent': self.user_agent,
|
||
|
'Host': 'marathon.jd.com',
|
||
|
'Referer': 'https://item.jd.com/{}.html'.format(self.sku_id),
|
||
|
}
|
||
|
self.session.get(url=url, params=payload, headers=headers, allow_redirects=False)
|
||
|
|
||
|
def _get_seckill_init_info(self):
|
||
|
"""获取秒杀初始化信息(包括:地址,发票,token)
|
||
|
:return: 初始化信息组成的dict
|
||
|
"""
|
||
|
logger.info('获取秒杀初始化信息...')
|
||
|
url = 'https://marathon.jd.com/seckillnew/orderService/pc/init.action'
|
||
|
data = {
|
||
|
'sku': self.sku_id,
|
||
|
'num': self.seckill_num,
|
||
|
'isModifyAddress': 'false',
|
||
|
}
|
||
|
headers = {
|
||
|
'User-Agent': self.user_agent,
|
||
|
'Host': 'marathon.jd.com',
|
||
|
}
|
||
|
resp = self.session.post(url=url, data=data, headers=headers)
|
||
|
|
||
|
resp_json = None
|
||
|
try:
|
||
|
resp_json = parse_json(resp.text)
|
||
|
except Exception:
|
||
|
raise SKException('抢购失败,返回信息:{}'.format(resp.text))
|
||
|
|
||
|
return resp_json
|
||
|
|
||
|
def _get_seckill_order_data(self):
|
||
|
"""生成提交抢购订单所需的请求体参数
|
||
|
:return: 请求体参数组成的dict
|
||
|
"""
|
||
|
logger.info('生成提交抢购订单所需参数...')
|
||
|
# 获取用户秒杀初始化信息
|
||
|
self.seckill_init_info[self.sku_id] = self._get_seckill_init_info()
|
||
|
init_info = self.seckill_init_info.get(self.sku_id)
|
||
|
default_address = init_info['addressList'][0] # 默认地址dict
|
||
|
invoice_info = init_info.get('invoiceInfo', {}) # 默认发票信息dict, 有可能不返回
|
||
|
token = init_info['token']
|
||
|
data = {
|
||
|
'skuId': self.sku_id,
|
||
|
'num': self.seckill_num,
|
||
|
'addressId': default_address['id'],
|
||
|
'yuShou': 'true',
|
||
|
'isModifyAddress': 'false',
|
||
|
'name': default_address['name'],
|
||
|
'provinceId': default_address['provinceId'],
|
||
|
'cityId': default_address['cityId'],
|
||
|
'countyId': default_address['countyId'],
|
||
|
'townId': default_address['townId'],
|
||
|
'addressDetail': default_address['addressDetail'],
|
||
|
'mobile': default_address['mobile'],
|
||
|
'mobileKey': default_address['mobileKey'],
|
||
|
'email': default_address.get('email', ''),
|
||
|
'postCode': '',
|
||
|
'invoiceTitle': invoice_info.get('invoiceTitle', -1),
|
||
|
'invoiceCompanyName': '',
|
||
|
'invoiceContent': invoice_info.get('invoiceContentType', 1),
|
||
|
'invoiceTaxpayerNO': '',
|
||
|
'invoiceEmail': '',
|
||
|
'invoicePhone': invoice_info.get('invoicePhone', ''),
|
||
|
'invoicePhoneKey': invoice_info.get('invoicePhoneKey', ''),
|
||
|
'invoice': 'true' if invoice_info else 'false',
|
||
|
'password': global_config.get('account', 'payment_pwd'),
|
||
|
'codTimeType': 3,
|
||
|
'paymentType': 4,
|
||
|
'areaCode': '',
|
||
|
'overseas': 0,
|
||
|
'phone': '',
|
||
|
'eid': global_config.getRaw('config', 'eid'),
|
||
|
'fp': global_config.getRaw('config', 'fp'),
|
||
|
'token': token,
|
||
|
'pru': ''
|
||
|
}
|
||
|
|
||
|
return data
|
||
|
|
||
|
def submit_seckill_order(self):
|
||
|
"""提交抢购(秒杀)订单
|
||
|
:return: 抢购结果 True/False
|
||
|
"""
|
||
|
url = 'https://marathon.jd.com/seckillnew/orderService/pc/submitOrder.action'
|
||
|
payload = {
|
||
|
'skuId': self.sku_id,
|
||
|
}
|
||
|
try:
|
||
|
self.seckill_order_data[self.sku_id] = self._get_seckill_order_data()
|
||
|
except Exception as e:
|
||
|
logger.info('抢购失败,无法获取生成订单的基本信息,接口返回:【{}】'.format(str(e)))
|
||
|
return False
|
||
|
|
||
|
logger.info('提交抢购订单...')
|
||
|
headers = {
|
||
|
'User-Agent': self.user_agent,
|
||
|
'Host': 'marathon.jd.com',
|
||
|
'Referer': 'https://marathon.jd.com/seckill/seckill.action?skuId={0}&num={1}&rid={2}'.format(
|
||
|
self.sku_id, self.seckill_num, int(time.time())),
|
||
|
}
|
||
|
resp = self.session.post(
|
||
|
url=url,
|
||
|
params=payload,
|
||
|
data=self.seckill_order_data.get(
|
||
|
self.sku_id),
|
||
|
headers=headers)
|
||
|
resp_json = None
|
||
|
try:
|
||
|
resp_json = parse_json(resp.text)
|
||
|
except Exception as e:
|
||
|
logger.info('抢购失败,返回信息:{}'.format(resp.text[0: 128]))
|
||
|
return False
|
||
|
# 返回信息
|
||
|
# 抢购失败:
|
||
|
# {'errorMessage': '很遗憾没有抢到,再接再厉哦。', 'orderId': 0, 'resultCode': 60074, 'skuId': 0, 'success': False}
|
||
|
# {'errorMessage': '抱歉,您提交过快,请稍后再提交订单!', 'orderId': 0, 'resultCode': 60017, 'skuId': 0, 'success': False}
|
||
|
# {'errorMessage': '系统正在开小差,请重试~~', 'orderId': 0, 'resultCode': 90013, 'skuId': 0, 'success': False}
|
||
|
# 抢购成功:
|
||
|
# {"appUrl":"xxxxx","orderId":820227xxxxx,"pcUrl":"xxxxx","resultCode":0,"skuId":0,"success":true,"totalMoney":"xxxxx"}
|
||
|
if resp_json.get('success'):
|
||
|
order_id = resp_json.get('orderId')
|
||
|
total_money = resp_json.get('totalMoney')
|
||
|
pay_url = 'https:' + resp_json.get('pcUrl')
|
||
|
logger.info('抢购成功,订单号:{}, 总价:{}, 电脑端付款链接:{}'.format(order_id, total_money, pay_url))
|
||
|
if global_config.getRaw('messenger', 'enable') == 'true':
|
||
|
success_message = "抢购成功,订单号:{}, 总价:{}, 电脑端付款链接:{}".format(order_id, total_money, pay_url)
|
||
|
sendMessage(success_message)
|
||
|
return True
|
||
|
else:
|
||
|
logger.info('抢购失败,返回信息:{}'.format(resp_json))
|
||
|
if global_config.getRaw('messenger', 'enable') == 'true':
|
||
|
error_message = '抢购失败,返回信息:{}'.format(resp_json)
|
||
|
sendMessage(error_message)
|
||
|
return False
|
||
|
|
||
|
|