您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > Python

Python异步编程全攻略

时间:2022-07-18 13:57:48  来源:  作者:邓big胖

Python/ target=_blank class=infotextkey>Python异步编程全攻略

如果你厌倦了多线程,不妨试试python的异步编程,再引入async, awAIt关键字之后语法变得更加简洁和直观,又经过几年的生态发展,现在是一个很不错的并发模型。

下面介绍一下python异步编程的方方面面。

在python异步编程中,可能出现很多其他的对象,比如Future, Task, 后者继承自前者,但是为了统一,无论是Future还是Task,本文中统一称呼为协程。

与多线程的比较

因为GIL的存在,所以Python的多线程在CPU密集的任务下显得无力,但是对于IO密集的任务,多线程还是足以发挥多线程的优势的,而异步也是为了应对IO密集的任务,所以两者是一个可以相互替代的方案,因为设计的不同,理论上异步要比多线程快,因为异步的花销更少, 因为不需要额外系统申请额外的内存,而线程的创建跟系统有关,需要分配一定量的内存,一般是几兆,比如linux默认是8MB。

虽然异步很好,比如可以使用更少的内存,比如更好地控制并发(也许你并不这么认为:))。但是由于async/await 语法的存在导致与之前的语法有些割裂,所以需要适配,需要付出额外的努力,再者就是生态远远没有同步编程强大,比如很多库还不支持异步,所以你需要一些额外的适配。

用于测试的web服务

为了不给其他网站带来困扰,这里首先在自己电脑启动web服务用于测试,代码很简单。

# web.py
import asyncio
from random import random

import uvicorn
from fastapi import FastAPI

App = FastAPI()


@app.get("/")
async def index():
    await asyncio.sleep(1)
    return {"msg": "ok"}


@app.get("/random")
async def index():
    await asyncio.sleep(1)
    return {"msg": random()}


if __name__ == "__main__":
    # uvicorn.run(app)
    # 如果需要热加载(reload), 需要传入一个字符串而不是application对象
    uvicorn.run("web:app", reload=True)import asyncio

import uvicorn
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def index():
    await asyncio.sleep(1)
    return {"msg": "ok"}



if __name__ == "__main__":
    uvicorn.run(app)

本文所有依赖如下:

  • Python > 3.7+
  • fastapi
  • aiohttp
  • uvicorn

所有依赖可通过代码仓库的requirements.txt一次性安装。

pip install requirements.txt

并发,并发,并发

首先看一个错误的例子

# test1.py
import asyncio
from datetime import datetime
import aiohttp

async def main(workers: int, url: str):
    async with aiohttp.ClientSession() as sess:
        for _ in range(workers):
            async with sess.get(url) as resp:
                print("响应内容", await resp.json())


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    start = datetime.now()
    loop.run_until_complete(main(3, "http://127.0.0.1:8000/"))
    end = datetime.now()
    print("耗时:", end - start)

输出如下:

$ python test1.py
响应内容 {'msg': 'ok'}
响应内容 {'msg': 'ok'}
响应内容 {'msg': 'ok'}
耗时: 0:00:03.011565

发现花费了3秒,不符合预期呀。。。。这是因为虽然用了协程,但是每个协程是串行的运行,也就是说后一个等前一个完成之后才开始,那么这样的异步代码并没有并发,所以我们需要让这些协程并行起来

# test2.py
import asyncio
from datetime import datetime
import aiohttp


async def run(sess: aiohttp.ClientSession, url: str):
    async with sess.get(url) as resp:
        print("响应内容", await resp.json())


async def main(workers: int, url: str):
    async with aiohttp.ClientSession() as sess:
        for _ in range(workers):
            asyncio.ensure_future(run(sess, url))
        await asyncio.sleep(1.1)

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    start = datetime.now()
    loop.run_until_complete(main(3, "http://127.0.0.1:8000/"))
    end = datetime.now()
    print("耗时:", end - start)

为了让代码变动的不是太多,所以这里用了一个笨办法来等待所有任务完成, 之所以在main函数中等待是为了不让ClientSession关闭, 如果你移除了main函数中的等待代码会发现报告异常RuntimeError: Session is closed,而代码里的解决方案非常的不优雅,需要手动的等待,为了解决这个问题,我们再次改进代码。

