当前位置: 首页 > news >正文

《500 Lines or Less》(5)异步爬虫

https://aosabook.org/en/500L/a-web-crawler-with-asyncio-coroutines.html
——A. Jesse Jiryu Davis and Guido van Rossum

介绍

网络程序消耗的不是计算资源,而是打开许多缓慢的连接,解决此问题的现代方法是异步IO。
本章介绍一个简单的网络爬虫,使用异步I/O实现。本章分三个部分:首先异步事件循环,以及一个带有回调的爬虫。其次,我们说明了Python协程高效且可扩展。我们使用生成器实现简单的协程。最后,我们使用Python标准库asyncio的协程,并使用异步队列来协调。

任务

网络爬虫的功能是下载网站上的所有页面,从根URL,获取页面,并解析页面中的链接,并对链接中的页面进行相同的操作,直到世界的尽头。我们可以通过并发进行加速,同时下载许多页面。

传统方法

如何实现并发的爬虫?传统上,我们使用线程池

def fetch(url):sock = socket.socket()sock.connect(('xkcd.com', 80))request = 'GET {} HTTP/1.0\r\nHost: xkcd.com\r\n\r\n'.format(url)sock.send(request.encode('ascii'))response = b''chunk = sock.recv(4096)while chunk:response += chunkchunk = sock.recv(4096)# Page is now downloaded.links = parse_links(response)q.add(links)

默认情况下,socket操作处于阻塞状态,线程调用connectrecv方法时,它会暂停。因此,要一次下载多个页面,我们需要许多线程。为了方便线程复用(减少创建和销毁线程的开销),我们使用线程池
但线程成本高,操作系统可能对线程数量设置了上限。如果我们在并发socket上同时扩展到数以计的操作,我们会在用完socket之前用完线程。每个线程的开销或系统限制是瓶颈。

Async(异步)

异步 I/O 框架使用非阻塞socket在单个线程上执行并发操作。在我们的异步爬虫程序中,我们在开始连接到服务器之前将套接字设置为非阻塞:

sock = socket.socket()
sock.setblocking(False) # 非阻塞 
try:sock.connect(('xkcd.com', 80))
except BlockingIOError:pass

恼火的是,非阻塞socket会从connect中抛出异常,即使它正常工作。此异常复制了底层的C函数的行为,该函数将EINPROGRESS设置为errno通知开始。
现在,我们的爬虫需要一种方法来知道连接何时建立,以便它可以发送 HTTP 请求。我们可以在一个循环中反复尝试:

request = 'GET {} HTTP/1.0\r\nHost: xkcd.com\r\n\r\n'.format(url)
encoded = request.encode('ascii')
while True:try:sock.send(encoded)break  # Done.except OSError as e:pass
print('sent')

这种方法不仅浪费电力,而且无法有效地等待多个sockets上的事件。在古代,BSD Unix 的解决方案是 select ,一个 C 函数,它等待事件发生在一个非阻塞套接字或它们的一个小数组上。如今,对具有大量连接的互联网应用程序的需求导致了像poll ,然后是BSD的 kqueue 和 Linux 上的epoll 等替代品的出现。这些 API 与 select 类似,但在连接数量非常多的情况下表现良好。

Python 3.4的 DefaultSelector 使用系统上可用的最好的 类select函数。要注册有关网络 I/O 的通知,我们创建一个非阻塞套接字,并将其注册到默认选择器

from selectors import DefaultSelector, EVENT_WRITE
selector = DefaultSelector() # 选择器
sock = socket.socket()     
sock.setblocking(False) # 非阻塞套接字
try:sock.connect(('xkcd.com', 80))
except BlockingIOError:pass
def connected():selector.unregister(sock.fileno())print('connected!')
selector.register(sock.fileno(), EVENT_WRITE, connected)

我们忽略虚假错误并调用 selector.register() ,传入套接字的文件描述符和一个表示我们正在等待的事件的常量。为了在建立连接时收到通知,我们传递 EVENT_WRITE :也就是说,我们想知道套接字何时是“可写”的。我们还传递了函数 connected ,以便在该事件发生时运行。这样的函数称为回调(callback)

当选择器接收 I/O 通知时,以遍历方式处理:

def loop():while True:events = selector.select()for event_key, event_mask in events:callback = event_key.datacallback()

event_key.data作为connected的回调函数 ,一旦连接了非阻塞套接字,我们就会检索并执行。
与上面的快速旋转循环不同,对 select()的调用会暂停,等待下一个 I/O 事件。然后,循环运行正在等待这些事件的回调。尚未完成的操作将保持挂起状态,直到事件循环的未来某个滴答声。

我们展示了如何在操作准备就绪时开始操作并执行回调。异步框架基于我们展示的两个功能(非阻塞套接字事件循环)构建,用于在单个线程上运行并发操作。

我们在这里实现了“并发性”,但不是传统上所说的“并行性”。也就是说,我们构建了一个执行重叠 I/O 的微型系统。它能够在其他人在飞行过程中开始新的操作。它实际上并不利用多个内核来并行执行计算。但是,这个系统是为 I/O 密集型问题而设计的,而不是 CPU 密集型问题。

因此,我们的事件循环在并发 I/O 时是有效的,因为它不会将线程资源专用于每个连接。但在我们继续之前,重要的是要纠正一个常见的误解,即异步比多线程更快。通常情况并非如此,事实上,在 Python 中,像我们这样的事件循环在处理少量非常活跃的连接时比多线程慢一些。在没有全局解释器锁的运行时中,线程在这样的工作负载上会表现得更好。异步 I/O 适用于具有许多缓慢或昏昏欲睡的连接且事件不频繁的应用程序。

使用回调进行编程

使用我们目前构建的简陋的异步框架,我们如何构建网络爬虫?即使是一个简单的 URL 获取器也很难编写。
我们从尚未获取的 URL 以及看过的 URL 开始:

urls_todo = set(['/'])
seen_urls = set(['/'])

获取页面将需要一系列回调。连接套接字时触发 connected 回调,并向服务器发送 GET 请求。但随后它必须等待响应,因此它会注册另一个回调。如果在触发该回调时,它还无法读取完整的响应,则会再次注册,依此类推。

让我们将这些回调收集到一个Fetcher对象中,它需要一个 URL、一个套接字对象和一个累积响应字节的位置

class Fetcher:def __init__(self, url):self.response = b''  # Empty array of bytes.self.url = urlself.sock = None

首先调用Fetcher.fetch()

    # Method on Fetcher class.def fetch(self):self.sock = socket.socket()self.sock.setblocking(False)try:self.sock.connect(('xkcd.com', 80))except BlockingIOError:pass# Register next callback.selector.register(self.sock.fileno(),EVENT_WRITE,self.connected)

fetch() 方法开始连接套接字。但请注意,该方法在(完全)建立连接之前返回。它必须将控制权返回到事件循环以等待连接。为了理解原因,想象一下我们的整个应用程序是这样的结构:

# Begin fetching http://xkcd.com/353/
fetcher = Fetcher('/353/')
fetcher.fetch()
while True:events = selector.select()for event_key, event_mask in events:callback = event_key.datacallback(event_key, event_mask)

当事件循环调用 select 时,所有事件通知都会在事件循环中处理。因此 fetch() 必须将控制权交给事件循环,以便程序知道套接字何时连接。只有这样,循环才会运行connected 回调,该回调已在上面的 fetch 末尾注册。

以下是 connected 的实现:

    # Method on Fetcher class.def connected(self, key, mask):print('connected!')selector.unregister(key.fd)request = 'GET {} HTTP/1.0\r\nHost: xkcd.com\r\n\r\n'.format(self.url)self.sock.send(request.encode('ascii'))# Register the next callback.selector.register(key.fd,EVENT_READ,self.read_response)

