返回博客
指南
Raluca PenciucLast updated on Apr 28, 20262 min read

用 Python 进行网络抓取:构建网络抓取工具的终极指南

用 Python 进行网络抓取:构建网络抓取工具的终极指南

如果说20世纪我们奉行“时间就是金钱”的理念,那么如今一切都围绕数据展开。更多的数据意味着更深入的洞察,从而做出更明智的决策,并创造更多利润。

尤其是在过去十年间,网络爬取和网络爬虫的普及度大幅提升。越来越多的企业需要制定精准的营销策略,这意味着必须在短时间内获取海量信息。

随着数据提取成为新的焦点,企业开始意识到其中的潜在价值。对于开发者而言,这既可能是拓展业务的好途径,也可以是一个磨练编程技能的绝佳项目。

即使你的工作与网络爬虫毫无关联,但只要你是 Python 开发者,读完本文后,你将发现一个能充分发挥自身技能的新领域。我们将探讨如何使用 Python 构建自己的网络爬虫。

了解 Python 网络爬虫

但首先,什么是网页抓取?从最基础的层面来说,网页抓取工具会从网站中提取数据——毕竟并非所有网站都通过公开API提供数据。

若考虑到信息越丰富,商业决策就越明智,这一过程的实际价值远超表面印象。

如今,网站内容日益丰富,因此完全依靠人工进行数据抓取绝非明智之举。这正是开发自动化抓取工具变得至关重要的原因。

“我需要这些数据做什么?”您可能会问。那么,让我们来看看网络爬虫在哪些关键场景中堪称救星:

  • 价格情报:电商企业需要掌握竞争对手的定价信息,以便制定更优的定价和营销策略。
  • 市场调研:市场分析意味着需要高质量、海量且富有洞察力的信息。
  • 房地产:个人或企业需要整合来自多个渠道的房源信息。
  • 潜在客户开发:为不断发展的业务寻找客户。
  • 品牌监测:企业会分析论坛、社交媒体平台及用户评价,以追踪品牌形象。
  • 最低广告价(MAP)监控可确保品牌的在线价格符合其定价政策。
  • 机器学习:开发者需要提供训练数据,以确保其人工智能解决方案能够正常运行。

您可以在此处查看更多用例及其详细说明

“太棒了,赶紧开始吧!”您可能会这么说。但别急。

即使您弄清楚了网络爬虫的工作原理及其如何提升业务,构建一个网络爬虫也并非易事。首先,出于各种原因,有些人并不希望自己的网站被爬虫访问。

其中一个原因在于,抓取会在一秒内发送大量请求,这可能导致服务器过载。网站所有者有时会将此视为黑客攻击(拒绝服务攻击),因此网站会采取措施通过屏蔽机器人来保护自身。

这些措施可能包括:

  • IP封禁:当网站检测到同一IP地址发出大量请求时,便会触发此机制;网站可能会完全禁止您访问,或大幅降低您的访问速度。
  • 验证码CAPTCHA,全自动公共图灵测试,用于区分计算机与人类):这类逻辑问题对人类而言轻而易举,却让爬虫头疼不已。
  • 蜜罐:嵌入对人类不可见但对机器人可见的链接;一旦机器人落入陷阱,网站便会封禁其IP。
  • 需登录:网站可能将您所需的部分信息隐藏在登录页面之后;即使您在网站上通过了身份验证,爬虫也无法获取您的凭据或浏览器Cookie。

虽然有些网站可能并未采用这些技术,但仅凭它们希望通过 JavaScript 提供更好的用户体验这一事实,就足以让网络爬虫的工作变得更加困难。

当网站使用 JavaScript 或 HTML 生成框架时,部分内容只有在与网站进行某些交互后,或执行生成 HTML 文档的脚本(通常用 JavaScript 编写)后才能访问。

此外,还需考虑所提取数据的质量。例如,在电商网站上,您看到的商品价格可能因所在地区而异。这类数据准确性有限,因此爬虫必须设法尽可能精确地提取数据。

