自动抢某投资公司的理财产品预购

朋友让帮忙做一个自动抢某投资公司的理财产品预购

主要学到两点:

  1. 使用的目标地址是移动端的,没有cookies。通过在HTTPS头中添加 Authorization 字段,值为:'Bearer ’ + token,来判断登录用户。大概过程如下:
    1. 对登录地址发出post,带上用户名和密码(json),会返回一个token。
    2. 预约产品时需要用到用户信息,所以要先获取用户信息:在HTTPS头中加上Authorization 字段,值为’Bearer ’ + token(token即为第1步登录后服务器返回的token值),对用获取用户信息的地址发出get。
    3. 预约产品:同样需要在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()
    
  2. 虽然个小公司,产品抢购人不会太多,每次需要帮忙抢的人也不多(每期最多也就10来人),其实不用并发也是可以的,估算了一下,10人也就1-2秒的事。但最终还是开发了并发的版本,正好拿来练手python asyncio aiohttp。
    1. 大概流程是使用通过读取用户信息文件(json格式),然后所有用户登录、获取信息、预约,这三个先后进程,就使用共发。
    2. 因为服务器使用https,aiohttp会提示SSL错误,只要在get、post中使用verify_ssl=False就可以了。
    3. 并发使用的是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('按任意键退出')

猜你喜欢

转载自blog.csdn.net/qq_41090453/article/details/106438920