该方法发送 GET 请求。一个真正的应用程序会检查 send 的返回值, 以防无法一次发送整个消息。但是我们的请求很小,应用程序也不复杂。它轻快地调用 send ,然后等待响应。当然,它必须注册另一个回调,并放弃对事件循环的控制权。下一个也是最后一个回调 read_response ,处理服务器的回复:

    # Method on Fetcher class.def read_response(self, key, mask):global stoppedchunk = self.sock.recv(4096)  # 4k chunk size.if chunk:self.response += chunkelse:selector.unregister(key.fd)  # Done reading.links = self.parse_links()# Python set-logic:for link in links.difference(seen_urls):urls_todo.add(link)Fetcher(link).fetch()  # <- New Fetcher.seen_urls.update(links)urls_todo.remove(self.url)if not urls_todo:stopped = True

每次选择器看到套接字是“可读的”时,都会执行回调,这可能意味着两件事:套接字有数据或已关闭。回调要求从套接字提供最多 4 KB 的数据。如果准备就绪, chunk 包含任何可用的数据。如果有更多 chunk ,则长度为 4 KB,并且套接字保持可读性,因此事件循环会在下一个时钟周期再次运行此回调。响应完成后,服务器已关闭套接字, chunk为空。

parse_links 方法(未显示)返回一组 URL。我们为每个新 URL 启动一个新的提取器,没有并发上限。请注意带有回调的异步编程的一个很好的功能:我们不需要对共享数据的更改进行互斥锁,例如当我们添加指向 seen_urls 的链接时。没有抢占式的多任务处理,因此我们不能在代码中的任意点被打断。

我们添加一个全局变量 stopped ,用来控制循环:

stopped = False
def loop():while not stopped:events = selector.select()for event_key, event_mask in events:callback = event_key.datacallback()

下载所有页面后,提取器将停止循环,程序将退出。

这个例子把异步的问题说得很清楚:意大利面条代码。我们需要某种方式来表达一系列计算和 I/O 操作,并安排多个这样的操作并发运行。但是如果没有线程,就无法将一系列操作收集到单个函数中:每当函数开始 I/O 操作时,它都会显式保存将来需要的任何状态,然后返回。您负责思考和编写此保存状态的代码。

让我们解释一下。考虑一下我们使用传统阻塞套接字在线程上获取 URL 是多么简单:

# Blocking version.
def fetch(url):sock = socket.socket()sock.connect(('xkcd.com', 80))request = 'GET {} HTTP/1.0\r\nHost: xkcd.com\r\n\r\n'.format(url)sock.send(request.encode('ascii'))response = b''chunk = sock.recv(4096)while chunk:response += chunkchunk = sock.recv(4096)# Page is now downloaded.links = parse_links(response)q.add(links)

此函数在一个套接字操作和下一个套接字操作之间记住什么状态?它有套接字、URL 和累积的 response 。在线程上运行的函数使用编程语言的基本功能将此临时状态存储在其堆栈上的局部变量中。该函数还具有“延续”,即它计划在 I/O 完成后执行的代码。运行时通过存储线程的指令指针来记住延续。您无需考虑恢复这些局部变量和 I/O 之后的延续

但是对于基于回调的异步框架,在等待 I/O 时,函数必须显式保存其状态,因为函数会在 I/O 完成之前返回并丢失其堆栈帧。代替局部变量,我们基于回调的示例将 sockresponse 存储 为 Fetcher 实例的 self 属性。代替指令指针,它通过注册回调 connectedread_response 来存储其延续。随着应用程序功能的增长,我们在回调中手动保存的状态的复杂性也在增加。如此繁重的会计工作使编码员容易患偏头痛。

更糟糕的是,如果回调在安排链中的下一个回调之前抛出异常,会发生什么?假设 parse_links() 做得很差,并且在解析一些 HTML 时会抛出异常:

Traceback (most recent call last):File "loop-with-callbacks.py", line 111, in <module>loop()File "loop-with-callbacks.py", line 106, in loopcallback(event_key, event_mask)File "loop-with-callbacks.py", line 51, in read_responselinks = self.parse_links()File "loop-with-callbacks.py", line 67, in parse_linksraise Exception('parse error')
Exception: parse error

堆栈跟踪仅显示事件循环正在运行回调。我们不记得是什么导致了错误。链条的两端都断了:我们忘记了我们要去哪里,我们从哪里来。这种上下文的丢失被称为“堆栈撕裂”,在许多情况下,它使调查人员感到困惑。堆栈撕裂还阻止我们为回调链安装异常处理程序,就像“try / except”块包装函数调用及其后代树的方式一样。

因此,即使除了关于多线程和异步的相对效率的长期争论之外,还有另一个关于哪个更容易出错的争论:如果线程在同步时出错,它们很容易受到数据争用的影响,但由于堆栈撕裂,回调很难调试

协程 Coroutines

我们用画饼来吸引你:编写异步代码,将回调的效率与多线程编程的经典美观相结合。这种组合是通过一种称为“协程”的模式实现的。使用 Python 3.4 的标准 asyncio 库和一个名为“aiohttp”的包,在协程中获取 URL 非常直接:

    @asyncio.coroutinedef fetch(self, url):response = yield from self.session.get(url)body = yield from response.read()

协程也是可扩展的。与每个线程 50k 的内存和操作系统对线程的硬性限制相比,Python 协程在 Jesse 的系统上仅占用 3k 的内存。Python 可以轻松启动数十万个协程。

协程的概念可以追溯到计算机科学的早期,很简单:它是一个可以暂停和恢复的子程序。虽然线程由操作系统抢先地进行多任务处理,但协程协同处理多任务:它们选择何时暂停,以及接下来要运行哪个协程。

协程有许多实现;即使在 Python 中也有几个。Python 3.4 中标准“asyncio”库中的协程基于生成器、Future 类和“yield from”语句构建。从 Python 3.5 开始,协程是语言本身 的原生特性;但是,理解协程最初是在 Python 3.4 中实现的,使用预先存在的语言工具,是解决 Python 3.5 原生协程的基础。

为了解释 Python 3.4 基于生成器的协程,我们将对生成器以及它们如何在 asyncio 中用作协程进行阐述,相信您会喜欢阅读它,就像我们喜欢编写它一样。一旦我们解释了基于生成器的协程,我们将在异步网络爬虫中使用它们。

Python 生成器(Generator) 的工作原理

在掌握 Python 生成器之前,您必须了解常规 Python 函数的工作原理。通常,当 Python 函数调用子例程时,子例程会保留控制权,直到它返回或引发异常。然后,控制权返回给调用方:

>>> def foo():
...     bar()
...
>>> def bar():
...     pass

标准的 Python 解释器是用 C 语言编写的。执行 Python 函数的 C 函数被巧妙地称为 PyEval_EvalFrameEx 。它采用一个 Python堆栈帧对象,并在帧的上下文中计算 Python 字节码。这是foo的字节码:

>>> import dis
>>> dis.dis(foo)2           0 LOAD_GLOBAL              0 (bar)3 CALL_FUNCTION            0 (0 positional, 0 keyword pair)6 POP_TOP7 LOAD_CONST               0 (None)10 RETURN_VALUE

foo 函数加载 bar 到其堆栈上并调用,然后从堆栈中弹出其返回值,加载 None 到堆栈上,然后返回 None

PyEval_EvalFrameEx 遇到字节码CALL_FUNCTION时 ,它会创建一个新的 Python 堆栈帧并递归:也就是说,它以递归方式调用 PyEval_EvalFrameEx 新帧,用于执行 bar

了解 Python 堆栈帧是在堆内存中分配的,这一点至关重要!Python 解释器是一个普通的 C 程序,所以它的堆栈帧是普通的堆栈帧。但是它操作的 Python 堆栈帧在上。令人惊讶的是,这意味着 Python 堆栈帧可以比其函数调用更长久。要以交互方式查看此内容,请从bar 中保存当前帧:

>>> import inspect
>>> frame = None
>>> def foo():
...     bar()
...
>>> def bar():
...     global frame
...     frame = inspect.currentframe()
...
>>> foo()
>>> # The frame was executing the code for 'bar'.
>>> frame.f_code.co_name
'bar'
>>> # Its back pointer refers to the frame for 'foo'.
>>> caller_frame = frame.f_back
>>> caller_frame.f_code.co_name
'foo'

函数调用舞台已为 Python 生成器搭建,它们利用相同的构建块——代码对象和堆栈框架——创造出令人惊叹的效果。

这是一个生成器函数:

>>> def gen_fn():
...     result = yield 1
...     print('result of yield: {}'.format(result))
...     result2 = yield 2
...     print('result of 2nd yield: {}'.format(result2))
...     return 'done'
...     

当 Python 编译 gen_fn 为字节码时,它会看到该 yield 语句并知道这是一个 gen_fn 生成器函数,而不是常规函数。它设置了一个标志来记住这个事实:

>>> # The generator flag is bit position 5.
>>> generator_bit = 1 << 5
>>> bool(gen_fn.__code__.co_flags & generator_bit)
True

调用生成器函数时,Python 会看到生成器标志,并且它实际上不会运行该函数,而是创建一个生成器

>>> gen = gen_fn()
>>> type(gen)
<class 'generator'>

Python 生成器封装了一个堆栈帧以及对某些代码的引用,即 gen_fn

>>> gen.gi_code.co_name
'gen_fn'

从调用到 gen_fn 的所有生成器都指向相同的代码。但每个都有自己的堆栈框架。此堆栈帧不在任何实际堆栈上,它位于堆内存中等待使用:
生成器
该帧有一个“最后一条指令(last instruction)”指针,即它最近执行的指令。一开始,最后一个指令指针是 -1,表示生成器尚未开始:

>>> gen.gi_frame.f_lasti
-1

当我们调用send 时,生成器到达它的第一个 yield ,并暂停。 send 的返回值为 1,因为这是 gen 传递给 yield 表达式的内容:

>>> gen.send(None)
1

生成器的指令指针现在从头开始的 3 个字节,是编译后的 Python 的 56 字节的一部分:

>>> gen.gi_frame.f_lasti
3
>>> len(gen.gi_code.co_code)
56

生成器可以在任何时候从任何函数恢复,因为它的堆栈帧实际上不在栈(stack)上:它在堆(heap)上。它在调用层次结构中的位置不是固定的,它不需要像常规函数那样遵循先进后出的执行顺序。它被解放了,像云一样自由地漂浮。

我们可以将值“hello”发送到生成器中,它成为 yield 表达式的结果,生成器继续直到它产生 2:

>>> gen.send('hello')
result of yield: hello
2

它的堆栈帧现在包含局部变量 result :

>>> gen.gi_frame.f_locals
{'result': 'hello'}

从 gen_fn 中创建的其他生成器将具有自己的堆栈帧和局部变量。

当我们再次调用 send 时,生成器从其第二个 yield 继续,并通过引发特殊 StopIteration 异常来结束:

>>> gen.send('goodbye')
result of 2nd yield: goodbye
Traceback (most recent call last):File "<input>", line 1, in <module>
StopIteration: done

异常有一个值,该值是生成器的返回值:字符串 “done” 。

使用生成器构建协程

所以生成器可以暂停,它可以用一个值恢复,它有一个返回值。听起来像是一个很好的原语,可以在其上构建异步编程模型,而无需意大利面条回调!我们想要构建一个“协程”:一个与程序中的其他例程协同调度的例程。我们的协程将是 Python 标准“asyncio”库中的协程的简化版本。与asyncio 一样,我们将使用生成器futuresyield from语句。

首先,我们需要一种方法来表示协程正在等待的一些future结果。
精简版Future

class Future:def __init__(self):self.result = Noneself._callbacks = []def add_done_callback(self, fn):self._callbacks.append(fn)def set_result(self, result):self.result = resultfor fn in self._callbacks:fn(self)

Future最初是“悬而未决”的。它通过调用 set_result 来“解决”。
让我们调整我们的Fetcher以使用 futures协程。我们为 fetch 写了一个回调:

class Fetcher:def fetch(self):self.sock = socket.socket()self.sock.setblocking(False)try:self.sock.connect(('xkcd.com', 80))except BlockingIOError:passselector.register(self.sock.fileno(),EVENT_WRITE,self.connected)def connected(self, key, mask):print('connected!')# And so on....

fetch()方法首先连接套接字,然后注册回调connected,以便在套接字准备就绪时执行。现在我们可以将这两个步骤合并到一个协程中:

    def fetch(self):sock = socket.socket()sock.setblocking(False)try:sock.connect(('xkcd.com', 80))except BlockingIOError:passf = Future()def on_connected():f.set_result(None)selector.register(sock.fileno(),EVENT_WRITE,on_connected)yield fselector.unregister(sock.fileno())print('connected!')

现在fetch 是一个生成器函数,它包含一个 yield 语句。我们创建一个挂起的Future,然后用yield暂停,直到套接字准备就绪。内部函数 on_connected 决定future

但是,当future确定时,谁来恢复生成器?我们需要一个协程驱动程序。我们称之为“任务”(task):

class Task:def __init__(self, coro):self.coro = corof = Future()f.set_result(None)self.step(f)def step(self, future):try:next_future = self.coro.send(future.result)except StopIteration:returnnext_future.add_done_callback(self.step)
# Begin fetching http://xkcd.com/353/
fetcher = Fetcher('/353/')
Task(fetcher.fetch())
loop()

任务通过 发送None 来启动 fetch 生成器。然后 fetch 运行直到产生一个future,任务将其捕获为 next_future。当套接字连接时,事件循环运行回调 on_connected,它确定future,调用 step(),恢复 fetch

分解 yield from 协程

连接套接字后,我们发送 HTTP GET 请求并读取服务器响应。这些步骤不再需要分散在回调中;我们将它们收集到相同的生成器函数中:

    def fetch(self):# ... connection logic from above, then:sock.send(request.encode('ascii'))while True:f = Future()def on_readable():f.set_result(sock.recv(4096))selector.register(sock.fileno(),EVENT_READ,on_readable)chunk = yield fselector.unregister(sock.fileno())if chunk:self.response += chunkelse:# Done reading.break

这段代码从套接字读取整条消息,似乎是通用的。我们如何将其从 fetch 中分解为一个子程序?这时,Python 3 中著名的 yield from 登场了,它允许将生成器委托给另一个生成器

为了了解如何操作,让我们回到简单的生成器示例:

>>> def gen_fn():
...     result = yield 1
...     print('result of yield: {}'.format(result))
...     result2 = yield 2
...     print('result of 2nd yield: {}'.format(result2))
...     return 'done'
...  

要从另一个生成器调用此生成器,通过yield from 委托:

>>> # Generator function:
>>> def caller_fn():
...     gen = gen_fn()
...     rv = yield from gen
...     print('return value of yield-from: {}'
...           .format(rv))
...
>>> # Make a generator from the
>>> # generator function.
>>> caller = caller_fn()

caller 生成器的行为就好像它是被委托的生成器gen

>>> caller.send(None)
1
>>> caller.gi_frame.f_lasti
15
>>> caller.send('hello')
result of yield: hello
2
>>> caller.gi_frame.f_lasti  # Hasn't advanced.
15
>>> caller.send('goodbye')
result of 2nd yield: goodbye
return value of yield-from: done
Traceback (most recent call last):File "<input>", line 1, in <module>
StopIteration