即使成功克服了所有这些困难,你仍需考虑到网站结构随时可能发生变化。毕竟,网站需要对用户友好,而非对爬虫友好,因此我们的自动化工具必须适应这些变化。

在这场永无止境的爬取大战中,爬虫会自行想出解决方案。所有这些方案的核心目标,都是尽可能逼真地模拟人类在互联网上的行为。

例如,您可以通过使用IP代理服务来规避IP封禁。建议选用付费服务,因为免费服务会公开其IP地址,导致网站可能将其封禁。

您还可以集成验证码破解工具。它们能帮助您实现持续的数据获取,但会稍微降低抓取速度。

针对蜜罐陷阱,你可以使用XPath(若足够大胆,甚至可使用正则表达式)来抓取指定元素,而非整个HTML文档。

考虑所有这些问题及其应对方案,可能成为一项费时费力的过程。正因如此,在过去十年间,网页抓取API获得了越来越多的关注。

在 WebScrapingAPI,我们能够从任何网站收集 HTML 内容,并能应对所有可能的挑战(如前文所述)。此外,我们基于亚马逊云服务(AWS)构建,因此速度和可扩展性绝非问题。心动了吗?您可以先注册一个免费账户,每月可享受 1000 次 API 调用。很棒,对吧?

理解网络

现在,让我们回到本文的主题。我们要学习如何使用 Python 构建一个网页爬虫。

首先必须了解的概念是超文本传输协议(HTTP),它定义了服务器与客户端之间的通信机制。其原理非常简单:客户端(应用程序)向服务器发送一条消息(HTTP请求),服务器则返回响应。

该消息包含多项信息,用于描述客户端及其数据处理方式:方法、HTTP 版本和头部。

在网页抓取中,HTTP请求最常用的方法是GET。这表示你将检索所请求的数据。如果你想了解更多相关内容,可以在这里找到一份完整且详细的列表。

头部包含关于 HTTP 请求或响应的附加信息。我们将重点介绍在网页抓取中最为相关的头部,但您也可以查阅完整列表

  • User-Agent:用于标识应用程序、操作系统、软件及其版本;网络爬虫利用此标头使请求看起来更真实。
  • Cookie:包含关于请求的状态信息(例如认证令牌)。
  • Host:指定服务器的域名,以及服务器监听的端口号。
  • Referrer:包含用户来源的源网站;根据该信息,显示的内容可能会有所不同,因此网络爬虫也必须将此因素纳入考量。
  • Accept:告知服务器响应中可返回的内容类型;尽管它能促进客户端与服务器之间更自然的交互,但在网页抓取中常被忽略。

了解 Python

Python 是一种通用且高级的编程语言,因多种原因始终深受开发者青睐:

  • 可读性强的代码:其简洁的语法使其成为初学者的理想选择。
  • 编程范式:面向对象、结构化、函数式和面向切面编程等只是其中几个例子。
  • 强大的标准库:涵盖各类需求的丰富模块
  • 活跃的社区:拥有众多开源库和工具

好了,说了这么多,让我们来准备工作环境。首先,我们需要 Python 3。您可以从这里下载并安装。

本教程不限定使用特定 IDE,请随意选择您习惯的工具。我们推荐 Visual Studio Code,因其轻量且支持多种编程语言。

我们还将使用以下 Python 库:

  • requests:用于发起 HTTP 请求
  • beautifulsoup:用于解析 HTML 文档
  • selenium:用于抓取动态内容
  • nltk(可选):用于处理自然语言

您无需提前安装所有这些库,因为每个步骤中都会提供更详细的说明和安装指南。

现在,让我们开始抓取网页吧!

制作自己的网页抓取工具

为了方便您理解,本教程将按步骤进行讲解。此外,您还会看到一些编号后带有 .1 标记的附加步骤。这些是您可能感兴趣的替代方案。

步骤 1:检查你要抓取的页面

好了,闲话少说,让我们开始吧。

首先,我们需要选择一个想要抓取的网站。为了说明原理,我们选择一个简单的教育网站:https://en.wikipedia.org/wiki/Beer

