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
源码注释:
# 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()
6 Running and Deploying