# test3.py
import asyncio
from datetime import datetime
import aiohttp


async def run(sess: aiohttp.ClientSession, url: str):
    async with sess.get(url) as resp:
        print("响应内容", await resp.json())


async def main(workers: int, url: str):
    async with aiohttp.ClientSession() as sess:
        futures = []
        for _ in range(workers):
            futures.append(asyncio.ensure_future(run(sess, url)))
        
        done, pending = await asyncio.wait(futures)
        print(done, pending)


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    start = datetime.now()
    loop.run_until_complete(main(3, "http://127.0.0.1:8000/"))
    end = datetime.now()
    print("耗时:", end - start)

这里解决的方式是通过asyncio.wait方法等待一个协程列表,默认是等待所有协程结束后返回,会返回一个完成(done)列表,以及一个待办(pending)列表。

如果我们不想要协程对象而是结果,那么我们可以使用asyncio.gather

# test4.py
import asyncio
from datetime import datetime
import aiohttp


async def run(sess: aiohttp.ClientSession, url: str, id: int):
    async with sess.get(url) as resp:
        print("响应内容", await resp.json())
        return id


async def main(workers: int, url: str):
    async with aiohttp.ClientSession() as sess:
        futures = []
        for i in range(workers):
            futures.append(asyncio.ensure_future(run(sess, url, i)))
        
        # 注意: 这里要讲列表解开
        rets = await asyncio.gather(*futures)
        print(rets)


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    start = datetime.now()
    loop.run_until_complete(main(3, "http://127.0.0.1:8000/"))
    end = datetime.now()
    print("耗时:", end - start)

结果输出如下:

$ python test4.py
响应内容 {'msg': 'ok'}
响应内容 {'msg': 'ok'}
响应内容 {'msg': 'ok'}
[0, 1, 2]
耗时: 0:00:01.011840

小结

通过asyncio.ensure_future我们就能创建一个协程,跟调用一个函数差别不大,为了等待所有任务完成之后退出,我们需要使用asyncio.wait等方法来等待,如果只想要协程输出的结果,我们可以使用asyncio.gather来获取结果。

同步

虽然前面能够随心所欲的创建协程,但是就像多线程一样,我们也需要处理协程之间的同步问题,为了保持语法及使用情况的一致,多线程中用到的同步功能,asyncio中基本也能找到, 并且用法基本一致,不一致的地方主要是需要用异步的关键字,比如async with/ await等

锁 lock

通过锁让并发慢下来,让协程一个一个的运行。

# test5.py
import asyncio
from datetime import datetime
import aiohttp


lock = asyncio.Lock()

async def run(sess: aiohttp.ClientSession, url: str, id: int):
    async with lock:
        async with sess.get(url) as resp:
            print("响应内容", await resp.json())
            return id

async def main(workers: int, url: str):
    async with aiohttp.ClientSession() as sess:
        futures = []
        for i in range(workers):
            futures.append(asyncio.ensure_future(run(sess, url, i)))
        
        # 注意: 这里要讲列表解开
        rets = await asyncio.gather(*futures)
        print(rets)

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    start = datetime.now()
    loop.run_until_complete(main(3, "http://127.0.0.1:8000/"))
    end = datetime.now()
    print("耗时:", end - start)

输出如下:

$ python test5.py
响应内容 {'msg': 'ok'}
响应内容 {'msg': 'ok'}
响应内容 {'msg': 'ok'}
[0, 1, 2]
耗时: 0:00:03.007251

通过观察很容易发现,并发的速度因为锁而慢下来了,因为每次只有一个协程能获得锁,所以并发变成了串行。

事件 event

通过事件来通知特定的协程开始工作,假设有一个任务是根据http响应结果选择是否激活。

# test6.py
import asyncio
from datetime import datetime
import aiohttp


big_event = asyncio.Event()
small_event = asyncio.Event()


async def big_waiter():
    await small_event.wait()
    print(f"{datetime.now()} big waiter 收到任务事件")