在此步骤中,我们只需查看页面的 HTML 文档,以大致了解其结构。无论你使用何种浏览器或操作系统,操作流程都是一样的。在任意位置(图片、链接或普通文本块上)右键点击,然后选择“检查元素”选项。

我们只需关注“元素”选项卡。根据网站的不同,您可能会看到大量的 HTML 代码。重要的是不要被这些代码吓倒,而应只查看您感兴趣的数据。

步骤 2:发送 HTTP 请求

现在我们可以开始编写网络爬虫的代码了。首先,我们只需向目标网站发送一个 HTTP 请求,以获取刚才在浏览器中看到的完整 HTML 代码。这在代码中会是什么样子呢?嗯,太棒了!毕竟我们使用的是 Python。

让我们安装 requests 库来发送 HTTP 请求:

pip install requests

现在来编写代码:

import requests
URL = 'https://en.wikipedia.org/wiki/Beer'
page = requests.get(URL)

很简单,对吧?我们导入了之前安装的库,然后定义了要抓取的网站 URL,并发送了一个 GET 请求。如果你想查看结果,不妨直接将其打印出来。目前,你看到的只是一串杂乱的字符串,它代表了你在浏览器中看到的 HTML。这并不能给我们提供太多帮助,因此我们需要对其进行处理。

步骤 3:抓取页面 HTML

为了从结果中提取有用的信息,我们将安装 beautifulsoup 库:

pip install beautifulsoup4

首先,让我们将结果格式化得更美观一些:

import requests
from bs4 import BeautifulSoup
 
URL = 'https://en.wikipedia.org/wiki/Beer'
page = requests.get(URL)
 
soup = BeautifulSoup(page.content, 'html.parser')
prettyHTML = soup.prettify()
print(prettyHTML)

我们将之前的结果转换为 BeautifulSoup 对象。通过 .content 属性,你可以访问 HTML 数据。调用 .prettify() 方法后,你会看到与之前在浏览器中看到的格式完全一致。

遗憾的是,并非所有网站都会像这样直接提供完整的 HTML 内容。如前所述,网络爬虫在实际操作中会遇到一些挑战。

步骤 3.1:动态内容

例如,某些页面只有在登录后才可见。即使您通过浏览器进行了身份验证,Python 脚本也无法访问这些数据。

另一种常见情况是动态网站。这意味着 GET 请求的响应并非 HTML 文档,而是 JavaScript 脚本。即使你在浏览器中能看到 HTML,那也是因为脚本被执行了。但在你的代码中,你需要在本地运行该脚本才能获取 HTML。

让我们通过实际案例来了解这种情况。我们将选择一个简约示例,即一个快速搭建的网站,该网站源自一则较早的大学练习:https://dynamic-website.surge.sh

你可以在浏览器中看到完整的 HTML 内容。在这个示例中,它是一个表格,其单元格中包含图片。

import requests
from bs4 import BeautifulSoup
 
URL = 'https://dynamic-website.surge.sh'
page = requests.get(URL)
soup = BeautifulSoup(page.content, 'html.parser')
 
file = open('page.txt', mode='w', encoding='utf-8')
file.write(soup.prettify())

现在让我们提取这个 HTML,这意味着我们将运行与之前相同的代码,只是做了一些改动:我们更新了 URL,并打开了一个文本文件来存储结果。我们运行程序,并在 page.txt 文件中查找相同的表格部分。

这是什么?表格在哪里?其实它就在那里,只是目前还没有人生成它。你可以查看 HTML 文档中的 <head> 标签,看看是否使用了脚本:

没错,我们确实用到了脚本。

要解决这个问题,我们需要 Selenium——一个用于网页测试和浏览器操作自动化的库。我们将以无头模式使用它,这意味着它将像普通浏览器一样执行 JavaScript 代码,但不会显示任何用户界面。

pip install selenium

在本教程中,我们将使用 ChromeDriver 来配置 Selenium 的 WebDriver。请务必记住下载文件的路径!我们将其保存到了 C 盘,但任何位置都可以。

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from bs4 import BeautifulSoup
 