callergen生成值时 ,不会前进。请注意,即使内部生成器gen从一个 yield 语句 前进到下一个语句,它的指令指针仍保持在 15 处(即其 yield from 语句的位置)。 从外部 caller 的角度来看,我们无法判断它产生的值是来自 caller 还是来自它委托给的生成器。从内部 gen ,我们无法判断值是从内部发送的 caller 还是从外部发送的。yield from 语句是一个无摩擦的通道,值通过该通道流入和流出 gen ,直到 gen 完成。

协程可以通过yield from 将工作委派给 子协程,并接收工作结果。请注意,上面 caller 打印了“yield-from: done”的返回值。 gen完成后 ,其返回值成为calleryield from的值:

rv = yield from gen

早些时候,当我们批评基于回调的异步编程时,我们最尖锐的抱怨是关于“堆栈撕裂”:当回调抛出异常时,堆栈跟踪通常是无用的。它只显示事件循环正在运行回调,而不是为什么。协程的表现如何?

>>> def gen_fn():
...     raise Exception('my error')
>>> caller = caller_fn()
>>> caller.send(None)
Traceback (most recent call last):File "<input>", line 1, in <module>File "<input>", line 3, in caller_fnFile "<input>", line 2, in gen_fn
Exception: my error

这更有用!堆栈跟踪显示 caller_fn 在抛出错误时委派给 gen_fn 。更令人欣慰的是,我们可以将对子协程的调用包装在异常处理程序中,普通子例程也是如此:

>>> def gen_fn():
...     yield 1
...     raise Exception('uh oh')
...
>>> def caller_fn():
...     try:
...         yield from gen_fn()
...     except Exception as exc:
...         print('caught {}'.format(exc))
...
>>> caller = caller_fn()
>>> caller.send(None)
1
>>> caller.send('hello')
caught uh oh

因此,就像使用常规子程序一样,我们将逻辑分解成子协程。让我们从fetcher中分解出一些子协程。我们编写
read 协程来接收一个块:

def read(sock):f = Future()def on_readable():f.set_result(sock.recv(4096))selector.register(sock.fileno(), EVENT_READ, on_readable)chunk = yield f  # Read one chunk.selector.unregister(sock.fileno())return chunk

read_all协程接收完整消息:

def read_all(sock):response = []# Read whole response.chunk = yield from read(sock)while chunk:response.append(chunk)chunk = yield from read(sock)return b''.join(response)

如果你眯着眼睛看,这些 yield from 语句就会消失,这些语句看起来像是执行阻塞 I/O 的传统函数。但实际上, readread_all 是协程。yield from read 会暂停 read_all,直到 I/O 完成。当read_all暂停时 ,asyncio 的事件循环执行其他工作并等待其他 I/O 事件; 一旦read_all就绪,就会在下个循环中以read的结果恢复。

在堆栈的根目录下,fetch调用read_all

class Fetcher:def fetch(self):# ... connection logic from above, then:sock.send(request.encode('ascii'))self.response = yield from read_all(sock)

奇迹般地,Task 类不需要修改。它像以前一样驱动外部 fetch 协程:

Task(fetcher.fetch())
loop()

read产生一个future时 ,task通过 yield from的通道接收它,就好像future是直接从 fetch 中产生的一样。当循环解决完future时,任务将其结果发送到 fetch 中,并且该值由 read 接收,就像task直接驱动 read 一样:
yield from
了完善我们的协程实现,我们去除了一个瑕疵:我们的代码在等待future时使用 yield ,但在委托给子协程时使用 yield from 。如果我们在协程暂停时始终使用 yield from ,那将更加精致。这样一来,协程就不需要关心等待的是什么类型的东西。

我们利用了 Python 中生成器和迭代器之间的深度对应关系。对于调用者来说,使用生成器与使用迭代器相同。因此,我们通过实现特殊方法__iter__使 Future可迭代

    # Method on Future class.def __iter__(self):# Tell Task to resume me here.yield selfreturn self.result

Future__iter__ 方法是一个协程序,返回Future自身。
现在,当我们将代码:

# f is a Future.
yield f

替换为

# f is a Future.
yield from f

…结果是一样的!驱动Task从其对send 的调用 中接收future,当future被决定时,它会将新结果发送回协程。

统一使用 yield from 有什么好处?为什么这比等待具有 yieldfuture和委托给具有 yield from 的子协程更好?这更好,因为现在,一个方法可以自由地更改其实现而不影响调用者:它可能是一个返回future的普通方法,也可能是一个包含 yield from 语句(并返回值)的协程。对于这两种情况,调用者只需要 yield from 方法以等待结果。

耐心的读者,我们已经结束了对异步协程的愉快阐述。我们窥视了生成器的机制,并勾勒出futuretask的实现。我们概述了 asyncio 如何的两全其美:并发 I/O 比线程更高效,比回调更清晰。当然,真正的异步比我们的草图要复杂得多。真正的异步框架解决了零拷贝 I/O、公平调度、异常处理和大量其他功能。

对于 asyncio 用户来说,使用协程进行编码比您在这里看到的要简单得多。在上面的代码中,我们从第一性原理实现了协程,因此您看到了回调、任务和未来。您甚至看到了非阻塞套接字和对 select .但是,当需要使用 asyncio 构建应用程序时,这些都不会出现在您的代码中。正如我们承诺的那样,您现在可以时尚地获取 URL:

    @asyncio.coroutinedef fetch(self, url):response = yield from self.session.get(url)body = yield from response.read()

对这个阐述感到满意,我们回到我们最初的任务:使用 asyncio 编写一个异步网络爬虫。

协调协程

我们首先描述了我们希望我们的爬虫如何工作。现在是时候用 asyncio 协程来实现它了。

我们的爬虫将获取第一页,解析其链接,并将链接添加到队列中。在此之后,它会在整个网站上扇出,同时获取页面。但是为了限制客户端和服务器上的负载,我们希望运行一些最大数量的工作线程,而不是更多。每当worker完成获取页面时,它应该立即从队列中拉取下一个链接。我们将经历没有足够的工作可以进行的时期,因此一些worker必须暂停。但是,当一个worker点击一个包含新链接的页面时,队列会突然增加,任何暂停的worker都应该醒来并破解。最后,完成工作后,程序就退出。

想象一下,如果worker线程,该如何表达爬虫的算法?我们可以使用 Python 标准库中的同步队列。每次将项目放入队列时,队列都会递增其“task”计数。工作线程在完成项目工作后进行调用 task_done 。主线程会在Queue.join 阻塞,直到队列中的每个项目都与 task_done 调用匹配,然后退出。

协程使用与异步队列完全相同的模式!首先我们导入Queue

try:from asyncio import JoinableQueue as Queue
except ImportError:# Python 3.5: asyncio.JoinableQueue is merged into Queue.from asyncio import Queue

我们在crawler类中

  • 收集worker的共享状态,
  • crawl 方法中编写主逻辑。我们从协程启动 crawl ,运行 asyncio 的事件循环,直到 crawl 完成:
loop = asyncio.get_event_loop()
crawler = crawling.Crawler('http://xkcd.com',max_redirect=10)
loop.run_until_complete(crawler.crawl())

爬虫以根 URL 和 max_redirect(某个 URL 的最大重定向数量)开始。它将(URL, max_redirect) 放入队列中。(原因敬请期待)

class Crawler:def __init__(self, root_url, max_redirect):self.max_tasks = 10self.max_redirect = max_redirectself.q = Queue()self.seen_urls = set()# aiohttp's ClientSession does connection pooling and# HTTP keep-alives for us.self.session = aiohttp.ClientSession(loop=loop)# Put (URL, max_redirect) in the queue.self.q.put((root_url, self.max_redirect))

队列中未完成的任务数现在为 1。回到我们的主脚本中,我们启动事件循环crawl方法:

loop.run_until_complete(crawler.crawl())

