tonado 使用2

1 介绍

~ 见官方文档 或tonado 使用

2 协程

2.0 异步HTTP请求示例代码

from tornado.httpclient import AsyncHTTPClient


async def asynchronous_fetch(url):
    http_client = AsyncHTTPClient()
    response = await http_client.fetch(url)
    return response.body

~ python3.5 之前实现异步HTTP请求是另外的实现, 详见tornado官方文档

 

2.1如何调用协程

1) 协程中如何引发异常

"""
协程不会以正常方式引发异常:它们引发的任何异常都会被捕获在可等待对象中,直到被yield。
"""
async def divide(x, y):
    return x / y


def bad_call():
    # 这应该引发ZeroDivisionError,但是由于
    # 协程的调用方式不正确,所以不会引发异常。
    divide(1, 0)

 

2) 几乎所有调用协程的函数本身都必须是协程, 调用时使用await或yield关键字

async def good_call():
    # await将解包divide()返回的对象并引发异常。
    await divide(1, 0)

 

3) 若希望`启动并忘记`一个协程, 而不是等待其结果

# IOLoop会捕获异常并在日志中打印堆栈跟踪。注意这看起来不像是普通的调用,
# 因为我们传递要由IOLoop调用的函数对象。
IOLoop.current().spawn_callback(divide, 1, 0)

 

4) 启动IOLoop、运行协程,然后停止IOLoop, 用于启动批处理程序的主函数

# run_sync()不接受参数,所以我们必须用lambda将调用包装起来。
IOLoop.current().run_sync(lambda: divide(1, 0))

 

2.2 协程模式

# 概念: 是指在处理协程时常用的一些模式或技巧
# 目的: 帮助开发者更好地控制协程的执行流程,并实现复杂的异步操作

1) 如何调用阻塞函数

# 使用IOLoop.run_in_executor,它返回与协程兼容的Future对象。
async def call_blocking():
    await IOLoop.current().run_in_executor(None, blocking_func, args)

 

2) 并行处理

# multi函数接受值为Future对象的列表或字典,并且可以并行等待所有这些Future的完成。
from tornado.gen import multi


async def parallel_fetch(url1, url2):
    resp1, resp2 = await multi([http_client.fetch(url1),
                                http_client.fetch(url2)])


async def parallel_fetch_many(urls):
    responses = await multi([http_client.fetch(url) for url in urls])
    # responses is a list of HTTPResponses in the same order


async def parallel_fetch_dict(urls):
    responses = await multi({url: http_client.fetch(url)
                             for url in urls})
    # responses is a dict {url: HTTPResponse}

3) 交错处理

# 有时,保存一个Future而不立即yield它是有用的,这样可以在等待之前开始另一个操作。
# 通过保存Future对象并在需要时进行yield操作,可以在协程中实现操作的交错执行
from tornado.gen import convert_yielded


async def get(self):
    # convert_yielded() starts the native coroutine in the background.
    # This is equivalent to asyncio.ensure_future() (both work in Tornado).
    fetch_future = convert_yielded(self.fetch_next_chunk())
    while True:
        chunk = yield fetch_future
        if chunk is None: break
        self.write(chunk)
        fetch_future = convert_yielded(self.fetch_next_chunk())
        yield self.flush()

 

2.3 循环迭代

# 在原生协程中,可以使用async for语法进行循环迭代
# ~ 在旧版本python中 使用携程进行循环迭代比较棘手-见官方文档


2.4 后台运行

"""
这段代码定义了一个名为minute_loop的异步函数(async function)。它的功能是在一个无限循环中执行两个异步操作。

在循环的每次迭代中,首先会调用await do_something(),这表示会等待do_something()函数执行完毕,并且在其完成后继续执行下一步。

接着,代码会调用await gen.sleep(60),这表示会等待60秒的时间。gen.sleep(60)是一个异步操作,它会暂停当前协程的执行,让其他任务有机会运行,然后在指定的时间间隔之后恢复执行。

最后一行代码IOLoop.current().spawn_callback(minute_loop)的作用是将minute_loop协程以后台任务(background task)的形式加入到当前的IOLoop循环中。这样可以保证minute_loop协程会被周期性地执行,而不会阻塞主线程或其他任务的执行。

综合来说,这段代码创建了一个每隔60秒执行一次do_something()操作的循环,并将该循环作为后台任务添加到当前的IOLoop循环中,以实现定期执行的效果
"""
async def minute_loop():
    while True:
        await do_something()
        await gen.sleep(60)


