流畅无缝的用户体验是任何面向用户的应用成功的关键因素。开发者通常努力最小化应用延迟以提升用户体验。通常,这些延迟的根本原因是数据访问的延迟。
通过缓存数据,开发者可以显著减少这些延迟,从而导致更快的加载时间和更满意的用户。网络爬虫也不例外——大规模项目的速度也可以大幅提升。
但是,究竟什么是缓存,以及如何实现它呢?本文将讨论缓存及其目的和用途,以及如何使用它来增强你的 Python 网络爬虫代码的效能。
在编程中什么是缓存?
缓存是一种用于提高任何应用程序性能的机制。从技术意义上讲,缓存是指将数据存储在缓存中并稍后检索它。但是缓存到底是什么?
缓存是一个快速存储空间(通常是临时的),用于存储频繁访问的数据,以加快系统性能并减少访问时间。例如,计算机的缓存是一个小但快速的存储芯片(通常是SRAM),位于CPU和主内存芯片(通常是DRAM)之间。
CPU在需要访问数据时首先检查缓存。如果数据在缓存中,就会发生缓存命中,数据因此从缓存而不是相对较慢的主内存中读取。这有利用于访问时间减少和性能提升。
缓存的作用
缓存可以通过几种方式提高应用程序和系统的性能。以下是使用缓存的主要原因:
减少访问时间
缓存的主要目标是加快对频繁使用的数据的访问。缓存通过将频繁使用的数据保存在一个比原始数据源更易于访问的临时存储区域中来实现这一点。缓存可以通过减少访问时间,显著提高应用程序或系统的整体性能。
减少系统负载
缓存还可以减少系统的负载。这是通过减少对外部数据源(例如,数据库)的请求次数来实现的。
缓存将频繁使用的数据存储在缓存存储中,允许应用程序从缓存访问数据,而不是反复请求数据源。这减少了外部数据源的负载,最终提高了系统的性能。
改善用户体验
缓存允许用户更快地获取数据,支持与系统或应用程序的更自然的交互。这对于实时系统和网络应用程序尤其重要,因为用户都是期望立即响应的。因此,缓存可以帮助提高应用程序或系统的整体用户体验。
缓存的常见用例
缓存是一个通用概念,有几个突出的用例。在任何数据访问存在某些模式且你能预测下一步将需要哪些数据的场景中,你都可以应用它。你可以在缓存存储中预取所需数据,以提高应用性能。
网络应用程序
在网络应用程序中,我们经常需要从数据库或外部API访问数据。缓存可以减少对数据库或API请求的访问时间,提高网络应用程序的性能。
在网络应用程序中,缓存既用在客户端也用在服务器端。在客户端,网络应用程序将静态资源(例如,图片)和用户偏好设置(例如,主题设置)存储在客户端浏览器的缓存中。
在服务器端,内存缓存(在系统内存中存储数据)位于数据库和网络服务器之间,可以显著减少对数据库的请求负载,从而实现更快的加载时间。
例如,购物网站展示产品列表,用户在多个产品之间来回浏览。为了防止每次访客打开页面时都重复访问数据库获取产品列表,我们可以使用内存缓存来缓存产品列表。
机器学习
机器学习应用程序经常需要大量数据集。在缓存中预取数据集的子集将减少数据访问时间,最终减少模型的训练时间。
完成训练后,机器学习模型经常学习权重向量。你可以缓存这些权重向量,然后快速访问它们以预测任何新的未见样本,这是最常见的操作。
中央处理器(CPU)
中央处理器使用专用缓存(例如,L1、L2和L3)来改善其操作。CPU根据空间和时间访问模式预取数据,从而节省了否则在从主内存(RAM)读取时浪费的几个CPU周期。
缓存策略
根据特定的空间或时间数据访问模式,可以设计不同的缓存策略。
空间缓存在面向用户的应用中较不常见,而在处理大量数据且需要高性能和计算能力的科学或技术应用中更为常见。它涉及预取空间上接近(在内存中)当前应用或系统正在处理的数据。这一想法利用了一个事实:程序或应用用户下一步更有可能需求与当前需求空间上接近的数据单元。因此,预取可以节省时间。
时间缓存,一种更常见的策略,涉及根据数据的使用频率保留缓存中的数据。以下是最流行的时间缓存策略:
先进先出(FIFO)
FIFO缓存方法的原理是,添加到缓存中的第一个项目将是第一个被移除的。这种方法涉及加载预定数量的项目到缓存中,当缓存达到总容量时,最旧的项目被移除以容纳新的项目。
这种方法适用于优先考虑访问顺序的系统,如消息处理或队列管理系统。
后进先出(LIFO)
它通过首先替换最后添加到缓存中的项目来工作。缓存在开始时加载了一定数量的项目。随着新项目的添加,最近添加的项目将是第一个被移除以腾出空间给最新的项目,而缓存中最旧的项目保留直到被移除以腾出空间。
这种方法适用于优先考虑最新数据而非旧数据的应用,如基于栈的数据结构或实时流媒体。
最近最少使用(LRU)
LRU缓存涉及存储频繁使用的项目,同时移除最近最少使用的项目以释放空间。这种方法在网络应用程序或数据库系统等场景中特别有用,其中更重视频繁访问的数据而非旧数据。
为了理解这个概念,假设我们正在托管一个电影网站,并需要缓存电影信息。假设我们有一个大小为四个单位的缓存,每部电影的信息占用一个这样的单位。因此,我们最多只能缓存四部电影的信息。
现在,假设我们有以下电影列表要托管:
- 电影 A
- 电影 B
- 电影 C
- 电影 D
- 电影 E
- 电影 F
假设缓存最初用这四部电影及其请求时间填充:
- (1:50) 电影 B
- (1:43) 电影 C
- (1:30) 电影 A
- (1:59) 电影 F
我们的缓存现在已满。如果有一个新电影的请求(比如说电影 D 在 02:30),我们必须移除任何一部电影并添加一个新的。在LRU缓存策略中,最近没有被观看的电影将首先被移除。这意味着电影 A 将被电影 D 替换,并附上新的时间戳:
- (1:50) 电影 B
- (1:43) 电影 C
- (2:30) 电影 D
- (1:59) 电影 F
最近最常使用(MRU)
MRU缓存基于最近使用的项目来消除项目。这与LRU缓存不同,LRU首先移除最近最少使用的项目。
以我们的电影托管示例来说明,最新时间的电影将被替换。让我们将缓存重置到最初满的时间:
- (1:50) 电影 B
- (1:43) 电影 C
- (1:30) 电影 A
- (1:59) 电影 F
我们的缓存现在已满。如果有一个新电影的请求(电影 D),我们必须移除任何一部电影并添加一个新的。在MRU策略下,最新时间的电影将被新电影替换。这意味着2:30的电影 D 将替换电影 F:
- (1:50) 电影 B
- (1:43) 电影 C
- (1:30) 电影 A
- (2:30) 电影 D
MRU在某些情况下很有帮助,即越长时间没有被使用的东西,将来被再次使用的可能性越大。
最不频繁使用(LFU)
最不频繁使用(LFU)策略移除自添加以来使用次数最少的缓存项。与LRU和MRU不同,LFU不需要存储访问时间。它简单地跟踪自添加以来一个项目被访问的次数。
再次使用电影示例。这次我们维护了电影被观看的次数:
- (2) 电影 B
- (2) 电影 C
- (1) 电影 A
- (3) 电影 F
我们的缓存现在已满。如果有一个新电影的请求(电影 D),我们必须移除任何一部电影并添加一个新的。LFU将替换观看次数最少的电影为新电影:
- (2) 电影 B
- (2) 电影 C
- (1) 电影 D
- (3) 电影 F
如何在Python中实现缓存
对于不同的缓存策略,在Python中实现缓存有多种方式。这里我们将通过一个简单的网络爬虫示例,看到两种Python缓存的方法。如果你对网络爬虫不熟悉,可以查看我们的逐步Python网络爬虫指南。
安装所需的库
我们将使用requests库来对网站进行HTTP请求。通过在终端输入以下命令用pip安装它:
python -m pip install requests
在这个项目中,我们还将使用其他库,如time和functools,它们随Python 3.11.2自带,因此你无需安装它们。
方法1:使用手动装饰器进行Python缓存
在Python中,装饰器是一个接受另一个函数作为参数并输出一个新函数的函数。我们可以使用装饰器来改变原始函数的行为,而无需更改其源代码。
装饰器的一个常见用途是实现缓存。这包括创建一个字典来存储函数的结果,然后将它们保存在缓存中以供将来使用。
让我们从创建一个简单的函数开始,该函数接受一个URL作为函数参数,请求该URL,并返回响应文本:
import requests def get_html_data(url): response = requests.get(url) return response.text
现在,让我们开始创建这个函数的备忘录版本:
def memoize(func): cache = {} def wrapper(*args): if args in cache: return cache[args] else: result = func(*args) cache[args] = result return result return wrapper @memoize def get_html_data_cached(url): response = requests.get(url) return response.text
wrapper function决定当前的输入参数是否已经被缓存过,如果是的话,就返回之前缓存的结果。如果不是,代码会调用原始函数并在返回之前缓存结果。在这种情况下,我们定义了一个memoize装饰器,它生成一个缓存字典来保存之前函数调用的结果。
通过在函数定义上方添加@memoize,我们可以使用memoize装饰器来增强get_html_data函数。这样生成了一个新的备忘录函数,我们称之为get_html_data_cached。它对一个URL只进行单次网络请求,然后将响应存储在缓存中以供进一步请求使用。
让我们使用time模块来比较get_html_data函数和备忘录的get_html_data_cached函数的执行速度:
import time start_time = time.time() get_html_data('https://books.toscrape.com/') print('Time taken (normal function):', time.time() - start_time) start_time = time.time() get_html_data_cached('https://books.toscrape.com/') print('Time taken (memoized function using manual decorator):', time.time() - start_time)
下面是完整的代码:
# Import the required modules import time import requests # Function to get the HTML Content def get_html_data(url): response = requests.get(url) return response.text # Memoize function to cache the data def memoize(func): cache = {} # Inner wrapper function to store the data in the cache def wrapper(*args): if args in cache: return cache[args] else: result = func(*args) cache[args] = result return result return wrapper # Memoized function to get the HTML Content @memoize def get_html_data_cached(url): response = requests.get(url) return response.text # Get the time it took for a normal function start_time = time.time() get_html_data('https://books.toscrape.com/') print('Time taken (normal function):', time.time() - start_time) # Get the time it took for a memoized function (manual decorator) start_time = time.time() get_html_data_cached('https://books.toscrape.com/') print('Time taken (memoized function using manual decorator):', time.time() - start_time)
下面是输出:
注意这两个函数之间的时间差异。两者几乎同时完成,但缓存的优势在于重新访问时的情况。
由于我们只发出了一次请求,备忘录函数也必须从主内存中访问数据。因此,在我们的示例中,不期望执行时间有显著差异。然而,如果你增加对这些函数的调用次数,时间差异将显著增加(请参见下面名为“性能比较”的部分)。
方法2:使用LRU缓存装饰器进行Python缓存
在Python中实现缓存的另一种方法是使用functools中内置的@lru_cache装饰器。这个装饰器使用最近最少使用(LRU)缓存策略来实现缓存。这个LRU缓存是一个固定大小的缓存,这意味着它将丢弃最近未使用的缓存数据。
要使用@lru_cache装饰器,我们可以创建一个新的函数来提取HTML内容,并将装饰器名称放在顶部。使用装饰器之前,请确保导入了functools模块:
from functools import lru_cache @lru_cache(maxsize=None) def get_html_data_lru(url): response = requests.get(url) return response.text
在上面的示例中,get_html_data_lru方法通过使用@lru_cache装饰器进行了备忘录化。当maxsize选项设置为None时,缓存可以无限增长。
要使用@lru_cache装饰器,只需将其添加到get_html_data_lru函数之上。以下是完整的代码示例:
# Import the required modules from functools import lru_cache import time import requests # Function to get the HTML Content def get_html_data(url): response = requests.get(url) return response.text # Memoized using LRU Cache @lru_cache(maxsize=None) def get_html_data_lru(url): response = requests.get(url) return response.text # Get the time it took for a normal function start_time = time.time() get_html_data('https://books.toscrape.com/') print('Time taken (normal function):', time.time() - start_time) # Get the time it took for a memoized function (LRU cache) start_time = time.time() get_html_data_lru('https://books.toscrape.com/') print('Time taken (memoized function with LRU cache):', time.time() - start_time)
这产生了以下输出:.
性能比较
在下表中,我们确定了对这些函数进行不同数量请求时,所有三个函数的执行时间:
请求次数 | 普通函数所需时间 | 使用手动装饰器备忘录函数所需时间 | 使用lru_cache装饰器备忘录函数所需时间 |
1 | 2.1秒 | 2.0秒 | 1.7秒 |
10 | 17.3秒 | 2.1秒 | 1.8秒 |
20 | 32.2秒 | 2.2秒 | 2.1秒 |
30 | 57.3秒 | 2.22秒 | 2.12秒 |
随着对函数的请求数量的增加,你可以看到使用缓存策略大大减少了执行时间。下面的对比图描述了这些结果:
比较结果清楚地显示,在代码中使用缓存策略可以显著提高整体性能和速度。
结 论
如果你想加快你的代码速度,缓存数据可以帮助到你。有许多情况下,缓存是改变游戏规则的关键,网络爬虫就是其中之一。当处理大规模项目时,考虑在代码中缓存频繁使用的数据,以大幅加速你的数据提取工作并提高整体性能。