zoukankan      html  css  js  c++  java
  • 四、Python-Redis、加密和解密、mock接口开发、网络请求

    (一)Redis

    1、数据库分为关系型数据库和非关系型数据库:

    (1)关系型数据库分为:MySQL、Oracle、SQL Server、SQLite...

      数据库

      表

      SQL语句

    (2)非关系型数据库分为:NoSQL(Redis、MongoDB...)

      key-value

      Redis:存在内存里面,做缓存用。

      MongoDB:放在磁盘里面。

    2、Redis(Remote Dictionary Server):(摘自:http://www.redis.cn/)

      Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

    3、Redis与其他key-value缓存产品的特点:

    (1)Redis支持数据的持久化,将内存中的数据保存在磁盘中,重启后再次可以加载使用;

    (2)Redis不仅支持简单的key-value类型的数据,还支持list、set、zset、hash数据结构存储;

    (3)Redis支持数据备份,master-slave模式的数据备份。

    4、根据自己的需求到官网下载安装:https://redis.io/download

    5、Redis 命令参考:http://doc.redisfans.com/

    6、数据结构:

    (1)String:字符串,最基本的数据类型,最大能存储512MB。

    (2)Hash:散列,是一个string类型的field和value的映射表,特别适合用于存储对象。(存储、读取、修改用户属性)

    (3)List:列表,是简单的字符串类别,按照插入顺序排序。

    (4)Set:集合,是string类型的无序集合,通过hash表实现的。

    (5)Sorted Set:有序集合,是string类型元素的集合,且不允许重复的成员。

    7、连接Redis:如图所示,填写自己的配置:

    (1)配置信息填写:

    (2)测试连接:

    (3)查看当前库里的信息,都是key-value 结构:

    8、使用Python来操作Redis-String类型,相当于字典类型

    (1)Python中使用connection连接Redis

    import redis
    r = redis.Redis(host='118.24.3.40',
                    password='HK139bc&*',
                    port=6379,
                    db=2,
                    decode_responses=True
                    )

    (2)Python中利用跳板机ssh远程连接redis

    from sshtunnel import SSHTunnelForwarder # ssh连接库
    import redis # redis模块
    server = SSHTunnelForwarder(
            ssh_address_or_host= , # ssh地址
            ssh_username=     , # ssh连接的用户名                    
            ssh_password=  ,  # ssh连接的用户名
            remote_bind_address=('远程机器地址', 端口号))
    
    server.start()
    r=redis.Redis(host='redis地址', port=server.local_bind_port, decode_responses=True)
    server.close() #关闭连接

    (3)string类型:添加数据

    import redis
    r = redis.Redis(host='118.24.3.40',
                    password='HK139bc&*',
                    port=6379,
                    db=2,
                    decode_responses=True
                    )
    #string
    r.set('iphone','{"price":6999,"count":50}')

    执行这个程序后,去Redis客户端查看是否有该数据存在:

    (4)string类型:获取数据

    r.set('students','{"name":John,"age":15,"hobby":"playing Games"}')
    print(r.get('students'))

      执行结果为:

    (5)string类型:删除数据

    import redis
    r = redis.Redis(host='118.24.3.40',
                    password='HK139bc&*',
                    port=6379,
                    db=2,
                    decode_responses=True
                    )
    r.set('矿泉水','{"price":10.5,"count":20}')
    data = r.get('矿泉水')
    r.delete('矿泉水')

    (6)string类型:修改数据

    import redis
    r = redis.Redis(host='118.24.3.40',
                    password='HK139bc&*',
                    port=6379,
                    db=2,
                    decode_responses=True
                    )
    r.set('矿泉水','{"price":10.5,"count":20}')
    r.set('矿泉水','xxxx')

    (7)bytes:字节类型(上传一个图片或者下载一个图片会遇到这个字节类型)

    # bytes类型通过wb写入
    f = open('a.jpg','wb')
    f.write('xxx')

             bytes类型和字符串类型相互转换

    data = r.get('矿泉水')
    data.decode()  # bytes类型转换成字符串
    s='hello'
    s.encode()     # 字符串转换成bytes类型

    (8)Python中设置过期时间:(指定某个key的过期时间)

    expire_time = 60 * 60 * 24
    r.set('Bob_session','sdgx312vsdrq',expire_time)

    Redis客户端过期时间显示:

     9、使用Python来操作Redis-hash类型,相当于二维字典

    (1)Hash添加数据

    r.hset('salary','Ann','{"money":2500}')
    r.hset('salary','Sarah','{"money":5500}')
    r.hset('salary','Sun','{"money":3860}')
    r.hset('salary','Beans','{"money":7800}')

      Redis客户端查看新增的数据:

    (2)Hash获取数据

    # 获取单个数据
    print(r.hget('salary','Ann'))
    
    # 获取所有的数据
    int(r.hgetall('salary'))

    (3)Hash-当含有bytes类型,需要变成正常的字典类型

    d = r.hgetall('students')
    new_d  = {}
    for k,v in d.items():
         new_d[k.decode()] = v.decode()
    print(new_d)

      从根源上解决Bytes类型问题:

    r = redis.Redis(host='118.24.3.40',
                    password='HK139bc&*',
                    port=6379,
                    db=2,
                    decode_responses=True     # 解决Bytes类型问题
                    )

    (4)Hash-修改数据

    (5)Hash-删除数据

    r.hdel('salary','Bob')

    (6)设置过期时间

    # 指定某个key的过期时间
    r.expire('salary',1000) 

    10、String和Hush(Redis客户端展示)

    (1)String

     (2)Hash

    (3)从Redis取数据:Key是什么,Key的类型是什么

    (4)删掉大Key值:

    r.delete('students')

    11、Redis其他命令:

    # 获取当前数据库有多少key
    print(r.keys())
    
    # 获取具体的某个开头的key进行匹配
    print(r.keys('s*'))
    
    #查看key的类型
    print(r.type('students'))
    
    # 清空当前数据库里面所有的key
    print(r.flushdb()) 
    
    # 清空所有数据库里面所有的key
    print(r.flushall())
    
    #判断Key存不存在
    print(r.exists('students'))    

    12、Redis-管道:批量操作

    (1)Python中建立管道,批量执行操作

    p= r.pipeline()  #建立管道
    
    p.hset('salary','Ann','{"money":2500}')
    p.hset('salary','Sarah','{"money":5500}')
    p.hset('salary','Sun','{"money":3860}')
    p.hset('salary','Beans','{"money":7800}')
    
    # 执行,返回一个list,这个list里面是每个命令执行的结果
    p.execute() 

    (2)使用管道和不用管道对比时间:

    import redis
    import time
    
    r = redis.Redis(host='118.24.3.40',
                    password='HK139bc&*',
                    port=6379,
                    db=2,
                    decode_responses=True
                    )
    # 不用管道:
    start_time =time.time()
    for i in range(100):
        r.set('key%s'%i,'%s'%i)
    print('不用管道的时间',time.time() - start_time)
    
    # 使用管道:
    start_time =time.time()
    p = r.pipeline()
    for i in range(100):
        p.set('pipeline_key%s'%i,'%s'%i)
    p.execute()
    print('用管道的时间',time.time() - start_time)

    执行最后的结果为:

    13、Python里新增数据带有文件夹,key里有冒号:就会有文件夹

    r.set('product:water','{"count":1,"price":5}')
    r.set('product:apple','{"count":1,"price":15}')
    r.set('product:melon','{"count":1,"price":35}')

      在Redis客户端中可以查看到添加的文件夹里数据信息

    14、当前服务器需要向另外一个服务器进行Redis数据迁移

    编程分析:

    r = redis.Redis(host='118.24.3.40',
                     password='HK139bc&*',
                     port=6379,
                     db=14,
                     decode_responses=True
                     )
    
    r2 = redis.Redis(host='118.24.3.40',
                     password='HK139bc&*',
                     port=6378,
                     db=14,
                     decode_responses=True
                     )
    p = r2.pipeline()
    
    for k in r.keys():
         key_type = r.type(k)
         if key_type == 'string':
             value = r.get(k)
             p.set(k,value)
         elif key_type =='hash':
             hash_data = r.hgetall(k) # {'XX':XXX}
             for field,data in hash_data.items():
                 p.hset(k,field,data)
    p.execute()

    15、若需要多个数据库操作,需要循环数据库:(一般用不到)

    for i in range(17):
        r = redis.Redis(host='118.24.3.40',
                        password='HK139bc&*',
                        port=6379,
                        db=i,
                        decode_responses=True
                        )
        r2 = redis.Redis(host='118.24.3.40',
                        password='HK139bc&*',
                        port=6379,
                        db=i,
                        decode_responses=True
                        )

    (二)加密和解密

    1、加密,md5是一种不可逆,同时也是常见的加密方式,需要导入hashlib模块:

    (1)实例1代码:

    import hashlib
    
    s = '123456'
    
    # 对字符串进行加密
    m = hashlib.md5(s) 
    
     # 加密后的结果用二进制表示 
    result = m.hexdigest() 
    print(result)

    (2)执行结果报错,这是因为MD5必须传Bytes类型的:hashlib.md5()

     (3)解决方法:将字符串转换成bytes类型

    #导入hashlib模块
    import hashlib
    
    # 定义字符串
    s = '123456'
    
    # 将字符串转换成bytes类型
    s =s.encode()
    
    # 对字符串进行加密
    m = hashlib.md5(s) 
    
    # 加密后的结果用二进制表示 
    result = m.hexdigest()  
    print(result)

    执行结果为:

    (4)同样的字符串,MD5出来的结果都一样

    (5)加盐,例如设置用户表

    import hashlib
    
    # 密码+随机字符串(随机字符串就叫盐值)
    s = '123=123' + 'djsjks51@#$$' 
    
    # 转换成bytes
    s = s.encode()   
    m = hashlib.md5(s)  # bytes,不可逆
    result = m.hexdigest()
    print(result)

    (6)定义函数

    def my_md5(s):
        s = str(s)
        s = s.encode()
        m = hashlib.md5(s)
        result = m.hexdigest()
        return result

    2、加密主要有两种方式:对称加密和非对称加密

    (1)对称加密:对称加密算法在加密和解密时使用的是同一个秘钥。

    对称加密的模式是:甲方选择某一种加密规则,对信息进行加密;乙方使用同一种规则,对信息进行解密;

    客户端和服务端进行通信,采用对称加密,若使用同一个秘钥很容易破解,若使用不同的秘钥,秘钥的管理和传输成本又较高;

    (2)非对称加密:非对称加密算法需要两个秘钥来进行加密和解密,分别为:公开密码(公钥)和私有秘钥(私钥)。

    乙方生成两把密钥(公钥和私钥)。公钥是公开的,任何人都可以获得,私钥是保密的。

    甲方获取乙方的公钥,然后用它对信息加密。

    (3)http和https

    常规的http请求,明文传播;

    https可以认为是:http + TLS ,TLS是传输层加密协议,前身是SSL协议

    3、解密(撞库解密)

    (1)拖库与撞库:

     拖库是指黑客盗取了网站的数据库。撞库是指黑客用拖库获得的用户名和密码在其它网站批量尝试登陆,进而盗取更有价值的东西。由于一些用户在多个网站用相同的用户名和密码,所以撞库是有一定成功率的。要想撞库,必须得知道密码的明文,也就是用户真正输入的密码。

    (2)MD5实际上只能破解比较简单的密码

     4、Base64加密:是可以破解的

    (1)例如百度搜索“动画片”url中里面是转译密文

    https://www.baidu.com/s?wd=%E5%8A%A8%E7%94%BB%E7%89%87&rsv_spt=1&rsv_iqid=0xcc0d32ae00192d4f&issp=1&f=8&rsv_bp=1&rsv_idx=2&ie=utf-8&rqlang=cn&tn=baiduhome_pg&rsv_enter=0&rsv_dl=tb&oq=%25E5%258A%25A0%25E5%25AF%2586%25E6%2592%259E%25E5%25BA%2593&rsv_t=298fJqJmONwDQA1vi18GP009wC%2Fh67nlOij54DtqzQ7X6efhzWNd7AcKIyB6FO0SBNyM&rsv_btype=t&inputT=10055&rsv_pq=b07987030011a206&rsv_sug3=80&rsv_sug1=32&rsv_sug7=101&bs=%E5%8A%A0%E5%AF%86%E6%92%9E%E5%BA%93

    (2)Base64使用:

    import base64
    # 一般用于传输数据过程中
    # 加密
    s = 'sjfkjdkjxh1145646你好'
    r = base64.b64encode(s.encode())
    result =r.decode()
    print(result)
    
    # 解密
    r = base64.b64decode('c2pma2pka2p4aDExNDU2NDbkvaDlpb0=')
    print(r.decode)

    (三)mock接口开发:开发接口,模拟数据

    1、Python中可安装flash模块或fastapi模块:

    # 建议安装FastAPI接口
    pip install fastapi
    
    # 若安装了FastAPI,还需要安装一个ASGI服务器,用于生产如Uvicorn或Hypercorn
    pip install uvicorn
    
    # 或者安装Flask
    pip install Flask

    2、FastAPI (摘自:https://fastapi.tiangolo.com/)

    FastAPI是一种现代、快速(高性能)的Web框架,快速编码,更少的错误,直观简易,短而健壮,基于API的开放标准:OpenAPI(旧称Swagger)和JSON Schema。

    3、如何mock接口开发:

    (1)例如银行姓名需要查询产品、开户接口

    # Product 
    {
    "code":0,
    "data":[
    {"id":1,"product_name":"XXX"},
    {"id":2,"product_name":"XXX"},
    {"id":3,"product_name":"XXX"},
    {"id":4,"product_name":"XXX"},
    {"id":5,"product_name":"XXX"}
    ]
    }

    (2)使用fastapi编写接口

    import fastapi   # 自动生成一个API接口文档
    import uvicorn
    
    # 启动一个服务
    server = fastapi.FastAPI()
    
    @server.get('/login')
    def login(username:str,password:str):
        return {'username':username,'password':password}
    uvicorn.run(server,port=8800,debug=True)

    执行结果如下:

     (3)复制url在浏览器打开,添加参数:http://127.0.0.1:8800/login

     (4)在http://127.0.0.1:8800/login,再次补充其他参数:http://127.0.0.1:8800/login?username=Ann&password=123456

     (5)定义一个不加参数的接口:

    @server.get('/test')
    def test():
        return {'msg':'hello world!'}
    uvicorn.run(server,port=8800,debug=True)

      在浏览器打开输入url:http://127.0.0.1:8800/test,就可以看到无参数的接口信息

     (6)查看接口文档:http://127.0.0.1:8800/docs(根据设置的端口进行查看)

     (7)在接口文档中修改参数进行执行

     执行结果如下:

    (8)定义一个product接口:

    import fastapi
    import uvicorn
    server = fastapi.FastAPI()
    
    @server.get('/product')
    def test():
        return {
            'code':0,
            'data':[
                {'product_name': '基金1号', 'status': 0},
                {'product_name': '基金2号', 'status': 2},
                {'product_name': '基金3号', 'status': 1},
                {'product_name': '基金4号', 'status': 0},
                {'product_name': '基金5号', 'status': 1}
            ]
        }

    执行结果是:

       

    注: http://127.0.0.1指的是本机的IP地址

    若想别人访问你的地址需要加上host:,例如:

    # 自己访问自己,无需加host
    
    uvicorn.run(server,port=8800,debug=True)
    
    # 若别人访问你的地址,需要加上host
    
    uvicorn.run(server,port=8800,debug=True,host='0.0.0.0')
    
    # http://127.0.0.1:8800/product
    # http://192.168.1.xx:8800/product

     (9)定义一个支付pay的接口

    import fastapi
    import uvicorn
    server = fastapi.FastAPI()
    
    @server.get('/pay')
    def pay(money:float,status:str):
        if status == '0':
            return {'code':1, 'status':'fail'}
        elif status=='1':
            return {'code':0, 'status':'success','balance':money}
    
    uvicorn.run(server,port=8800,debug=True)

    打开浏览器,url中输入:http://127.0.0.1:8800/pay?money=1&status=0,如图所示:

      

    pay接口中,status加上默认值1:

    @server.get('/pay')
    
    # status加上默认值1
    def pay(money:float,status='1'):
        if status == '0':
            return {'code':1, 'status':'fail'}
        elif status=='1':
            return {'code':0, 'status':'success','balance':money}
    
    uvicorn.run(server,port=8800,debug=True)

    接口文档中显示默认值1:

      (10)定义一个注册接口

    @server.post('/register')
    def register(username:str,password:str,cpassword:str):
        if username.strip() and password.strip() and cpassword.strip():
            if password.strip() != cpassword.strip():
                return {'code':-1, 'msg':'两次输入的密码不一致!'}
            else:
                sql = 'select * from app_myuser where username="%s";'%username
                if tools.execute_sql(sql):
                    return {'code':-1, 'msg':'用户已经存在!'}
                else:
                    p = tools.my_md5(password)
                    insert_sql = 'insert into app_myuser (username,password) values ("%s","%s");'%(username,p)
                    tools.execute_sql(insert_sql)
                    return {'code':0, 'msg':'注册成功!'}
        else:
            return {'code':-1, 'msg':'必填参数不能为空!'}

    前往postman应用中,进行接口测试:

     (11)定义一个登录接口:

    @server.post('/login')
    def login(username:str=Form(...),password:str=Form(...)):
        if username.strip() and password.strip():
            p = tools.my_md5(password)
            sql = 'select * from app_myuser where username="%s" and password="%s";' % (username,p)
            if tools.execute_sql(sql):
                return {'code': 0, 'msg': '登录成功!'}
            else:
                return {'code':-1, 'msg':'输入的账号/密码不存在!'}
        else:
            return {'code':-1, 'msg':'必填参数不能为空!'}

     4、flask:开发接口

    (1)安装flask模块:

    pip install flask

    (2)用flask定义一个登录接口:

    # flask 是一个轻量级web开发框架
    import flask
    
    import tools
    
    # 因为flask返回的是一个字符串,故需要导入JSON模块
    import json
    server = flask.Flask(__name__)
    
    
    @server.route('/login',methods=['post','get'])
    def login():
        username = flask.request.values.get('username')
        password = flask.request.values.get('password')
    
        # flask.json.get('xxxx')           # 如果入参是json类型的话,使用这样的方式
        # flask.request.cookies.get('xxx') # 获取cookie里面的数据
        # flask.request.headers.get('xx')
        # flask.request.files.get("xxx")   # 文件
    
        if username.strip() and password.strip():
            p = tools.my_md5(password)
            query_sql = 'select * from app_myuser where username= "%s" and passwd="%s";' % (username, p)
            print(query_sql)
            if tools.execute_sql(query_sql):
                return json.dumps({'code': '0', 'msg': '登录成功'},ensure_ascii=False)
            else:
                return json.dumps({'code': '-1', 'msg': '输入的用户名/密码错误'})
        else:
            return json.dumps({'code': '-1', 'msg': '不能为空'})


    server.run(host='0.0.0.0',port=8999,debug=True)

    (3)定义一个注册接口:

    import flask
    
    import tools
    
    # 因为flask返回的是一个字符串,故需要导入JSON模块
    import json
    server = flask.Flask(__name__)
    
    @server.route('/reg',methods=['post','get'])
    def reg():
        username = flask.request.values.get('username')
        password = flask.request.values.get('password')
        cpassword = flask.request.values.get('cpassword')
        if username.strip() and password.strip() and cpassword.strip():
            if password.strip() != cpassword.strip():
                return json.dumps({'code': -1, 'msg': '两次输入的密码不一样'})
            else:
                sql='select * from app_myuser where username="%s";'%username
                if tools.execute_sql(sql):
                    return json.dumps({'code':-1,'msg':'用户已经存在'})
                else:
                    p = tools.my_md5(password)
                    insert_sql = 'insert into app_myuser (username,passwd) value ("%s","%s");'%(username,p)
                    tools.execute_sql(insert_sql)
                    return json.dumps({'code':0,'msg':'注册成功!'},ensure_ascii=False)
    
        else:
            return json.dumps({'code':-1,'msg':'必填参数不能为空'})
    
    
    server.run(host='0.0.0.0',port=8999,debug=True)

    (四)网络请求:做接口自动化

    1、Python自带的模块发送网络请求:urllib

    (1)安装urllib模块

    import urllib

    (2)urllib模块:get请求

    from urllib import request
    import json
    
    url = 'http://127.0.0.1:8999/login?username=Bob&password=123456'
    
    # get请求
    req = request.urlopen(url)
    
    dic = json.loads(req.read().decode())
    
    print(dic)

    (3)urllib模块:post请求

    # post请求
    from urllib import request
    from urllib.parse import urlencodeimport json
    
    url = 'http://127.0.0.1:8999/login'
    data ={'username':'Ann01','password':'123456'}
    req = request.urlopen(url,urlencode(data).encode())
    dic = json.loads(req.read().decode())
    print(dic)

    2、requests模块

    (1)接口测试的请求方式:get、post、传cookie、传文件、传json、传headers

    (2)安装requests模块

    pip install requests

    (3)使用requests查看请求

    import requests
    import json
    
    url = 'http://127.0.0.1:8999/login'
    data ={'username':'Ann01','password':'123456'}
    
    r = requests.get(url,data)
    
    # 字典类型
    print(r.json)
    
    # 字符串格式
    print(r.text)
    
    # bytes类型的
    print(r.content)
    
    #返回的状态码:200、404、500、502
    print(r.status_code)

    (4)get请求方式(根据使用情况选择不同的方法):

    # 字典格式,为了获取某个字段的值:d.get('XXX')
    r.json
    
    # 字符串格式:接口返回结果不处理,直接存入库里或者json文件里面
    r.text
    
    # bytes类型,例如下载一个图片
    r.content
    
    #返回的状态码
    r.status_code

    (5)post请求方式:

    r = requests.post(url,data)
    
    print(r.json())
    
    print(r.text)
    
    print(r.content)
    
    print(r.status_code)

    (6)post请求url中,有时会遇到url带有参数,这时候需要:

    r = requests.post(url,data,params={"version":1.0)
    # params是把参数传到url后头的

    (7)传cookie:

    # url=http://www.nnzhp.cn/
    # cookie:PHPSESSID=8a97d6ac860319bf067a674b8b5a8e34
    
    cookie = {'PHPSESSID':'8a97d6ac860319bf067a674b8b5a8e34'}
    r = requests.post(url,data=data,params={"version":1.0},cookies=cookie)

    (8)传headers:

    headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36','cookie':'PHPSESSID':'8a97d6ac860319bf067a674b8b5a8e34'}
    r = requests.post(url,data=data,params={"version":1.0},headers=headers)

    (9)传json和传文件

    # 传json
    
    r = requests.post(url,json=data)
    print(r.json())
    
    # 传文件
    
    url = 'http://api.nnzhp.cn/api/file/file_upload'
    data = {'file':open('XXX.xls','rb')} 
    r = requests.post(url,files=data)

    (10)下载文件

    #下载文件
    
    r = requests.get('https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3086278442,1750390944&fm=26&gp=0.jpg')
    
    f = open('dog.jpg','wb')
    print(r.content)
    f.close()

    注:有时会遇到https的报错,这时需要:

    r = requests.get('https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3086278442,1750390944&fm=26&gp=0.jpg',verify=False)
    温故而知新
  • 相关阅读:
    SSL JudgeOnline 1194——最佳乘车
    SSL JudgeOnline 1457——翻币问题
    SSL JudgeOnlie 2324——细胞问题
    SSL JudgeOnline 1456——骑士旅行
    SSL JudgeOnline 1455——电子老鼠闯迷宫
    SSL JudgeOnline 2253——新型计算器
    SSL JudgeOnline 1198——求逆序对数
    SSL JudgeOnline 1099——USACO 1.4 母亲的牛奶
    SSL JudgeOnline 1668——小车载人问题
    SSL JudgeOnline 1089——USACO 1.2 方块转换
  • 原文地址:https://www.cnblogs.com/krystal-xiao/p/13667628.html
Copyright © 2011-2022 走看看