async def small_waiter():
    await big_event.wait()
    print(f"{datetime.now()} small waiter 收到任务事件")


async def run(sess: aiohttp.ClientSession, url: str, id: int):
    async with sess.get(url) as resp:
        ret = await resp.json()
        print("响应内容", ret)
        data = ret["msg"]
        if data > 0.5:
            big_event.set()
        else:
            small_event.set()
        return data

async def main(workers: int, url: str):
    asyncio.ensure_future(big_waiter())
    asyncio.ensure_future(big_waiter())
    asyncio.ensure_future(small_waiter())
    asyncio.ensure_future(small_waiter())

    async with aiohttp.ClientSession() as sess:
        futures = []
        for i in range(workers):
            futures.append(asyncio.ensure_future(run(sess, url, i)))
        await asyncio.wait(futures)

    if not big_event.is_set():
        big_event.set()
    
    if not small_event.is_set():
        small_event.set()

    # 等到其他pending可马上运行完成的任务运行结束
    await asyncio.sleep(0)

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    start = datetime.now()
    loop.run_until_complete(main(3, "http://127.0.0.1:8000/random"))
    end = datetime.now()
    print("耗时:", end - start)

输出如下:

响应内容 {'msg': 0.9879470259657458}
2022-07-11 10:16:51.577579 small waiter 收到任务事件
2022-07-11 10:16:51.577579 small waiter 收到任务事件
响应内容 {'msg': 0.33312954919903903}
2022-07-11 10:16:51.578574 big waiter 收到任务事件
2022-07-11 10:16:51.578574 big waiter 收到任务事件
响应内容 {'msg': 0.41934453838367824}
耗时: 0:00:00.996697

可以看到事件(Event)等待者都是在得到响应内容之后输出,并且事件(Event)可以是多个协程同时等待。

条件 Condition

上面的事件虽然很棒,能够在不同的协程之间同步状态,并且也能够一次性同步所有的等待协程,但是还不够精细化,比如想通知指定数量的等待协程,这个时候Event就无能为力了,所以同步原语中出现了Condition。

# test7.py
import asyncio
from datetime import datetime
import aiohttp


cond = asyncio.Condition()


async def waiter(id):
    async with cond:
        await cond.wait()
        print(f"{datetime.now()} waiter[{id}]等待完成")


async def run(sess: aiohttp.ClientSession, url: str, id: int):
    async with sess.get(url) as resp:
        ret = await resp.json()
        print("响应内容", ret)
        data = ret["msg"]
        async with cond:
            # cond.notify()
            # cond.notify_all()
            cond.notify(2)
        return data

async def main(workers: int, url: str):
    for i in range(workers):
        asyncio.ensure_future(waiter(i))

    async with aiohttp.ClientSession() as sess:
        futures = []
        for i in range(workers):
            futures.append(asyncio.ensure_future(run(sess, url, i)))
        await asyncio.wait(futures)


    # 等到其他pending可马上运行完成的任务运行结束
    await asyncio.sleep(0)

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    start = datetime.now()
    loop.run_until_complete(main(3, "http://127.0.0.1:8000/random"))
    end = datetime.now()
    print("耗时:", end - start)

输出如下:

$ python test7.py
响应内容 {'msg': 0.587516452693613}
2022-07-11 10:26:13.482781 waiter[0]等待完成
2022-07-11 10:26:13.483778 waiter[1]等待完成
响应内容 {'msg': 0.3391774763719556}
响应内容 {'msg': 0.2653464378663153}
2022-07-11 10:26:13.484771 waiter[2]等待完成
耗时: 0:00:01.013655

可以看到,前面两个等待的协程是在同一时刻完成,而不是全部等待完成。

信号量 Semaphore

通过创建协程的数量来控制并发并不是非常优雅的方式,所以可以通过信号量的方式来控制并发。

# test8.py
import asyncio
from datetime import datetime
import aiohttp


semp = asyncio.Semaphore(2)


