朋友让帮忙做一个自动抢某投资公司的理财产品预购
主要学到两点:
- 使用的目标地址是移动端的,没有cookies。通过在HTTPS头中添加 Authorization 字段,值为:'Bearer ’ + token,来判断登录用户。大概过程如下:
- 对登录地址发出post,带上用户名和密码(json),会返回一个token。
- 预约产品时需要用到用户信息,所以要先获取用户信息:在HTTPS头中加上Authorization 字段,值为’Bearer ’ + token(token即为第1步登录后服务器返回的token值),对用获取用户信息的地址发出get。
- 预约产品:同样需要在HTTPS头中带上token,并将用户信息和理财产品的id构造一个json data,并发出post。
headers = { 'Authorization': 'Bearer ' + token, #token添加在这里 'Host': 'api.***.com', 'Accept': 'application/json, text/plain, */*', 'Content-Type': 'application/json;charset=UTF-8', 'Referer': 'https://m.***.com', 'Origin': 'https://m.***.com', 'User-Agent': self._get_user_agent() } async with aiohttp.ClientSession() as session: async with session.post(self.urls['order'], json=data, headers=self.headers, verify_ssl=False, timeout=self.time_out) as response: if response.status == 200: return await response.json()
- 虽然个小公司,产品抢购人不会太多,每次需要帮忙抢的人也不多(每期最多也就10来人),其实不用并发也是可以的,估算了一下,10人也就1-2秒的事。但最终还是开发了并发的版本,正好拿来练手python asyncio aiohttp。
- 大概流程是使用通过读取用户信息文件(json格式),然后所有用户登录、获取信息、预约,这三个先后进程,就使用共发。
- 因为服务器使用https,aiohttp会提示SSL错误,只要在get、post中使用verify_ssl=False就可以了。
- 并发使用的是asyncio.gather(),把所有的任务都封装成task并发运行了。但是在处理超时重发的情况下突然懵了,不知在并发的情况下如何再次发起。起初就想着怎么再把超时的请求重新放到asyncio.loop中,但是怎么都没有一个比较好的法子去获得结果。后来直接就在处理超时异常(aiohttp.ServerTimeoutError)时,再一次await 本身这个异步函数。
完整代码如下:
user.py
# -*- coding: utf-8 -*-
import random
import aiohttp
import asyncio
class User:
"""
用户
number:注册的手机号码
pwd:密码
order_amt:预约金额
urls: url列表:登录url,获取用户信息url,提交预约url
semaphore: 并发数
time_out: 超时
retry_times: 重试次数
"""
def __init__(self, number, pwd, order_amt, urls, semaphore, time_out, retry_times):
self.number = number
self.pwd = pwd
self.order_amt = order_amt
self.urls = urls
self.semaphore = semaphore
self.time_out = time_out
self.RETRY_TIMES = retry_times
self.retry = retry_times
self.token = None
self.login_msg = ''
self.headers = {
}
self.msg = {
}
async def login(self):
"""用户登录"""
self.retry -= 1
async with self.semaphore:
try:
async with aiohttp.ClientSession() as session:
async with session.post(self.urls['login'], json={
'phoneNo': self.number, 'password': self.pwd},
verify_ssl=False, timeout=self.time_out) as response:
if response.status == 200:
return await response.json()
else:
return {
'msg': 'response_status {}'.format(response.status)}
except aiohttp.ServerTimeoutError as e:
retry_msg = '{} 重试登录'.format(self.number)
false_msg = '{} 登录失败'.format(self.number)
await self._handle_timeout(self.login, self.parse_login, retry_msg, false_msg, e)
except Exception as e:
raise e
def parse_login(self, task):
"""解析登录结果,获取token,并构造headers"""
self.retry = self.RETRY_TIMES
data = task.result()
self.login_msg = data.get('msg', '')
if self.login_msg == '登录成功':
self.token = data['data']['token']
self.make_headers()
def _get_user_agent(self):
"""返回随机User-Agent"""
USER_AGENT_LIST = [
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36 OPR/26.0.1656.60",
"Opera/8.0 (Windows NT 5.1; U; en)",
"Mozilla/5.0 (Windows NT 5.1; U; en; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 Opera 9.50",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; en) Opera 9.50",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0",
"Mozilla/5.0 (X11; U; Linux x86_64; zh-CN; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.57.2 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.133 Safari/534.16",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.11 TaoBrowser/2.0 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.71 Safari/537.1 LBBROWSER",
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; LBBROWSER) ",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E; LBBROWSER)",
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; QQBrowser/7.0.3698.400)"
]
return random.choice(USER_AGENT_LIST)
def make_headers(self):
"""构造requests的HTML头部信息,主要在于添加token"""
if self.token:
self.headers = {
'Authorization': 'Bearer ' + self.token,
'Host': 'api.***.com',
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json;charset=UTF-8',
'Referer': 'https://m.***.com',
'Origin': 'https://m.***.com',
'User-Agent': self._get_user_agent()
}
async def get_msg(self):
"""通过服务器获取用户信息"""
if self.headers:
self.retry -= 1
async with self.semaphore:
try:
async with aiohttp.ClientSession() as session:
async with session.get(self.urls['getuser'], headers=self.headers,
verify_ssl=False, timeout=self.time_out) as response:
if response.status == 200:
return await response.json()
else:
return {
'msg': 'response_status {}'.format(response.status)}
except aiohttp.ServerTimeoutError as e:
retry_msg = '{} 重试读取用户信息'.format(self.number)
false_msg = '{} 读取用户信息失败'.format(self.number)
await self._handle_timeout(self.get_msg, self.parse_msg, retry_msg, false_msg, e)
except Exception as e:
raise e
def parse_msg(self, task):
"""解析用户信息"""
self.retry = self.RETRY_TIMES
data = task.result()
msg = data['msg']
if msg == 'success':
self.msg = data['data']
else:
print('get_user_msg server back_error: ', self.number, msg)
raise Exception('get_user_msg server back_error: ', self.number, msg)
async def order(self, product_id):
"""认购"""
if self.headers and self.msg:
self.retry -= 1
data = {
"productId": product_id,
"phoneNo": self.msg.get('phoneNo', ''),
"certNo": self.msg.get('certNo', ''),
"preOrderAmt": self.order_amt,
"bankInfo": self.msg.get('bankInfo', ''),
"bankCode": self.msg.get('bankCode', ''),
"introducer": self.msg.get('introducer', ''),
"custType": self.msg.get('custType', '')
}
async with self.semaphore:
try:
async with aiohttp.ClientSession() as session:
async with session.post(self.urls['order'], json=data, headers=self.headers,
verify_ssl=False, timeout=self.time_out) as response:
if response.status == 200:
return await response.json()
else:
return {
'msg': 'response_status {}'.format(response.status)}
except aiohttp.ServerTimeoutError as e:
retry_msg = '{} 重试预约'.format(self.number)
false_msg = '{} 预约失败'.format(self.number)
await self._handle_timeout(self.order, self.parse_order, retry_msg, false_msg, e, product_id)
except Exception as e:
raise e
else:
print('{} 认购失败:未登录或未获取用户信息'.format(self.number))
raise Exception('{} 认购失败:未登录或未获取用户信息'.format(self.number))
def parse_order(self, task):
"""解析并打印预约结果"""
self.retry = self.RETRY_TIMES
data = task.result()
print('{} 结果:{}'.format(self.msg['custName'], data['msg']))
async def _handle_timeout(self, func, callback, retry_msg, false_msg, e, para=None):
if self.retry:
print(retry_msg)
if para:
task = asyncio.create_task(func(para))
else:
task = asyncio.create_task(func())
task.add_done_callback(callback)
await task
else:
print(false_msg)
self.retry = self.RETRY_TIMES
raise e
users.py
# -*- coding: utf-8 -*-
import asyncio
from pprint import pp
from .user import User
class Users:
"""
用户集合类
config: 配置信息:超时、重试次数、并发数、用户信息等
"""
def __init__(self, config):
self.users = []
self.logined, self.not_logined_msg = [], []
time_out = config['time_out']
retry_times = config['retry_times']
semaphore = asyncio.Semaphore(config['concurrent_num'])
for u in config['users']:
user = User(u['number'], u['pwd'], u['order_amt'], config['urls'], semaphore, time_out, retry_times)
self.users.append(user)
async def login(self):
"""登录所有用户,并分离登录成功或未成功的用户"""
tasks = []
for user in self.users:
task = asyncio.create_task(user.login())
task.add_done_callback(user.parse_login)
tasks.append(task)
await asyncio.gather(*tasks)
self.logined = [user for user in self.users if user.token]
self.not_logined_msg = ['{}: {}'.format(user.number, user.login_msg) for user in self.users if
user.token is None]
if self.not_logined_msg:
print('\n登录失败的用户:\n\t{}'.format('\n\t'.join(self.not_logined_msg)))
async def get_logined_users_msg(self):
"""获取所有已登录用户在服务器上的信息"""
tasks = []
for user in self.logined:
task = asyncio.create_task(user.get_msg())
task.add_done_callback(user.parse_msg)
tasks.append(task)
await asyncio.gather(*tasks)
def show_logined_users(self):
"""显示已登录用户的信息"""
print('已登录用户:{} 位'.format(len(self.logined)))
for user in self.logined:
pp(user.msg)
print('\n')
async def order(self, product_id):
"""已登录的用户预约产品"""
tasks = []
for user in self.logined:
task = asyncio.create_task(user.order(product_id))
task.add_done_callback(user.parse_order)
tasks.append(task)
await asyncio.gather(*tasks)
orderer.py
# -*- coding: utf-8 -*-
from .users import Users
class Orderer:
"""
预约抢购器类
"""
def __init__(self, config):
self.product_id = ''
self.users = Users(config)
async def go(self):
"""初始化所有用户:登录、获取用户服务器上的信息"""
await self.users.login()
await self.users.get_logined_users_msg()
await self.run()
async def run(self):
"""预约抢购器主程序"""
while True:
print('\n操作选项:\n\t[1]-显示已登录用户信息\n\t[2]-输入产品ID\n\t[3]-开始预约 ({})\n\t[4]-退出'.format(self.product_id))
choice = input('请选择操作:')
if choice == '1':
self.users.show_logined_users()
elif choice == '2':
self.product_id = input('请输入产品ID: ')
elif choice == '3':
if not self.product_id:
print('\n*** 请先输入产品ID ***')
else:
await self.users.order(self.product_id)
elif choice == '4':
break
else:
print('\n*** 无效操作 ***')
main.py
# -*- coding: utf-8 -*-
"""
投资产品预定抢购 V3
2020-05-27
增加: 请求超时(5秒)重试
2020-05-22
修改: 并发抢购
修改: 通过配置文件读取用户信息、urls、并发数
"""
import os
import asyncio
from orderer import Orderer
from tools import init_logger, get_config
logger = init_logger('Production')
config_file = os.path.join(os.path.dirname(__file__), 'config.json')
if __name__ == '__main__':
try:
config = get_config(config_file)
order = Orderer(config)
asyncio.run(order.go())
except Exception as e:
logger.error(e, exc_info=True)
print(e)
input('按任意键退出')