IOLoop.current().spawn_callback(minute_loop)

 

"""
上面的循环每隔60+N秒运行一次,其中N是do_something()的运行时间。要确保每隔准确的60秒运行一次,可以使用上面提到的交错模式
"""
# >>> 需要实际验证
async def minute_loop2():
    while True:
        nxt = gen.sleep(60)  # Start the clock.
        await do_something()  # Run while the clock is ticking.
        await nxt  # Wait for the timer to run out.

 

3 异步网络爬虫(异步队列使用)

示例案例

队列会维护一个未完成任务的计数,初始值为零。put操作会增加计数,task_done操作会减少计数。

在这个网络爬虫的示例中,队列最初只包含base_url。当一个工作线程获取一个页面并解析链接后,会将新链接放入队列中,然后调用task_done一次来减少计数。最终,一个工作线程获取的页面中的URL都已经被访问过,并且队列中也没有剩余工作。因此,该工作线程调用task_done将计数减少到零。等待join的主协程会被恢复执行并完成任务

#!/usr/bin/env python3
# 指定脚本的解释器
"""
这段代码是tornado框架编写的异步网络爬虫. 用于从指定url下载页面,并解析其中的链接.
"""

import time
from datetime import timedelta

from html.parser import HTMLParser
from urllib.parse import urljoin, urldefrag
# gen(用于定义协程)、httpclient(用于发起HTTP请求)、ioloop(用于事件循环)和queues(用于实现异步队列)
from tornado import gen, httpclient, ioloop, queues

base_url = "http://www.tornadoweb.org/en/stable/"
concurrency = 10  # 定义并发数


async def get_links_from_url(url):
    """
    定义了一个异步函数get_links_from_url,用于下载指定URL的页面内容并解析其中的链接。
    它使用httpclient.AsyncHTTPClient().fetch(url)异步地发送HTTP请求获取页面内容,然后通过get_links函数解析页面中的链接。
    返回的链接经过处理,去除了#后的片段,并且转换为绝对URL
    """
    response = await httpclient.AsyncHTTPClient().fetch(url)
    print("fetched %s" % url)

    html = response.body.decode(errors="ignore")
    return [urljoin(url, remove_fragment(new_url)) for new_url in get_links(html)]


def remove_fragment(url):
    """
    定义了一个函数, 用于处理URL中片段部分
    """
    pure_url, frag = urldefrag(url)
    return pure_url


def get_links(html):
    """
    定义了一个函数get_links,用于解析HTML页面中的链接。它使用HTMLParser类进行解析,
    找到所有<a>标签的href属性,并将链接存储在URLSeeker类的实例中
    """

    class URLSeeker(HTMLParser):
        def __init__(self):
            HTMLParser.__init__(self)
            self.urls = []

        def handle_starttag(self, tag, attrs):
            href = dict(attrs).get("href")
            if href and tag == "a":
                self.urls.append(href)

    url_seeker = URLSeeker()
    url_seeker.feed(html)
    return url_seeker.urls