async def run(sess: aiohttp.ClientSession, url: str, id: int):
    async with semp:
        async with sess.get(url) as resp:
            ret = await resp.json()
            print(f"{datetime.now()} worker[{id}] 响应内容", ret)
            data = ret["msg"]
            return data

async def main(workers: int, url: str):
    async with aiohttp.ClientSession() as sess:
        futures = []
        for i in range(workers):
            futures.append(asyncio.ensure_future(run(sess, url, i)))
        await asyncio.wait(futures)


    # 等到其他pending可马上运行完成的任务运行结束
    await asyncio.sleep(0)

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    start = datetime.now()
    loop.run_until_complete(main(3, "http://127.0.0.1:8000/random"))
    end = datetime.now()
    print("耗时:", end - start)

输出如下:

$ python test8.py
2022-07-11 10:30:40.634801 worker[0] 响应内容 {'msg': 0.21337652123021056}
2022-07-11 10:30:40.634801 worker[1] 响应内容 {'msg': 0.7591980200967501}
2022-07-11 10:30:41.636346 worker[2] 响应内容 {'msg': 0.8282581038608438}
耗时: 0:00:02.011661

可以发现,虽然同时创建了三个协程,但是同一时刻只有两个协程工作,而另外一个协程需要等待一个协程让出信号量才能运行。

小结

无论是协程还是线程,任务之间的状态同步还是很重要的,所以有了应对各种同步机制的同步原语,因为要保证一个资源同一个时刻只能一个任务访问,所以引入了锁,又因为需要一个任务等待另一个任务,或者多个任务等待某个任务,因此引入了事件(Event),但是为了更精细的控制通知的程度,所以又引入了条件(Condition), 通过条件可以控制一次通知多少的任务。

有时候的并发需求是通过一个变量控制并发任务的并发数而不是通过创建协程的数量来控制并发,所以引入了信号量(Semaphore),这样就可以在创建的协程数远远大于并发数的情况下让协程在指定的并发量情况下并发。

兼容多线程,多进程

不得不承认异步编程相比起同步编程的生态要小的很多,所以不可能完全异步编程,因此需要一种方式兼容。

多线程是为了兼容同步得代码。

多进程是为了利用CPU多核的能力。

# test9.py
import time
from datetime import datetime

import asyncio
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor


semp = asyncio.Semaphore(2)


def wait_io(id: int):
    # 为了简单起见,直接使用sleep模拟io
    time.sleep(1)
    return f"threading({id}): done at {datetime.now()}"

def more_cpu(id: int):
    sum(i * i for i in range(10 ** 7))
    return f"process({id}): done at {datetime.now()}"


async def main(workers: int):
    loop = asyncio.get_event_loop()
    futures = []
    thread_pool = ThreadPoolExecutor(workers+1)
    process_pool = ProcessPoolExecutor(workers)
    ret = loop.run_in_executor(thread_pool, wait_io, 0, )


    for i in range(workers):
        futures.append(loop.run_in_executor(thread_pool, wait_io, i))

    for i in range(workers):
        futures.append(loop.run_in_executor(process_pool, more_cpu, i))

    print("n".join(await asyncio.gather(*futures)))


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    start = datetime.now()
    loop.run_until_complete(main(3))
    end = datetime.now()
    print("耗时:", end - start)

输出如下:

threading(0): done at 2022-07-11 15:38:36.073547
threading(1): done at 2022-07-11 15:38:36.074540
threading(2): done at 2022-07-11 15:38:36.074540
process(0): done at 2022-07-11 15:38:36.142233
process(1): done at 2022-07-11 15:38:36.177190
process(2): done at 2022-07-11 15:38:36.162244
耗时: 0:00:01.107643

可以看到总耗时1秒,说明所有的线程跟进程是同时运行的。

生态

下面是本人使用过的一些异步库,仅供参考

web框架

  • fastapi 超级棒的web框架,使用过就不再想使用其他的了

http客户端

  • httpie
  • aiohttp

数据库

  • aioredis redis异步库
  • motor mongodb异步库

ORM

  • sqlmodel 超级棒的ORM

虽然异步库发展得还算不错,但是中肯的说并没有覆盖方方面面。

总结