crawl 协程启动了worker。它类似主线程:一直在 join 处阻塞,直到所有任务完成,而worker在后台运行。

    @asyncio.coroutinedef crawl(self):"""Run the crawler until all work is done."""workers = [asyncio.Task(self.work())for _ in range(self.max_tasks)]# When all work is done, exit.yield from self.q.join()for w in workers:w.cancel()

如果 worker 是线程,我们可能不希望一次启动它们。为了避免在确定需要之前创建昂贵的线程,线程池通常会按需增长。但是协程很便宜,所以我们从允许的最大数量开始。

有趣的是如何关闭爬虫。当 join future确定时,工作线程任务处于活动状态,但已暂停:它们等待更多 URL,但没有一个 URL 出现。因此,主协程在退出之前会取消它们。否则,当 Python 解释器关闭并调用所有对象的析构函数时,活动任务会大叫:

ERROR:asyncio:Task was destroyed but it is pending!

cancel 如何 工作?生成器具有我们尚未向您展示的功能。您可以从外部将异常抛入生成器:

>>> gen = gen_fn()
>>> gen.send(None)  # Start the generator as usual.
1
>>> gen.throw(Exception('error'))
Traceback (most recent call last):File "<input>", line 3, in <module>File "<input>", line 2, in gen_fn
Exception: error

生成器由 throw 恢复,但现在引发异常。如果生成器的调用堆栈中没有捕获它,则异常会冒泡回顶部。因此,要取消任务的协程:

    # Method of Task class.def cancel(self):self.coro.throw(CancelledError)

无论生成器在哪里暂停(在某个 yield from 语句中),它都会恢复并抛出异常。我们在任务的 step 的方法处理取消:

    # Method of Task class.def step(self, future):try:next_future = self.coro.send(future.result)except CancelledError:self.cancelled = Truereturnexcept StopIteration:returnnext_future.add_done_callback(self.step)

现在任务知道它被取消了,所以当它被摧毁时,它不会对光的消逝感到愤怒。

一旦 crawl 取消了工作线程,它就会退出。事件循环看到协程完成了(我们稍后会看到),它也会退出:

loop.run_until_complete(crawler.crawl())

crawl 方法包含了我们的主要协程必须执行的所有操作。工作器协程从队列中获取 URL,获取它们,并解析它们以获取新链接。每个 worker 独立运行 work 协程:

    @asyncio.coroutinedef work(self):while True:url, max_redirect = yield from self.q.get()# Download page and add new links to self.q.yield from self.fetch(url, max_redirect)self.q.task_done()

Python 看到此代码包含 yield from 语句,并将其编译为生成器函数。所以在 crawl中 ,当主协程调用self.work 十次时,它实际上并没有执行这个方法:它只创建十个引用此代码的生成器对象。它将每个包装在一个 Task 中。task接收 生成器产生的future,并通过在future确定时,以每个future的结果调用send 来驱动生成器。由于生成器有自己的堆栈帧,因此它们独立运行,具有单独的局部变量和指令指针。

worker通过队列和同伴进行协调,通过下面代码等待新的url:

url, max_redirect = yield from self.q.get()

队列的 get 方法本身就是一个协程:它会暂停,直到有人将一个项目放入队列中,然后恢复并返回该项目。
顺便说一句,这是在爬虫结束时,当主协程取消它时,worker暂停的位置。从协程的角度来看,它在循环中的最后一次行程在 yield from 引发 CancelledError。

当worker获取页面时,它会解析链接并将新链接放入队列中,然后调用 task_done 以递减计数器。最终,worke获取一个页面,其 URL 已全部获取,并且队列中也没有剩余工作。因此,该worke的调用 task_done 将计数器递减为零。然后 crawl ,正在等待队列 join 方法的 取消暂停并完成。

我们承诺解释为什么队列中的项目是成对的,例如:

# URL to fetch, and the number of redirects left.
('http://xkcd.com/353', 10)

新 URL 还剩下 10 个重定向。获取此特定 URL 会导致重定向到带有尾部斜杠的新位置。我们减少剩余的重定向数量,并将下一个位置放入队列中:

# URL with a trailing slash. Nine redirects left.
('http://xkcd.com/353/', 9)

默认情况下, aiohttp 将遵循重定向并给我们最终响应。但是,我们告诉它不要这样做,并在爬虫中处理重定向,因此它可以合并指向同一目的地的重定向路径:如果我们已经看到此 URL,则它已在 self.seen_urls 并且我们已经从不同的入口点开始此路径:
重定向
爬虫获取“foo",并看到它重定向到"baz",因此将"baz"添加到队列和"seen_urls"。如果下一页是”bar",并且也重定向到"baz",则不会再次添加"baz"。如果响应是页面而不是重定向,fetch将解析链接并添加新链接到队列。

    @asyncio.coroutinedef fetch(self, url, max_redirect):# Handle redirects ourselves.response = yield from self.session.get(url, allow_redirects=False)try:if is_redirect(response):if max_redirect > 0:next_url = response.headers['location']if next_url in self.seen_urls:# We have been down this path before.return# Remember we have seen this URL.self.seen_urls.add(next_url)# Follow the redirect. One less redirect remains.self.q.put_nowait((next_url, max_redirect - 1))else:links = yield from self.parse_links(response)# Python set-logic:for link in links.difference(self.seen_urls):self.q.put_nowait((link, self.max_redirect))self.seen_urls.update(links)finally:# Return connection to pool.yield from response.release()

如果这是多线程代码,那么在竞争条件下会很糟糕。例如,工作线程检查链接是否在 seen_urls中 ,如果不是,则工作线程将其放入队列中并将其添加到 seen_urls 中。如果它在两个操作之间被中断,那么另一个工作线程可能会从不同的页面解析相同的链接,同时观察它不在seen_urls 中 ,并将其添加到队列中。现在,同一个链接在队列中两次,导致(充其量)重复工作和错误的统计数据。

但是,协程仅在语句yield from时会受到中断。这是一个关键的区别,使协程代码比多线程代码更不容易发生争用:多线程代码必须通过抓取锁来显式进入关键部分,否则它是可中断的。默认情况下,Python 协程是不可中断的,并且仅在显式产生控制权时才放弃控制权。
.
我们不再需要像在基于回调的程序中那样的 fetcher 类。该类是解决回调缺点的方法:它们在等待 I/O 时需要一些地方来存储状态,因为它们的局部变量不会在调用之间保留。但是 fetch 协程可以像常规函数一样将其状态存储在局部变量中,因此不再需要类。

fetch 处理完服务器响应后 ,它会返回给调用方 work 。work 方法调用队列的task_done ,然后从队列中获取下一个要提取的URL。

fetch将新链接放入队列中时 ,它会增加未完成任务的计数,并暂停正在等待 q.join 的主协程。但是,如果没有看不见的链接,并且这是队列中的最后一个 URL,则work调用 task_done 时 ,未完成任务的计数将降至零。该事件解除join的暂停,主协程完成。

协调 worker 和主协程的队列代码是这样的

class Queue:def __init__(self):self._join_future = Future()self._unfinished_tasks = 0# ... other initialization ...def put_nowait(self, item):self._unfinished_tasks += 1# ... store the item ...def task_done(self):self._unfinished_tasks -= 1if self._unfinished_tasks == 0:self._join_future.set_result(None)@asyncio.coroutinedef join(self):if self._unfinished_tasks > 0:yield from self._join_future

主协程 crawl 从 join中获取值。 因此,当最后一个worker将未完成任务的计数减少到零时,它会向crawl 发出恢复并完成的信号。

旅程快结束了。程序从调用crawl开始:

loop.run_until_complete(self.crawler.crawl())

该程序如何结束?由于 crawl 是一个生成器函数,调用它将返回一个生成器。为了驱动生成器,asyncio 将其包装在一个任务中:

class EventLoop:def run_until_complete(self, coro):"""Run until the coroutine is done."""task = Task(coro)task.add_done_callback(stop_callback)try:self.run_forever()except StopError:pass
class StopError(BaseException):"""Raised to stop the event loop."""
def stop_callback(future):raise StopError

当任务完成时,它会发出 StopError ,循环将其用作已正常完成的信号。
但这是什么?该任务具有名

add_done_callbackresult 的方法?你可能会认为一项任务类似于一个future。你的直觉是正确的。我们必须承认一个关于我们向你隐瞒的任务类的细节:task就是future

class Task(Future):"""A coroutine wrapped in a Future."""

通常future是由其他人调用 set_result 解决的。但是,当task的协程停止时,task会自行解决。请记住,当生成器返回时,它会抛出特殊 StopIteration 异常:

    # Method of class Task.def step(self, future):try:next_future = self.coro.send(future.result)except CancelledError:self.cancelled = Truereturnexcept StopIteration as exc:# Task resolves itself with coro's return# value.self.set_result(exc.value)returnnext_future.add_done_callback(self.step)

因此,当事件循环调用 task.add_done_callback(stop_callback) 时,它准备被任务停止。
这是 run_until_complete

    # Method of event loop.def run_until_complete(self, coro):task = Task(coro)task.add_done_callback(stop_callback)try:self.run_forever()except StopError:pass

当任务捕获 StopIteration 并解决自己时,回调将从循环中引发 StopError。循环停止,调用堆栈回到 run_until_complete 。程序完成。

结论

现代程序越来越频繁地受 I/O 限制,而不是受 CPU 限制。对于这样的程序来说,Python 线程是完美的错误选择:全局解释器锁阻止它们实际并行计算,而抢占式切换使它们容易出现争用。异步通常是正确的模式。但随着基于回调的异步代码的增长,它往往会变得一团糟。协程是一个简洁的替代方案。它们自然而然地融入到子例程中,具有合理的异常处理和堆栈跟踪。

如果我们眯着眼睛使yield from 语句变得模糊,协程看起来像一个执行传统阻塞 I/O 的线程。我们甚至可以将协程与多线程编程中的经典模式进行协调。没有必要重新发明。因此,与回调相比,协程对于具有多线程经验的编码人员来说是一个诱人的习惯。

但是,当我们睁开眼睛并专注于这些 yield from 语句时,我们会看到它们在协程放弃控制权并允许其他人运行时标记点。与线程不同,协程显示我们的代码可以中断和不能中断的位置。Glyph Lefkowitz 在他的启发性文章“不屈不挠” 中写道:“线程使局部推理变得困难,而局部推理也许是软件开发中最重要的事情。然而,显式让权使得“通过检查例程本身而不是检查整个系统来理解例程的行为(以及由此而来的正确性)”成为可能。

本章是在 Python 和异步历史上的复兴时期写成的。2014 年 3 月,基于生成器的协程在 Python 3.4 的“asyncio”模块中发布。2015 年 9 月,Python 3.5 发布,其中内置了协程。这些原生协程使用新语法async def声明,而不是“yield from”,并且代指协程或等待 Future 时,不再使用“yield from”,而是使用新的“await”关键字。

尽管取得了这些进展,核心思想依然保留。Python 的新原生协程在语法上与生成器将有所区分,但工作方式非常类似;事实上,它们将在 Python 解释器内共享实现。TaskFuture事件循环将继续在其 asyncio 中扮演它们的角色。

现在你知道了 asyncio 协程的工作原理,你可以大部分忘记细节。这个机制被隐藏在一个时髦的接口后面。但是你对基本原理的掌握使你能够在现代异步环境中正确高效地编写代码。

相关文章:

《500 Lines or Less》(5)异步爬虫

https://aosabook.org/en/500L/a-web-crawler-with-asyncio-coroutines.html ——A. Jesse Jiryu Davis and Guido van Rossum 介绍 网络程序消耗的不是计算资源&#xff0c;而是打开许多缓慢的连接&#xff0c;解决此问题的现代方法是异步IO。 本章介绍一个简单的网络爬虫&a…...

Transformer!自注意力机制的高层级理解Attention Is All You Need!

背景 最近在不断深入学习LLM的相关内容&#xff0c;那么transformer就是一个绕不开的话题。然而对于一个NLP门外汉来说&#xff0c;论文看得是真头疼&#xff0c;总览全网&#xff0c;我们似乎缺少一个至高而下的高层级理解。所以本文就来弥补此方面的缺失~ 本文并不讲解有关…...

关于使用Postman在请求https网址没有响应,但是用浏览器有响应的问题解决

一、问题描述 使用postman调用正式环境的公共接口&#xff0c;无需鉴权&#xff0c;但是产生了返回状态码200&#xff0c;但是data中却无数据&#xff0c;如下 {"code": "200","message": "操作成功","data": {"qr_c…...

【React 】开发环境搭建详细指南

文章目录 一、准备工作1. 安装 Node.js 和 npm2. 选择代码编辑器 二、创建 React 项目1. 使用 Create React App2. 手动配置 React 项目 三、集成开发工具1. ESLint 和 Prettier2. 使用 Git 进行版本控制 在现代前端开发中&#xff0c;React 是一个非常流行的框架&#xff0c;用…...

结构体笔记

结构体 C语言中的数据类型&#xff1a; 基本数据类型&#xff1a;char/int/short/double/float/long 构造数据类型&#xff1a;数组&#xff0c;指针&#xff0c;结构体&#xff0c;共用体&#xff0c;枚举 概念&#xff1a; 结构体是用户自定义的一种数据类型&#xff0c…...

Elasticsearch:Golang ECS 日志记录 - zerolog

ECS 记录器是你最喜欢的日志库的格式化程序/编码器插件。它们可让你轻松地将日志格式化为与 ECS 兼容的 JSON。在本教程中&#xff0c;我将详述如何 编码器以 JSON 格式记录日志&#xff0c;并以 ECS 错误格式处理错误字段的记录。 默认情况下&#xff0c;会添加以下字段&…...

Ip2region - 基于xdb离线库的Java IP查询工具提供给脚本调用

文章目录 Pre效果实现git clone编译测试程序将ip2region.xdb放到指定目录使用改进最终效果 Pre OpenSource - Ip2region 离线IP地址定位库和IP定位数据管理框架 Ip2region - xdb java 查询客户端实现 效果 最终效果 实现 git clone git clone https://github.com/lionsou…...

研发管理革命:探索顶尖的工时系统选择

国内外主流的10款研发工时管理系统对比&#xff1a;PingCode、Worktile、无鱼项目工时系统、Toggl Track、泽众ALM、Asana、Jira、GitHub、Trello、TrackingTime。 在研发团队中&#xff0c;工时管理常常成为效率瓶颈&#xff0c;尤其是在资源分配和项目进度跟踪方面。选择合适…...

微服务-MybatisPlus下

微服务-MybatisPlus下 文章目录 微服务-MybatisPlus下1 MybatisPlus扩展功能1.1 代码生成1.2 静态工具1.3 逻辑删除1.4 枚举处理器1.5 JSON处理器**1.5.1.定义实体****1.5.2.使用类型处理器** **1.6 配置加密&#xff08;选学&#xff09;**1.6.1.生成秘钥**1.6.2.修改配置****…...

【python_将一个列表中的几个字典改成二维列表,并删除不需要的列】

def 将一个列表中的几个字典改成二维列表(original_list,headersToRemove_list):# 初始化一个列表用于存储遇到的键&#xff0c;保持顺序ordered_keys []# 遍历data中的每个字典&#xff0c;添加其键到ordered_keys&#xff0c;如果该键还未被添加for d in original_list:for …...

IDEA的pom.xml显示ignored 的解决办法