async def main():
    """
    1. 创建一个异步队列`q`,用于存储待下载的URL。

    2. 定义`fetch_url`函数,用于下载指定URL的页面内容并解析其中的链接,并将解析得到的新链接加入队列。

    3. 定义`worker`函数,用于异步处理队列中的URL。

    4. 将基础URL加入队列。

    5. 启动多个`worker`协程,处理队列中的URL,并等待队列为空。

    6. 输出程序的执行时间和下载的URL数量。

    """
    q = queues.Queue()
    start = time.time()
    # fetching表示正在获取的项目集合,fetched表示已经获取的项目集合,而dead表示已经处理完毕的项目集合。
    fetching, fetched, dead = set(), set(), set()

    async def fetch_url(current_url):
        if current_url in fetching:
            return

        print("fetching %s" % current_url)
        fetching.add(current_url)
        urls = await get_links_from_url(current_url)
        fetched.add(current_url)

        for new_url in urls:
            # Only follow links beneath the base URL
            if new_url.startswith(base_url):
                await q.put(new_url)

    async def worker():
        async for url in q:
            if url is None:
                return
            try:
                await fetch_url(url)
            except Exception as e:
                print("Exception: %s %s" % (e, url))
                dead.add(url)
            finally:
                q.task_done()

    await q.put(base_url)

    # Start workers, then wait for the work queue to be empty.
    workers = gen.multi([worker() for _ in range(concurrency)])
    await q.join(timeout=timedelta(seconds=300))
    assert fetching == (fetched | dead)
    """
    这段代码是一个断言语句,用于验证两个集合是否相等。以确保没有任何项目被遗漏或重复处理
        如果断言条件为真,程序继续执行。
        如果断言条件为假,会引发AssertionError异常。
    """

    # Signal all the workers to exit.
    """
    向工作队列中放入None值来通知工作线程退出
    将 None 值放入队列 q 中,重复 concurrency 次。以确保每个工作线程都能够获取到一个任务
    若没有该段代码:
        并且工作线程将在完成当前任务后继续等待获取新的任务。
    """
    for _ in range(concurrency):
        await q.put(None)
    await workers


if __name__ == "__main__":
    io_loop = ioloop.IOLoop.current()
    io_loop.run_sync(main)  # 启动IOLoop、运行协程,然后停止IOLoop
View Code

 

    源码注释:

# 1 Queue类put方法的文档注释
def put(
self, item: _T, timeout: Optional[Union[float, datetime.timedelta]] = None
) -> "Future[None]":
"""将一个项目放入队列中,可能会等待直到有空间。

返回一个Future对象,在超时后引发`tornado.util.TimeoutError`异常。

``timeout``可以是一个数值,表示时间(与`tornado.ioloop.IOLoop.time`的时间尺度相同,通常是`time.time`),
或者是一个相对于当前时间的截止时间的`datetime.timedelta`对象。
"""


# 2 Queue类task_done方法的文档注释
def task_done(self) -> None:
"""表示先前入队的任务已完成。

用于队列的消费者。对于每个使用`.get`获取任务的操作,随后调用`.task_done`告诉队列该任务的处理已完成。

如果`.join`方法阻塞,它将在所有项都被处理完后恢复;也就是说,每个`.put`都有相应的`.task_done`调用。

如果调用次数超过`.put`的次数,则引发`ValueError`异常。
"""


# 3 Queue类join方法的文档注释
def join(
self, timeout: Optional[Union[float, datetime.timedelta]] = None
) -> Awaitable[None]:
"""阻塞直到队列中的所有项目都被处理完。
返回一个可等待对象,在超时后引发`tornado.util.TimeoutError`异常。
"""

4 Tornado web application

1) 组成

"""
web appication 由三部分组成:
1 Requesthandler子类
处理请求.
RequestHandler是处理HTTP请求的基础类
2 Application对象
路由URL和分发请求
将请求的URL路由到对应的RequestHandler子类
3 main()函数
启动服务
指定服务器端口
启动服务
"""

5 Auth and Security

代码

import tornado.web
import tornado.ioloop
import tornado.httpserver


class BaseHandler(tornado.web.RequestHandler):
    def get_current_user(self):
        return self.get_secure_cookie("user")


class MainHandler(BaseHandler):
    def get(self):
        if not self.current_user:
            self.redirect("/login")
            return
        self.write("Hello, " + 'name')


class LoginHandler(BaseHandler):
    def get(self):
        self.write('<html><body><form action="/login" method="post">'
                   'Name: <input type="text" name="name">'
                   '<input type="submit" value="Sign in">'
                   '</form></body></html>')

    def post(self):
        self.set_secure_cookie("user", self.get_argument("name"))
        self.redirect("/")


settings = {
    'cookie_secret': "__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__",
    'xsrf_cookies': True
}


def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
        (r"/login", LoginHandler),
    ], **settings)


if __name__ == '__main__':
    app = make_app()
    # app.listen(8888)
    http_server = tornado.httpserver.HTTPServer(app)
    # http_server.listen(8888)
    http_server.bind(8880)
    http_server.start()
    tornado.ioloop.IOLoop.current().start()
View Code

 

6 Running and Deploying

 



 

posted @ 2024-01-22 13:58  tslam  阅读(42)  评论(0编辑  收藏  举报