并发与并行:网络抓取的重大区别

扫描, 差异, Mar-06-20245 分钟阅读

说到并发(concurrency)与并行(parallelism),可能很明显,因为它们指的是在多线程环境下执行计算机程序的相同概念。看了牛津词典中的定义,你可能会这么想。但是,当你深入研究这些概念时,就会发现它们与

说到并发(concurrency)与并行(parallelism),可能很明显,因为它们指的是在多线程环境下执行计算机程序的相同概念。看了牛津词典中的定义,你可能会这么想。然而,当你深入研究这些与 CPU 如何执行程序指令有关的概念时,你会发现并发和并行是两个截然不同的概念。 

本文将深入探讨并发性和并行性、它们之间的差异以及它们如何共同提高程序执行效率。最后,本文将讨论哪两种策略最适合网络刮擦。那么,让我们开始吧。

什么是并发执行?

首先,为了使问题简单化,我们将从在单个处理器中执行的单个应用程序的并发性入手。Dictionary.com将并发定义为联合行动或努力以及同时发生的事件。不过,并行执行也可以这样说,因为执行是同时进行的,因此在计算机编程领域,这个定义有些误导。

在日常生活中,您会在电脑上同时执行多个任务。例如,您可能一边用浏览器阅读博客文章,一边用 Windows Media Player 听音乐。还有另一个进程在运行:从另一个网页下载 PDF 文件--所有这些例子都是独立的进程。

在发明并发执行应用程序之前,CPU 是按顺序执行程序的。这意味着一个程序的指令必须在 CPU 进入下一个程序之前执行完毕。

相比之下,并发执行是交替执行每个进程的一小部分,直到所有进程都完成。

在单处理器多线程执行环境中,当另一个程序因用户输入而阻塞时,一个程序就会执行。现在你可能会问什么是多线程环境。它是相互独立运行的线程集合--下一节将详细介绍线程。

并发与并行执行不能混为一谈

这样一来,并发和并行就容易混淆了。在上述例子中,我们所说的并发是指进程不是并行运行的。 

相反,如果一个进程需要完成输入/输出操作,那么操作系统就会在另一个进程完成输入/输出操作时将 CPU 分配给它。这个过程会一直持续到所有进程都执行完毕。

不过,由于操作系统切换任务的时间只有纳秒或微秒,因此在用户看来,进程是并行执行的、 

什么是线程?

与顺序执行不同,在当前的架构下,CPU 可能无法一次性执行整个进程/程序。相反,大多数计算机可能会将整个进程拆分成几个轻量级组件,这些组件以任意顺序独立运行。这些轻量级组件被称为线程。

例如,谷歌文档可能有多个线程同时运行。当一个线程自动保存你的工作时,另一个线程可能会在后台运行,检查拼写和语法。  

操作系统决定线程的顺序和优先级,这与系统有关。

什么是并行执行?

现在,您已经了解了在单 CPU 环境中执行计算机程序的情况。相比之下,现代计算机在多个 CPU 中同时执行多个进程,即并行执行。目前大多数架构都有多个 CPU。

如下图所示,CPU 会并行执行属于进程的每个线程。  

在并行执行中,操作系统会根据系统结构,在宏或微秒级的时间内将线程切换到 CPU 或从 CPU 切换到线程。为使操作系统实现并行执行,计算机程序员使用了并行编程的概念。在并行编程中,程序员开发的代码可充分利用多个中央处理器。 

并发如何加速网络搜索

由于许多领域都在利用网络抓取技术从网站上抓取数据,因此一个显著的缺点是抓取大量数据需要耗费大量时间。如果你不是一个经验丰富的开发人员,你可能会浪费大量时间来试验特定的技术,最终才能无差错、完美地运行代码。

下文概述了网络搜索速度慢的一些原因。

网络搜索速度慢的重要原因?

首先,刮板必须导航到网络刮削的目标网站。然后,它必须从您希望搜刮的 HTML 标记中提取和检索实体。最后,在大多数情况下,您需要将数据保存到 CSV 格式等外部文件中。  

因此,正如你所看到的,上述大多数任务都需要进行大量绑定 I/O 操作,例如从网站上提取数据,然后将其保存到外部文件中。导航到目标网站通常取决于网络速度或等待网络可用等外部因素。

从下图中可以看出,当您需要搜索三个或更多网站时,这种极慢的时间消耗可能会进一步阻碍搜索过程。假定您按顺序执行刮擦操作。

因此,无论采用哪种方式,您都必须将并发性或并行性应用到您的刮擦操作中。我们将在下一节首先探讨并行性。

使用 Python 在网络搜索中实现并发性

我相信你现在已经对并发和并行有了大致的了解。本节将通过一个简单的 Python 代码示例,重点介绍网络刮擦中的并发性。

无需并发执行的简单示例演示

在本示例中,我们将根据维基百科中的人口数量,通过首都城市列表抓取各国的 URL。程序将保存链接,然后访问 240 个页面中的每个页面,并将这些页面的 HTML 保存在本地。

 为了演示并发的效果,我们将展示两个程序--一个是顺序执行程序,另一个是多线程并发程序。

代码如下:

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import time