问题&#xff1a; idea中创建Maven module时&#xff0c;pom.xml出现ignored。 原因&#xff1a; 相同名称的module在之前被创建删除过&#xff0c;IDEA会误以为新的同名文件是之前删除掉的&#xff0c;将这个新的module的pom.xml文件忽略掉显示ignored. 解决&#xff1a; 在…...

2. 卷积神经网络无法绕开的神——LeNet

卷积神经网络无法绕开的大神——LeNet 1. 基本架构2. LeNet 53. LeNet 5 代码 1. 基本架构 特征抽取模块可学习的分类器模块 2. LeNet 5 LeNet 5: 5 表示的是5个核心层&#xff0c;2个卷积层&#xff0c;3个全连接层.核心权重层&#xff1a;卷积层、全连接层、循环层&#xff…...

【区块链】JavaScript连接web3钱包,实现测试网络中的 Sepolia ETH余额查询、转账功能

审核看清楚了 &#xff01; 这是以太坊测试网络&#xff01;用于学习的测试网络&#xff01;&#xff01;&#xff01; 有关web3 和区块链的内容为什么要给我审核不通过&#xff1f; 别人凭什么可以发&#xff01; 目标成果&#xff1a; 实现功能分析&#xff1a; 显示账户信…...

关于珞石机器人二次开发SDK的posture函数的算法RX RY RZ纠正 C#

在珞石SDK二次开发的函数钟&#xff0c;获取当前机器人位姿的函数posture函数在输出时会发现数据不正确&#xff0c;与示教器数据不一致。 其中第一个数据正确 第二三各数据为相反 第四五六各数据为弧度制 转换方法为(弧度/PI)*180度 然后发现第四个数据还要加上180度 第五…...

【Three.js基础学习】17.imported-models

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 前言 课程回顾&#xff1a; 如何在three.js 中引入不同的模型&#xff1f; 1. 格式 &#xff08;不同的格式&#xff09; https://en.wikipedia.org/wiki/List_of_file_form…...

Spring Bean - xml 配置文件创建对象

类型&#xff1a; 1、值类型 2、null &#xff08;标签&#xff09; 3、特殊符号 &#xff08;< -> < &#xff09; 4、CDATA <?xml version"1.0" encoding"UTF-8"?> <beans xmlns"http://www.springframework.org/schema/bea…...

uniapp map组件自定义markers标记点

需求是根据后端返回数据在地图上显示标记点&#xff0c;并且根据数据状态控制标记点颜色&#xff0c;标记点背景通过两张图片实现控制 <mapstyle"width: 100vw; height: 100vh;":markers"markers":longitude"locaInfo.longitude":latitude&…...

Windows:批处理脚本学习

目录 一、第一个批处理文件 1. &&和 | | 2. | 和 & 二、变量 1.传参变量%name 2.初始化变量set命令 3.变量的使用 4.局部变量与全局变量 5.使用环境变量 6.扩充变量语法 三、注释REM和 &#xff1a;&#xff1a; 四&#xff1a;函数 1.定义函数 2.…...

Dav_笔记10:Using SQL Plan Management之4

SQL管理库 SQL管理库(SMB)是驻留在SYSAUX表空间中的数据字典的一部分。它存储语句日志,计划历史记录,SQL计划基准和SQL配置文件。为了允许每周清除未使用的计划和日志,SMB使用自动空间管理。 您还可以手动将计划添加到SMB以获取一组SQL语句。从Oracle Database 11g之前的…...

通过json传递请求参数,如何处理动态参数和接口依赖

嗨&#xff0c;大家好&#xff0c;我是兰若姐姐&#xff0c;今天给大家讲一下如何通过json传递请求参数&#xff0c;如何处理动态参数和接口依赖 1. 使用配置文件和模板 在 test_data.json 中&#xff0c;你可以使用一些占位符或模板变量&#xff0c;然后在运行测试之前&…...

[240727] Qt Creator 14 发布 | AMD 推迟 Ryzen 9000芯片发布

目录 Qt Creator 14 发布Qt Creator 14 版本发布&#xff0c;带来一系列新功能和改进终端用户可通过命令行方式查看此新闻终端用户可通过命令行方式安装软件&#xff1a; AMD 推迟 Ryzen 9000芯片发布 Qt Creator 14 发布 Qt Creator 14 版本发布&#xff0c;带来一系列新功能…...

PLSQL Developer工具查询数据,报错(动态性能表不可访问)

解决的问题&#xff1a; 解决方案&#xff1a; 在配置-首选项-选项&#xff0c;取消勾选“自动统计”&#xff0c;保存之后即可查询数据...

基于 HTML+ECharts 实现智慧交通数据可视化大屏(含源码)

构建智慧交通数据可视化大屏&#xff1a;基于 HTML 和 ECharts 的实现 随着城市化进程的加快&#xff0c;智慧交通系统已成为提升城市管理效率和居民生活质量的关键。通过数据可视化&#xff0c;交通管理部门可以实时监控交通流量、事故发生率、道路状况等关键指标&#xff0c;…...

探索 IT 领域的新宠儿:量子计算

目录 引言&#xff1a;从经典到量子的飞跃 量子计算的基本概念 量子计算的独特优势 量子计算的深度剖析 量子计算的最新进展 量子计算的行业应用前景 面临的挑战与未来展望 结语&#xff1a;迎接量子计算的新时代 引言&#xff1a;从经典到量子的飞跃 在信息技术飞速发…...

TSPNet代码分析

论文《Realigning Confidence with Temporal Saliency Information for Point-Level Weakly-Supervised Temporal Action Localization》的official code分析 论文解读 代码分析 先看看训练过程,执行main if __name__ == __main__:exp = Exp()if exp.config.mode == eval:…...

Ubuntu上安装anaconda创建虚拟环境(各种踩坑版)

之前都是在Windows桌面版进行深度学习的环境部署及训练&#xff0c;今天尝试了一下在Ubuntu上进行环境部署&#xff0c;踩了不少坑&#xff0c;提供一些解决办法给大家避雷。 目录 一、下载和安装anaconda 1. 下载 2. 安装 二、创建虚拟环境 一、下载和安装anaconda 1. …...

DC-5靶机通关

今天我们来学习DC-5靶机&#xff01;&#xff01;&#xff01; 1.实验环境 攻击机&#xff1a;kali2023.2 靶机&#xff1a;DC-5 2.1扫描网段 2.2扫描端口 这里后面这俩端口有点似曾相识啊&#xff0c;在dc3里面好像见过&#xff0c;那咱们给这两个端口来个更详细的扫描&…...

AI学习记录 -使用react开发一个网页,对接chatgpt接口,附带一些英语的学习prompt

实现了如下功能&#xff08;使用react实现&#xff0c;原创&#xff09; 实现功能&#xff1a; 1、对接gpt35模型问答&#xff0c;并实现了流式传输&#xff08;在java端&#xff09; 2、在实际使用中&#xff0c;我们的问答历史会经常分享给他人&#xff0c;所以下图的 copy …...

MongoDB多数据源配置与切换

在MongoDB中配置和使用多数据源主要涉及以下几个步骤&#xff1a; 定义多个数据源的配置&#xff1a; 在应用程序的配置文件中&#xff0c;定义多个MongoDB的数据源&#xff0c;例如在Spring Boot中可以通过application.yml或application.properties文件进行配置。 创建多个Mo…...

Mongodb入门介绍

文章目录 1、Mongodb&#xff1a;NoSQL数据库&#xff0c;分布式的文档型数据库2、适合场景&#xff1a;3、不适合场景&#xff1a;4、概念5、总结 1、Mongodb&#xff1a;NoSQL数据库&#xff0c;分布式的文档型数据库 2、适合场景&#xff1a; 1、web网站数据存储&#xff…...

docker前端部署

挂载&#xff0c;把自己的目录位置&#xff0c;挂载到容器内的HTML...