虽然我鼓励大家尝试异步编程,但是本文的最后却是让大家谨慎的选择开发环境,如果你觉得本文的并发,同步,兼容多线程,多进程不值得一提,那么我十分推荐你尝试以异步编程的方式开始一个新的项目,如果你对其中一些还有疑问或者你确定了要使用的依赖库并且大多数是没有异步库替代的,那么我还是建议你直接按照自己擅长的同步编程开始。

异步编程虽然很不错,不过,也许你并不需要。



Tags:Python   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
一篇文章教会你使用Python中三种简单的函数
所谓函数,就是指:把某些特定功能的代码组成为一个整体,这个整体就叫做函数。一、函数简介所谓函数,就是指:把某些特定功能的代码组成为一个整体,这个整体就叫做函数。二、函数定义...【详细内容】
2024-04-11  Search: Python  点击:(12)  评论:(0)  加入收藏
一篇文章带你了解Python的分布式进程接口
在Thread和Process中,应当优选Process,因为Process更稳定,而且,Process可以分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上。一、前言在Thread和Process中,应当优...【详细内容】
2024-04-11  Search: Python  点击:(10)  评论:(0)  加入收藏
Python 可视化:Plotly 库使用基础
当使用 Plotly 进行数据可视化时,我们可以通过以下示例展示多种绘图方法,每个示例都会有详细的注释和说明。1.创建折线图import plotly.graph_objects as go# 示例1: 创建简单...【详细内容】
2024-04-01  Search: Python  点击:(15)  评论:(0)  加入收藏
Python 办公神器:教你使用 Python 批量制作 PPT
介绍本文将介绍如何使用openpyxl和pptx库来批量制作PPT奖状。本文假设你已经安装了python和这两个库。本文的场景是:一名基层人员,要给一次比赛活动获奖的500名选手制作奖状,并...【详细内容】
2024-03-26  Search: Python  点击:(21)  评论:(0)  加入收藏
Python实现工厂模式、抽象工厂,单例模式
工厂模式是一种常见的设计模式,它可以帮助我们创建对象的过程更加灵活和可扩展。在Python中,我们可以使用函数和类来实现工厂模式。一、Python中实现工厂模式工厂模式是一种常...【详细内容】
2024-03-07  Search: Python  点击:(38)  评论:(0)  加入收藏
不可不学的Python技巧:字典推导式使用全攻略
Python的字典推导式是一种优雅而强大的工具,用于创建字典(dict)。这种方法不仅代码更加简洁,而且执行效率高。无论你是Python新手还是有经验的开发者,掌握字典推导式都将是你技能...【详细内容】
2024-02-22  Search: Python  点击:(43)  评论:(0)  加入收藏
如何进行Python代码的代码重构和优化?
Python是一种高级编程语言,它具有简洁、易于理解和易于维护的特点。然而,代码重构和优化对于保持代码质量和性能至关重要。什么是代码重构?代码重构是指在不改变代码外部行为的...【详细内容】
2024-02-22  Search: Python  点击:(44)  评论:(0)  加入收藏
Python开发者必备的八个PyCharm插件
在编写代码的过程中,括号几乎无处不在,以至于有时我们会拼命辨别哪个闭合括号与哪个开头的括号相匹配。这款插件能帮助解决这个众所周知的问题。前言在PyCharm中浏览插件列表...【详细内容】
2024-01-26  Search: Python  点击:(92)  评论:(0)  加入收藏
Python的Graphlib库,再也不用手敲图结构了
Python中的graphlib库是一个功能强大且易于使用的工具。graphlib提供了许多功能,可以帮助您创建、操作和分析图形对象。本文将介绍graphlib库的主要用法,并提供一些示例代码和...【详细内容】
2024-01-26  Search: Python  点击:(95)  评论:(0)  加入收藏
大语言模型插件功能在携程的Python实践
作者简介成学,携程高级安全研发工程师,关注Python/Golang后端开发、大语言模型等领域。一、背景2023年初,科技圈最火爆的话题莫过于大语言模型了,它是一种全新的聊天机器人模型,...【详细内容】
2024-01-26  Search: Python  点击:(84)  评论:(0)  加入收藏
▌简易百科推荐
一篇文章教会你使用Python中三种简单的函数
所谓函数,就是指:把某些特定功能的代码组成为一个整体,这个整体就叫做函数。一、函数简介所谓函数,就是指:把某些特定功能的代码组成为一个整体,这个整体就叫做函数。二、函数定义...【详细内容】
2024-04-11  Go语言进阶学习  微信公众号  Tags:Python   点击:(12)  评论:(0)  加入收藏
一篇文章带你了解Python的分布式进程接口
在Thread和Process中,应当优选Process,因为Process更稳定,而且,Process可以分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上。一、前言在Thread和Process中,应当优...【详细内容】
2024-04-11  Go语言进阶学习    Tags:Python   点击:(10)  评论:(0)  加入收藏
Python 可视化:Plotly 库使用基础
当使用 Plotly 进行数据可视化时,我们可以通过以下示例展示多种绘图方法,每个示例都会有详细的注释和说明。1.创建折线图import plotly.graph_objects as go# 示例1: 创建简单...【详细内容】
2024-04-01  Python技术    Tags:Python   点击:(15)  评论:(0)  加入收藏
Python 办公神器:教你使用 Python 批量制作 PPT
介绍本文将介绍如何使用openpyxl和pptx库来批量制作PPT奖状。本文假设你已经安装了python和这两个库。本文的场景是:一名基层人员,要给一次比赛活动获奖的500名选手制作奖状,并...【详细内容】
2024-03-26  Python技术  微信公众号  Tags:Python   点击:(21)  评论:(0)  加入收藏
Python实现工厂模式、抽象工厂,单例模式
工厂模式是一种常见的设计模式,它可以帮助我们创建对象的过程更加灵活和可扩展。在Python中,我们可以使用函数和类来实现工厂模式。一、Python中实现工厂模式工厂模式是一种常...【详细内容】
2024-03-07  Python都知道  微信公众号  Tags:Python   点击:(38)  评论:(0)  加入收藏
不可不学的Python技巧:字典推导式使用全攻略
Python的字典推导式是一种优雅而强大的工具,用于创建字典(dict)。这种方法不仅代码更加简洁,而且执行效率高。无论你是Python新手还是有经验的开发者,掌握字典推导式都将是你技能...【详细内容】
2024-02-22  子午Python  微信公众号  Tags:Python技巧   点击:(43)  评论:(0)  加入收藏
如何进行Python代码的代码重构和优化?
Python是一种高级编程语言,它具有简洁、易于理解和易于维护的特点。然而,代码重构和优化对于保持代码质量和性能至关重要。什么是代码重构?代码重构是指在不改变代码外部行为的...【详细内容】
2024-02-22  编程技术汇    Tags:Python代码   点击:(44)  评论:(0)  加入收藏
Python开发者必备的八个PyCharm插件
在编写代码的过程中,括号几乎无处不在,以至于有时我们会拼命辨别哪个闭合括号与哪个开头的括号相匹配。这款插件能帮助解决这个众所周知的问题。前言在PyCharm中浏览插件列表...【详细内容】
2024-01-26  Python学研大本营  微信公众号  Tags:PyCharm插件   点击:(92)  评论:(0)  加入收藏
Python的Graphlib库,再也不用手敲图结构了
Python中的graphlib库是一个功能强大且易于使用的工具。graphlib提供了许多功能,可以帮助您创建、操作和分析图形对象。本文将介绍graphlib库的主要用法,并提供一些示例代码和...【详细内容】
2024-01-26  科学随想录  微信公众号  Tags:Graphlib库   点击:(95)  评论:(0)  加入收藏
Python分布式爬虫打造搜索引擎
简单分布式爬虫结构主从模式是指由一台主机作为控制节点负责所有运行网络爬虫的主机进行管理,爬虫只需要从控制节点那里接收任务,并把新生成任务提交给控制节点就可以了,在这个...【详细内容】
2024-01-25  大雷家吃饭    Tags:Python   点击:(63)  评论:(0)  加入收藏
站内最新
站内热门
站内头条