def get_countries():
    countries = 'https://en.wikipedia.org/wiki/List_of_national_capitals_by_population'
    all_countries = []
    response = requests.get(countries)
    soup = BeautifulSoup(response.text, "html.parser")
    countries_pl = soup.select('th .flagicon+ a')
    for link_pl in countries_pl:
        link = link_pl.get("href")
        link = urljoin(countries, link)
        
        all_countries.append(link)
    return all_countries
  
def fetch(link):
    res = requests.get(link)
    with open(link.split("/")[-1]+".html", "wb") as f:
        f.write(res.content)
  

        
def main():
    clinks = get_countries()
    print(f"Total pages: {len(clinks)}")
    start_time = time.time()
    for link in clinks:
        fetch(link)
 
    duration = time.time() - start_time
    print(f"Downloaded {len(links)} links in {duration} seconds")
main()

代码解释

首先,我们导入包括 BeautifulSoap 在内的库,以提取 HTML 数据。其他库包括用于访问网站的 request 库、用于连接 URL 的 urllib 库和用于计算程序总执行时间的 time 库。

导入请求
from bs4 import BeautifulSoup
从 urllib.parse 导入 urljoin
导入时间

程序首先从主模块开始,调用 get_countries() 函数。然后,该函数通过 HTML 解析器,通过 BeautifulSoup 实例访问 countries 变量中指定的维基百科 URL。

然后,通过提取锚标签 href 属性中的值,搜索表中国家列表的 URL。

您获取的链接是相对链接。urljoin 函数将把它们转换为绝对链接。然后将这些链接追加到 all_countries 数组中,并返回给主函数 

然后,提取函数会将每个链接中的 HTML 内容保存为 HTML 文件。这就是这些代码的作用:

def fetch(link):
    res = requests.get(link)
    with open(link.split("/")[-1]+".html", "wb") as f:
        f.write(res.content)

最后,主功能会打印将文件保存为 HTML 格式所需的时间。在我们的电脑中,用时 131.22 秒。

当然,这个时间可以更快。我们将在下一节中找到答案,在这一节中,我们将用多个线程执行同一个程序。

具有并发性的同一程序

在多线程版本中,我们需要做一些细微的调整,以使程序执行得更快。

请记住,并发就是创建多个线程并执行程序。创建线程有两种方法:手动和使用 ThreadPoolExecutor 类。 

手动创建线程后,可以在手动方法的所有线程上使用 join 函数。这样,主方法就会等待所有线程完成执行。

在本程序中,我们将使用 ThreadPoolExecutor 类执行代码,该类是并发期货模块的一部分。因此,首先要在上述程序中加入下面一行。 

from concurrent.futures import ThreadPoolExecutor

之后,您可以将以 HTML 格式保存 HTML 内容的 for 循环修改如下:

  使用 ThreadPoolExecutor(max_workers=32) 作为 executor:
           executor.map(fetch, clinks)

上述代码创建了一个最多有 32 个线程的线程池。对于每个 CPU,max_workers 参数都不同,因此需要尝试使用不同的值。并不是线程数越多,执行时间就越快。

因此,在我们的 PC 中,输出为15.14 秒,比顺序执行时要好得多。

在进入下一节之前,下面是并发执行程序的最终代码:

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
from concurrent.futures import ThreadPoolExecutor
import time

def get_countries():
    countries = 'https://en.wikipedia.org/wiki/List_of_national_capitals_by_population'
    all_countries = []
    response = requests.get(countries)
    soup = BeautifulSoup(response.text, "html.parser")
    countries_pl = soup.select('th .flagicon+ a')
    for link_pl in countries_pl:
        link = link_pl.get("href")
        link = urljoin(countries, link)
        
        all_countries.append(link)
    return all_countries
  
def fetch(link):
    res = requests.get(link)
    with open(link.split("/")[-1]+".html", "wb") as f:
        f.write(res.content)


def main():
  clinks = get_countries()
  print(f"Total pages: {len(clinks)}")
  start_time = time.time()
  

  with ThreadPoolExecutor(max_workers=32) as executor:
           executor.map(fetch, clinks)
        
 
  duration = time.time()-start_time
  print(f"Downloaded {len(clinks)} links in {duration} seconds")
main()

并行化如何加快网络搜索速度

现在,我们希望您已经对并发执行有了一定的了解。为了帮助你更好地分析,让我们来看看在多处理器环境下,同一个程序在多个 CPU 中并行执行进程时的表现。

首先,您必须导入所需的模块:

from multiprocessing import Pool,cpu_count

Python 提供了 cpu_count()方法,用于计算机器中 CPU 的数量。这无疑有助于确定它可以并行执行的任务的精确数量。

现在,您必须用以下代码替换顺序执行中的 for 循环代码:

以 Pool (cpu_count()) 作为 p:
 
   p.map(fetch,clinks)

运行这段代码后,总执行时间为20.10 秒,比第一个程序的顺序执行时间要快。

结论

至此,我们希望您已经对并行和顺序编程有了一个全面的了解--选择使用其中一种还是另一种主要取决于您所面临的特定场景。

对于网络搜刮场景,我们建议从并发执行开始,然后过渡到并行解决方案。我们希望您喜欢阅读本文。请不要忘记阅读我们博客中与网络搜索相关的其他文章。