CHROMEDRIVER_PATH = "your/path/here/chromedriver_win32/chromedriver"
URL = "https://dynamic-website.surge.sh"
 
options = Options()
options.headless = True
driver = webdriver.Chrome(CHROMEDRIVER_PATH, options=options)
 
driver.get(URL)
soup = BeautifulSoup(driver.page_source, 'html.parser')
 
file = open('page.txt', mode='w', encoding='utf-8')
file.write(soup.prettify())

后续步骤基本相同,只是我们不再使用 requests 库来发送 HTTP 请求。

我们重新运行程序,然后……

……瞧!现在我们已经获取到了完整的 HTML 页面。

步骤 4:提取特定部分

好,让我们回到正题。

虽然获取完整的 HTML 是一个巨大的进步,但过程尚未结束。大多数情况下,我们需要从网站中提取特定信息,所以让我们看看如何提取它们。

我们先从一个简单的内容开始——网站的标题。你可以在 HTML 的 <head> 部分,位于 &lt;title> 标签找到它。

我们知道网站只有一个标题,因此将使用 .find() 方法。该方法以标签名称为输入并返回 HTML 元素,若需获取其内容,只需通过 .text 属性访问即可。此外,我们将为这个小爬虫添加一些结构。

def extract_title(soup):
    title = soup.find('title')
 
    #output: <title>Beer - Wikipedia</title>    
    print('Title element: ', title)
 
    #output: Beer - Wikipedia
    print('Title: ', title.text)
 
def main():
    URL = 'https://en.wikipedia.org/wiki/Beer'
    page = requests.get(URL)
    soup = BeautifulSoup(page.content, 'html.parser')
 
    extract_title(soup)
 
main()

这没什么高深的学问。BeautifulSoup 是一个功能强大的库,支持多种提取特定数据的模式。你可以通过 HTML 元素的名称、ID 和 class 属性来获取它们,甚至可以使用 CSS 选择器。可能性无穷无尽!

让我们进一步探索,例如提取出现多次的元素。这种情况下,我们使用 .find_all() 方法。唯一的区别在于它返回的是元素列表,而不是单个元素。因此,之后我们需要遍历该列表并显示每个元素的属性。例如,我们提取了文章中的所有图片:

def extract_images(soup):
    images = soup.find_all('img')
    for image in images:
        imageAlt = image.get('alt')
        imageSrc = image.get('src')
        print("ALT: ", imageAlt, "SRC: ", imageSrc)
 
def main():
    URL = 'https://en.wikipedia.org/wiki/Beer'
    page = requests.get(URL)
    soup = BeautifulSoup(page.content, 'html.parser')
 
    extract_images(soup)
 
main()

步骤 5:在抓取过程中传递函数

在网页抓取中,常会遇到解析结果列表非常长且包含混合信息的情况。

例如,您可能已经注意到,我们之前提取的图片可能包含 alt 属性,也可能不包含。

或者设想我们要从文章中提取所有链接。众所周知,维基百科文章包含大量链接,而我们可能并不需要完整的列表。结果中将包含外部链接、内部链接、参考文献和引用,因此我们需要将其分类为多个类别。

为解决这个问题,我们将使用 lambda 函数。基本上,lambda 会将结果列表中的每个元素作为参数,并应用我们定义的条件,就像使用 filter 函数一样。

以一个实际案例为例,假设我们需要提取所有内部链接,访问对应文章,并为每篇撰写摘要。考虑到 Python 的应用场景之一是人工智能,这个示例可以成为获取训练数据的绝佳应用。

首先,我们需要安装 NLTK 库,因为生成摘要涉及自然语言处理。

pip install -U nltk

当然,还需要在代码中导入该库:

import re
import nltk
import heapq
# need to download only for the first execution
# warning: the size of the dataset is big; hence it will take time
nltk.download()