指标体系建设的方法论

一、分析痛点 了解当前数仓侧与业务应用方对指标到不到、难使用的痛点及日常指标使用习惯&#xff0c;制定指标中心所需功能并设计指标中心样式。 二、指定指标规范 定义指标类型、指标使用方、确定指标域(这里是数据域)、指标要具备的属性(业务/技术口径、负责人、类型等)。 …...

乐鑫ESP32-H2设备联网芯片,集成多种安全功能方案,启明云端乐鑫代理商

在数字化浪潮的推动下&#xff0c;物联网正以前所未有的速度融入我们的日常生活。然而&#xff0c;随着设备的激增&#xff0c;安全问题也日益成为公众关注的焦点。 乐鑫ESP32-H2致力于为所有开发者提供高性价比的安全解决方案&#xff0c;这款芯片经过专门设计以集成多种安全…...

C++调用Java接口

一、配置Java环境 安装jdk&#xff0c;我这里使用jdk1.8 32位版本&#xff0c;下载地址&#xff1a;https://www.oracle.com/java/technologies/downloads/#java8-windows 下载安装后&#xff0c;设置环境变量&#xff1a; JAVA_HOME C:\Program Files (x86)\Java\jdk-1.…...

C# datetimePicker

1. 直接把控件拉到设计器中&#xff0c;此时不要调整控件的values属性&#xff0c;这样就可以 打开后每次默认显示当天日期。 2. 属性Format long长日期格式默认值short短日期格式Time时间格式custom自定义时间格式在customFormat这个属性设置&#xff0c;比如yyyy-MM-dd HH…...

AI有关的学习和python

一、基本概念 AIGC&#xff08;AI Generated content AI 生成内容&#xff09; AI生成的文本、代码、图片、音频、视频。都可以成为AIGC。 Generative AI&#xff08;生成式AI&#xff09;所生成的内容就是AIGC AI指代计算机人工智能&#xff0c;模仿人类的智能从而解决问题…...

前端node.js入门

(创作不易&#xff0c;感谢有你&#xff0c;你的支持&#xff0c;就是我前行的最大动力&#xff0c;如果看完对你有帮助&#xff0c;请留下您的足迹&#xff09; 目录 Node.js 入门概览 什么是Node.js&#xff1f; 为什么选择Node.js&#xff1f; 基础安装与环境配置 安装…...

无需标注的数据集

0&#xff1a;人 1&#xff1a;自行车 2&#xff1a;汽车 3&#xff1a;摩托车 4&#xff1a;飞机 5&#xff1a;公交车 6&#xff1a;火车 7&#xff1a;卡车 8&#xff1a;船 9&#xff1a;交通信号灯 10&#xff1a;消火栓 11&#xff1a;停车标志 12&#xff1a;停车计时器…...

C# 抽象工厂模式

栏目总目录 概念 抽象工厂模式是一种创建型设计模式&#xff0c;它提供了一种创建一系列相关或相互依赖对象的接口&#xff0c;而无需指定它们具体的类。在抽象工厂模式中&#xff0c;一个抽象的工厂类负责定义创建产品对象的接口&#xff0c;但是具体工厂类将负责创建具体的产…...

java中 两个不同类对象list,属性一样,如何copy

如果您有两个不同的类&#xff0c;但它们拥有相同的属性&#xff0c;并且您想要从一个类的列表复制到另一个类的列表&#xff0c;您可以使用以下方法&#xff1a; 使用循环&#xff1a; 您可以遍历原始列表&#xff0c;并为每个元素创建目标类的新实例。 使用 Stream API&…...

文件上传总结

一、原理 通过界面上的上传功能上传了一个可执行的脚本文件&#xff0c;而WEB端的系统并未对其进行检测或者检测的逻辑做的不够好&#xff0c;使得恶意用户可以通过文件中上传的一句话木马获得操控权 二、绕过方法 1>前端绕过 1.删除前端校验函数 checkFile() 2.禁用js…...

网页突然被恶意跳转或无法打开?DNS污染怎么解决?

前言 在网上冲浪时&#xff0c;我们时常会遭遇DNS污染这一区域性攻击&#xff0c;几乎无人能幸免。受影响时&#xff1a;尝试访问正规网站可能会被错误导向赌博、色情或其他恶意站点。 1.我们为什么需要DNS 当我们想要访问一个网站时&#xff0c;就像拨打朋友的电话号码一样…...

Matlab进阶绘图第65期—带分组折线段的柱状图

带分组折线段的柱状图是在原始柱状图的基础上&#xff0c;在每组柱状图位置处分别添加折线段&#xff0c;以进行对比或添加额外信息。 由于Matlab中未收录带分组折线段的柱状图的绘制函数&#xff0c;因此需要大家自行设法解决。 本文使用自制的BarwithGroupedLine小工具进行…...

EasyMedia转码rtsp视频流flv格式,hls格式,H5页面播放flv流视频

在本文中&#xff0c;我们将介绍如何使用 EasyMedia 将 RTSP 视频流转码为 FLV 和 HLS 格式&#xff0c;并在 H5 页面上播放 FLV 流视频。EasyMedia 是一个支持多种流媒体协议的开源项目&#xff0c;非常适合用于这种转码和流媒体传输的场景。 前提条件 已经安装并配置好 Eas…...

FPGA实验6: 有时钟使能两位十进制计数器的设计

一、实验目的与要求 1.. 熟练掌握使用原理图设计较复杂电路&#xff1b; 2. 学习原理图设计中总线的表示以及使用方法。 二、实验原理 运用Quartus II 集成环境下的图形设计方法设计有时钟使能的两位十进制计数器。进行波形仿真和分析、引脚分配并下载到实验设备上进行功能…...

C# 委托函数 delegate

在C#中&#xff0c;委托&#xff08;Delegate&#xff09;是一种特殊的类型&#xff0c;它可以持有对方法的引用。 委托是实现事件的基础。事件本质上是多播委托&#xff0c;允许多个方法被触发 委托允许你将方法作为参数传递给其他方法&#xff0c;或者将方法作为返回值从方法…...

Vue3响应式高阶用法之`shallowReadonly()`

Vue3响应式高阶用法之shallowReadonly() 在现代前端开发中&#xff0c;Vue3 提供了丰富的响应式 API 来帮助开发者更高效地管理状态和数据。其中&#xff0c;shallowReadonly() 是一个非常有用的工具&#xff0c;适用于需要部分只读状态的场景。本文将详细介绍 shallowReadonl…...

Windows系统安全加固方案:快速上手系统加固指南 (下)

这里写目录标题 一、概述二、IP协议安全配置启用SYN攻击保护 三、文件权限3.1 关闭默认共享3.2 查看共享文件夹权限3.3 删除默认共享 四、服务安全4.1禁用TCP/IP 上的NetBIOS4.2 ### 禁用不必要的服务 五、安全选项5.1启动安全选项5.2禁用未登录前关机 六、其他安全配置**6.1防…...

记一次因敏感信息泄露而导致的越权+存储型XSS

1、寻找测试目标 可能各位师傅会有苦于不知道如何寻找测试目标的烦恼&#xff0c;这里我惯用的就是寻找可进站的思路。这个思路分为两种&#xff0c;一是弱口令进站测试&#xff0c;二是可注册进站测试。依照这个思路&#xff0c;我依旧是用鹰图进行了一波资产的搜集&#xff…...

Java笔试面试题AI答之线程Thread(1)

答案来自 Kimi AI 目录 1. 进程和线程的区别&#xff1f;2. Java语言创建线程的方式有哪些&#xff1f;3. Java线程有哪几种可用状态&#xff1f;4. Java同步方法和同步代码块的区别&#xff1f;5. 在监视器(Monitor)内部&#xff0c;如何做线程同步的&#xff1f;6. 什么是死…...