注意:如果您是 macOS 用户,可能会遇到“SSL: certificate verify failed”错误。原因可能是 Python 3.6 使用了内置版本的 OpenSSL。您只需打开 Python 的安装目录,并运行以下文件:

/Your/Path/Here/Python 3.6/Install Certificates.command

如您所见,我们还导入了 re 库(用于正则表达式操作)以及 heapq(一种堆队列的实现)。

很好,我们已经具备了编写代码所需的一切。让我们先从提取内部链接开始。如果你回到浏览器,会注意到我们感兴趣的这些元素有几个特点。

这些特征包括:

  • href 属性具有值;
  • href 属性的值以“/wiki/”开头;
  • 链接的父元素是 标签;

这些特征将帮助我们从所有其他链接中区分出我们需要的链接。

既然我们已经知道如何查找链接,接下来看看如何提取它们。

count = 0
 
def can_do_summary(tag):
    global count
    if count > 10: return False
 
    # Reject if parent is not a paragraph
    if not tag.parent.name == 'p': return False
 
    href = tag.get('href')
    # Reject if href is not set
    if href is None: return False
 
    # Reject is href value does not start with /wiki/
    if not href.startswith('/wiki/'): return False
 
    compute_summary(href)
    return True
 
 
def extract_links(soup):
    soup.find_all(lambda tag: tag.name == 'a' and can_do_summary(tag))
 
def main():
    URL = 'https://en.wikipedia.org/wiki/Beer'
    page = requests.get(URL)
    soup = BeautifulSoup(page.content, 'html.parser')
 
    extract_links(soup)
 
main()

好的,这里发生了什么?观察 extract_links() 函数,我们可以发现,我们没有传入标签名称,而是将一个 lambda 函数作为参数传递给了 .find_all() 方法。这意味着,我们从整个 HTML 文档的所有标签中,只挑选出符合我们条件的那些。

如你所见,标签的筛选条件是:必须是链接,且必须通过上面定义的 can_do_summary() 函数的验证。在那里,我们会剔除所有不符合先前观察到的特征的链接。此外,我们使用了一个全局变量来将提取的链接数量限制为 10 个。如果你需要所有链接,请随意移除 count 变量。

最后,我们对新发现的链接调用 compute_summary() 函数。该函数负责对文章进行摘要处理。

def compute_summary(href):
    global count
    full_link = 'https://en.wikipedia.org' + href
    page = requests.get(full_link)
    soup = BeautifulSoup(page.content, 'html.parser')
 
    # Concatenate article paragraphs
    paragraphs = soup.find_all('p')
    article_text = ""
    for p in paragraphs:
        article_text += p.text
 
    # Removing Square Bracket, extra spaces, special characters and digits
    article_text = re.sub(r'\[[0-9]*\]', ' ', article_text)
    article_text = re.sub(r'\s+', ' ', article_text)
    formatted_article_text = re.sub('[^a-zA-Z]', ' ', article_text)
    formatted_article_text = re.sub(r'\s+', ' ', formatted_article_text)
 
    # Converting text to sentences
    sentence_list = nltk.sent_tokenize(article_text)
 
    # Find frequency of occurrence of each word
    stopwords = nltk.corpus.stopwords.words('english')
 
    word_frequencies = {}
    for word in nltk.word_tokenize(formatted_article_text):
        if word not in stopwords:
            if word not in word_frequencies.keys():
                word_frequencies[word] = 1
            else:
                word_frequencies[word] += 1
 
    maximum_frequency = max(word_frequencies.values())
 
    for word in word_frequencies.keys():
        word_frequencies[word] = (word_frequencies[word] / maximum_frequency)
 
    # Calculate the score of each sentence
    sentence_scores = {}
    for sent in sentence_list:
        for word in nltk.word_tokenize(sent.lower()):
            if word in word_frequencies.keys():
                if len(sent.split(' ')) < 30:
                    if sent not in sentence_scores.keys():
                        sentence_scores[sent] = word_frequencies[word]
                    else:
                        sentence_scores[sent] += word_frequencies[word]
 
    # Pick top 7 sentences with highest score
    summary_sentences = heapq.nlargest(7, sentence_scores, key=sentence_scores.get)
    summary = '\n'.join(summary_sentences)
    count += 1

简而言之,我们向新发现的 URL 发起 HTTP 请求,并将结果转换为 BeautifulSoup 对象,就像我们在文章开头所做的那样。

为了生成摘要,我们会从文章中提取所有段落并将其拼接在一起。随后,我们会移除所有可能干扰计算的特殊字符。

简而言之,摘要的生成是通过计算高频词,并根据各句子中词汇的出现频率为其赋予分数来实现的。最后,我们选取分数最高的7个句子。

虽然这不是本文的重点,但如果您对自然语言处理感兴趣甚至充满热情,可以在此处阅读更多相关内容。

第 6 步:生成 CSV 文件以存储数据

进入本指南的最后一步,我们需要查看爬取结果。此前,由于数据行数较少,我们仅在终端中进行了显示。

但实际的爬取通常涉及海量信息,因此我们需要了解如何将结果保存到文件中。

让我们使用原生库 csv(因此无需安装其他软件),并打开一个名为 summaries.csv 的文件。

import csv
summaries_file = open('summaries.csv', mode='a', encoding='utf-8')

如果文件不存在,该库会自动创建。此外,我们以“追加”模式打开文件,因为每个链接都是按顺序逐个处理的。

    summaries_writer = csv.writer(summaries_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL)
    summaries_writer.writerow([full_link, summary])

compute_summary() 函数的结尾,我们只需初始化写入器并开始追加数据。每一行由文章的 URL 及其摘要组成。

步骤 6.1:生成 JSON 文件

近年来另一种流行起来的数据序列化格式是 JavaScript 对象表示法(JSON)。它易于人类阅读,且在将抓取的数据传递给 API 或其他应用程序时非常方便。

在 Python 中,编写 JSON 文件最简单的方法是将数据传递给一个字典对象。

import json
 
summaries_file = open('summaries.json', mode='a', encoding='utf-8')
data = {}
data['summaries'] = []

我们将使用 Python 内置的 JSON 库,并像之前处理 CSV 文件那样打开一个新文件。然后初始化一个空字典对象和一个空列表,用于存放摘要内容。

data['summaries'].append({
'url': full_link,
      'summary': summary
})

在 compute_summary() 函数的结尾处,也就是之前写入 CSV 数据的位置,现在我们将一个新的字典对象追加到最终列表中。

json.dump(data, summaries_file, indent=4)

最后,在 main() 函数中,执行完 extract_links() 过程后,我们将最终对象写入文件。indent 参数仅用于美化结果的格式。

结论与替代方案

好了,本教程到此结束。希望它对您有所帮助,并让您对使用 Python 进行网页抓取有了更深入的了解。

我们了解了其优势以及它如何提升您的业务或应用程序。同时,我们也结合了网络爬虫过程中遇到的挑战,对这些信息进行了平衡探讨。

如果你是一名开发者,克服所有这些问题并亲手构建一个网络爬虫可能会让你感到兴奋。这会是一次很棒的学习体验。

但作为企业主(或需要为实际大规模应用获取数据的人),您可能希望避免相关成本(时间、金钱、人力)。

在这种情况下,使用专业的 API 就能解决问题。WebScrapingAPI 能够克服所有可能的阻碍点:JavaScript 渲染、代理、验证码等,并提供可定制的功能。此外,如果您还不太确定,请记住我们提供免费套餐选项,何不试一试呢?

关于作者
Raluca Penciuc, 全栈开发工程师 @ WebScrapingAPI
Raluca Penciuc全栈开发工程师

Raluca Penciuc 是 WebScrapingAPI 的全栈开发工程师,主要负责开发爬虫、优化规避机制,并探索可靠的方法以降低在目标网站上的被检测概率。

开始构建

准备好扩展您的数据收集规模了吗?

加入2,000多家企业,使用WebScrapingAPI在无需任何基础设施开销的情况下,以企业级规模提取网络数据。