Python-Web-爬虫实用指南(全)

Python Web 爬虫实用指南(全)

原文:zh.annas-archive.org/md5/AB12C428C180E19BF921ADFBD1CC8C3E

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

网页抓取是许多组织中使用的一种重要技术,用于从网页中抓取有价值的数据。网页抓取是为了从网站中提取和收集数据而进行的。网页抓取在模型开发中非常有用,这需要实时收集数据。它也适用于真实且与主题相关的数据,其中准确性是短期内所需的,而不是实施数据集。收集的数据存储在包括 JSON、CSV 和 XML 在内的文件中,也写入数据库以供以后使用,并作为数据集在线提供。本书将为您打开网页抓取技术和方法的大门,使用 Python 库和其他流行工具,如 Selenium。通过本书,您将学会如何高效地抓取不同的网站。

本书适合对象

这本书适用于 Python 程序员、数据分析师、网页抓取新手以及任何想要学习如何从头开始进行网页抓取的人。如果你想开始学习如何将网页抓取技术应用于各种网页,那么这本书就是你需要的!

本书内容

第一章,网页抓取基础知识,探讨了一些与 WWW 相关的核心技术和工具,这些技术和工具对网页抓取是必需的。

第二章,Python 和 Web-使用 URLlib 和 Requests,演示了 Python 库中可用的一些核心功能,如requestsurllib,并探索了各种格式和结构的页面内容。

第三章,使用 LXML、XPath 和 CSS 选择器,描述了使用 LXML 的各种示例,实现了处理元素和 ElementTree 的各种技术和库特性。

第四章,使用 pyquery 进行抓取-一个 Python 库,更详细地介绍了网页抓取技术和一些部署这些技术的新 Python 库。

第五章,使用 Scrapy 和 Beautiful Soup 进行网页抓取,检查了使用 Beautiful Soup 遍历网页文档的各个方面,同时还探索了一个专为使用蜘蛛进行爬行活动而构建的框架,换句话说,Scrapy。

第六章,处理安全网页,涵盖了许多常见的基本安全措施和技术,这些措施和技术经常遇到,并对网页抓取构成挑战。

第七章,使用基于 Web 的 API 进行数据提取,涵盖了 Python 编程语言以及如何与 Web API 交互以进行数据提取。

第八章,使用 Selenium 进行网页抓取,涵盖了 Selenium 以及如何使用它从网页中抓取数据。

第九章,使用正则表达式提取数据,更详细地介绍了使用正则表达式进行网页抓取技术。

第十章,下一步,介绍并探讨了使用文件进行数据管理,使用 pandas 和 matplotlib 进行分析和可视化的基本概念,同时还介绍了机器学习和数据挖掘,并探索了一些相关资源,这些资源对进一步学习和职业发展都有帮助。

充分利用本书

读者应该具有一定的 Python 编程语言工作知识。

下载示例代码文件

您可以从www.packt.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packt.com/support并注册,以便文件直接通过电子邮件发送给您。

您可以按照以下步骤下载代码文件:

  1. www.packt.com上登录或注册。

  2. 选择“支持”选项卡。

  3. 点击“代码下载和勘误”。

  4. 在“搜索”框中输入书名,然后按照屏幕上的说明操作。

下载文件后,请确保使用最新版本的解压缩或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-Web-Scraping-with-Python。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还有来自丰富书籍和视频目录的其他代码包,可以在github.com/PacktPublishing/上找到。去看看吧!

下载彩色图片

我们还提供了一份 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图片。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781789533392_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。这是一个例子:“<p><h1> HTML 元素包含与它们一起的一般文本信息(元素内容)。”

代码块设置如下:

import requests
link="http://localhost:8080/~cache"

queries= {'id':'123456','display':'yes'}

addedheaders={'user-agent':''}

当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:

import requests
link="http://localhost:8080/~cache"

queries= {'id':'123456','display':'yes'}

addedheaders={'user-agent':''}

任何命令行输入或输出都以以下方式编写:

C:\> pip --version

pip 18.1 from c:\python37\lib\site-packages\pip (python 3.7)

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会以这样的方式出现在文本中。这是一个例子:“如果通过 Chrome 菜单访问开发者工具,请单击更多工具|开发者工具”

警告或重要说明会以这种方式出现。提示和技巧会以这种方式出现。

第一部分:网络抓取简介

在本节中,您将获得有关网络抓取(抓取要求、数据重要性)、网页内容(模式和布局)、Python 编程和库(基础和高级)、以及数据管理技术(文件处理和数据库)的概述。

本节包括以下章节:

  • 第一章,网络抓取基础

第一章:网络爬虫基础知识

在本章中,我们将学习和探讨与网络爬取和基于网络的技术相关的某些基本概念,假设您没有网络爬取的先验经验。

因此,让我们从以下一些问题开始:

  • 为什么会出现对数据的不断增长需求?

  • 我们将如何管理和满足来自“万维网”(WWW)资源的数据需求?

网络爬虫解决了这两个问题,因为它提供了各种工具和技术,可以用来提取数据或协助信息检索。无论是基于网络的结构化数据还是非结构化数据,我们都可以使用网络爬虫过程来提取数据,并将其用于研究、分析、个人收藏、信息提取、知识发现等多种目的。

我们将学习通用技术,用于从网络中查找数据,并在接下来的章节中使用 Python 编程语言深入探讨这些技术。

在本章中,我们将涵盖以下主题:

  • 网络爬虫介绍

  • 了解网络开发和技术

  • 数据查找技术

网络爬虫介绍

爬取是从网络中提取、复制、筛选或收集数据的过程。从网络(通常称为网站、网页或与互联网相关的资源)中提取或提取数据通常被称为“网络爬取”。

网络爬虫是一种适用于特定需求的从网络中提取数据的过程。数据收集和分析,以及其在信息和决策制定中的参与,以及与研究相关的活动,使得爬取过程对所有类型的行业都很敏感。

互联网及其资源的普及每天都在引起信息领域的演变,这也导致了对原始数据的不断增长需求。数据是科学、技术和管理领域的基本需求。收集或组织的数据经过不同程度的逻辑处理,以获取信息并获得进一步的见解。

网络爬虫提供了用于根据个人或业务需求从网站收集数据的工具和技术,但需要考虑许多法律因素。

在执行爬取任务之前,有许多法律因素需要考虑。大多数网站包含诸如“隐私政策”、“关于我们”和“条款和条件”等页面,其中提供了法律条款、禁止内容政策和一般信息。在计划从网站进行任何爬取和抓取活动之前,开发者有道德责任遵守这些政策。

在本书的各章中,爬取和抓取两个术语通常可以互换使用。抓取,也称为蜘蛛,是用于浏览网站链接的过程,通常由搜索引擎用于索引目的,而爬取大多与从网站中提取内容相关。

了解网络开发和技术

网页不仅仅是一个文档容器。当今计算和网络技术的快速发展已经将网络转变为动态和实时的信息来源。

在我们这一端,我们(用户)使用网络浏览器(如 Google Chrome、Firefox Mozilla、Internet Explorer 和 Safari)来从网络中获取信息。网络浏览器为用户提供各种基于文档的功能,并包含对网页开发人员通常有用的应用级功能。

用户通过浏览器查看或浏览的网页不仅仅是单个文档。存在各种技术可用于开发网站或网页。网页是包含 HTML 标记块的文档。大多数情况下,它是由各种子块构建而成,这些子块作为依赖或独立组件来自各种相互关联的技术,包括 JavaScript 和 CSS。

对网页的一般概念和网页开发技术的理解,以及网页内部的技术,将在抓取过程中提供更多的灵活性和控制。很多时候,开发人员还可以使用反向工程技术。

反向工程是一种涉及分解和检查构建某些产品所需概念的活动。有关反向工程的更多信息,请参阅 GlobalSpec 文章反向工程是如何工作的?,网址为insights.globalspec.com/article/7367/how-does-reverse-engineering-work

在这里,我们将介绍和探讨一些可以帮助和指导我们进行数据提取过程的技术。

HTTP

超文本传输协议HTTP)是一种应用协议,用于在客户端和 Web 服务器之间传输资源,例如 HTML 文档。HTTP 是一种遵循客户端-服务器模型的无状态协议。客户端(Web 浏览器)和 Web 服务器使用 HTTP 请求和 HTTP 响应进行通信或交换信息:

HTTP(客户端-服务器通信)

通过 HTTP 请求或 HTTP 方法,客户端或浏览器向服务器提交请求。有各种方法(也称为 HTTP 请求方法)可以提交请求,例如GETPOSTPUT

  • GET:这是请求信息的常见方法。它被认为是一种安全方法,因为资源状态不会被改变。此外,它用于提供查询字符串,例如http://www.test-domain.com/,根据请求中发送的iddisplay参数从服务器请求信息。

  • POST:用于向服务器发出安全请求。所请求的资源状态可以被改变。发送到请求的 URL 的数据不会显示在 URL 中,而是与请求主体一起传输。它用于以安全的方式向服务器提交信息,例如登录和用户注册。

使用浏览器开发者工具显示的以下屏幕截图,可以显示请求方法以及其他与 HTTP 相关的信息:

一般的 HTTP 头(使用浏览器开发者工具访问)

我们将在第二章中更多地探讨 HTTP 方法,

Python 和 Web-使用 urllib 和 Requests,在实现 HTTP 方法部分。

HTTP 头在请求或响应过程中向客户端或服务器传递附加信息。头通常是客户端和服务器在通信过程中传输的信息的名称-值对,并且通常分为请求头和响应头:

  • 请求头:这些是用于发出请求的头。在发出请求时,会向服务器提供诸如语言和编码请求 -*、引用者、cookie、与浏览器相关的信息等信息。以下屏幕截图显示了在向www.python.org发出请求时从浏览器开发者工具中获取的请求头:

请求头(使用浏览器开发者工具访问)

  • 响应头:这些头包含有关服务器响应的信息。响应头通常包含有关响应的信息(包括大小、类型和日期)以及服务器状态。以下屏幕截图显示了在向www.python.org发出请求后从浏览器开发者工具中获取的响应头:

响应头(使用浏览器开发者工具访问)

在之前的屏幕截图中看到的信息是在对www.python.org发出的请求期间捕获的。

在向服务器发出请求时,还可以提供所需的 HTTP 头部。通常可以使用 HTTP 头部信息来探索与请求 URL、请求方法、状态代码、请求头部、查询字符串参数、cookie、POST参数和服务器详细信息相关的信息。

通过HTTP 响应,服务器处理发送到它的请求,有时也处理指定的 HTTP 头部。当接收并处理请求时,它将其响应返回给浏览器。

响应包含状态代码,其含义可以使用开发者工具来查看,就像在之前的屏幕截图中看到的那样。以下列表包含一些状态代码以及一些简要信息:

  • 200(OK,请求成功)

  • 404(未找到;请求的资源找不到)

  • 500(内部服务器错误)

  • 204(无内容发送)

  • 401(未经授权的请求已发送到服务器)

有关 HTTP、HTTP 响应和状态代码的更多信息,请参阅官方文档www.w3.org/Protocols/developer.mozilla.org/en-US/docs/Web/HTTP/Status

HTTP cookie是服务器发送给浏览器的数据。cookie 是网站在您的系统或计算机上生成和存储的数据。cookie 中的数据有助于识别用户对网站的 HTTP 请求。cookie 包含有关会话管理、用户偏好和用户行为的信息。

服务器根据存储在 cookie 中的信息来识别并与浏览器通信。cookie 中存储的数据帮助网站访问和传输某些保存的值,如会话 ID、过期日期和时间等,从而在 web 请求和响应之间提供快速交互:

网站设置的 cookie(使用浏览器开发者工具访问)有关 cookie 的更多信息,请访问www.allaboutcookies.org/的 AboutCookies 和www.allaboutcookies.org/的 allaboutcookies。

通过HTTP 代理,代理服务器充当客户端和主要 web 服务器之间的中间服务器。网页浏览器发送的请求实际上是通过代理传递的,代理将服务器的响应返回给客户端。

代理通常用于监视/过滤、性能改进、翻译和互联网相关资源的安全性。代理也可以作为一种服务购买,也可以用来处理跨域资源。还有各种形式的代理实现,比如网页代理(可以用来绕过 IP 封锁)、CGI 代理和 DNS 代理。

通过使用GET请求传递的基于 cookie 的参数、HTML 表单相关的POST请求以及修改或调整头部,在网页抓取过程中管理代码(即脚本)和访问内容将至关重要。

有关 HTTP、头部、cookie 等的详细信息将在即将到来的网络数据查找技术部分中更详细地探讨。请访问 MDN web docs-HTTP (developer.mozilla.org/en-US/docs/Web/HTTP)获取有关 HTTP 的更详细信息。

HTML

网站由包含文本、图像、样式表和脚本等内容的页面或文档组成。它们通常是用标记语言(如超文本标记语言HTML)和可扩展超文本标记语言XHTML))构建的。

HTML 通常被称为用于构建网页的标准标记语言。自上世纪 90 年代初以来,HTML 已经独立使用,也与基于服务器的脚本语言(如 PHP、ASP 和 JSP)一起使用。

XHTML 是 HTML 的高级和扩展版本,是用于 Web 文档的主要标记语言。XHTML 也比 HTML 更严格,从编码的角度来看,它是一个 XML 应用程序。

HTML 定义并包含网页的内容。可以在 HTML 页面中找到可以提取的数据,以及任何揭示信息的数据源,这些数据源位于预定义的指令集或标记元素标签内。HTML 标签通常是一个带有特定预定义属性的命名占位符。

HTML 元素和属性

HTML 元素(也称为文档节点)是 Web 文档的构建块。HTML 元素由开始标签<..>和结束标签</..>以及其中的特定内容构成。HTML 元素也可以包含属性,通常定义为attribute-name = attribute-value,提供额外的信息给元素:

<p>normal paragraph tags</p>
<h1>heading tags there are also h2, h3, h4, h5, h6</h1>
<a href="https://www.google.com">Click here for Google.com</a>
<img src="myphoto1.jpg" width="300" height="300" alt="Picture" />
<br />

前面的代码可以分解如下:

  • <p><h1> HTML 元素包含一般文本信息(元素内容)。

  • <a>定义了一个包含实际链接的href属性,当点击文本点击这里前往 Google.com时将被处理。链接指向www.google.com/

  • <img>图像标签也包含一些属性,比如srcalt,以及它们各自的值。src保存资源,即图像地址或图像 URL 作为值,而alt保存<img>的替代文本的值。

  • <br />代表 HTML 中的换行,没有属性或文本内容。它用于在文档的布局中插入新行。

HTML 元素也可以以树状结构嵌套,具有父子层次结构:

<div>
   <p id="mainContent" class="content"> 
        <i> Paragraph contents </i>
        <img src="mylogo.png" id="pageLogo" class="logo"/>
        ….
    </p>
    <p class="content" id="subContent">
        <i style="color:red"> Sub paragraph content </i>
        <h1 itemprop="subheading">Sub heading Content! </h1>
        ….
    </p>
</div>

如前面的代码所示,在 HTML<div>块内找到了两个<p>子元素。两个子元素都带有特定的属性和各种子元素作为它们的内容。通常,HTML 文档是按照上述结构构建的。

全局属性

HTML 元素可以包含一些额外的信息,如键/值对。这些也被称为 HTML 元素属性。属性保存值并提供标识,或包含在许多方面有用的附加信息,比如在爬取活动中识别确切的网页元素和提取值或文本,遍历元素等。

有一些属性是通用的 HTML 元素属性,或者可以应用于所有 HTML 元素,如下所示。这些属性被标识为全局属性(developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes):

  • id

  • class

  • style

  • lang

HTML 元素属性,如idclass,主要用于标识或格式化单个元素或元素组。这些属性也可以由 CSS 和其他脚本语言管理。

id属性值应该对应于它们所应用的元素是唯一的。class属性值通常与 CSS 一起使用,提供相同的状态格式选项,并且可以用于多个元素。

当与 CSS、遍历和解析技术一起使用时,通过在属性名称前面分别放置#.来识别idclass等属性。

HTML 元素属性也可以通过脚本语言动态地覆盖或实现。

如下例所示,itemprop属性用于向元素添加属性,而data-*用于存储元素本身的本地数据:

<div itemscope itemtype ="http://schema.org/Place">
    <h1 itemprop="univeristy">University of Helsinki</h1>
     <span>Subject:
         <span itemprop="subject1">Artificial Intelligence</span>   
    </span>
     <span itemprop="subject2">Data Science</span>
</div>

<img class="dept" src="logo.png" data-course-id="324" data-title="Predictive Aanalysis"  data-x="12345" data-y="54321" data-z="56743" onclick="schedule.load()">
</img>

当涉及到提取时,HTML 标签和属性是数据的主要来源。

请访问www.w3.org/html/www.w3schools.com/html/了解更多关于 HTML 的信息。

在接下来的章节中,我们将使用不同的工具来探索这些属性。我们还将执行各种逻辑操作,并使用它们来提取内容。

XML

可扩展标记语言XML)是一种用于在互联网上传输数据的标记语言,具有一组规则,用于对可读和易于在机器和文档之间交换的文档进行编码。

XML 可以在各种格式和系统之间使用文本数据。XML 旨在携带可移植数据或未使用 HTML 标记预定义的标记中存储的数据。在 XML 文档中,标记是由文档开发人员或自动化程序创建的,用于描述它们携带的内容。

以下代码显示了一些示例 XML 内容。<employees>父节点有三个<employee>子节点,这些子节点又包含其他子节点<firstName><lastName><gender>

<employees>
    <employee>
        <firstName>Rahul</firstName>
        <lastName>Reddy</lastName>
        <gender>Male</gender>
    </employee>
    <employee>
        <firstName>Aasira</firstName>
        <lastName>Chapagain</lastName>
        <gender>Female</gender> 
    </employee>
    <employee>
        <firstName>Peter</firstName>
        <lastName>Lara</lastName>
        <gender>Male</gender>        
    </employee>
</employees>

XML 是一种使用 Unicode 字符集的开放标准。XML 用于在各种平台之间共享数据,并已被各种 Web 应用程序采用。许多网站使用 XML 数据,使用脚本语言实现其内容,并以 HTML 或其他文档格式呈现给最终用户查看。

还可以执行从 XML 文档中提取任务,以获取所需格式的内容,或者通过过滤数据需求来满足特定的需求。此外,还可以从某些网站获取幕后数据。

请访问www.w3.org/XML/www.w3schools.com/xml/了解更多关于 XML 的信息。

JavaScript

JavaScript 是一种编程语言,用于编写在浏览器中运行的 HTML 和 Web 应用程序。JavaScript 主要用于添加动态功能,并在网页内提供基于用户的交互。JavaScript、HTML 和 CSS 是最常用的 Web 技术之一,现在它们也与无头浏览器一起使用。JavaScript 引擎的客户端可用性也加强了它在应用程序测试和调试中的地位。

JavaScript 代码可以使用<script>添加到 HTML 中,也可以嵌入为文件。<script>包含具有 JavaScript 变量、运算符、函数、数组、循环、条件和事件的编程逻辑,目标是 HTML 文档对象模型DOM):

<!DOCTYPE html>
<html>
<head>
    <script>
        function placeTitle() {
            document.getElementById("innerDiv").innerHTML = "Welcome to WebScraping";
        }
    </script>
</head>
<body>
    <div>Press the button: <p id="innerDiv"></p></div>
    <br />
    <button id="btnTitle" name="btnTitle" type="submit" onclick="placeTitle()">
        Load Page Title!
    </button>
</body>
</html>

HTML DOM 是如何获取、更改、添加或删除 HTML 元素的标准。JavaScript HTML DOM,可以参考 W3Schools 的 URLwww.w3schools.com/js/js_htmldom.asp

通过可访问的内部函数和编程功能对 HTML 内容、元素、属性值、CSS 和 HTML 事件进行动态操作,使 JavaScript 在 Web 开发中非常受欢迎。与 JavaScript 相关的许多基于 Web 的技术,包括 JSON、jQuery、AngularJS 和 AJAX 等。

jQuery 是一个 JavaScript 库,解决了浏览器之间的不兼容性,提供了处理 HTML DOM、事件和动画的 API 功能。

jQuery 因为为 Web 提供交互性以及使用 JavaScript 进行编码而在全球受到赞誉。与 JavaScript 框架相比,jQuery 轻量级,易于实现,并且具有简短和可读的编码方法。

有关 jQuery 的更多信息,请访问www.w3schools.com/jquery/jquery.com/

异步 JavaScript 和 XMLAJAX)是一种 Web 开发技术,它在客户端使用一组 Web 技术来创建异步 Web 应用程序。JavaScript XMLHttpRequestXHR)对象用于在网页上执行 AJAX,并在不刷新或重新加载页面的情况下加载页面内容。有关 AJAX 的更多信息,请访问 AJAX W3Schools(www.w3schools.com/js/js_ajax_intro.asp)。

从抓取的角度来看,对 JavaScript 功能的基本概述将有助于理解页面的构建或操作,以及识别所使用的动态组件。

有关 JavaScript 的更多信息,请访问developer.mozilla.org/en-US/docs/Web/JavaScriptwww.javascript.com/

JSON

JavaScript 对象表示法JSON)是一种用于从服务器传输数据到网页的格式。它与语言无关,并且由于其大小和可读性,在基于网络的数据交换操作中很受欢迎。

JSON 数据通常是一个名称/值对,被视为 JavaScript 对象,并遵循 JavaScript 操作。JSON 和 XML 经常被比较,因为它们都在各种 Web 资源之间携带和交换数据。JSON 的结构比 XML 更简单、可读、自我描述、易于理解和处理。对于使用 JavaScript、AJAX 或 RESTful 服务的 Web 应用程序,由于其快速和简便的操作,JSON 比 XML 更受青睐。

JSON 和 JavaScript 对象是可以互换的。JSON 不是一种标记语言,它不包含任何标签或属性。相反,它是一种仅限于文本的格式,可以通过服务器发送/访问,并且可以由任何编程语言管理。JSON 对象也可以表示为数组、字典和列表,如下面的代码所示:

{"mymembers":[
 { "firstName":"Aasira", "lastName":"Chapagain","cityName":"Kathmandu"},
 { "firstName":"Rakshya", "lastName":"Dhungel","cityName":"New Delhi"},
 { "firstName":"Shiba", "lastName":"Paudel","cityName":"Biratnagar"},
 { "firstName":"Rahul", "lastName":"Reddy","cityName":"New Delhi"},
 { "firstName":"Peter", "lastName":"Lara","cityName":"Trinidad"}
]}

JSON Lines:这是一种类似 JSON 的格式,其中每条记录的每行都是有效的 JSON 值。它也被称为换行符分隔的 JSON,即用换行符(\n)分隔的单独的 JSON 记录。处理大量数据时,JSON Lines 格式非常有用。

由于易于数据模式和代码可读性,JSON 或 JSON Lines 格式的数据源比 XML 更受青睐,这也可以通过最少的编程工作来管理:

 {"firstName":"Aasira", "lastName":"Chapagain","cityName":"Kathmandu"}
 {"firstName":"Rakshya", "lastName":"Dhungel","cityName":"New Delhi"}
 {"firstName":"Shiba", "lastName":"Paudel","cityName":"Biratnagar"}
 {"firstName":"Rahul", "lastName":"Reddy","cityName":"New Delhi"}
 {"firstName":"Peter", "lastName":"Lara","cityName":"Trinidad"}

从数据提取的角度来看,由于 JSON 格式的轻量和简单结构,网页使用 JSON 内容与其脚本技术结合,以添加动态功能。

有关 JSON 和 JSON Lines 的更多信息,请访问www.json.org/jsonlines.org/www.w3schools.com/js/js_json_intro.asp

CSS

到目前为止,我们介绍的基于网络的技术涉及内容、内容绑定、内容开发和处理。层叠样式表CSS)描述了 HTML 元素的显示属性和网页的外观。CSS 用于为 HTML 元素提供样式和所需的外观和呈现。

开发人员/设计人员可以使用 CSS 来控制网页文档的布局和呈现。CSS 可以应用于页面中的特定元素,也可以通过单独的文档进行嵌入。可以使用<style>标签来描述样式细节。

<style>标签可以包含针对块中重复和各种元素的详细信息。如下面的代码所示,存在多个<a>元素,并且还具有classid全局属性:

<html>
<head>
      <style>
        a{color:blue;}
        h1{color:black; text-decoration:underline;}
        #idOne{color:red;}
        .classOne{color:orange;}
      </style>
</head>
<body>
      <h1> Welcome to Web Scraping </h1>
      Links:
      <a href="https://www.google.com"> Google </a> 
      <a class='classOne' href="https://www.yahoo.com"> Yahoo </a> 
      <a id='idOne' href="https://www.wikipedia.org"> Wikipedia </a>
</body>
</html>

与 CSS 属性一起提供的属性,或者在前面的代码块中在<style>标签中进行了样式化的属性,将导致在此处看到的输出:

HTML 输出(使用 CSS 进行样式化的元素)

CSS 属性也可以以内联结构出现在每个特定元素中。内联 CSS 属性会覆盖外部 CSS 样式。CSS 的color属性已经内联应用到元素中。这将覆盖<style>中定义的color值:

  <h1 style ='color:orange;'> Welcome to Web Scraping </h1>
  Links:
  <a href="https://www.google.com" style ='color:red;'> Google </a> 
  <a class='classOne' href="https://www.yahoo.com"> Yahoo </a> 
  <a id='idOne' href="https://www.wikipedia.org" style ='color:blue;'> Wikipedia </a>

CSS 也可以使用外部样式表文件嵌入到 HTML 中:

<link href="http://..../filename.css" rel="stylesheet" type="text/css">

尽管 CSS 用于 HTML 元素的外观,但 CSS 选择器(用于选择元素的模式)在抓取过程中经常起着重要作用。我们将在接下来的章节中详细探讨 CSS 选择器。

请访问www.w3.org/Style/CSS/www.w3schools.com/css/获取有关 CSS 的更详细信息。

AngularJS

到目前为止,我们在本章中介绍了一些选定的与 Web 相关的技术。让我们通过介绍 AngularJS 来了解 Web 框架的概述。Web 框架涉及许多与 Web 相关的工具,并用于开发与采用最新方法的 Web 相关资源。

AngularJS(也被称为Angular.jsAngular)主要用于构建客户端 Web 应用程序。这是一个基于 JavaScript 的框架。AngularJS 是通过<script>标签添加到 HTML 中的,它将 HTML 属性扩展为指令,并将数据绑定为表达式。AngularJS 表达式用于将数据绑定到从静态或动态 JSON 资源中检索的 HTML 元素。AngularJS 指令以ng-为前缀。

AngularJS 与 HTML 一起用于动态内容开发。它提供了性能改进、测试环境、元素操作和数据绑定功能,并通过在文档、数据、平台和其他工具之间提供更加动态和灵活的环境,帮助构建基于模型-视图-控制器MVC)框架的 Web 应用程序。

我们可以将外部 JavaScript 文件链接到我们的 HTML 文档中,如下所示:

<!doctype html>
<html ng-app>
    <head>
        <script 
 src="https://ajax.googleapis.com/ajax/libs/angularjs/1.7.5/angular.min.js">
 </script>
    </head>
    <body>
        <div>
            <label> Place: </label>
            <input type="text" ng-model="place" placeholder="Visited place!">
            <label> Cost :</label>
            <input type="text" ng-model="price" placeholder="Ticket Price!">
            <br>
            <b>Wow! {{place}} for only {{price}}</b>
        </div>
    </body>
</html>

此外,我们可以将脚本和元素块一起包含在页面中,如下所示:

<script>
     var app = angular.module('myContact', []);
     app.controller('myDiv', function($scope) {
         $scope.firstName = "Aasira";
         $scope.lastName = "Chapagain";
         $scope.college= "London Business School";
         $scope.subject= "Masters in Analytics and Management";
     });
</script>
<div ng-app="myContact" ng-controller="myDiv">
     First Name: <input type="text" ng-model="firstName"><br>
     Last Name: <input type="text" ng-model="lastName"><br>
     College Name: <input type="text" ng-model="college"><br>
     Subjects: <input type="text" ng-model="subject"><br>
     <br>
     Full Name: {{firstName + " " + lastName}}
     <br>
     Enrolled on {{college + " with " + subject}}
</div>

我们在这里提供的 AngularJS 及其工作方法的概述允许更灵活地追踪和遍历数据。

请访问 AngularJS(angularjs.org/angular.io/)获取有关 AngularJS 的更详细信息。

前面讨论的技术是 Web 的一些核心组件;它们相互关联,相互依赖,以产生最终用户与之交互的网站或 Web 文档。在接下来的章节中,我们将识别脚本并进一步分析其中包含的代码。

在接下来的章节中,我们将探索 Web 内容,并寻找可以在 Web 页面内找到的数据,我们将在接下来的章节中使用 Python 编程语言提取这些数据。

网络数据查找技术

有各种技术可用于开发网站。使用 Web 浏览器向最终用户呈现的内容也可以存在于各种其他格式和模式中。

如前所述,动态生成或操作网页内容也是可能的。页面内容也可以包括使用 HTML 和相关技术呈现的静态内容,或者实时呈现和创建的内容。内容也可以使用第三方来源检索并呈现给最终用户。

HTML 页面源代码

Web 浏览器用于基于客户端服务器的 GUI 交互,探索 Web 内容。浏览器地址栏提供了 Web 地址或 URL,并将请求的 URL 发送到服务器(主机),然后由浏览器接收响应,即加载。获取的响应或页面源代码可以进一步探索,并以原始格式搜索所需的内容。

用户可以自由选择他们的 Web 浏览器。我们将在大部分书中使用安装在 Windows 操作系统OS)上的 Google Chrome。

在抓取过程中,页面的 HTML 源将经常被打开和调查以获取所需的内容和资源。右键单击网页。然后会出现一个菜单,您可以在其中找到查看页面源选项。或者,按Ctrl + U

案例 1

让我们通过以下步骤来看一个网页抓取的例子:

  1. 在您选择的浏览器中打开www.google.com

  2. 在搜索框中输入Web Scraping

  3. Enter或点击页面上的谷歌搜索按钮

  4. 您应该看到类似以下屏幕截图的内容:

从谷歌搜索中获取网页抓取的搜索结果

谷歌已经为我们提供了我们所要求的搜索信息。这些信息以段落形式显示,还有许多链接。显示的信息是互动的、丰富多彩的,并且以维护的结构呈现,搜索内容采用了布局。

这是我们正在查看的前端内容。这些内容是根据我们与谷歌的互动动态提供给我们的。现在让我们查看一下提供给我们的原始内容。

  1. 右键单击网页。然后会出现一个菜单,您可以在其中找到查看页面源的选项。或者,按Ctrl + U。在这里,将会打开一个新标签页,其中包含页面的 HTML 源代码。在浏览器的 URL 开头检查view-source

HTML 页面源:从谷歌搜索中获取网页抓取的搜索结果

我们现在正在访问上一个屏幕截图中显示的页面的 HTML 源代码。HTML 标签和 JavaScript 代码可以很容易地看到,但没有以正确的格式呈现。这些是浏览器呈现给我们的核心内容。

在页面源中搜索一些文本,在页面源中找到文本、链接和图片的位置。您将能够在 HTML 标签中找到页面源中的文本(但并不总是,我们将看到!)

网页开发可以使用各种技术和工具进行,正如我们在前面的部分中讨论的那样。浏览器显示的网页内容在探索其源代码时,可能并不总是存在于 HTML 标签中。内容也可能存在于脚本中,甚至在第三方链接上。这就是使得网页抓取经常具有挑战性的原因,因此需要存在于网页开发中的最新工具和技术。

案例 2

让我们探索另一个案例,使用我们在案例 1部分应用的浏览过程:

  1. 在谷歌上搜索2018 年美国最佳酒店,并选择您喜欢的任何酒店名称。

  2. 直接在谷歌中搜索酒店名称(或者您可以忽略前面的步骤)。例如,尝试芝加哥半岛酒店

  3. 谷歌将加载搜索酒店的详细信息以及地图和预订和评论部分。结果将类似于以下屏幕截图:

芝加哥半岛酒店的谷歌搜索结果

  1. 在左侧,您可以找到谷歌评论的链接。点击链接后,将会弹出一个新页面,如下屏幕截图所示:

来自搜索页面的谷歌评论页面

  1. 右键单击弹出的评论页面,选择查看页面源,或按Ctrl + U查看页面源。

尝试从页面源中找到用户的评论和回复文本。

开发者工具

开发者工具(或DevTools)现在嵌入在市面上大多数浏览器中。开发人员和最终用户都可以识别和定位在客户端-服务器通信期间使用的资源和搜索网页内容,或者在进行 HTTP 请求和响应时使用的资源。

DevTools 允许用户检查、创建、编辑和调试 HTML、CSS 和 JavaScript。它们还允许我们处理性能问题。它们有助于提取浏览器动态或安全呈现的数据。

DevTools 将用于大多数数据提取案例,以及类似于页面源部分中提到的案例 2。有关开发人员工具的更多信息,请探索这些链接:

在 Google Chrome 中,我们可以通过以下任一指示加载 DevTools:

  • 只需按下Ctrl + Shift + I

  • 另一个选项是右键单击页面,然后选择“检查”选项

  • 或者,如果通过 Chrome 菜单访问开发者工具,请单击“更多工具”|“开发者工具”:

加载评论页面的 Chrome DevTools

上述屏幕截图显示了开发者工具面板:元素、控制台、网络、来源等。在我们的情况下,让我们从评论页面中找一些文本。按照这些步骤将允许我们找到它:

  1. 在开发者工具中打开“网络”面板。

  2. 选择“XHR”过滤器选项。(在“名称”面板下将找到列出的多个资源,如 HTML 文件、图像和 JSON 数据。)

  3. 我们需要遍历“名称”窗格下的资源,寻找我们寻找的选择文本片段。(“响应”选项卡显示所选资源的内容。)

  4. 找到以reviewDialog?开头的资源,其中包含搜索的文本。

这里概述的搜索评论文本的步骤是定位确切内容的最常用技术之一。当内容是动态获取的并且不在页面源中时,通常会遵循这些步骤。

开发者工具中有各种面板,与特定功能相关,用于提供给 Web 资源或进行分析,包括“来源”、“内存”、“性能”和“网络”。我们将探索在 Chrome DevTools 中找到的一些面板,如下所示:

在基于浏览器的 DevTools 中找到的面板的具体名称可能在所有浏览器中都不相同。

  • 元素:显示所查看页面的 HTML 内容。用于查看和编辑 DOM 和 CSS,以及查找 CSS 选择器和 XPath。

HTML 元素显示或位于“元素”面板中,可能不会在页面源中找到。

  • 控制台:用于运行和交互 JavaScript 代码,并查看日志消息:

Chrome DevTools 中的控制台面板

  • 来源:用于导航页面,查看可用的脚本和文档来源。基于脚本的工具可用于任务,如脚本执行(即,恢复、暂停)、跳过函数调用、激活和停用断点,以及处理异常,如暂停异常(如果遇到):

Chrome DevTools 的“来源”面板

  • 网络:提供与 HTTP 请求和响应相关的资源,并显示加载页面时使用的网络资源。在网络功能选项中找到的资源,如记录数据到网络日志,捕获屏幕截图,过滤 Web 资源(JavaScript、图像、文档和 CSS),搜索 Web 资源,分组 Web 资源,也可用于调试任务:

Chrome DevTools 网络面板

请求也可以按类型进行过滤:

  • 全部:列出与网络相关的所有请求,包括文档请求、图像请求和字体和 CSS 请求。资源按加载顺序排列。

  • XHR:列出XmlHttpRequest对象,动态加载 AJAX 内容

  • JS:列出请求的脚本文件

  • CSS:列出请求的样式文件

  • Img:列出请求的图像文件

  • 文档:列出请求的 HTML 或 Web 文档

  • 其他:任何未列出的与请求相关的资源类型

对于先前列出的过滤选项,在 Name 面板中选择的资源有标签(标题、预览、响应、时间、Cookie):

  • 标题:加载特定请求的 HTTP 头数据。显示的信息包括请求 URL、请求方法、状态码、请求头、查询字符串参数和POST参数。

  • 预览:加载响应的格式化预览。

  • 响应:加载特定请求的响应。

  • 时间:查看时间分解信息。

  • Cookie:加载 Name 面板中选择的资源的 cookie 信息。

从爬取的角度来看,DevTools Network 面板对于查找和分析 Web 资源非常有用。这些信息对于检索数据和选择处理这些资源的方法非常有用。

有关网络面板的更多信息,请访问developers.google.com/web/tools/chrome-devtools/network-performance/reference/developer.mozilla.org/en-US/docs/Tools/Network_Monitor/。网络面板提供了各种元素,下面将对其进行解释:

  • 性能:可以记录屏幕截图页面和内存时间轴。获取的视觉信息用于优化网站速度,改善加载时间和分析运行时性能。在较早的 Chrome 版本中,性能面板提供的信息曾存在于一个名为时间轴的面板中:

Chrome DevTools 中的性能面板

  • 内存:在较早的 Chrome 版本中,这个面板也被称为面板配置文件。从这个面板获得的信息用于修复内存问题和跟踪内存泄漏。开发人员还使用性能和内存面板来分析整体网站性能。

  • 应用程序:最终用户可以检查和管理所有加载的资源的存储,包括 cookie、会话、应用程序缓存、图像和数据库。

在探索 HTML 页面源代码和 DevTools 之后,我们现在大致知道可以在哪里探索或搜索数据。总的来说,爬取涉及从网页中提取数据,我们需要确定或定位携带我们想要提取的数据的资源。在进行数据探索和内容识别之前,计划和确定包含数据的页面 URL 或链接将是有益的。

用户可以选择任何 URL 进行爬取。指向单个页面的页面链接或 URL 也可能包含分页链接或将用户重定向到其他资源的链接。跨多个页面分布的内容需要通过识别页面 URL 来单独爬取。网站提供站点地图和robots.txt文件,其中包含用于爬取相关活动的链接和指令。

站点地图

sitemap.xml文件是一个包含与页面 URL 相关信息的 XML 文件。维护站点地图是通知搜索引擎网站包含的 URL 的简单方法。基于搜索引擎的脚本会爬取站点地图中的链接,并将找到的链接用于索引和各种用途,如搜索引擎优化(SEO)。

在站点地图中找到的 URL 通常包含额外的信息,如创建日期、修改日期、新 URL、已删除 URL 等。这些通常包含在 XML 标记中。在这种情况下,我们有<sitemap><loc>,如下面的屏幕截图所示:

来自 https://www.samsclub.com/的站点地图内容

通过在 URL 中添加sitemap.xml来访问站点地图,例如,www.samsclub.com/sitemap.xml

并非所有网站都必须存在sitemap.xml。站点地图可能包含页面、产品、类别和内部站点地图文件的单独 URL,这些可以轻松地用于抓取目的,而不是从每个网站逐个探索网页链接并收集它们。

robots.txt 文件

robots.txt,也称为机器人排除协议,是网站用于与自动脚本交换信息的基于 Web 的标准。一般来说,robots.txt包含有关网站上的 URL、页面和目录的指令,用于指导网页机器人(也称为网络漫游者爬虫蜘蛛)的行为,如允许、禁止、站点地图和爬行延迟。

来自 https://www.samsclub.com/的 robots.txt 文件

对于任何提供的网站地址或 URL,可以通过在 URL 中添加robots.txt来访问robots.txt文件,例如,https://www.samsclub.com/robots.txthttps://www.test-domainname.com/robots.txt

如前面的屏幕截图所示(来自 https://www.samsclub.com/的 robots.txt 文件),在www.samsclub.com/robots.txt中列出了允许、禁止和站点地图指令:

  • 允许许可网页机器人访问它所携带的链接

  • Disallow 表示限制对给定资源的访问

  • User-agent: *表示列出的指令应由所有代理遵循

对于由网络爬虫和垃圾邮件发送者引起的访问违规,网站管理员可以采取以下步骤:

  • 增强安全机制,限制对网站的未经授权访问

  • 对被跟踪的 IP 地址施加阻止

  • 采取必要的法律行动

网络爬虫应遵守文件中列出的指令,但对于正常的数据提取目的,除非爬行脚本妨碍网站流量,或者它们从网络中获取个人数据,否则不会施加限制。再次强调,并非每个网站都必须提供robots.txt文件。

有关指令和robots.txt的更多信息,请访问www.robotstxt.org/

总结

在本章中,我们探讨了一些与万维网相关的核心技术和工具,这些技术和工具对于网页抓取是必需的。

通过介绍 Web 开发工具来识别和探索内容,并寻找目标数据的页面 URL,是本章的主要重点。

在下一章中,我们将使用 Python 编程语言与网络进行交互,并探索主要的与网络相关的 Python 库,这些库将用于检查网络内容。

进一步阅读

第二部分:开始网页抓取

在本节中,您将学习如何通过使用网页抓取和 Python 编程来规划、分析和处理来自目标网站的所需数据。将探讨有关有效工具和各种数据收集技术的信息。

本节包括以下章节:

  • 第二章,Python 和 Web-使用 urllib 和 Requests

  • 第三章,使用 LXML、XPath 和 CSS 选择器

  • 第四章,使用 pyquery 进行网页抓取-一个 Python 库

  • 第五章,使用 Scrapy 和 Beautiful Soup 进行网页抓取

第二章:Python 和 Web - 使用 urllib 和 Requests

从上一章,我们现在对 Web 抓取是什么,存在哪些核心开发技术以及我们可以计划在哪里或如何找到我们正在寻找的信息有了一个概念。

Web 抓取需要使用脚本或程序实施和部署的工具和技术。Python 编程语言包括一大批适用于与 Web 交互和抓取目的的库。在本章中,我们将使用 Python 与 Web 资源进行通信;我们还将探索并搜索要从 Web 中提取的内容。

本章还将详细介绍使用 Python 库,如requestsurllib

特别是,我们将学习以下主题:

  • 设置 Python 及其所需的库requestsurllib来加载 URL

  • requestsurllib的详细概述

  • 实现 HTTP 方法(GET/POST

我们假设您具有一些使用 Python 编程语言的基本经验。如果没有,请参考 W3schools 的 Python 教程(www.w3schools.com/python/default.asp)、Python 课程(python-course.eu/)或在 Google 上搜索学习 Python 编程

技术要求

我们将使用已安装在 Windows 操作系统上的 Python 3.7.0。有很多选择的代码编辑器;选择一个方便使用并处理本章代码示例中使用的库的编辑器。我们将同时使用来自 JetBrains 的 PyCharm(社区版www.jetbrains.com/pycharm/download/download-thanks.html?platform=windows&code=PCC)和 Python IDLE(www.python.org/downloads/)。

要跟着本章进行,您需要安装以下应用程序:

本章所需的 Python 库如下:

  • requests

  • urllib

本章的代码文件可在 GitHub 上找到:github.com/PacktPublishing/Hands-On-Web-Scraping-with-Python/tree/master/Chapter02

使用 Python 访问网络

Python 是一种用于编写各种类型应用程序的编程语言,从简单脚本到人工智能算法和 Web 框架。我们将使用 Python 编写脚本来从数据提取或抓取的角度访问我们感兴趣的 URL。

存在许多用于 HTTP 通信和与 Web 相关目的的 Python 库(包括httpcookieliburllibrequestshtmlsocketjsonxmlrpchttplib2urllib3)。我们将探索并使用一些被程序员社区赞扬的用于 HTTP 访问或客户端-服务器通信的库。我们感兴趣使用的是urllibrequests Python 模块。这些库具有各种函数,可用于使用 Python 与 Web 通信并处理 HTTP 请求和响应。

为了立即开始一些编码任务并探索基于 Python 的模块,让我们在继续之前验证我们已经安装了所有想要的 Python 资源。

设置事物

假设 Python 已预先安装。如果没有,请访问www.python.org/downloads/www.python.org/download/other/获取您操作系统的最新 Python 版本。关于一般设置和安装程序,请访问realpython.com/installing-python/了解如何在您选择的平台上安装 Python。我们将在这里使用 Windows 操作系统。

为了验证我们是否拥有所有所需的工具,请检查 Python 和pip是否已安装并且是否是最新版本。

pip包管理系统用于安装和管理用 Python 编写的软件包。有关安装 Python 软件包和pip的更多信息,请访问packaging.python.org/tutorials/installing-packages/

我们将在 Windows 操作系统上使用 Python 3.7。按下 Windows + R打开运行框,输入cmd以获取命令行界面:

在 Windows 操作系统上打开命令行界面

现在,转到您的根目录并键入以下命令:

C:\> python –version
Python 3.7.0

上述命令将为我们提供当前系统上的 Python 版本。让我们获取一些关于我们正在使用的pip版本的信息。以下命令将显示当前的pip版本以及其位置:

C:\> pip --version

pip 18.1 from c:\python37\lib\site-packages\pip (python 3.7)

在看到前面的响应后,我们很高兴继续进行。如果遇到“找不到应用程序”或“不被识别为内部或外部命令”的错误,则需要重新安装 Python 或检查安装过程中使用的正确驱动器。

始终建议检查系统和库的版本,并保持它们更新,除非需要特定版本。

要将pip更新到最新版本,请使用以下命令:

C:\> python -m pip install --upgrade pip

您可以验证我们希望使用的库,即requestsurllib,可以从命令行或通过导入 Python IDE 并使用help()方法获取有关包的详细信息:

C:\> pip install requests

Requirement already satisfied: requests in c:\python37\lib\site-packages (2.19.1)

如前面的代码所示,我们尝试安装requests,但命令返回“要求已满足”。pip命令在安装新库之前会检查系统上是否已存在安装。

在下面的代码块中,我们将使用 Python IDE 来导入urllib。我们将使用 Python 的内置help()方法查看其详细信息。

代码中的>>>符号表示使用 Python IDE;它接受代码或指令,并在下一行显示输出:

>>> import urllib 
>>> help(urllib) #display documentation available for urllib

以下是输出:

Help on package urllib:
NAME
 urllib
PACKAGE CONTENTS
 error
 parse
 request
 response
 robotparser
FILE
 c:\python37\lib\urllib\__init__.py

与之前的代码类似,让我们在 Python IDE 中导入requests

>>> import requests 
>>> requests.__version__ #display requests version 

'2.21.0'

>>> help(requests)   #display documentation available for requests

Help on package requests:
NAME
 requests
DESCRIPTION
 Requests HTTP Library
 ~~~~~~~~~~~~~~~~~~
 Requests is an HTTP library, written in Python, for human beings.

如果我们导入urllibrequests,并且这些库不存在,结果将会抛出错误:

ModuleNotFoundError: No module named 'requests'

对于缺少的模块或在先前的情况下,首先安装模块;使用以下pip安装或升级。您可以按照以下方式从命令行安装它:

C:\> pip install requests

您还可以使用--upgrade参数升级模块版本:

C:\> pip install requests -–upgrade

加载 URL

现在我们已确认所需的库和系统要求,我们将继续加载 URL。在查找 URL 的内容时,还需要确认和验证已选择的所需内容的确切 URL。内容可以在单个网页上找到,也可以分布在多个页面上,并且可能并非始终是我们要寻找的 HTML 源。

我们将加载一些 URL 并使用一些任务来探索内容。

在使用 Python 脚本加载 URL 之前,还建议使用 Web 浏览器验证 URL 是否正常工作并包含我们正在寻找的详细信息。开发人员工具也可以用于类似的场景,如第一章中所讨论的Web Scraping FundamentalsDeveloper tools部分。

任务 1:查看来自维基百科的最受欢迎网站列表相关的数据。我们将从页面源中识别SiteDomainType列中的数据。

我们将按照以下链接中的步骤来完成我们的任务(第三章将进行与数据提取相关的活动,Using LXML, XPath and CSS Selectors):en.wikipedia.org/wiki/List_of_most_popular_websites

搜索维基百科以获取我们正在寻找的信息。前面的链接可以在 Web 浏览器中轻松查看。内容以表格格式呈现(如下面的屏幕截图所示),因此可以通过重复使用选择、复制和粘贴操作,或者收集表格内的所有文本来收集数据。

然而,这样的操作不会导致我们感兴趣的内容以理想的格式显示,或者将需要在文本上执行额外的编辑和格式化任务才能实现所需的结果。我们也对从浏览器获取的页面源不感兴趣:

来自维基百科的页面,即 https://en.wikipedia.org/wiki/List_of_most_popular_websites

在确定包含我们需要的内容的链接后,让我们使用 Python 加载链接。我们正在请求链接,并希望看到由urllibrequests返回的响应:

  1. 让我们使用urllib
>>> import urllib.request as req #import module request from urllib
>>> link = "https://en.wikipedia.org/wiki/List_of_most_popular_websites"
>>> response = req.urlopen(link)  #load the link using method urlopen()

>>> print(type(response))   #print type of response object
 <class 'http.client.HTTPResponse'>

>>> print(response.read()) #read response content
b'<!DOCTYPE html>\n<html class="client-nojs" lang="en" dir="ltr">\n<head>\n<meta charset="UTF-8"/>\n<title>List of most popular websites - Wikipedia</title>\n<script>…..,"wgCanonicalSpecialPageName":false,"wgNamespaceNumber":0,"wgPageName":"List_of_most_popular_websites","wgTitle":"List of most popular websites",……

urllib.request中的urlopen()函数已经传递了所选的 URL 或对 URL 进行的请求,并收到了response,即HTTPResponse。可以使用read()方法读取对请求的response

  1. 现在,让我们使用requests
>>> import requests
>>> link = "https://en.wikipedia.org/wiki/List_of_most_popular_websites"
>>> response = requests.get(link)

>>> print(type(response))
 <class 'requests.models.Response'>

>>> content = response.content #response content received
>>> print(content[0:150])  #print(content) printing first 150 character from content

b'<!DOCTYPE html>\n<html class="client-nojs" lang="en" dir="ltr">\n<head>\n<meta charset="UTF-8"/>\n<title>List of most popular websites - Wikipedia</title>'

在这里,我们使用requests模块来加载页面源,就像我们使用urllib一样。requests使用get()方法,该方法接受 URL 作为参数。对于这两个示例,也已经检查了response类型。

在前面的代码块中显示的输出已经被缩短。您可以在github.com/PacktPublishing/Hands-On-Web-Scraping-with-Python找到此代码文件。

在上述示例中,页面内容或response对象包含了我们正在寻找的详细信息,即SiteDomainType列。

我们可以选择任何一个库来处理 HTTP 请求和响应。关于这两个 Python 库的详细信息和示例将在下一节URL handling and operations with urllib and requests中提供。

让我们看一下下面的屏幕截图:

使用 Python 库查看维基百科页面内容

进一步的活动,如处理和解析,可以应用于这样的内容,以提取所需的数据。有关进一步处理工具/技术和解析的更多详细信息可以在第三章、Using LXML, XPath, and CSS Selectors,第四章、Scraping Using pyquery – a Python Library和第五章、Web Scraping Using Scrapy and Beautiful Soup中找到。

任务 2:使用urllibrequests加载并保存来自www.samsclub.com/robots.txtwww.samsclub.com/sitemap.xml的页面内容。

通常,网站在其根路径中提供文件(有关这些文件的更多信息,请参阅第一章,网络抓取基础知识网络数据查找技术部分):

  • robots.txt:其中包含爬虫、网络代理等的信息

  • sitemap.xml:其中包含最近修改的文件、发布的文件等的链接

任务 1中,我们能够加载 URL 并检索其内容。将内容保存到本地文件并使用文件处理概念将在此任务中实现。将内容保存到本地文件并处理内容,如解析和遍历等任务,可以非常快速,甚至可以减少网络资源:

  1. 使用urllib加载并保存来自www.samsclub.com/robots.txt的内容:
>>> import urllib.request 

>>> urllib.request.urlretrieve('https://www.samsclub.com/robots.txt')
('C:\\Users\\*****\AppData\\Local\\Temp\\tmpjs_cktnc', <http.client.HTTPMessage object at 0x04029110>)

>>> urllib.request.urlretrieve(link,"testrobots.txt") #urlretrieve(url, filename=None)
('testrobots.txt', <http.client.HTTPMessage object at 0x04322DF0>)

urlretrieve()函数,即urlretrieve(url, filename=None, reporthook=None, data=None),从urllib.request返回一个包含文件名和 HTTP 头的元组。如果没有给出路径,可以在C:\\Users..Temp目录中找到此文件;否则,文件将在当前工作目录中生成,文件名由urlretrieve()方法的第二个参数提供。在前面的代码中,这是testrobots.txt

>>> import urllib.request
>>> import os
>>> content = urllib.request.urlopen('https://www.samsclub.com/robots.txt').read() #reads robots.txt content from provided URL

>>> file = open(os.getcwd()+os.sep+"contents"+os.sep+"robots.txt","wb") #Creating a file robots.txt inside directory 'contents' that exist under current working directory (os.getcwd()) 

>>> file.write(content) #writing content to file robots.txt opened in line above. If the file doesn't exist inside directory 'contents', Python will throw exception "File not Found"

>>> file.close() #closes the file handle

在前面的代码中,我们正在读取 URL 并使用文件处理概念编写找到的内容。

  1. 使用requests加载并保存来自www.samsclub.com/sitemap.xml的内容:
>>> link="https://www.samsclub.com/sitemap.xml"
>>> import requests
>>> content = requests.get(link).content
>>> content 

b'<?xml version="1.0" encoding="UTF-8"?>\n<sitemapindex >\n<sitemap><loc>https://www.samsclub.com/sitemap_categories.xml</loc></sitemap>\n<sitemap><loc>https://www.samsclub.com/sitemap_products_1.xml</loc></sitemap>\n<sitemap><loc>https://www.samsclub.com/sitemap_products_2.xml</loc></sitemap>\n<sitemap><loc>https://www.samsclub.com/sitemap_locators.xml</loc></sitemap>\n</sitemapindex>'

>>> file = open(os.getcwd()+os.sep+"contents"+os.sep+"sitemap.xml","wb") #Creating a file robots.txt inside directory 'contents' that exist under current working directory (os.getcwd()) 

>>> file.write(content) #writing content to file robots.txt opened in line above. If the file doesn't exist inside directory 'contents', Python will throw exception "File not Found"

>>> file.close() #closes the file handle

在这两种情况下,我们都能够从相应的 URL 中找到内容并将其保存到各自的文件和位置。前面的代码中的内容被发现为字节文字,例如b'<!DOCTYPE …b'<?xml。页面内容也可以以文本格式检索,例如requests.get(link).text

我们可以使用decode()方法将字节转换为字符串,使用encode()方法将字符串转换为字节,如下面的代码所示:

>>> link="https://www.samsclub.com/sitemap.xml"
>>> import requests
>>> content = requests.get(link).text  #using 'text'
>>> content

'<?xml version="1.0" encoding="UTF-8"?>\n<sitemapindex >\n<sitemap><loc>https://www.samsclub.com/sitemap_categories.xml</loc></sitemap>\n<sitemap><loc>https://www.samsclub.com/sitemap_products_1.xml</loc></sitemap>\n<sitemap><loc>https://www.samsclub.com/sitemap_products_2.xml</loc></sitemap>\n<sitemap><loc>https://www.samsclub.com/sitemap_locators.xml</loc></sitemap>\n</sitemapindex>' >>> content = requests.get(link).content 
>>> content.decode() # decoding 'content' , decode('utf-8')

'<?xml version="1.0" encoding="UTF-8"?>\n<sitemapindex >\n<sitemap><loc>https://www.samsclub.com/sitemap_categories.xml</loc></sitemap>\n<sitemap><loc>https://www.samsclub.com/sitemap_products_1.xml</loc></sitemap>\n<sitemap><loc>https://www.samsclub.com/sitemap_products_2.xml</loc></sitemap>\n<sitemap><loc>https://www.samsclub.com/sitemap_locators.xml</loc></sitemap>\n</sitemapindex>'

在处理各种域和文档类型时,识别适当的字符集或charset是很重要的。要识别适当的charset编码类型,我们可以通过使用content-typecharset从页面源中寻求<meta>标签的帮助。

从页面源中识别带有charset属性的<meta>标签,如下面的屏幕截图所示(或<meta http-equiv="content-type" content="text/html; charset=utf-8">

从文档响应或页面源中识别字符集

此外,<meta http-equiv="content-type" content="text/html; charset=utf-8">的内容可以从响应头中获取,如下面的屏幕截图所示:

通过浏览器 DevTools、Network 面板、Headers 选项卡和响应头识别字符集

使用 Python 代码,我们可以在 HTTP 头中找到charset

>>> import urllib.request
>>> someRequest = urllib.request.urlopen(URL) #load/Open the URL
>>> urllib.request.getheaders() #Lists all HTTP headers. 

>>> urllib.request.getheader("Content-Type") #return value of header 'Content-Type'

'text/html; charset=ISO-8859-1' or 'utf-8'

识别的charset将用于使用requests.get(link).content.decode('utf-8')进行编码和解码。

Python 3.0 使用文本和(二进制)数据的概念,而不是 Unicode 字符串和 8 位字符串。所有文本都是 Unicode;然而,编码的 Unicode 被表示为二进制数据。用于保存文本的类型是str(docs.python.org/3/library/stdtypes.html#str),用于保存数据的类型是 bytes(docs.python.org/3/library/stdtypes.html#bytes)。有关 Python 3.0 的更多信息,请访问docs.python.org/3/whatsnew/3.0.html

在本节中,我们设置并验证了我们的技术要求,并探索了 URL 加载和内容查看。在下一节中,我们将探索 Python 库,找到一些有用的函数及其属性。

使用 urllib 和 requests 进行 URL 处理和操作

对于从网页中提取数据的主要动机,需要使用 URL。在我们迄今为止看到的示例中,我们注意到 Python 与其源或内容通信时使用了一些非常简单的 URL。网络爬虫过程通常需要使用来自不同域的不同格式或模式的 URL。

开发人员可能还会面临许多情况,需要对 URL 进行操作(更改、清理)以便快速方便地访问资源。URL 处理和操作用于设置、更改查询参数或清理不必要的参数。它还传递了所需的请求标头和适当值,并确定了适当的 HTTP 方法来进行请求。您将发现许多与 URL 相关的操作,这些操作可以使用浏览器 DevTools 或网络面板进行识别。

urllibrequests Python 库将贯穿本书使用,处理 URL 和基于网络的客户端-服务器通信。这些库提供了各种易于使用的函数和属性,我们将探索一些重要的函数和属性。

urllib

urllib库是一个标准的 Python 包,它收集了几个模块,用于处理与 HTTP 相关的通信模型。urllib内部的模块经过特别设计,包含处理各种类型的客户端-服务器通信的函数和类。

类似命名的包也存在,如urllib2,一个可扩展的库,以及urllib3,一个功能强大的 HTTP 客户端,解决了 Python 标准库中缺少的功能。

处理 URL 请求和响应的两个最重要的urllib模块如下。我们将在本章和接下来的章节中使用这些模块:

  • urllib.request:用于打开和读取 URL 以及请求或访问网络资源(cookie、身份验证等)

  • urllib.response:该模块用于提供对生成的请求的响应

存在许多函数和公共属性来处理与 HTTP 请求相关的请求信息和处理响应数据,例如urlopen()urlretrieve()getcode()getheaders()getheader()geturl()read()readline()等等。

我们可以使用 Python 内置的dir()函数来显示模块的内容,例如其类、函数和属性,如下面的代码所示:

>>> import urllib.request
>>> dir(urllib.request) #list features available from urllib.request

['AbstractBasicAuthHandler', 'AbstractDigestAuthHandler', 'AbstractHTTPHandler', 'BaseHandler', 'CacheFTPHandler', 'ContentTooShortError', 'DataHandler', 'FTPHandler', 'FancyURLopener', 'FileHandler', 'HTTPBasicAuthHandler', 'HTTPCookieProcessor',....'Request', 'URLError', 'URLopener',......'pathname2url', 'posixpath', 'proxy_bypass', 'proxy_bypass_environment', 'proxy_bypass_registry', 'quote', 're', 'request_host', 'socket', 'splitattr', 'splithost', 'splitpasswd', 'splitport', 'splitquery', 'splittag', 'splittype', 'splituser', 'splitvalue', 'ssl', 'string', 'sys', 'tempfile', 'thishost', 'time', 'to_bytes', 'unquote', 'unquote_to_bytes', 'unwrap', 'url2pathname', 'urlcleanup', 'urljoin', 'urlopen', 'urlparse', 'urlretrieve', 'urlsplit', 'urlunparse', 'warnings']

urlopen()函数接受 URL 或urllib.request.Request对象(如requestObj),并通过urllib.responseread()函数返回响应,如下面的代码所示:

>>> import urllib.request
>>> link='https://www.google.com' [](https://www.google.com) 
>>> linkRequest = urllib.request.urlopen(link) #open link
>>> print(type(linkRequest)) #object type
 <class 'http.client.HTTPResponse'> [](https://www.google.com) 
>>> linkResponse = urllib.request.urlopen(link).read() #open link and read content
>>> print(type(linkResponse))
 <class 'bytes'>
 [](https://www.google.com) >>> requestObj = urllib.request.Request('https:/www.samsclub.com/robots.txt')
>>> print(type(requestObj)) #object type
 <class 'urllib.request.Request'>

>>> requestObjResponse = urllib.request.urlopen(requestObj).read()
>>> print(type(requestObjResponse))  #object type
 <class 'bytes'>

linkRequestrequestObjurlopen()函数和类请求返回的对象类型是不同的。还创建了linkResponserequestObjResponse对象,其中包含urllib.responseread()函数的信息。

通常,urlopen()用于从 URL 读取响应,而urllib.request.Request用于发送额外的参数,如dataheaders,甚至指定 HTTP 方法并检索响应。可以如下使用:

urllib.request.Request(url, data=None, headers={}, origin_req_host=None, unverifiable=False, method=None)

urllib.response及其函数,如read()readline(),与urllib.request对象一起使用。

如果所做的请求成功并从正确的 URL 收到响应,我们可以检查 HTTP 状态码,使用的 HTTP 方法,以及返回的 URL 来查看描述:

  • getcode() 返回 HTTP 状态码。如下面的代码所示,也可以使用 codestatus 公共属性获得相同的结果:
>>> linkRequest.getcode()  #can also be used as: linkRequest.code or linkRequest.status 

 200
  • geturl() 返回当前的 URL。有时很方便验证是否发生了任何重定向。url 属性可用于类似的目的:
>>> linkRequest.geturl()   # can also be used as: linkRequest.url

 'https://www.google.com'
  • _method 返回一个 HTTP 方法;GET 是默认响应:
>>> linkRequest._method 
'GET'
  • getheaders() 返回一个包含 HTTP 头的元组列表。如下面的代码所示,我们可以从输出中确定有关 cookie、内容类型、日期等的值:
>>> linkRequest.getheaders()

[('Date','Sun, 30 Dec 2018 07:00:25 GMT'),('Expires', '-1'),('Cache-Control','private, max-age=0'),('Content-Type','text/html; charset=ISO-8859-1'),('P3P', 'CP="This is not a P3P policy! See g.co/p3phelp for more info."'),('Server', 'gws'),('X-XSS-Protection', '1; mode=block'),('X-Frame-Options','SAMEORIGIN'),('Set-Cookie', '1P_JAR=…..; expires=Tue, 29-Jan-2019 07:00:25 GMT; path=/; domain=.google.com'),('Set-Cookie 'NID=152=DANr9NtDzU_glKFRgVsOm2eJQpyLijpRav7OAAd97QXGX6WwYMC59dDPe.; expires=Mon, 01-Jul-2019 07:00:25 GMT; path=/; domain=.google.com; HttpOnly'),('Alt-Svc', 'quic=":443"; ma=2592000; v="44,43,39,35"'),('Accept-Ranges', 'none'),('Vary', 'Accept-Encoding'),('Connection', 'close')] 
  • 当使用 getheader() 传递所需的头元素时,也可以检索单个基于请求的头,如下面的代码所示。在这里,我们可以看到我们可以获取 Content-Type 头的值。相同的结果也可以使用 info() 函数实现:
>>> linkRequest.getheader("Content-Type") 

 'text/html; charset=ISO-8859-1'

>>> linkRequest.info()["content-type"]

 'text/html; charset=ISO-8859-1'

我们已经使用了代码块,并找到了与我们的请求和响应相关的输出。Web 浏览器还允许我们使用浏览器 DevTools(基于浏览器的开发人员工具)跟踪请求/响应相关的信息。

以下截图显示了网络面板和文档选项卡,其中包括头选项。其中包含各种部分,如常规、响应头和请求头。头选项中可以找到基本的请求和响应相关信息:

网络面板和文档选项卡显示了常规和请求头信息

urllib.error 处理 urllib.request 引发的异常。例如,URLErrorHTTPError 可能会为请求引发异常。以下代码演示了 urllib.error 的使用:

异常处理处理编程中的错误处理和管理。使用异常处理的代码也被认为是一种有效的技术,并经常被推荐用于适应。

>>> import urllib.request as request
>>> import urllib.error as error

>>> try:  #attempting an error case
 request.urlopen("https://www.python.ogr") #wrong URL is passed to urlopen()
 except error.URLError as e:
 print("Error Occurred: ",e.reason)

Error Occurred: [Errno 11001] getaddrinfo failed #output

urllib.parse 用于编码/解码请求(数据)或链接,添加/更新头,并分析、解析和操作 URL。解析的 URL 字符串或对象使用 urllib.request 处理。

此外,urlencode()urlparse()urljoin()urlsplit()quote_plus()urllib.parse 中可用的一些重要函数,如下面的代码所示:

>>> import urllib.parse as urlparse
>>> print(dir(urlparse)) #listing features from urlparse

我们得到以下输出:


['DefragResult', 'DefragResultBytes', 'MAX_CACHE_SIZE', 'ParseResult', 'ParseResultBytes', 'Quoter', 'ResultBase', 'SplitResult', 'SplitResultBytes', .........'clear_cache', 'collections', 'namedtuple', 'non_hierarchical', 'parse_qs', 'parse_qsl', 'quote', 'quote_from_bytes', 'quote_plus', 're', 'scheme_chars', 'splitattr', 'splithost', 'splitnport', 'splitpasswd', 'splitport', 'splitquery', 'splittag', 'splittype', 'splituser', 'splitvalue', 'sys', 'to_bytes', 'unquote', 'unquote_plus', 'unquote_to_bytes', 'unwrap', 'urldefrag', 'urlencode', 'urljoin', 'urlparse', 'urlsplit', 'urlunparse', 'urlunsplit', 'uses_fragment', 'uses_netloc', 'uses_params', 'uses_query', 'uses_relative']

urllib.parse 中的 urlsplit() 函数将传递的 URL 拆分为 namedtuple 对象。元组中的每个名称标识 URL 的部分。这些部分可以分开并在其他变量中检索和根据需要使用。以下代码实现了 urlsplit() 用于 amazonUrl

>>> amazonUrl ='https://www.amazon.com/s/ref=nb_sb_noss?url=search-alias%3Dstripbooks-intl-ship&field-keywords=Packt+Books'

>>> print(urlparse.urlsplit(amazonUrl)) #split amazonURL
SplitResult(scheme='https', netloc='www.amazon.com', path='/s/ref=nb_sb_noss', query='url=search-alias%3Dstripbooks-intl-ship&field-keywords=Packt+Books', fragment='')

>>> print(urlparse.urlsplit(amazonUrl).query) #query-string from amazonURL
'url=search-alias%3Dstripbooks-intl-ship&field-keywords=Packt+Books'

>>> print(urlparse.urlsplit(amazonUrl).scheme) #return URL scheme
'https'

使用 urllib.parse 中的 urlparse() 函数会得到 ParseResult 对象。与 urlsplit() 相比,它在检索 URL 中的参数(paramspath)方面有所不同。以下代码打印了从 urlparse() 中获取的对象:

>>> print(urlparse.urlparse(amazonUrl)) #parsing components of amazonUrl

 ParseResult(scheme='https', netloc='www.amazon.com', path='/s/ref=nb_sb_noss', params='', query='url=search-alias%3Dstripbooks-intl-ship&field-keywords=Packt+Books', fragment='')

让我们确认 urlparse()urlsplit() 之间的区别。创建的 localUrl 使用 urlsplit()urlparse() 进行解析。params 仅在 urlparse() 中可用:

import urllib.parse as urlparse
>>> localUrl= 'http://localhost/programming/books;2018?browse=yes&sort=ASC#footer'

>>> print(urlparse.urlsplit(localUrl))
SplitResult(scheme='http', netloc='localhost', path='/programming/books;2018', query='browse=yes&sort=ASC', fragment='footer')

>>> parseLink = urlparse.urlparse(localUrl)
ParseResult(scheme='http', netloc='localhost', path='/programming/books', params='2018', query='browse=yes&sort=ASC', fragment='footer')

>>> print(parseLink.path) #path without domain information
 '/programming/books'

>>> print(parseLink.params) #parameters 
 '2018'

>>> print(parseLink.fragment) #fragment information from URL
 'footer'

基本上,urllib.request.Request 接受数据和与头相关的信息,headers 可以使用 add_header() 赋值给一个对象;例如,object.add_header('host','hostname')object.add_header('referer','refererUrl')

为了请求 data,需要使用 Query InformationURL arguments 作为附加到所需 URL 的键值对信息。这样的 URL 通常使用 HTTP GET 方法处理。传递给请求对象的查询信息应使用 urlencode() 进行编码。

urlencode() 确保参数符合 W3C 标准并被服务器接受。parse_qs() 将百分比编码的查询字符串解析为 Python 字典。以下代码演示了使用 urlencode() 的示例:

>>> import urllib.parse as urlparse
>>> data = {'param1': 'value1', 'param2': 'value2'}

>>> urlparse.urlencode(data)
 'param1=value1&param2=value2'

>>> urlparse.parse_qs(urlparse.urlencode(data))
 {'param1': ['value1'], 'param2': ['value2']}

>>> urlparse.urlencode(data).encode('utf-8')
 b'param1=value1&param2=value2'

在处理请求发送到服务器之前,您可能还需要对 URL 中的特殊字符进行编码:

请注意,urllib.parse包含quote()quote_plus()unquote()函数,这些函数允许无误的服务器请求:

  • quote()通常应用于 URL 路径(与urlsplit()urlparse()一起列出)或在传递给urlencode()之前使用保留和特殊字符(由 RFC 3986 定义)进行查询,以确保服务器的可接受性。默认编码使用UTF-8进行。

  • quote_plus()还对特殊字符、空格和 URL 分隔符进行编码。

  • unquote()unquote_plus()用于恢复使用quote()quote_plus()应用的编码。

这些函数在以下代码中进行了演示:

>>> import urllib.parse as urlparse
>>> url="http://localhost:8080/~cache/data file?id=1345322&display=yes&expiry=false"

>>> urlparse.quote(url) 
 'http%3A//localhost%3A8080/~cache/data%20file%3Fid%3D1345322%26display%3Dyes%26expiry%3Dfalse'

>>> urlparse.unquote(url)
 'http://localhost:8080/~cache/data file?id=1345322&display=yes&expiry=false'

>>> urlparse.quote_plus(url) 'http%3A%2F%2Flocalhost%3A8080%2F~cache%2Fdata+file%3Fid%3D1345322%26display%3Dyes%26expiry%3Dfalse' 

>>> urlparse.unquote_plus(url)
 'http://localhost:8080/~cache/data file?id=1345322&display=yes&expiry=false'

urllib.parse中的urljoin()函数有助于从提供的参数中获取 URL,如下面的代码所示:

>>> import urllib.parse as urlparse

>>> urlparse.urljoin('http://localhost:8080/~cache/','data file') #creating URL
 'http://localhost:8080/~cache/data file'

>>> urlparse.urljoin('http://localhost:8080/~cache/data file/','id=1345322&display=yes')
 'http://localhost:8080/~cache/data file/id=1345322&display=yes'

urllib.robotparser,顾名思义,帮助解析robots.txt并识别基于代理的规则。有关robots.txt的更详细信息,请参阅第一章,网络爬虫基础网络数据查找技术部分。

如下面的代码所示,parRobotFileParser的对象,可以通过set_url()函数设置 URL。它还可以使用read()函数读取内容。诸如can_fetch()的函数可以返回对评估条件的布尔答案:

>>> import urllib.robotparser as robot
>>> par = robot.RobotFileParser()
>>> par.set_url('https://www.samsclub.com/robots.txt') #setting robots URL
>>> par.read()  #reading URL content

>>> print(par)
User-agent: *
Allow: /sams/account/signin/createSession.jsp
Disallow: /cgi-bin/
Disallow: /sams/checkout/
Disallow: /sams/account/
Disallow: /sams/cart/
Disallow: /sams/eValues/clubInsiderOffers.jsp
Disallow: /friend
Allow: /sams/account/referal/

>>> par.can_fetch('*','https://www.samsclub.com/category') #verify if URL is 'Allow' to Crawlers 
True

>>> par.can_fetch('*','https://www.samsclub.com/friend')
False

正如我们所看到的,当使用can_fetch()函数传递https://www.samsclub.com/friend时,返回False,从而满足了robots.txt中找到的Disallow: /friend指令。同样,https://www.samsclub.com/category返回True,因为没有列出限制类别 URL 的指令。

然而,使用urllib.request存在一些限制。在使用urlopen()urlretrieve()等函数时可能会出现基于连接的延迟。这些函数返回原始数据,需要在它们可以在爬取过程中使用之前转换为解析器所需的类型。

部署线程或线程在处理 HTTP 请求和响应时被认为是一种有效的技术。

请求

requests HTTP Python 库于 2011 年发布,是近年来开发人员中最著名的 HTTP 库之一。

Requests 是一个优雅而简单的 Python HTTP 库,专为人类而建。(来源:2.python-requests.org/en/master/)。

有关requests的更多信息,请访问docs.python-requests.org/en/master/

与 Python 中的其他 HTTP 库相比,requests在处理 HTTP 方面的功能能力得到了高度评价。它的一些功能如下:

  • 简短、简单和可读的函数和属性

  • 访问各种 HTTP 方法(GET、POST 等)

  • 摆脱手动操作,如编码表单值

  • 处理查询字符串

  • 自定义标头

  • 会话和 cookie 处理

  • 处理 JSON 请求和内容

  • 代理设置

  • 部署编码和合规性

  • 基于 API 的链接标头

  • 原始套接字响应

  • 超时等等...

我们将使用requests库并访问一些其属性。requests中的get()函数用于向提供的 URL 发送 GET HTTP 请求。返回的对象是requests.model.Response类型,如下面的代码所示:

>>> import requests
>>> link="http://www.python-requests.org"
>>> r = requests.get(link)

>>> dir(r)
['__attrs__', '__bool__', '__class__'......'_content', '_content_consumed', '_next', 'apparent_encoding', 'close', 'connection', 'content', 'cookies', 'elapsed', 'encoding', 'headers', 'history', 'is_permanent_redirect', 'is_redirect', 'iter_content', 'iter_lines', 'json', 'links', 'next', 'ok', 'raise_for_status', 'raw', 'reason', 'request', 'status_code', 'text', 'url']

>>> print(type(r)) 
<class 'requests.models.Response'>

requests库还支持 HTTP 请求,如PUTPOSTDELETEHEADOPTIONS,分别使用put()post()delete()head()options()方法。

以下是一些requests属性,以及对每个属性的简要解释:

  • url输出当前 URL

  • 使用status_code找到 HTTP 状态代码

  • history用于跟踪重定向:

>>> r.url #URL of response object`
 'http://www.python-requests.org/en/master/'

>>> r.status_code #status code
 200

>>> r.history #status code of history event
 [<Response [302]>]

我们还可以获取一些在使用开发人员工具时发现的细节,例如 HTTP 标头、编码等等:

  • headers返回与响应相关的 HTTP 标头

  • requests.header返回与请求相关的 HTTP 标头

  • encoding显示从内容中获取的charset

>>> r.headers #response headers with information about server, date.. 
{'Transfer-Encoding': 'chunked', 'Content-Type': 'text/html', 'Content-Encoding': 'gzip', 'Last-Modified': '....'Vary': 'Accept-Encoding', 'Server': 'nginx/1.14.0 (Ubuntu)', 'X-Cname-TryFiles': 'True', 'X-Served': 'Nginx', 'X-Deity': 'web02', 'Date': 'Tue, 01 Jan 2019 12:07:28 GMT'}

>>> r.headers['Content-Type'] #specific header Content-Type
 'text/html'

>>> r.request.headers  #Request headers 
{'User-Agent': 'python-requests/2.21.0', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive'}

>>> r.encoding  #response encoding
 'ISO-8859-1'

可以使用content以字节形式检索页面或响应内容,而text返回一个str字符串:

>>> r.content[0:400]  #400 bytes characters

b'\n<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"\n ....... <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />\n <title>Requests: HTTP for Humans\xe2\x84\xa2 — Requests 2.21.0 documentation'

>>> r.text[0:400]  #sub string that is 400 string character from response

'\n<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"\n......\n <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />\n <title>Requests: HTTP for Humansâ\x84¢ — Requests 2.21.0 documentation'

此外,requests还通过在get()请求中使用stream参数返回服务器的raw套接字响应。我们可以使用raw.read()函数读取原始响应:

>>> r = requests.get(link,stream=True) #raw response

>>> print(type(r.raw))   #type of raw response obtained
 <class 'urllib3.response.HTTPResponse'>

>>> r.raw.read(100)  #read first 100 character from raw response
 b"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\xed}[o\xdcH\x96\xe6{\xfe\x8a\xa8\xd4\xb4%O\x8bL2/JI\x96\xb2Z\x96e[U\xbe\xa8-\xb9\xaa\x1b\x85^!\x92\x8c\xcc\xa4\xc5$Y\xbc(\x95\xae)\xa0\x1e\x06\x18\xcc\xf3\xce\xcb\x00\xbbX`\x16\xd8\xc7\xc5>\xed\xeb\x02\xfb3f_\x16\xf5\x0b\xf6'\xec9'\x82\x97\xbc\xc9\xb2+#g"

使用raw属性接收的原始响应是未经转换或自动解码的原始字符字节。

requests使用其内置解码器非常有效地处理 JSON 数据。正如我们所看到的,具有 JSON 内容的 URL 可以使用requests进行解析并根据需要使用:

>>> import requests
>>> link = "https://feeds.citibikenyc.com/stations/stations.json"
>>> response = requests.get(link).json()

>>> for i in range(10): #read 10 stationName from JSON response.
 print('Station ',response['stationBeanList'][i]['stationName'])

Station W 52 St & 11 Ave
Station Franklin St & W Broadway
Station St James Pl & Pearl St
........
Station Clinton St & Joralemon St
Station Nassau St & Navy St
Station Hudson St & Reade St

请注意,requests使用urllib3进行会话和原始套接字响应。在撰写本文时,requests版本 2.21.0 可用。

爬取脚本可能使用任何提到的或可用的 HTTP 库来进行基于 Web 的通信。大多数情况下,来自多个库的函数和属性将使这个任务变得容易。在下一节中,我们将使用requests库来实现 HTTP(GET/POST)方法。

实现 HTTP 方法

通常,网页与用户或读者之间的基于 Web 的交互或通信是这样实现的:

  • 用户或读者可以访问网页阅读或浏览呈现给他们的信息

  • 用户或读者还可以通过 HTML 表单提交某些信息到网页,比如搜索、登录、用户注册、密码恢复等

在本节中,我们将使用requests Python 库来实现常见的 HTTP 方法(GETPOST),执行我们之前列出的基于 HTTP 的通信场景。

GET

请求信息的一种命令方式是使用安全方法,因为资源状态不会被改变。GET参数,也称为查询字符串,在 URL 中是可见的。它们使用?附加到 URL,并以key=value对的形式可用。

通常,未指定任何 HTTP 方法的处理 URL 是正常的 GET 请求。使用 GET 发出的请求可以被缓存和书签标记。在进行GET请求时也有长度限制。以下是一些示例 URL:

在前面的部分,对正常的 URL(如robots.txtsitemap.xml)进行了请求,这两个 URL 都使用了 HTTP GET方法。requestsget()函数接受 URL、参数和标头:

import requests
link="http://localhost:8080/~cache"

queries= {'id':'123456','display':'yes'}

addedheaders={'user-agent':''}

#request made with parameters and headers
r = requests.get(link, params=queries, headers=addedheaders) 
print(r.url)

这是前面代码的输出:

http://localhst:8080/~cache?id=123456+display=yes

POST

这些被称为安全请求,这些请求是向源发出的。请求的资源状态可以被改变。发送到请求的 URL 的数据在 URL 中是不可见的;相反,它被传输到请求体中。使用POST发出的请求不会被缓存或书签标记,并且在长度方面没有限制。

在下面的示例中,使用了一个简单的 HTTP 请求和响应服务 (来源:httpbin.org/) 来发出POST请求。

pageUrl接受要发布的数据,如params中定义的内容到postUrl。自定义标头被分配为headersrequests库的post()函数接受 URL、数据和标头,并以 JSON 格式返回响应:

import requests pageUrl="http://httpbin.org/forms/post"
postUrl="http://httpbin.org/post"

params = {'custname':'Mr. ABC','custtel':'','custemail':'abc@somedomain.com','size':'small', 'topping':['cheese','mushroom'],'delivery':'13:00','comments':'None'} headers={ 'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8','Content-Type':'application/x-www-form-urlencoded', 'Referer':pageUrl }

#making POST request to postUrl with params and request headers, response will be read as JSON response = requests.post(postUrl,data=params,headers=headers).json()
print(response)

前面的代码将产生以下输出:

{
'args': {}, 
'data': '', 
'files': {}, 
'form': {
'comments': 'None', 
'custemail': 'abc@somedomain.com',
'custname': 'Mr. ABC', 
'custtel': '',
'delivery': '13:00', 
'size': 'small', 
'topping': ['cheese', 'mushroom']
}, 
'headers': {    'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', 'Accept-Encoding': 'gzip, deflate', 
'Connection': 'close', 
'Content-Length': '130', 
'Content-Type': 'application/x-www-form-urlencoded', 
'Host': 'httpbin.org', 
'Referer': 'http://httpbin.org/forms/post', 
'User-Agent': 'python-requests/2.21.0'
}, 
'json': None, 'origin': '202.51.76.90', 
'url': 'http://httpbin.org/post'
}

对于我们尝试的POST请求,我们可以使用 DevTools Network 面板找到有关请求标头、响应标头、HTTP 状态和POST数据(参数)的详细信息,如下图所示:

在 DevTools 网络面板中提交的 POST 数据并作为表单数据找到总是有益的学习和检测通过浏览器和可用的 DevTools 进行的 URL 的请求和响应序列。

总结

在本章中,我们学习了如何使用 Python 库向网络资源发出请求并收集返回的响应。本章的主要目标是演示通过urllibrequests Python 库提供的核心功能,以及探索以各种格式找到的页面内容。

在下一章中,我们将学习并使用一些技术来识别和提取网页内容中的数据。

进一步阅读

第三章:使用 LXML、XPath 和 CSS 选择器

到目前为止,我们已经了解了 Web 开发技术、数据查找技术以及使用 Python 编程语言访问 Web 内容。

基于 Web 的内容以一些预定义的文档表达式存在于部分或元素中。分析这些部分的模式是处理方便的抓取的主要任务。元素可以使用 XPath 和 CSS 选择器进行搜索和识别,这些选择器会根据抓取逻辑处理所需的内容。lxml 将用于处理标记文档中的元素。我们将使用基于浏览器的开发工具进行内容阅读和元素识别。

在本章中,我们将学习以下内容:

  • XPath 和 CSS 选择器简介

  • 使用浏览器开发者工具

  • 学习并使用 Python lxml 库进行抓取

技术要求

需要一个 Web 浏览器(Google Chrome 或 Mozilla Firefox),我们将使用以下 Python 库:

  • lxml

  • 请求

如果当前 Python 设置中不存在上述库,可以参考上一章的设置部分进行设置或安装。

代码文件可在 GitHub 上找到:github.com/PacktPublishing/Hands-On-Web-Scraping-with-Python/tree/master/Chapter03

XPath 和 CSS 选择器简介

在第一章的了解 Web 开发和技术部分,Web 抓取基础中,我们介绍了 XML 作为一个包含可在 Web 和文档相关技术中交换和分发的数据的文档。XML 包含用户定义的标签,也称为节点,它们以树状结构保存数据。

树状结构(也称为元素树)是大多数标记语言的基本模型,并经常被称为文档对象模型DOM)。借助 DOM 及其定义的约定,我们可以访问、遍历和操作元素。

元素被结构化在一些父元素内部,这些父元素又位于它们自己的父元素内部,依此类推;这描述了标记语言最重要的特征,即父子关系。许多支持 XML 或标记语言的应用程序支持 DOM,甚至包含解析器来使用。

为了提取信息,有必要确定信息的确切位置。信息可能嵌套在树状结构内,并可能具有一些额外的属性来表示内容。XPath 和 CSS 选择器都用于沿着 DOM 导航并搜索文档中的所需元素或节点。

在接下来的部分中,我们将介绍 XPath 和 CSS 选择器,并使用它们来进行 Web 抓取,并使用支持的 Python 库。

XPath

XML PathXPath)语言是基于 XML 的技术(XML、XSLT 和 XQuery)的一部分,用于通过表达式导航 DOM 元素或在 XML(或 HTML)文档中定位节点。XPath 通常是标识文档中节点的路径。XPath 也是W3C万维网联盟)的推荐(www.w3.org/TR/xpath/all/)。

XPath 或 XPath 表达式也被识别为绝对和相对:

  • 绝对路径是表示从根元素到所需元素的完整路径的表达式。它以/html开头,看起来像/html/body/div[1]/div/div[1]/div/div[1]/div[2]/div[2]/div/span/b[1]。单个元素通过其位置进行识别,并由索引号表示。

  • 相对路径表示从某些选定的元素中选择的表达式到所需的元素。相对路径比绝对路径更短,更易读,并且看起来像//*[@id="answer"]/div/span/b[@class="text"]。相对路径通常优先于绝对路径,因为元素索引、属性、逻辑表达式等可以组合在一个表达式中。

使用 XPath 表达式,我们可以在元素之间进行层次导航并达到目标元素。XPath 也由各种编程语言实现,例如 JavaScript、Java、PHP、Python 和 C++。Web 应用程序和浏览器也内置了对 XPath 的支持。

可以使用各种内置函数来构建表达式,这些函数适用于各种数据类型。与一般数学相关的操作(+、-、*、/)、比较(<、>、=、!=、>=、<=)和组合运算符(andormod)也可以用于构建表达式。XPath 也是 XML 技术(如 XQuery 和eXtensible Stylesheet Language TransformationsXSLT))的核心组成部分。

XML 查询XQuery)是一种使用 XPath 表达式从 XML 文档中提取数据的查询语言。

XSLT 用于以更易读的格式呈现 XML。

让我们从food.xml文件中的 XML 内容中探索一些 XPath 表达式:

XML 内容

在以下示例中,我们将使用 Code Beautify 的 XPath-Tester(codebeautify.org/Xpath-Tester)。使用前面提供的 XML 源 URL 获取 XML 内容,并将其与 Code Beautify XPath-Tester 一起使用。

您可以使用codebeautify.org/Xpath-Testerwww.freeformatter.com/xpath-tester.htm或任何其他免费提供的 XPath 测试工具。

在 XML 文档中,一切都是一个节点,例如menusfoodprice。XML 节点本身可以是一个元素(元素是具有开始和结束标记的类型或实体)。

前面的 XML 文档也可以被视为继承的元素块。父节点menus包含多个子节点food,这些子节点区分适当的值和适当的数据类型。如下截图所示,XPath 表达式//food显示了所选节点food的结果。节点选择还检索了父节点中的子节点,如下截图所示:

XPath //food 的结果(使用 https://codebeautify.org/Xpath-Tester)

以下截图中的 XPath 表达式选择了所有父节点food中找到的子节点price。有六个可用的子food节点,每个节点都包含pricenamedescriptionfeedbackrating

XPath //food/price 的结果(使用 https://codebeautify.org/Xpath-Tester)

从前面测试的两个 XPath 可以看出,表达式几乎像文件系统(命令行或终端路径)一样创建,我们在各种操作系统中使用。XPath 表达式包含代码模式、函数和条件语句,并支持使用谓词。

谓词用于识别特定的节点或元素。谓词表达式使用方括号编写,类似于 Python 列表或数组表达式。

在前面的 XML 中给出的 XPath 表达式的简要解释列在以下表中:

XPath 表达式 描述
// 选择文档中的节点,无论它们位于何处
//* 选择文档中的所有元素
//food 选择元素food
* 选择所有元素

| //food/name &#124; //food/price | 选择在food节点中找到的nameprice元素:

<name>Butter Milk with Vanilla</name>
 <name>Fish and Chips</name>
 <price>$5.50</price>
 <price>$2.99</price>

|

| //food/name | 选择food中的所有name元素:

<name>Butter Milk with Vanilla</name>
 <name>Eggs and Bacon</name>
 <name>Orange Juice</name>

|

| //food/name/text() | 仅选择所有food/name元素的text

Butter Milk with Vanilla Orange Juice

|

| //food/name &#124; //rating | 选择文档中foodrating中找到的所有name元素:

<name>Butter Milk with Vanilla</name>
 <name>Fish and Chips</name><rating>4.5</rating>
 <rating>4.9</rating>

|

| //food[1]/name | 选择第一个food节点的name元素:

<name>Butter Milk with Vanilla</name>

|

| //food[feedback<9] | 选择满足谓词条件feedback<9food节点及其所有元素:

<food>
 <name>Butter Milk with Vanilla</name>
 <name>Egg Roll</name>
 <name>Eggs and Bacon</name>
 </food>

|

| //food[feedback<9]/name | 选择满足条件的food节点和name元素:

<name>Butter Milk with Vanilla</name>
 <name>Egg Roll</name>
 <name>Eggs and Bacon</name>

|

| //food[last()]/name | 选择最后一个food节点的name元素:

<name>Orange Juice</name>

|

| //food[last()]/name/text() | 选择最后一个food节点的name元素的text

Orange Juice

|

| sum(//food/feedback) | 提供所有food节点中反馈的总和:

47.0

|

| //food[rating>3 and rating<5]/name | 选择满足谓词条件的foodname

<name>Egg Roll</name>
<name>Eggs and Bacon</name>
<name>Orange Juice</name>

|

| //food/name[contains(.,"Juice")] | 选择包含Juice字符串的foodname

<name>Orange Juice</name>

|

| //food/description[starts-with(.,"Fresh")]/text() | 选择以“新鲜”开头的描述节点的文本:

Fresh egg rolls filled with ground chicken, ... cabbage
Fresh Orange juice served

|

| //food/description[starts-with(.,"Fresh")] | 选择以“新鲜”开头的description节点的text

<description>Fresh egg rolls filled with.. cabbage</description>
 <description>Fresh Orange juice served</description>

|

| //food[position()<3] | 根据其位置选择第一个和第二个食物:

<food>
 <name>Butter Milk with Vanilla</name>
 <price>$3.99</price>
 ...
 <rating>5.0</rating>
 <feedback>10</feedback>
 </food>

|

XPath 谓词可以包含从1(而不是0)开始的数字索引和条件语句,例如//food[1]//food[last()]/price

现在我们已经使用各种 XPath 表达式测试了前面的 XML,让我们考虑一个带有一些属性的简单 XML。属性是用于标识给定节点或元素的某些参数的额外属性。单个元素可以包含唯一的属性集。在 XML 节点或 HTML 元素中找到的属性有助于识别具有其所包含值的唯一元素。正如我们在以下 XML 代码中所看到的,属性以key=value信息对的形式出现,例如id="1491946008"

<?xml version="1.0" encoding="UTF-8"?>
<books>
     <book id="1491946008" price='47.49'>
        <author>Luciano Ramalho</author>
         <title>
            Fluent Python: Clear, Concise, and Effective Programming
        </title>
     </book>
     <book id="1491939362" price='29.83'>
         <author>Allen B. Downey</author>
         <title>
 Think Python: How to Think Like a Computer Scientist
        </title>
     </book>
</books>

XPath 表达式通过在键名前面添加@字符来接受key属性。以下表中列出了使用属性的 XPath 的一些示例,并附有简要描述。

XPath 表达式 描述

| //book/@price | 选择bookprice属性:

price="47.49"
price="29.83"

|

| //book | 选择book字段及其元素:

<book id="1491946008" price="47.49">

<author>Luciano Ramalho</author>
 <title>Fluent Python: Clear, Concise, and Effective Programming
 Think Python: How to Think Like a Computer Scientist
 </title></book>

|

| //book[@price>30] | 选择price属性大于30book中的所有元素:

<book id="1491946008" price="47.49">
 <author>Luciano Ramalho</author>
 <title>Fluent Python: Clear, Concise, and Effective Programming </title> </book>

|

| //book[@price<30]/title | 选择price属性小于30的书籍的title

<title>Think Python: How to Think Like a Computer Scientist</title>

|

| //book/@id | 选择id属性及其值。//@id表达式也会产生相同的输出:

id="1491946008"
 id="1491939362"

|

| //book[@id=1491939362]/author | 选择id=1491939362book中的author

<author>Allen B. Downey</author>

|

我们已经尝试探索和学习了一些关于 XPath 和编写表达式以检索所需内容的基本特性。在使用 lxml 进行爬虫-一个 Python 库部分,我们将使用 Python 编程库进一步探索使用 XPath 部署代码来爬取提供的文档(XML 或 HTML),并学习使用浏览器工具生成或创建 XPath 表达式。有关 XPath 的更多信息,请参考进一步阅读部分中的链接。

CSS 选择器

在第一章中,网络爬虫基础,在了解网页开发和技术部分,我们学习了 CSS 及其用于样式化 HTML 元素的用法,以及使用全局属性。 CSS 通常用于样式化 HTML,有各种方法可以将 CSS 应用于 HTML。

CSS 选择器(也称为 CSS 查询或 CSS 选择器查询)是 CSS 使用的定义模式,用于选择 HTML 元素,使用元素名称或全局属性(IDClass)。 CSS 选择器如其名称所示,以各种方式选择或提供选择 HTML 元素的选项。

在下面的示例代码中,我们可以看到在<body>中找到的一些元素:

  • <h1>是一个元素和选择器。

  • <p>元素或选择器具有class属性和header样式类型。在选择<p>时,我们可以使用元素名称、属性名称或类型名称。

  • 多个<a><div>中找到,但它们的class属性、idhref属性的值不同:

<html>
<head>
    <title>CSS Selectors: Testing</title>
    <style>
        h1{color:black;}
        .header,.links{color: blue;}
        .plan{color: black;}
        #link{color: blue;}
    </style>
</head>
<body>
    <h1>Main Title</h1>
    <p class=”header”>Page Header</p>
    <div class="links">
         <a class="plan" href="*.pdf">Document Places</a>
         <a id="link" href="mailto:xyz@domain.com">Email Link1!</a>
         <a href="mailto:abc@domain.com">Email Link2!</a>    
    </div>
</body>
</html>

我们在前面的代码中识别出的可区分的模式可以用于单独或分组选择这些特定元素。在线上有许多 DOM 解析器,它们提供了与 CSS 查询相关的功能。其中一个,如下面的屏幕截图所示,是try.jsoup.org/

https://try.jsoup.org/评估 CSS 查询 DOM 解析器将提供的 XML 或 HTML 转换为 DOM 对象或树类型的结构,从而便于访问和操作元素或树节点。有关 DOM 的更多详细信息,请访问dom.spec.whatwg.org/

在 CSS 查询中,以下代码文本中列出的各种符号代表特定的特征,并且可以在 CSS 查询中使用:

  • 全局id属性和class#.表示,如此查询所示:

  • a#link: <a id="link" href="mailto:xyz@domain.com">Email Link1!</a>

  • a.plan: <a class="plan" href="*.pdf">Document Places</a>

  • 组合符(显示元素之间的关系)也被使用,例如+>~和空格字符,如此查询所示:

  • h1 + p: <p class=”header”>Page Header</p>

  • div.links a.plan: <a class="plan" href="*.pdf">Document Places</a>

  • 诸如^*$之类的运算符用于定位和选择,如此查询所示:

  • a[href$="pdf"]: <a class="plan" href="*.pdf">Document Places</a>

  • a[href^="mailto"]: <a id="link" href="mailto:xyz@domain.com">Email Link1!</a><a href="mailto:abc@domain.com">Email Link2!</a>

这些符号在以下各节中并排使用和解释,参考前面 HTML 代码中各种类型的选择器。

元素选择器

元素选择器是从 HTML 中选择元素的基本选择器。通常,这些元素是 HTML 的基本标签。以下表格列出了此类别的一些选择器及其用法:

CSS 查询 描述
h1 选择<h1>元素
a 选择所有<a>元素
* 选择 HTML 代码中的所有元素
body * 选择<body>中的所有<h1><p><div><a>元素
div a 选择<div>中的所有<a>(使用空格字符之间)
h1 + p 选择<h1>后面的直接<p>元素
h1 ~ p 选择<h1>之前的每个<p>元素
h1,p 选择所有<h1><p>元素
div > a 选择所有是<div>的直接子元素的<a>元素

ID 和类选择器

ID 和类选择器是元素选择器的附加功能。我们可以找到具有classid属性的 HTML 标签。这些也被称为全局属性。这些属性通常优先于其他属性,因为它们定义了结构和标识的标签。

有关全局属性的更多详细信息,请参阅第一章,Web Scraping Fundamentals全局属性部分。以下表格列出了此类选择器的用法:

CSS 查询 描述
.header 选择具有class=header的元素
.plan 选择具有class=plan<a>
div.links 选择class=plan<div>
#link 选择具有id=link的元素
a#link 选择具有id=link<a>元素
a.plan 选择具有class=plan<a>元素

属性选择器

属性选择器用于定义具有可用属性的选择器。HTML 标签包含一个属性,该属性有助于识别具有该属性和其携带的值的特定元素。

以下表格列出了一些显示属性选择器用法的方式:

CSS 查询 描述

| a[href*="domain"] | 选择<a>元素,其href中包含domain子字符串:

<a id="link" href="mailto:xyz@domain.com">Email Link1!</a> 
<a href="mailto:abc@domain.com">Email Link2!</a>

|

| a[href^="mailto"] | 选择以href属性的mailto子字符串开头的<a>元素:

<a id="link" href="mailto:xyz@domain.com">Email Link1!</a> 
<a href="mailto:abc@domain.com">Email Link2!</a>

|

| a[href$="pdf"] | 选择<a>元素,其href属性末尾有pdf子字符串:

<a class="plan" href="*.pdf"> Document Places </a>

|

| [href~=do] | 选择所有具有href属性并在值中匹配do的元素。以下两个<a>元素的href值中都包含do

<a id="link" href="mailto:xyz@domain.com">Email Link1!</a> 
<a href="mailto:abc@domain.com">Email Link2!</a>

|

| [class] | 选择所有具有class属性的元素或<p><div><a>

<p class='header'>Page Header</p>
<div class="links">
<a class="plan" href="*.pdf"> Document Places </a>

|

| [class=plan] | 选择class=plan<a>

<a class="plan" href="*.pdf"> Document Places </a>

|

伪选择器

伪选择器是一组方便的选择,用于根据其位置识别或选择元素。

以下表格列出了这些类型选择器的一些用法及简要描述:

CSS 查询 描述

| a:gt(0) | 选择除了索引为0的所有<a>元素:

<a id="link" href="mailto:xyz@domain.com">Email Link1!</a> <a href="mailto:abc@domain.com">Email Link2!</a> 

|

| a:eq(2) | 选择索引为2<a>元素:

<a href="mailto:abc@domain.com">

|

| a:first-child | 选择其父元素中是第一个子元素的每个<a>元素:

<a class="plan" href="*.pdf">Document Places</a>

|

| a:last-child | 选择其父元素中是最后一个子元素的每个<a>元素:

<a href="mailto:abc@domain.com">Email Link2!</a>

|

| a:last-of-type | 选择其父元素的最后一个<a>元素:

<a href="mailto:abc@domain.com">Email Link2!</a>

|

:not(p) 选择除了<p>之外的所有元素。

| a:nth-child(1) | 选择其父元素中是第一个子元素的每个<a>元素:

<a class="plan" href="*.pdf">Document Places</a>

|

| a:nth-last-child(3) | 选择其父元素中倒数第三个位置的每个<a>元素:

<a class="plan" href="*.pdf">Document Places</a>

|

| a:nth-of-type(3) | 选择其父元素的每第三个<a>元素:

<a href="mailto:abc@domain.com">Email Link2!</a>

|

| a:nth-last-of-type(3) | 选择其父元素中倒数第三个位置的每个<a>元素:

<a class="plan" href="*.pdf">Document Places</a>

|

CSS 选择器被用作选择元素的方便替代方法,与绝对 XPath 相比,它们长度更短,并且在表达式中使用简单模式,易于阅读和管理。CSS 选择器可以转换为 XPath 表达式,但反之则不行。

还有许多在线工具可用,允许将 CSS 选择器查询转换为 XPath 表达式;其中一个是css-selector-to-xpath.appspot.com/,如下截图所示;我们不应总是信任可用的工具,应在应用于代码之前进行测试结果:

CSS 选择器转 XPath 转换器

如前面的截图所述,CSS 选择器用于从数据提取的角度选择元素,并且可以在Scraper代码中使用,甚至可以在应用样式到所选元素的样式方面使用。

在本节中,我们学习了 XPath 和 CSS 选择器的最流行的与网络相关的模式查找技术。在下一节中,我们将探索基于浏览器的开发者工具(DevTools),并学习如何使用 DevTools 内部的功能。DevTools 可用于搜索、分析、识别和选择元素,并获取 XPath 表达式和 CSS 选择器。

使用 Web 浏览器开发者工具访问 Web 内容

在第一章中,网络抓取基础知识,在数据查找技术(从网络中获取数据)部分和开发者工具(DevTools)内部,我们介绍了基于浏览器的 DevTools 来定位内容和探索各种面板。DevTools 提供各种功能面板,为我们提供支持工具来管理相关资源。

在这个特定的部分,我们的目的将是特定地识别持有我们正在寻找的内容的特定元素。这种基于标识的信息,比如 XPath 表达式、CSS 查询,甚至是基于 DOM 的导航流,在编写Scraper时将会很有帮助。

我们将使用 Google Chrome 浏览网页。Chrome 内置了开发者工具,具有许多功能(用于元素识别、选择、DOM 导航等)。在接下来的部分,我们将探索并使用这些功能。

HTML 元素和 DOM 导航

我们将使用books.toscrape.com/来自toscrape.com/toscrape提供了与网页抓取相关的资源,供初学者和开发人员学习和实施Scraper

让我们使用网页浏览器 Google Chrome 打开books.toscrape.com的 URL,如下所示:

books.toscrape.com 的检查视图

当页面内容成功加载后,我们可以右键单击页面并选择选项检查,或者按Ctrl + Shift + I来加载 DevTools。如果通过 Chrome 菜单访问,点击更多工具和开发者工具。浏览器应该看起来与前面的屏幕截图中的内容类似。

正如您在前面的屏幕截图中所看到的,在检查模式下,加载了以下内容:

  • 面板元素默认位于左侧。

  • 基于 CSS 样式的内容位于右侧。

  • 我们注意到在左下角有 DOM 导航或元素路径,例如,html.no-js body .... div.page_inner div.row

我们在第一章中已经对这些面板进行了基本概述,Web Scraping Fundamentals,在Developer Tools部分。随着开发者工具的加载,我们可以首先找到一个指针图标,这是用于从页面中选择元素的,如下图所示;这个元素选择器(检查器)可以使用Ctrl + Shift + C打开/关闭:

检查栏上的元素选择器(检查器)

打开元素选择器后,我们可以在页面上移动鼠标。基本上,我们正在使用鼠标搜索我们指向的确切 HTML 元素:

在书籍图片上使用元素选择器

如前面的屏幕截图所示,该元素已被选中,当我们将鼠标移动到第一本书的图片上时,这个动作会导致以下结果:

  • div.image_container元素在页面中显示并被选中。

  • 在元素面板源中,我们可以找到特定的 HTML 代码<div class="image_container">,也被突出显示。这些信息(书籍图片的位置)也可以通过右键单击+页面源或Ctrl + U,然后搜索特定内容来找到。

我们可以重复对我们希望抓取的 HTML 内容的各个部分执行相同的操作,就像以下示例中所示的那样:

  • 列出的书籍价格位于div.product_price元素内。

  • 星级评分位于p.star-rating内。

  • 书名位于*<*h3>内,在div.product_price之前或在p.star-rating之后。

  • 书籍详细链接位于<a>内,该链接存在于<h3>内。

  • 从下面的屏幕截图中,也清楚地看到了先前列出的元素都位于article.product_prod内。此外,在下面的屏幕截图底部,我们可以确定 DOM 路径为article.product_prod

检查模式下的元素选择

在前面的截图中找到的 DOM 导航在处理 XPath 表达式时可能会有所帮助,并且可以使用页面源代码验证内容,如果元素检查器显示的路径或元素实际存在于获取的页面源代码中。

DOM 元素、导航路径和使用元素检查器或选择器找到的元素应该进行交叉验证,以确保它们在页面源代码或网络面板中存在。

使用 DevTools 获取 XPath 和 CSS 选择器

在本节中,我们将收集所需元素的 XPath 表达式和 CSS 查询。与我们在前一节中探索页面检查和元素面板的方式类似,让我们继续以下步骤,获取所选元素的 XPath 表达式和 CSS 查询:

  1. 选择元素选择器并获取元素代码

  2. 右键单击鼠标获取元素代码

  3. 从菜单中选择复制选项

  4. 从子菜单选项中,选择复制 XPath 以获取所选元素的 XPath 表达式

  5. 或选择 CSS 选择器(查询)的复制选择器

如下截图所示,我们选择单个图书项目的各个部分,并获取相应的 CSS 选择器或 XPath 表达式,访问菜单选项:

使用页面检查复制 XPath 和 CSS 选择器

以下是使用 DevTools 收集的一些 XPath 和 CSS 选择器,用于产品的可用项目,如图书标题和价格。

使用 DevTools 获取 XPath 选择器

  • 图书标题://*[@id="default"]/div/div/div/div/section/div[2]/ol/li[1]/article/h3/a

  • 价格://*[@id="default"]/div/div/div/div/section/div[2]/ol/li[1]/article/div[2]

  • 图片://*[@id="default"]/div/div/div/div/section/div[2]/ol/li[1]/article/div[1]

  • 库存信息://*[@id="default"]/div/div/div/div/section/div[2]/ol/li[1]/article/div[2]/p[2]

  • 星级评分://*[@id="default"]/div/div/div/div/section/div[2]/ol/li[1]/article/p

使用 DevTools 获取 CSS 查询选择器

  • 图书标题:#default > div > div > div > div > section > div:nth-child(2) > ol > li:nth-child(1) > article > h3 > a

  • 价格:#default > div > div > div > div > section > div:nth-child(2) > ol > li:nth-child(1) > article > div.product_price

  • 图片:#default > div > div > div > div > section > div:nth-child(2) > ol > li:nth-child(1) > article > div.image_container

  • 库存信息:#default > div > div > div > div > section > div:nth-child(2) > ol > li:nth-child(1) > article > div.product_price > p.instock.availability

  • 星级评分:#default > div > div > div > div > section > div:nth-child(2) > ol > li:nth-child(1) > article > p.star-rating

同样,其他必要的 XPath 或 CSS 选择器也将根据需要收集。在收集和验证或清理(缩短)这些表达式和查询之后,使用 Python 编程应用爬虫逻辑来自动化数据收集。

同样,没有特定的方法可以避开前一节中讨论的步骤。XPath 或 CSS 选择器也可以通过显示 HTML 源代码或页面源代码来确定或形成;还有许多支持类似任务的基于浏览器的扩展。开发人员可以选择任何我们讨论过的处理 XPath 和 CSS 选择器的方法来感到舒适。

最近列出的基于浏览器的扩展之一,用于生成 Google Chrome 的 XPath 和 CSS 选择器是 ChroPath (autonomiq.io/chropath/)。建议自行练习和了解编写自定义表达式和查询。在处理大量信息源时,应使用扩展和其他类似应用程序。

在本节中,我们检查和探索了元素面板,用于元素识别和 DOM 导航:修改、删除元素、修改脚本等。元素面板中也存在相关选项。在接下来的部分中,我们将使用 Python 库lxml来编写Scraper,并使用 XPath 和 CSS 选择器从选择的网站收集数据。

使用 lxml,一个 Python 库

lxml 是一个 XML 工具包,具有丰富的库集来处理 XML 和 HTML。lxml 在 Python 中比其他基于 XML 的库更受青睐,因为它具有高速和有效的内存管理。它还包含各种其他功能,用于处理小型或大型 XML 文件。Python 程序员使用 lxml 来处理 XML 和 HTML 文档。有关 lxml 及其库支持的更详细信息,请访问lxml.de/.

lxml 提供了对 XPath 和 XSLT 的本机支持,并构建在强大的 C 库libxml2libxslt之上。它的库集通常与 XML 或 HTML 一起使用,用于访问 XPath、解析、验证、序列化、转换和扩展 ElementTree 的功能(effbot.org/zone/element-index.htm#documentation)。从 lxml 中解析、遍历 ElementTree、XPath 和类似 CSS 选择器的功能使其足够方便用于诸如网络抓取之类的任务。lxml 还用作 Python Beautiful Soup (www.crummy.com/software/BeautifulSoup/bs4/doc/)和 pandas (pandas.pydata.org/)中的解析引擎。

标记语言的元素,如 XML 和 HTML,具有开始和结束标记;标记也可以具有属性并包含其他元素。ElementTree 是一个加载 XML 文件为元素树的包装器。Python 内置库 ElementTree (etree) 用于搜索、解析元素和构建文档树。元素对象还具有与 Python 列表和字典相关的各种可访问属性。

XSLT 是一种将 XML 文档转换为 HTML、XHML、文本等的语言。XSLT 使用 XPath 在 XML 文档中导航。XSLT 是一种模板类型的结构,用于将 XML 文档转换为新文档。

lxml 库包含以下重要模块:

通过示例学习 lxml

lxml 具有大量的模块集,在本节中,我们将学习使用大部分功能的示例来探索 lxml,然后再进行抓取任务。这些示例旨在进行提取活动,而不是开发。

示例 1 - 从文件中读取 XML 并遍历其元素

在这个例子中,我们将读取food.xml文件中可用的 XML 内容。我们将使用 XML 内容:

from lxml import etree
xml = open("food.xml","rb").read() #open and read XML file

从前面的代码中获得的 XML 响应需要使用lxml.etree.XML()进行解析和遍历。XML()函数解析 XML 文档并返回menus根节点,在这种情况下。有关lxml.etree的更详细信息,请参阅lxml.de/api/lxml.etree-module.html

tree = etree.XML(xml) 
#tree = etree.fromstring(xml) #tree = etree.parse(xml) 

在前面的代码中找到的fromstring()parse()函数也提供了内容给lxml.etree使用的默认或选择的解析器。

lxml 提供了多个解析器(XMLParser 和 HTMLParser),可以使用>>> etree.get_default_parser()来查找代码中使用的默认解析器。在前面的情况下,结果是<lxml.etree.XMLParser>

让我们验证解析后得到的tree

print(tree)  
print(type(tree))   

<Element menus at 0x3aa1548>
<class 'lxml.etree._Element'>

前两个语句证实了treelxml.etree._Element类型的 XML 根元素。要遍历树中的所有元素,可以使用树迭代,这会按照它们被找到的顺序返回元素。

使用iter()函数执行树迭代。可以通过元素属性tag访问元素的标签名称;类似地,可以通过text属性访问元素的文本,如下所示:

for element in tree.iter():
    print("%s - %s" % (element.tag, element.text))

前述树迭代将产生以下输出:

menus - 
food - 

name - Butter Milk with Vanilla
price - $3.99
description - Rich tangy buttermilk with vanilla essence
rating - 5.0
feedback - 6
.............
food - 

name - Orange Juice
price - $2.99
description - Fresh Orange juice served
rating - 4.9
feedback - 10

我们也可以将子元素作为参数传递给树迭代器(pricename),以获取基于选定元素的响应。在通过tree.iter()传递子元素后,可以使用element.tagelement.text分别获取TagTextContent子元素,如下所示:

#iter through selected elements found in Tree
for element in tree.iter('price','name'):
 print("%s - %s" % (element.tag, element.text))

name - Butter Milk with Vanilla
price - $3.99
name - Fish and Chips
price - $4.99
...........
name - Eggs and Bacon
price - $5.50
name - Orange Juice
price - $2.99

还要注意的是,food.xml文件是以rb模式而不是r模式打开的。处理本地基于文件的内容和带有编码声明的文件时,比如<?xml version="1.0" encoding="UTF-8"?>,有可能会遇到错误,如ValueError: Unicode strings with encoding declaration are not supported. Please use bytes input or XML fragments without declaration。对内容进行编码/解码可能会解决这个问题,这也取决于文件模式。

要处理前述条件或从文件、HTTP URL 或 FTP 中读取内容,parse()是一个非常有效的方法。它使用默认解析器,除非指定了一个额外的参数。以下代码演示了parse()函数的使用,它被迭代以获取元素名称以获取其文本:

from lxml import etree

#read and parse the file
tree = etree.parse("food.xml")

#iterate through 'name' and print text content
for element in tree.iter('name'):
    print(element.text)

前面的代码会产生以下输出:Butter Milk with VanillaFish and Chips等,这些都是从name元素和food.xml文件中获取的。

Butter Milk with Vanilla
Fish and Chips
Egg Roll
Pineapple Cake
Eggs and Bacon
Orange Juice

多个树元素也可以被迭代,如下所示:

for element in tree.iter('name','rating','feedback'):
 print("{} - {}".format(element.tag, element.text))

name - Butter Milk with Vanilla
rating - 5.0
feedback - 6
name - Fish and Chips
rating - 5.0
...........
feedback - 4
name - Orange Juice
rating - 4.9
feedback - 10

示例 2 - 使用 lxml.html 读取 HTML 文档

在这个例子中,我们将使用lxml.html模块来遍历来自httpbin.org/forms/post的元素:

from lxml import html
from urllib.request import urlopen

root = html.parse(urlopen('http://httpbin.org/forms/post')).getroot()
tree = html.parse(urlopen('http://httpbin.org/forms/post')) print(type(root)) #<class 'lxml.html.HtmlElement'> print(type(tree)) #<class 'lxml.etree._ElementTree'>

我们正在使用lxml.html中的parse()来加载给定 URL 的内容。parse()的作用类似于lxml.etree,但在这种情况下,得到的root是 HTML 类型。getroot()方法返回文档根。可以比较roottree的对象类型,如前面的代码所示。在这个例子中,我们对root或 HTMLElement 感兴趣。解析为root的内容如下截图所示:

页面源代码:http://httpbin.org/forms/post

HTMLElement root具有各种属性,如下所示:

print(dir(root)) 

[...'addnext', 'addprevious', 'append', 'attrib', 'base', 'base_url', 'body', 'clear', 'cssselect', 'drop_tag', 'drop_tree', 'extend', 'find', 'find_class', 'find_rel_links', 'findall', 'findtext', 'forms', 'get', 'get_element_by_id', 'getchildren', 'getiterator', 'getnext', 'getparent', 'getprevious', 'getroottree', 'head', 'index', 'insert', 'items', 'iter', 'iterancestors', 'iterchildren', 'iterdescendants', 'iterfind', 'iterlinks', 'itersiblings', 'itertext', 'keys', 'label', 'make_links_absolute', 'makeelement', 'nsmap', 'prefix', 'remove', 'replace', 'resolve_base_href', 'rewrite_links', 'set', 'sourceline', 'tag', 'tail', 'text', 'text_content', 'values', 'xpath']

让我们从root中找到<p>;可以使用find()来定位路径中的第一个元素。可以使用text_content()函数检索文本。findtext()函数也可以用于类似情况,如下所示:

p = root.find('.//p') #find first <p> from root

print(p.text_content())  *# Customer name:*
print(root.findtext('.//p/label')) *#Customer name:* 

如下代码所示,findall()用于查找和遍历root中的所有元素:

elemP = root.findall('.//p') #find all <p> element from root
for p in elemP  :
    print(p.text_content())

前面的代码列出了查找所有p标签的文本,如下所示:

Customer name: 
Telephone: 
E-mail address: 
 Small 
 Medium 
 Large 
 Bacon 
 Extra Cheese 
 Onion 
 Mushroom 
Preferred delivery time: 
Delivery instructions: 
Submit order

HTMLElement root也支持 XPath 和 CSSSelect:

print(root.xpath('//p/label/input/@value'))
print(root.xpath('//legend/text()')) 

这将产生以下输出:

['small','medium','large','bacon','cheese','onion','mushroom']
['Pizza Size', 'Pizza Toppings'] 

CSSSelect 将 CSS 选择器转换为 XPath 表达式,并与相关对象一起使用:

#print text_content() for label inside <p>
for e in root.cssselect('p label'):
    print(e.text_content())

Customer name: 
Telephone: 
E-mail address: 
 Small 
 ......
 Mushroom 
Preferred delivery time: 
Delivery instructions:

#print text_content for element <p> inside <form>
for e in root.cssselect('form > p'):
    print(e.text_content())

Customer name: 
Telephone: 
E-mail address: 
Preferred delivery time: 
Delivery instructions: 
Submit order

以下代码演示了 HTML <form>元素被探索其属性和属性。我们首先针对root中的<form>元素进行操作,即<form method="post" action="/post">

print(root.forms[0].action)  #http://httpbin.org/post
print(root.forms[0].keys())  #['method', 'action']
print(root.forms[0].items()) #[('method', 'post'), ('action', '/post')]
print(root.forms[0].method) # POST

从前面的代码中可以看到,输出显示为内联注释:

  • action返回key属性action的 URL 值。获得的 URL 实际上是一个将处理提交的信息或选择的选项的链接。

  • items()返回包含元素键和值的元组列表。

  • keys()返回元素键的列表。

  • method 返回属性method的值,即 HTTP 请求或 HTTP 方法。有关 HTTP 方法的更多信息,请参阅第一章,Web Scraping Fundamentals了解 Web 开发和技术部分。

示例 3 - 读取和解析 HTML 以检索 HTML 表单类型元素属性

在这个例子中,我们将从httpbin.org/forms/post的 URL 中读取 HTML,其中包含基于 HTML 的表单元素。表单元素具有各种预定义属性,例如类型,值和名称,并且可以存在手动属性。在前面的示例中,我们尝试实现各种函数 - XPath 和 CSSSelect - 以从所需元素中检索内容。

在这里,我们将尝试收集在 HTML 表单元素中找到的属性及其值:

from lxml import html
import requests
response = requests.get('http://httpbin.org/forms/post')

# build the DOM Tree
tree = html.fromstring(response.text)

for element in tree.iter('input'):
     print("Element: %s \n\tvalues(): %s \n\tattrib: %s \n\titems(): %s \n\tkeys(): %s"%
     (element.tag, element.values(),element.attrib,element.items(),element.keys()))
     print("\n")

在前面的代码中,对于给定的 URL 获得了response.text和一个str类型对象。fromstring()函数解析提供的字符串对象并返回根节点或 HTMLElement tree类型。

在这个例子中,我们正在迭代input元素或<input...>,并试图识别每个输入所拥有的属性。

前面的代码导致了以下输出:

Element: input
     values(): ['custname']
     attrib: {'name': 'custname'}
     items(): [('name', 'custname')]
     keys(): ['name']
Element: input
     values(): ['tel', 'custtel']
     attrib: {'name': 'custtel', 'type': 'tel'}
     items(): [('type', 'tel'), ('name', 'custtel')]
     keys(): ['type', 'name']
.......
.......
Element: input
     values(): ['checkbox', 'topping', 'mushroom']
     attrib: {'name': 'topping', 'type': 'checkbox', 'value': 'mushroom'}
     items(): [('type', 'checkbox'), ('name', 'topping'), ('value', 'mushroom')]
     keys(): ['type', 'name', 'value']
Element: input
     values(): ['time', '11:00', '21:00', '900', 'delivery']
     attrib: {'max': '21:00', 'type': 'time', 'step': '900', 'min': '11:00', 'name': 'delivery'}
     items(): [('type', 'time'), ('min', '11:00'), ('max', '21:00'), ('step', '900'), ('name',     'delivery')]
     keys(): ['type', 'min', 'max', 'step', 'name']

在代码输出中,使用了一些与<input>元素一起使用的函数和属性。以下是示例中使用的一些代码及其解释:

  • element.tag:这 r

  • 返回元素tag名称(例如,input)。

  • element.values():HTML 表单元素的属性存在为key:value对。value属性包含特定元素的确切数据。values()返回List对象中所选元素的value属性。

  • element.attribattrib返回一个Dict类型对象(字典),其中包含key:value对。

  • element.items()items()返回一个包含键和值的元组的List对象。

  • element.keys():类似于

  • items()keys() 返回List对象中的属性key

通过前面的示例对 lxml 及其特性进行了概述,现在我们将执行一些网络抓取任务。

使用 lxml 进行网页抓取

在本节中,我们将利用迄今为止学到的大部分技术和概念,并实施一些抓取任务。

对于即将进行的任务,我们将首先选择所需的 URL。在这种情况下,它将是books.toscrape.com/,但是通过定位音乐类别,即books.toscrape.com/catalogue/category/books/music_14/index.html。有了选择的目标 URL,现在是时候探索网页并识别我们愿意提取的内容了。

我们希望收集每个页面中列出的每个个体项目(即Article元素)的标题,价格,可用性,imageUrl和评级等特定信息。我们将尝试使用 lxml 和 XPath 从单个和多个页面中抓取数据,以及使用 CSS 选择器。

关于元素识别,XPath,CSS 选择器和使用 DevTools,请参阅使用 Web 浏览器开发人员工具访问 Web 内容部分。

示例 1 - 使用 lxml.html.xpath 从单个页面提取选定的数据

在这个例子中,我们将使用 XPath 从提供的 URL 中收集信息并使用 lxml 特性。

在下面的代码中,musicUrl字符串对象包含一个指向页面的链接。musicUrl使用parse()函数进行解析,结果是doclxml.etree.ElementTree对象:

import lxml.html
musicUrl= "http://books.toscrape.com/catalogue/category/books/music_14/index.html"
doc = lxml.html.parse(musicUrl)

现在我们有了一个可用的 ElementTree doc;我们将收集musicUrl页面上找到的标题和价格等字段的 XPath 表达式。有关生成 XPath 表达式,请参考使用 DevTools 的 XPath 和 CSS 选择器部分。

#base element
articles = doc.xpath("//*[@id='default']/div/div/div/div/section/div[2]/ol/li[1]/article")[0]

#individual element inside base
title = articles.xpath("//h3/a/text()")
price = articles.xpath("//div[2]/p[contains(@class,'price_color')]/text()")
availability = articles.xpath("//div[2]/p[2][contains(@class,'availability')]/text()[normalize-space()]")
imageUrl = articles.xpath("//div[1][contains(@class,'image_container')]/a/img/@src")
starRating = articles.xpath("//p[contains(@class,'star-rating')]/@class")

上述articles的 XPath 包含了<article>内所有可用字段,例如titlepriceavailabilityimageUrlstarRatingarticles字段是一种具有子元素的父元素的表达式类型。此外,还声明了子元素的单独 XPath 表达式,例如title字段,即title = articles.xpath("//h3/a/text()")。我们可以注意到表达式中使用了articles

还要注意,在子表达式中,元素属性或键名,如classsrc也可以分别使用@class@src

现在,一旦设置了单独的表达式,我们就可以打印收集到的所有表达式的信息,并将其返回到 Python 列表中。收到的数据也已经使用map()replace()strip() Python 函数以及 Lambda 运算符进行了清理和格式化,如下面的代码所示:

#cleaning and formatting 
stock = list(map(lambda stock:stock.strip(),availability))
images = list(map(lambda img:img.replace('../../../..','http://books.toscrape.com'),imageUrl))
rating = list(map(lambda rating:rating.replace('star-rating ',''),starRating))

print(title)
print(price)
print(stock)
print(images)
print(rating)

收集或提取的数据可能需要额外的清理任务,即删除不需要的字符、空格等。它可能还需要格式化或将数据转换为所需的格式,例如将字符串日期和时间转换为数值,等等。这两个操作有助于保持一些预定义或相同结构的数据。

上述代码的最终输出如下截图所示:

从所选页面获取各种数据的 Python 列表

从上述截图中可以看出,有一个针对目标数据的单独收集。以这种方式收集的数据可以合并到单个 Python 对象中,如下面的代码所示,也可以写入外部文件,例如 CSV 或 JSON,以进行进一步处理:

#Merging all 
dataSet = zip(title,price,stock,images,rating)
print(list(dataSet))

[('Rip it Up and ...', '£35.02', 'In stock', 'http://books.toscrape.com/media/cache/81/c4/81c4a973364e17d01f217e1188253d5e.jpg', 'Five'), 
('Our Band Could Be ...', '£57.25', 'In stock', 'http://books.toscrape.com/media/cache/54/60/54607fe8945897cdcced0044103b10b6.jpg', 'Three'),
.........
......... 
('Old Records Never Die: ...', '£55.66', 'In stock', 'http://books.toscrape.com/media/cache/7e/94/7e947f3dd04f178175b85123829467a9.jpg', 'Two'), 
('Forever Rockers (The Rocker ...', '£28.80', 'In stock', 'http://books.toscrape.com/media/cache/7f/b0/7fb03a053c270000667a50dd8d594843.jpg', 'Three')]

上述代码中的dataSet是使用zip() Python 函数生成的。zip()收集所有提供的列表对象的单个索引,并将它们附加为元组。dataSet的最终输出对于每个<article>都有特定的值,就像前面的代码中所示的那样。

示例 2 - 使用 XPath 循环并从多个页面抓取数据

在示例 1 中,我们尝试了基于简单 XPath 的技术,用于单个页面上有限数量的结果的 URL。在这种情况下,我们将针对食品和饮料类别进行操作,即books.toscrape.com/catalogue/category/books/food-and-drink_33/index.html,该类别的内容跨页面存在。在本例中将使用基于 XPath 的循环操作,这支持更有效地收集数据。

由于我们将处理多个页面,因此最好的做法是在浏览器中查找一些单独页面的 URL,以便在浏览列出的页面时找到这些 URL。大多数情况下,它可能包含一些模式,可以轻松解决难题,就像以下代码中使用的那样:

import lxml.html
from lxml.etree import XPath

baseUrl = "http://books.toscrape.com/"

#Main URL
bookUrl = "http://books.toscrape.com/catalogue/category/books/food-and-drink_33/index.html"

#Page URL Pattern obtained (eg: page-1.html, page-2.html...)
pageUrl = "http://books.toscrape.com/catalogue/category/books/food-and-drink_33/page-"

bookUrl是我们感兴趣的主要 URL;它还包含下一页的页面链接,其中包含一个模式,如pageUrl中所找到的那样,例如page-2.html

dataSet = []
page=1
totalPages=1
while(page<=totalPages):
    print("Rows in Dataset: "+str(len(dataSet)))
    if(page==1):
        doc = lxml.html.parse(pageUrl+str(page)+".html").getroot()
        perPageArticles = doc.xpath("//*[@id=\"default\"]//form/strong[3]/text()")
        totalArticles = doc.xpath("//*[@id=\"default\"]//form/strong[1]/text()")
        totalPages = round(int(totalArticles[0])/int(perPageArticles[0]))
        print(str(totalArticles[0])+" Results, showing "+str(perPageArticles[0])+" Articles per page")
    else:
        doc = lxml.html.parse(pageUrl+str(page)+".html").getroot()

    #used to find page URL pattern
    nextPage = doc.xpath("//*[@id=\"default\"]//ul[contains(@class,'pager')]/li[2]/a/@href")
    if len(nextPage)>0: 
        print("Scraping Page "+str(page)+" of "+str(totalPages)+". NextPage > "+str(nextPage[0]))
    else:
        print("Scraping Page "+str(page)+" of "+str(totalPages))

定义了一个空的dataSet列表,用于保存跨页面找到的每篇文章的数据。

个人页面 URL 是通过将pageUrl与页面编号和.html连接而获得的。在从页面本身跟踪到的totalArticlesperPageArticles计算后找到totalPages。获得的totalPages将给出一个确切的循环计数,并且更容易应用于循环(while循环在代码中找到):

articles = XPath("//*[@id='default']//ol/li[position()>0]")

titlePath = XPath(".//article[contains(@class,'product_pod')]/h3/a/text()")
pricePath = XPath(".//article/div[2]/p[contains(@class,'price_color')]/text()")
stockPath = XPath(".//article/div[2]/p[2][contains(@class,'availability')]/text()[normalize-space()]")
imagePath = XPath(".//article/div[1][contains(@class,'image_container')]/a/img/@src")
starRating = XPath(".//article/p[contains(@class,'star-rating')]/@class")

正如我们在前面的代码中所看到的,articles是用于循环查找<article>字段内的各个元素的主要 XPath 表达式。该表达式应包含一个特定条件,可以满足以执行循环;在这种情况下,我们确定<article>字段存在于<ol><li>元素内部。

因此,我们可以使用li[position()>0]执行循环,该循环标识每个在<ol>中存在的<li>内找到的<article>字段,即articles = XPath("//*[@id='default']//ol/li[position()>0]")

#looping through 'articles' found in 'doc' i.e each <li><article> found in Page Source
for row in articles(doc): 
     title = titlePath(row)[0]
     price = pricePath(row)[0]
     availability = stockPath(row)[0].strip()
     image = imagePath(row)[0]
     rating = starRating(row)[0]

     #cleaning and formatting applied to image and rating
     dataSet.append([title,price,availability,image.replace('../../../..',baseUrl),rating.replace('star-rating','')])

page+=1 #updating Page Count for While loop

#Final Dataset with data from all pages. 
print(dataSet)

XPath 表达式的各个元素被定义为titlePath元素,imagePath元素等,以定位要获取的特定元素。最后,为文章设置的表达式被循环到每个页面获得的 HTMLElement 中,即doc元素,并收集每个titleimage元素的第一次出现以及找到的其他元素。这些收集的数据被附加到dataSet字段中,作为经过清理和格式化的列表,其结果显示在以下截图中:

带有分页信息和 dataSet 内容的输出

示例 3 - 使用 lxml.cssselect 从页面中抓取内容

CSS 选择器具有广泛的查询选项,如XPath 和 CSS 选择器简介部分所述,并且通常用作 XPath 的简单替代方法。在前面的两个示例中,我们探索了 XPath 以收集所需的信息。在这个例子中,我们将使用 lxml 中的cssselectdeveloper.ibm.com/announcements/category/data-science/?fa=date%3ADESC&fb=上的单个页面收集相关数据。

要识别 CSS 查询,可以浏览页面源代码或使用 DevTools。有关使用 DevTools 的更多详细信息,请参阅使用 DevTools 进行 XPath 和 CSS 选择器部分。在这种情况下,我们正在使用 DevTools 识别和收集 CSS 查询,如下截图所示:

使用 DevTools 并从 https://developer.ibm.com/announcements 选择选择器

从上述截图中,我们可以看到,个别公告是由a.ibm--card__block_linkdiv.ibm--card内找到的块标识的,该块具有具有类的 HTML 元素,例如ibm--card__bodyibm--card__type。使用所描述的过程复制 CSS 选择器将分别为a.ibm--card__block_linkdiv.ibm--card__body生成以下列表:

  • #content > div > div.code_main > div > div.cpt-content > div > div.bx--grid.no-pad.cpt--item__row > div:nth-child(1) > div:nth-child(1) > div > a

  • #content > div > div.code_main > div > div.cpt-content > div > div.bx--grid.no-pad.cpt--item__row > div:nth-child(1) > div:nth-child(1) > div > a > div.ibm--card__body

让我们使用 Python 代码部署前面的概念,如下片段所示:

from lxml import html
import requests
from lxml.cssselect import CSSSelector
url = 'https://developer.ibm.com/announcements/category/data-science/?fa=date%3ADESC&fb='
url_get = requests.get(url)
tree = html.document_fromstring(url_get.content)

所需的 Python 库和 URL 已声明,并且页面内容url_get已使用lxml.html进行解析。通过获得的lxml.html.HTMLElement,我们现在可以使用 XPath 或 CSS 选择器选择和导航到树中的所需元素:

announcements=[]
articles = tree.cssselect('.ibm--card > a.ibm--card__block_link')

for article in articles:
    link = article.get('href')
    atype = article.cssselect('div.ibm--card__body > h5')[0].text.strip()
    adate = article.cssselect('div.ibm--card__body > h5 > .ibm--card__date')[0].text
    title = article.cssselect('div.ibm--card__body > h3.ibm--card__title')[0].text_content()
    excerpt= article.cssselect(' div.ibm--card__body > p.ibm--card__excerpt')[0].text
    category= article.cssselect('div.ibm--card__bottom > p.cpt-byline__categories span')

    #only two available on block: except '+'
    #announcements.append([link,atype,adate,title,excerpt,[category[0].text,category[1].text]])

    announcements.append([link,atype,adate,title,excerpt,[span.text for span in category if     span.text!='+']])

print(announcements)

articles是一个定义好的主要 CSS 查询,并且对在页面中找到的所有可用articles进行循环,作为article。每篇文章都有不同的元素,如类型、日期、标题、类别等。使用texttext_content()get()来收集元素数据或属性。cssselect返回 Python 列表对象,因此使用索引,如[0],来收集特定元素内容。

前面的代码中的category没有任何索引,因为它包含多个<span>元素,其值是使用列表推导技术提取的,同时附加或使用索引如注释中所示。代码获得的输出如下截图所示。尝试对数据进行了轻微的清理,但最终列表仍然包含获得的原始数据:

使用 lxml.cssselect 获取的列表公告的输出还要注意的是,使用 DevTools 复制或获取的 CSS 选择器查询在表达和长度上似乎与示例代码中的不同。DevTools 提供的查询包含从找到的所有选择的元素的父元素的详细信息和链接表达式。在代码中,我们只使用了特定元素的 CSS 查询。

总结

元素识别、基于 DOM 的导航、使用基于浏览器的开发者工具、部署数据提取技术以及对 XPath 和 CSS 选择器的概述,以及在 Python 库中使用 lxml,这些都是本章探讨的主要主题。

我们还通过使用 lxml 探索了各种示例,实现了不同的技术和库特性来处理元素和 ElementTree。最后,通过示例探讨了网页抓取技术,重点关注了在实际情况中可能出现的不同情况。

在下一章中,我们将学习更多关于网页抓取技术以及一些使用这些技术的新 Python 库。

进一步阅读

第四章:使用 pyquery 进行抓取-一个 Python 库

从本章开始,我们将探索与抓取相关的工具和技术,同时还将部署一些抓取代码。与 Web 探索、Python 库、元素识别和遍历相关的功能是我们迄今为止学到的主要概念。

Web 抓取通常是一个具有挑战性和漫长过程,需要了解网站的运行方式。基本的理解和识别用于构建网站的后端或工具将有助于任何抓取任务。这也与一种称为逆向工程的过程有关。有关此类工具的更多信息,请参阅第三章,使用 LXML、XPath 和 CSS 选择器,以及使用 Web 浏览器开发工具访问 Web 内容部分。除此之外,还需要识别用于遍历和操作 HTML 标记等元素的工具,pyquery就是其中之一。

在之前的章节中,我们探索了 XPath、CSS 选择器和 LXML。在本章中,我们将研究使用pyquery,它具有类似 jQuery 的能力,似乎更高效,因此在进行 Web 抓取过程时更容易处理。

在本章中,您将学习以下主题:

  • pyquery 简介

  • 探索pyquery(主要方法和属性)

  • 使用pyquery进行 Web 抓取

技术要求

本章需要一个 Web 浏览器(Google Chrome 或 Mozilla Firefox)。我们将使用以下 Python 库:

  • pyquery

  • urllib

  • 请求

如果您当前的 Python 设置中不存在这些库,请参阅第二章,Python 和 Web-使用 urllib 和 Requests,以及设置部分,获取安装和设置帮助。

本章的代码文件可在本书的 GitHub 存储库中找到:github.com/PacktPublishing/Hands-On-Web-Scraping-with-Python/tree/master/Chapter04

pyquery 简介

pyquery是 Python 的类似 jQuery 的库,使用lxml库。这为处理标记元素提供了一个简单和交互式的环境,用于操作和遍历目的。

pyquery表达式也类似于jquery,具有jquery知识的用户将发现在 Python 中更方便使用。

pyquery Python 库,正如其名称所示,增强了与在 XML 和 HTML 中找到的元素相关的query编写过程。pyquery缩短了元素处理,并提供了更具洞察力的脚本编写方法,适用于抓取和基于 DOM 的遍历和操作任务。

pyquery表达式使用 CSS 选择器执行查询,以及它实现的其他功能。例如,pyquery使用以下表达式:

page.find('a').attr('href')    -- (pyquery expression) 

cssselect 使用以下表达式:

cssselect('a').get('href')      -- (cssselect expression)

jQuery(写得更少,做得更多)是最受欢迎的 JavaScript 库之一,体积小,速度快,具有许多支持 DOM/HTML/CSS 等功能。网页文档遍历、操作、事件处理、动画、AJAX 等是其主要特点。请访问jquery.com/获取更多信息。有关pyquery及其文档的更多信息,请访问pythonhosted.org/pyquery/github.com/gawel/pyquery/

探索 pyquery

在继续探索pyquery及其特性之前,让我们先通过使用pip来安装它:

C:\> pip install pyquery

有关使用pip和库安装的更多信息,请参阅第二章中的设置部分,Python 和 Web-使用 urllib 和 Requests

成功安装pyquery后,使用pip安装了以下库:

  • cssselect-1.0.3

  • lxml-4.3.1

  • pyquery-1.4.0

>>>在代码中表示使用 Python IDE;它接受代码或指令,并在下一行显示输出。

安装完成并成功后,我们可以使用pyquery,如下面的代码所示,来确认设置。我们可以使用dir()函数来探索它包含的属性:

>>> from pyquery import PyQuery as pq

>>> print(dir(pq))
['Fn', '__add__', '__call__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__',  '_filter_only', '_get_root', '_next_all', '_prev_all', '_translator_class', '_traverse','addClass', 'add_class', 'after', 'append', 'appendTo', 'append_to','attr','base_url','before','children', 'clear', 'clone', 'closest', 'contents', 'copy', 'count', 'css','each','empty', 'encoding','end','eq', 'extend', 'filter', 'find','fn','hasClass','has_class','height','hide', 'html', 'index','insert','insertAfter', 'insertBefore', 'insert_after','insert_before', 'is_', 'items', 'length','make_links_absolute',
'map','next','nextAll','next_all','not_','outerHtml','outer_html','parent','parents', 'pop', 'prepend', 'prependTo', 'prepend_to','prev', 'prevAll', 'prev_all', 'remove', 'removeAttr', 'removeClass', 'remove_attr', 'remove_class','remove_namespaces', 'replaceAll', 'replaceWith', 'replace_all', 'replace_with', 'reverse', 'root','show', siblings','size','sort','text', 'toggleClass', 'toggle_class', 'val', 'width', 'wrap', 'wrapAll','wrap_all','xhtml_to_html']

现在,我们将探索与抓取概念相关的pyquery的某些功能。为此,我们将使用从www.python.org获取的页面源代码,已将其保存为test.html以提供真实世界的可用性:

https://www.python.org 获取的页面源代码在 Google Chrome 中,您可以右键单击网页,选择“查看页面源代码”菜单选项,或按Ctrl + U获取页面源代码。

但仅仅获取页面源代码或 HTML 代码是不够的,因为我们需要将这些内容加载到库中,以获得更多的探索工具。我们将在接下来的部分中进行这样的操作。

在测试或跟踪代码时,您可能会发现或需要对pyquery代码表达式进行更改,以获得真实的输出。现在获取的页面源代码可能已更新或更改。建议您从源 URL(www.python.org)获取最新的页面源代码。

加载文档

在大多数情况下,通过使用requestsurllib获取文档的内容,并将其提供给pyquery如下:

>>> from pyquery import PyQuery as pq
>>> import requests
>>> response = requests.get('http://www.example.com').text #content

>>> from urllib.request import urlopen
>>> response = urlopen('http://www.example.com').read()
>>> docTree = pq(response)

pyquery还可以使用 Python 库urllib(默认)或 requests 加载 URL。它还支持基于 requests 的参数:

>>> pq("https://www.python.org")
[<html.no-js>] 

>>> site=pq("https://www.python.org")
>>> print(type(site))
<class 'pyquery.pyquery.PyQuery'> 

>>> pq("https://www.samsclub.com")
[<html>]

我们从前面的代码中获得的pq对象正在使用 XML 解析器(默认)进行解析,该解析器可通过传递给它的额外parser参数进行更新:

>>> doc = pq('http://www.exaple.com', parser = 'xml')  #using parser xml

>>> doc = pq('http://www.exaple.com', parser = 'html') #using parser html

通常,HTML 代码来自页面源代码或其他来源,比如文件,作为字符串提供给pyquery进行进一步处理,如下面的代码所示:

>>> doc = pq('<div><p>Testing block</p><p>Second block</p></div>')
>>> print(type(doc))
<class 'pyquery.pyquery.PyQuery'>

>>> pagesource = open('test.html','r').read() #reading locally saved HTML
>>> print(type(pagesource))
<class 'str'>

>>> page = pq(pagesource)
>>> print(type(page))
<class 'pyquery.pyquery.PyQuery'>

使用从已加载的文档或 URL 接收到的PyQuery对象或pq,我们可以继续并探索pyquery提供的功能。

元素遍历、属性和伪类

pyquery具有大量的属性和方法,可用于获取所需的内容。在以下示例中,我们将识别在本节中找到的代码的实现:

>>> page('title') #find element <title>
[<title>]

>>> page.find('title').text() #find element <title> and return text content
'Welcome to Python.org'

>>> page.find('meta[name="description"]').attr('content')
'The official home of the Python Programming Language'

>>> page.find('meta[name="keywords"]').attr('content')
'Python programming language object oriented web free open source software license documentation download community'

>>> buttons = page('a.button').html() #return HTML content for element <a> with class='button'
>>> buttons
'>_\n <span class="message">Launch Interactive Shell</span>\n ' 

以下是它们的一些功能及其描述,可以在前面的代码中看到:

  • find(): 搜索提供的元素或评估使用 CSS 选择器构建的查询表达式

  • text(): 返回元素内容作为字符串

  • attr(): 识别属性并返回其内容

  • html(): 返回评估表达式的 HTML 内容

classid CSS 属性分别用.#表示,并前缀于属性的值。例如,<a class="main" id="mainLink">将被识别为a.maina#mainLink

在下面的代码中,我们列出了所有已识别的具有class属性和menu值的<ul>元素:

>>> page('ul.menu') #<ul> element with attribute class='menu'
[<ul.menu>, <ul.navigation.menu>, <ul.subnav.menu>, <ul.navigation.menu>, <ul.subnav.menu>, <ul.navigation.menu>,..............,<ul.subnav.menu>, <ul.footer-links.navigation.menu.do-not-print>]

表达式传递给 PyQuery 对象,生成了一个评估元素的列表。这些元素被迭代以获取其确切值或内容。

PyQuery 还包含伪类或:pseudo element,用于索引和获取预定义表达式的结果。:pseudo element也可以附加到现有的选择器查询中。以下代码实现了一些常见的伪元素遍历:

>>> page('nav:first') #first <nav> element
[<nav.meta-navigation.container>]

>>> page('a:first') #first <a> element
[<a>]

>>> page('ul:first') #first <ul> element
[<ul.menu>]

>>> page('ul:last') #last <ul> element
[<ul.footer-links.navigation.menu.do-not-print>]

让我们回顾一下前面代码中使用的伪元素:

  • :first:返回提供的内容中元素的第一个出现

  • :last: 返回提供的内容中元素的最后一次出现

让我们看一下几个更多的:伪元素的一般实现,以列出 HTML 元素:

>>> page(':header') #header elements found 
[<h1.site-headline>, <h1>, <h1>, <h1>, <h1>, <h1>, <h2.widget-title>, <h2.widget-title>..........,<h2.widget-title>, <h2.widget-title>, <h2.widget-title>]

>>> page(':input') #input elements found
[<input#id-search-field.search-field>, <button#submit.search-button>]

>>> page(':empty') #empty elements found
[<meta>, <meta>, <link>, <meta>, <meta>, <meta>, <meta>,<script>, <link>, <link>,........,<img.python-logo>, <span.icon-search>,<span.icon-facebook>, <span.icon-twitter>, <span.icon-freenode>, ...........,<span.icon-feed>, <div.python-logo>, <span#python-status-indicator.python
-status-indicator-default>, <script>, <script>, <script>]

>>> page(':empty:odd') #empty elements, only Odd ones are listed
[<meta>, <meta>, <meta>, <meta>, <meta>, <meta>, <script>, <link>, <link>, <link>, <link>, <meta>, .......,<img.python-logo>, <span.icon-google-plus>, <span.icon-twitter>, <span.breaker>, <span.icon-download>, <span.icon-jobs>, <span.icon-calendar>, <span.icon-python>, <div.python-logo>, <script>,<script>]

以下是我们在前面的代码中使用的:伪元素

  • :header: 返回页面中找到的标题元素(h1, h2,..., h5, h6)。

  • :input: 返回所有输入元素。存在大量基于 HTML <form>的伪元素。请参考pythonhosted.org/pyquery/获取更多信息。

  • :empty: 返回所有没有任何子元素的元素。

  • :odd: 返回索引为奇数的元素。它们可以与其他:伪元素一起使用,如:empty:odd

  • :even: 类似于:odd,但返回偶数索引的元素。

下面的代码演示了遍历、:伪元素和元素属性的表达式:

>>> page.find('ul:first').attr('class') #class name of first <ul> element
'menu'

>>> page.find('a:first').attr('href') #href value of first <a> element
'#content'

>>> page.find('a:last').attr('href') #href value of last <a> element
'/psf/sponsorship/sponsors/'

>>> page.find('a:eq(0)').attr('href') #href value of first <a> element using Index!
'#content'

>>> page.find('a:eq(0)').text() #text from first <a> element
'Skip to content' 

以下是一些更多的:伪元素。我们可以使用这些来处理元素的index

  • :eq: 选择特定的索引号;评估为等于

  • :lt: 对于提供的索引号,评估为小于。例如,page('a:lt(2)')

  • :gt: 对于提供的索引号,评估为大于。例如,page('a:gt(0)')

除了用于识别索引和查找元素的一般特性之外,:伪元素也可以用于搜索包含提供的文本的元素,如下面的代码所示:

>>> page('p:contains("Python")') #return elements <p> with text 'Python"
[<p>, <p>, <p>, <p>, <p>, <p>, <p>, <p>, <p>, <p>, <p>, <p>, <p>, <p>]

>>> page('p:contains("python.org")') #return elements <p> with text "python.org"
[<p>, <p>]

#return text from second <p> element containing text "python.org"
>>> page('p:contains("python.org")').eq(1).text() 
'jobs.python.org'

以下列表描述了在前面的代码中使用的:containseq()的简单定义:

  • :contains: 匹配包含提供的文本的所有元素。

  • eq(): 返回找到的特定索引号的元素。评估为等于,类似于:eq

pyquery有一些返回布尔答案的函数,在需要搜索具有属性并确认属性值的元素的情况下非常有效:

#check if class is 'python-logo' >>> page('h1.site-headline:first a img').is_('.python-logo') 
*True*

#check if <img> has class 'python-logo' >>> page('h1.site-headline:first a img').has_class('python-logo') 
*True*

以下是在前面的代码中使用的函数,以及它们的定义:

  • is_(): 接受选择器作为参数,如果选择器匹配元素则返回True,否则返回False

  • has_class(): 如果选择器匹配提供的类,则返回True。它对于识别具有class属性的元素非常有用。

我们已经使用了一些重要的函数和工具与pyquery一起,以增强元素识别和遍历相关属性。在下一节中,我们将学习和演示迭代。

迭代

在本节中,我们将演示pyquery中可用的迭代(重复执行)功能。在许多情况下,这是一种有效且易于处理的方法。

在下面的代码中,我们正在搜索包含单词Python.org<meta>标签中找到的nameproperty属性。我们还使用 Python 的List Comprehension技术来演示一行代码的特性:

#Find <meta> with attribute 'content' containing '..Python.org..' 
#and list the attribute 'name' that satisfies the find()

>>> meta=page.find('meta[content*="Python.org"]')
>>> [item.attr('name') for item in meta.items() if item.attr('name') is not None]
['application-name', 'apple-mobile-web-app-title']

#Continuing from code above list value for attribute 'property'

>>> [item.attr('property') for item in meta.items() if item.attr('property') is not None]
['og:site_name', 'og:title']

正如我们在前面的代码中所看到的,我们正在使用items()函数在循环中与元素 meta 一起迭代提供的选项。可以使用items()来探索产生可迭代对象的表达式。返回None的结果将从列表中排除:

>>> social = page.find('a:contains("Socialize") + ul.subnav li a') 
>>> [item.text() for item in social.items() if item.text() is not None]
['Google+', 'Facebook', 'Twitter', 'Chat on IRC']

>>> [item.attr('href') for item in social.items() if item.attr('href') is not None]
['https://plus.google.com/+Python', 'https://www.facebook.com/pythonlang?fref=ts', 'https://twitter.com/ThePSF', '/community/irc/']

>>> webdevs = page.find('div.applications-widget:first ul.menu li:contains("Web Development") a')
>>> [item.text() for item in webdevs.items() if item.text() is not None]
['Django', 'Pyramid', 'Bottle', 'Tornado', 'Flask', 'web2py']

在前面的代码中,pyquery对象收集了社交和网页开发部分提供的名称和链接。这些可以在下面的屏幕截图中的“Use Python for...”下找到。使用 Python 的列表推导技术对对象进行迭代:

使用 pyquery 提取即将到来的活动

在下面的代码中,我们将探索从upcomingevents迭代中检索到的一些更多细节:

>>> eventsList = []
>>> upcomingevents = page.find('div.event-widget ul.menu li')
>>> for event in upcomingevents.items():
 ...     time = event.find('time').text()
 ...     url = event.find('a[href*="events/python"]').attr('href')
 ...     title = event.find('a[href*="events/python"]').text()
 ...     eventsList.append([time,title,url])
 ...
>>> eventsList

eventsList包含了从即将到来的活动中提取的详细信息,如前面的屏幕截图所示。eventsList的输出如下:

[['2019-02-19', 'PyCon Namibia 2019', '/events/python-events/790/'], ['2019-02-23', 'PyCascades 2019', '/events/python-events/757/'],
['2019-02-23', 'PyCon APAC 2019', '/events/python-events/807/'], ['2019-02-23', 'Berlin Python Pizza', '/events/python-events/798/'],
['2019-03-15', 'Django Girls Rivers 2019 Workshop', '/events/python-user-group/816/']]

DevTools 可以用于识别特定部分的 CSS 选择器,并可以通过循环功能进一步处理。有关 CSS 选择器的更多信息,请参阅第三章使用 LXML、XPath 和 CSS 选择器以及 使用 DevTools 的 XPath 和 CSS 选择器部分。

以下代码举例说明了通过使用find()items()来迭代pyquery的过程:

>>> buttons = page.find('a.button')
>>> for item in buttons.items():
...     print(item.text(),' :: ',item.attr('href'))
...

>_ Launch Interactive Shell  ::  /shell/
Become a Member  ::  /users/membership/
Donate to the PSF  ::  /psf/donations/

>>> buttons = page.find('a.button:odd')
>>> for item in buttons.items():
...     print(item.text(),' :: ',item.attr('href'))
...

Become a Member  ::  /users/membership/

>>> buttons = page.find('a.button:even')
>>> for item in buttons.items():
...     print(item.text(),' :: ',item.attr('href'))
...

>_ Launch Interactive Shell  ::  /shell/
Donate to the PSF  ::  /psf/donations/

有关pyquery的功能、属性和方法的更多信息,请参阅pythonhosted.org/pyquery/index.html

使用 pyquery 进行网页抓取

在前一节中,我们学习了如何使用pyquery提供的一些重要功能,并使用这些功能来遍历或识别元素。在本节中,我们将使用pyquery的大部分功能,并将它们用于通过提供各种用例示例从网络上抓取数据。

示例 1-抓取数据科学公告

在此示例中,我们将从developer.ibm.com/announcements/category/data-science/中的数据科学类别中抓取公告相关的详细信息。

同样的 URLdeveloper.ibm.com/也被用于在第三章使用 LXML、XPath 和 CSS 选择器中的示例 3下使用lxml.cssselect来收集数据。建议您探索这两个示例并比较所使用的功能。

首先,让我们导入pyqueryrequests

from pyquery import PyQuery as pq
import requests
dataSet = list()

创建dataSet,以便您有一个空列表来收集我们将从各个页面找到的数据,以及要使用的库。我们声明了read_url(),它将用于读取提供的 URL 并返回一个PyQuery对象。在这个例子中,我们将使用sourceUrl,即developer.ibm.com/announcements/

sourceUrl='https://developer.ibm.com/announcements/' 
def read_url(url):
 """Read given Url , Returns pyquery object for page content"""
  pageSource = requests.get(url).content
 return pq(pageSource)

要收集的信息可以从developer.ibm.com/announcements/category/data-science/?fa=date:DESC&fb=中检索,也可以使用sourceUrl+"category/data-science/?fa=date:DESC&fb="获取。在这里,我们将循环遍历pageUrls

pageUrls导致以下页面 URL。这些是通过使用列表推导和range()获得的:

如下面的代码所示,pageUrls生成了一个基于页面的 URL 列表,可以通过get_details()函数进一步处理。这用于检索文章:

if __name__ == '__main__':
    mainUrl = sourceUrl+"category/data-science/?fa=date:DESC&fb="
  pageUrls = [sourceUrl+"category/data-science/page/%(page)s?fa=date:DESC&fb=" % {'page': page} for page in range(1, 3)]

    for pages in pageUrls:
        get_details(pages)

    print("\nTotal articles collected: ", len(dataSet))
    print(dataSet)

从上述代码中可以看到,列出了以下 URL:

pageUrls中迭代 URL,并将其传递给get_details()进行进一步处理,如下面的代码所示:

def get_details(page):
    """read 'page' url and append list of queried items to dataSet"""
  response = read_url(page)

    articles = response.find('.ibm--card > a.ibm--card__block_link')
    print("\nTotal articles found :", articles.__len__(), ' in Page: ', page)

    for article in articles.items():
        link = article.attr('href')
        articlebody = article.find('div.ibm--card__body')

        adate = articlebody.find('h5 > .ibm--card__date').text()
        articlebody.find('h5 > .ibm--card__date').remove()
        atype = articlebody.find('h5').text().strip()
        title = articlebody.find('h3.ibm--card__title').text().encode('utf-8')
        excerpt = articlebody.find('p.ibm--card__excerpt').text().encode('utf-8')
        category = article.find('div.ibm--card__bottom > p.cpt-byline__categories span')

        if link:
            link = str(link).replace('/announcements/', mainUrl)
            categories = [span.text for span in category if span.text != '+']
            dataSet.append([link, atype, adate, title, excerpt,",".join(categories)])

传递给get_details()的页面 URL 由read_url()读取,并从PyQuery对象中获得response。包含块的信息被识别为使用 CSS 选择器的文章。由于有多个articles迭代可用,我们使用items()。然后,通过清理、替换和合并活动处理单个数据元素,然后将其附加到主数据集中,本例中为dataSet。PyQuery 表达式也可以通过使用articlebody来缩短。

此外,使用remove() PyQuery(操作)方法来删除<h5>中找到的.ibm--card__date,以获取atype。如果使用以下代码而不进行删除,atype内容还将包含额外的.ibm--card__date详细信息:

articlebody.find('h5 > .ibm--card__date').remove())

从前面的代码中获得的最终输出如下:

Total articles found : 8 in Page: https://developer.ibm.com/announcements/category/data-science/page/1?fa=date:DESC&fb=

Total articles found : 2 in Page: https://developer.ibm.com/announcements/category/data-science/page/2?fa=date:DESC&fb=

Total articles collected: 10

[['https://developer.ibm.com/announcements/model-mgmt-on-watson-studio-local/', 'Announcement', 'Nov 05, 2018', b'Perform feature engineering and model scoring', b'This code pattern demonstrates how data scientists can leverage IBM Watson Studio Local to automate the building and training of\xe2\x80\xa6', 'Analytics,Apache Spark'], ..........................., ['https://developer.ibm.com/announcements/algorithm-that-gives-you-answer-to-any-particular-question-based-on-mining-documents/', 'Announcement', 'Sep 17, 2018', b'Query a knowledge base to get insights about data', b'Learn a strategy to query a knowledge graph with a question and find the right answer.', 'Artificial Intelligence,Data Science'], ['https://developer.ibm.com/announcements/build-a-domain-specific-knowledge-graph-from-given-set-of-documents/', 'Announcement', 'Sep 14, 2018', b'Walk through the process of building a knowledge base by mining information stored in the documents', b'Take a look at all of the aspects of building a domain-specific knowledge graph.', 'Artificial Intelligence,Data Science']]

例 2 - 从嵌套链接中提取信息

在这个例子中,我们将从quotes.toscrape.com/tag/books/中提取书籍中的引用的详细信息。每个单独的引用包含某些信息,以及指向作者详细页面的链接,这也将被处理,以便我们可以获取有关作者的信息:

来自 http://quotes.toscrape.com/tag/books/的主页面

在下面的代码中,keys中的元素将被用作输出的键,并将包含 Python 字典。基本上,我们将收集keys中的元素的数据:

from pyquery import PyQuery as pq
sourceUrl = 'http://quotes.toscrape.com/tag/books/' dataSet = list()
keys = ['quote_tags','author_url','author_name','born_date','born_location','quote_title']

def read_url(url):
    """Read given Url , Returns pyquery object for page content"""
  pageSource = pq(url)
    return pq(pageSource)

read_url()从前面的代码中也得到更新,并且与我们在示例 1 - 爬取数据科学公告部分使用的库不同。在这个例子中,它返回提供的 URL 的 PyQuery 对象:

if __name__ == '__main__':
    get_details(sourceUrl)

    print("\nTotal Quotes collected: ", len(dataSet))
    print(dataSet)

    for info in dataSet:
        print(info['author_name'],' born on ',info['born_date'], ' in ',info['born_location'])

dataSet进行了额外的迭代,以获取dataSet中的info字典的某些值。

如下面的代码所示,get_details()使用while循环进行分页,并由nextPage值控制:

def get_details(page):
    """read 'page' url and append list of queried items to dataSet"""
  nextPage = True
  pageNo = 1
  while (nextPage):
        response = read_url(page + 'page/' + str(pageNo))
        if response.find("ul.pager:has('li.next')"):
            nextPage = True
 else:
            nextPage = False    quotes = response.find('.quote')
        print("\nTotal Quotes found :", quotes.__len__(), ' in Page: ', pageNo)
        for quote in quotes.items():
            title = quote.find('[itemprop="text"]:first').text()
            author = quote.find('[itemprop="author"]:first').text()
            authorLink = quote.find('a[href*="/author/"]:first').attr('href')
            tags = quote.find('.tags [itemprop="keywords"]').attr('content')

            if authorLink:
                authorLink = 'http://quotes.toscrape.com' + authorLink
                linkDetail = read_url(authorLink)
                born_date = linkDetail.find('.author-born-date').text()
                born_location = linkDetail.find('.author-born-location').text()
                if born_location.startswith('in'):
                    born_location = born_location.replace('in ','')

            dataSet.append(dict(zip(keys,[tags,authorLink,author,born_date,born_location,title[0:50]])))

        pageNo += 1

:has()返回与传递给它的选择器匹配的元素。在这个例子中,我们正在确认pager类是否有一个带有next类的<li>元素,即ul.pager:has('li.next')。如果表达式为true,则存在另一页的页面链接,else终止循环。

使用items()迭代获得的quotes以获取titleauthortagsauthorLink。使用read_url()函数进一步处理authorLink URL,以从.author-born-date.author-born-location类中获取born_dateborn_location的作者相关特定信息。

我们在前面的代码中使用的元素类可以在页面源中找到,如下面的屏幕截图所示:

包含作者详细信息的内部页面

zip() Python 函数与keys和引用字段一起使用,将其附加到dataSet作为 Python 字典。

前面代码的输出如下:

Total Quotes found : 10 in Page: 1
Total Quotes found : 1 in Page: 2
Total Quotes collected: 11

[{'author_name': 'Jane Austen', 'born_location': 'Steventon Rectory, Hampshire, The United Kingdom', 'quote_tags': 'aliteracy,books,classic,humor', 'author_url': 'http://quotes.toscrape.com/author/Jane-Austen', 'quote_title': '“............................... ', 'born_date': 'December 16, 1775'}, 
{'author_name': 'Mark Twain', 'born_location': 'Florida, Missouri, The United States', 'quote_tags': 'books,contentment,friends,friendship,life', 'author_url': 'http://quotes.toscrape.com/author/Mark-Twain', 'quote_title': '“.........................................', 'born_date': 'November 30, 1835'}
,..................................................................................................., 
{'author_name': 'George R.R. Martin', 'born_location': 'Bayonne, New Jersey, The United States', 'quote_tags': 'books,mind', 'author_url': 'http://quotes.toscrape.com/author/George-R-R-Martin', 'quote_title': '“... ...................................', 'born_date': 'September 20, 1948'}]

对获得的dataSet进行了额外的循环,结果是一个字符串,如下所示:

Jane Austen born on December 16, 1775 in Steventon Rectory, Hampshire, The United Kingdom
Mark Twain born on November 30, 1835 in Florida, Missouri, The United States
............................
............................
George R.R. Martin born on September 20, 1948 in Bayonne, New Jersey, The United States

例 3 - 提取 AHL 季后赛结果

在这个例子中,我们将从www.flyershistory.com/cgi-bin/ml-poffs.cgi提取美国曲棍球联盟AHL)季后赛结果的数据:

AHL 季后赛结果

前面的 URL 包含 AHL 的季后赛结果。该页面以表格格式呈现有关结果的信息。显示相关信息的页面源的部分如下屏幕截图所示:

来自 http://www.flyershistory.com/cgi-bin/ml-poffs.cgi 的页面源。前面的屏幕截图包含了来自源 URL 的表格信息的顶部和底部部分,并呈现了页面源中可用的两种不同格式的<tr>。在<tr>中可用的<td>数量有不同的额外信息。

分析了源格式后,还需要指出的是包含所需值的<td>没有可用于识别特定表格单元的属性。在这种情况下,可以使用 CSS 选择器,即伪选择器,如td:eq(0)td:eq(1)来定位包含数据的<td>或单元的位置。

有关 CSS 选择器的更多信息,请访问第三章,使用 LXML、XPath 和 CSS 选择器XPath 和 CSS 选择器简介部分,在CSS 选择器伪选择器子部分。

由于我们将在此示例中使用pyquery,因此我们将使用eq()方法,该方法接受索引并返回元素。例如,我们可以使用tr.find('td').eq(1).text()来选择 PyQuery 对象tr,搜索索引为1的元素td,即<td>,并返回元素的文本。

在这里,我们对keys中列出的列的数据感兴趣:

keys = ['year','month','day','game_date','team1', 'team1_score', 'team2', 'team2_score', 'game_status']

现在,让我们导入带有pyqueryre的代码。将使用 Regex 来分隔从页面源获取的日期:

from pyquery import PyQuery as pq
import re

sourceUrl = 'http://www.flyershistory.com/cgi-bin/ml-poffs.cgi' dataSet = list()
keys = ['year','month','day','game_date','team1', 'team1_score', 'team2', 'team2_score', 'game_status']

def read_url(url):
    """Read given Url , Returns pyquery object for page content"""
  pageSource = pq(url)
  return pq(pageSource)

if __name__ == '__main__':
    page = read_url(sourceUrl)  

在这里,read_url()接受一个参数,即页面链接,并返回页面源或pageSource的 PyQuery 对象。PyQuery 会自动返回提供的 URL 的页面源。也可以使用其他库(如urlliburllib3requests和 LXML)获取页面源,并传递给创建 PyQuery 对象:

tableRows = page.find("h1:contains('AHL Playoff Results') + table tr")
print("\nTotal rows found :", tableRows.__len__())

tableRows是一个 PyQuery 对象,将用于遍历位于<h1>之后的<table>内存在的<tr>。它包含使用find()函数获取的AHL Playoff Results文本。如下面的输出所示,存在 463 个<tr>元素,但实际获取的记录数量可能较低,即实际数据的可用<td>数量可能较低:

Total rows found : 463

让我们进行更多处理。每个<tr>tr元素都是tableRows的一个项目,并且可以使用items()方法来通过使用它们的索引来查找确切的<td>td并检索它包含的数据:

for tr in tableRows.items():
    #few <tr> contains single <td> and is omitted using the condition
    team1 = tr.find('td').eq(1).text() 

    if team1 != '':
        game_date = tr.find('td').eq(0).text()
        dates = re.search(r'(.*)-(.*)-(.*)',game_date)
        team1_score = tr.find('td').eq(2).text()
        team2 = tr.find('td').eq(4).text()
        team2_score = tr.find('td').eq(5).text()

        #check Game Status should be either 'W' or 'L'
  game_status = tr.find('td').eq(6).text()
        if not re.match(r'[WL]',game_status):
            game_status = tr.find('td').eq(7).text()

        #breaking down date in year,month and day
  year = dates.group(3)
        month = dates.group(2)
        day = dates.group(1)

        #preparing exact year value
        if len(year)==2 and int(year)>=68:
            year = '19'+year
        elif len(year)==2 and int(year) <68:
            year = '20'+year
        else:
            pass  

到目前为止,已经收集了目标<td>中的所需数据,并且在year的情况下也进行了格式化。在代码中还应用了 Regex,并与datesgame_status一起使用。最后,收集的对象被附加为列表到dataSet

#appending individual data list to the dataSet dataSet.append([year,month,day,game_date,team1,team1_score,team2,team2_score,game_status])

print("\nTotal Game Status, found :", len(dataSet))
print(dataSet)

有关总记录数和dataSet的输出如下:

Total Game Status, found : 341 
[['1968', 'Apr', '3', '3-Apr-68', 'Buff', '2', 'Que', '4', 'W'],
['1968', 'Apr', '5', '5-Apr-68', 'Buff', '1', 'Que', '3', 'W'], 
['1968', 'Apr', '9', '9-Apr-68', 'Que', '7', 'Buff', '10', 'L'], 
['1968', 'Apr', '10', '10-Apr-68', 'Que', '4', 'Buff', '7', 'L'], 
['1968', 'Apr', '12', '12-Apr-68', 'Buff', '1', 'Que', '3', 'W'],
.................
['2008', 'May', '9', '9-May-2008', 'Phantoms', '3', 'Wilkes-Barre', '1', 'L'], 
['2009', 'Apr', '16', '16-Apr-09', 'Phantoms', '2', 'Hershey', '4', 'L'], 
['2009', 'Apr', '18', '18-Apr-09', 'Phantoms', '2', 'Hershey', '6', 'L'], 
['2009', 'Apr', '22', '22-Apr-09', 'Hershey', '2', 'Phantoms', '3', 'L'], 
['2009', 'Apr', '24', '24-Apr-09', 'Hershey', '0', 'Phantoms', '1', 'L']]

示例 4-从 sitemap.xml 收集 URL

在此示例中,我们将提取在webscraping.com/sitemap.xml中找到的博客的 URL。

在前面的示例中,我们使用了 HTML 内容,但 PyQuery 也可以用于遍历 XML 文件内容。默认情况下,pyquery使用基于 LXML 的xml解析器,可以在创建 PyQuery 对象时提供。我们将在文件内容中同时使用lxml.htmlxml

有关pyqueryparser的更多信息,请访问本章的探索 pyquery部分。有关站点地图的信息,请访问第一章,网络抓取基础知识数据查找技术(从网络中获取数据)部分,在Sitemaps子部分。

以下屏幕截图显示了sitemap.xml文件中的内容:

https://webscraping.com 获取 sitemap.xml 文件

首先,让我们导入pyquery并将文件内容读取为xmlFile

from pyquery import PyQuery as pq

if __name__ == '__main__':
    # reading file
  xmlFile = open('sitemap.xml', 'r').read()   

Case 1 – using the HTML parser

在这里,我们将使用lxml.html解析器通过向 PyQuery 传递解析器参数parser='html'来解析xmlFile

# creating PyQuery object using parser 'html'
  urlHTML = pq(xmlFile, parser='html')

print("Children Length: ",urlHTML.children().__len__())
print("First Children: ",urlHTML.children().eq(0))
print("Inner Child/First Children: ",urlHTML.children().children().eq(0))

使用 PyQuery 的urlHTML对象允许我们检查从数据中获取的计数和子元素,如下所示的输出:

Children Length: 137

First Children: 
<url>
<loc>https://webscraping.com</loc>
</url>

Inner Child/First Children: <loc>https://webscraping.com</loc>

正如我们所看到的,urlHTML.children()包含了查找 URL 所需的元素。我们可以使用items()方法处理这些数据,该方法遍历获取的每个元素。让我们创建dataSet(Python list()),并将提取的 URL 附加到其中。

基于元素的迭代可以使用urlHTML.children().find('loc:contains("blog")').items()来执行,通过使用包含blog字符串的选择器:

dataSet=list()
for url in urlHTML.children().find('loc:contains("blog")').items():
    dataSet.append(url.text())

print("Length of dataSet: ", len(dataSet))
print(dataSet)

最后,我们将收到以下输出:

Length of dataSet: 131

['https://webscraping.com/blog', 'https://webscraping.com/blog/10/', 'https://webscraping.com/blog/11/', 'https://webscraping.com/blog/12/', 'https://webscraping.com/blog/13/', 'https://webscraping.com/blog/2/'
,.................................................................................,
'https://webscraping.com/blog/Reverse-Geocode/', 'https://webscraping.com/blog/Scraping-Flash-based-websites/', 'https://webscraping.com/blog/Scraping-JavaScript-based-web-pages-with-Chickenfoot/', 'https://webscraping.com/blog/category/web2py', 'https://webscraping.com/blog/category/webkit', 'https://webscraping.com/blog/category/website/', 'https://webscraping.com/blog/category/xpath']

Case 2 – using the XML parser

在这种情况下,我们将使用 PyQuery urlXML对象处理 XML 内容,该对象使用parser='xml'

#creating PyQuery object using parser 'xml'
urlXML = pq(xmlFile, parser='xml')

print("Children Length: ",urlXML.children().__len__())

上述代码返回了子节点计数的长度,即137个总 URL:

Children Length: 137 

如下所示的代码,第一个和内部子元素返回了我们希望提取的所需 URL 内容:

print("First Children: ", urlXML.children().eq(0))
print("Inner Child/First Children: ", urlXML.children().children().eq(0))

First Children: 
<url >
<loc>https://webscraping.com</loc>
</url>

Inner Child/First Children: 
<loc >https://webscraping.com</loc>

让我们继续使用类似于Case 1 – using the HTML parser部分中使用的选择器来处理子元素:

dataSet=list()
for url in urlXML.children().find('loc:contains("blog")').items():
    dataSet.append(url.text())

print("Length of dataSet: ", len(dataSet))
print(dataSet)

在这里,我们在dataSet中没有收到任何输出,看起来选择器的工作方式不像在Case 1 – using the HTML parser中那样:

Length of dataSet: 0
[]

让我们使用以下代码验证这种情况:

for url in urlXML.children().children().items():
    print(url)
    break

<loc >https://webscraping.com</loc>

我们收到的节点属于www.sitemaps.org/schemas/sitemap/0.9。如果不删除命名空间选择器,它将无法工作。

remove_namespace()函数可以用于 PyQuery 对象,并且可以处理其最终输出,如下所示的代码:

for url in urlXML.remove_namespaces().children().find('loc:contains("blog")').items():
    dataSet.append(url.text())

print("Length of dataSet: ", len(dataSet))
print(dataSet)

我们收到以下输出:

Length of dataSet: 131

['https://webscraping.com/blog', 'https://webscraping.com/blog/10/', 'https://webscraping.com/blog/11/', 'https://webscraping.com/blog/12/', 'https://webscraping.com/blog/13/', 'https://webscraping.com/blog/2/', 'https://webscraping.com/blog/3/', 'https://webscraping.com/blog/4/', 'https://webscraping.com/blog/5/', 'https://webscraping.com/blog/6/', 'https://webscraping.com/blog/7/', 'https://webscraping.com/blog/8/', 
.................................................................
'https://webscraping.com/blog/category/screenshot', 'https://webscraping.com/blog/category/sitescraper', 'https://webscraping.com/blog/category/sqlite', 'https://webscraping.com/blog/category/user-agent', 'https://webscraping.com/blog/category/web2py', 'https://webscraping.com/blog/category/webkit', 'https://webscraping.com/blog/category/website/', 'https://webscraping.com/blog/category/xpath']

PyQuery remove_namespace()xhtml_to_html()方法分别从 XML 和 XHTML 中删除命名空间。使用这两种方法允许我们处理使用 HTML 相关属性的元素。

我们还可以使用不同的方法处理相同的内容;也就是说,通过使用正则表达式并获取所需的输出。让我们继续使用以下代码:

print("URLs using Children: ",urlXML.children().text()) 
#print("URLs using Children: ",urlXML.children().children().text()) 
#print("URLs using Children: ",urlXML.text())

PyQuery children()对象方法返回所有子节点,text()将提取文本内容,如下所示:

URLs using Children: https://webscraping.com https://webscraping.com/about 
https://webscraping.com/blog .............https://webscraping.com/blog/Converting-UK-Easting-Northing-coordinates/ https://webscraping.com/blog/Crawling-with-threads/ https://webscraping.com/blog/Discount-coupons-for-data-store/ https://webscraping.com/blog/Extracting-article-summaries/ https://webscraping.com/blog/10/ https://webscraping.com/feedback..........

如前面的输出所示,所有子节点的链接都作为单个字符串返回:

blogXML = re.split(r'\s',urlXML .children().text())
print("Length of blogXML: ",len(blogXML))

#filter(), filters URLs from blogXML that matches string 'blog'
dataSet= list(filter(lambda blogXML:re.findall(r'blog',blogXML),blogXML))
print("Length of dataSet: ",len(dataSet))
print("Blog Urls: ",dataSet)

在这里,re.split()用于使用空格字符\s拆分收到的 URL 字符串。这返回了总共139个元素。最后,使用re.findall()过滤blogXML,该方法在blogXML元素中查找blog字符串,并得到以下结果:

Length of blogXML: 139
Length of dataSet: 131

Blog Urls: ['https://webscraping.com/blog', 'https://webscraping.com/blog/10/', 'https://webscraping.com/blog/11/', 'https://webscraping.com/blog/12/', 'https://webscraping.com/blog/13/', 'https://webscraping.com/blog/2/', 'https://webscraping.com/blog/3/', 'https://webscraping.com/blog/4/', 'https://webscraping.com/blog/5/', 'https://webscraping.com/blog/6/', 'https://webscraping.com/blog/7/', 'https://webscraping.com/blog/8/',...............................................
'https://webscraping.com/blog/category/web2py', 'https://webscraping.com/blog/category/webkit', 'https://webscraping.com/blog/category/website/', 'https://webscraping.com/blog/category/xpath']

在本节中,我们使用了一些抓取技术来从文件和网站中提取所需的内容。内容识别和抓取需求非常动态,也取决于网站的结构。使用pyquery等库,我们可以以有效和高效的方式获取和部署抓取所需的工具和技术。

总结

pyquery似乎更有效地处理 CSS 选择器,并提供了许多与 LXML 相关的功能。简单易读的代码总是受欢迎的,pyquery为抓取提供了这些功能。在本章中,我们探讨了在执行抓取任务时可能遇到的各种情况,并成功地实现了期望的结果。

在下一章中,我们将探索与网络抓取相关的几个其他库。

进一步阅读

第五章:使用 Scrapy 和 Beautiful Soup 进行网络抓取

到目前为止,我们已经了解了 Web 开发技术、数据查找技术,并访问了各种 Python 库,以从 Web 上抓取数据。

在本章中,我们将学习和探索两个用于文档解析和抓取活动的流行 Python 库:Scrapy 和 Beautiful Soup。

Beautiful Soup 处理文档解析。解析文档是为了遍历元素并提取其内容。Scrapy 是用 Python 编写的网络爬虫框架。它为网络抓取提供了面向项目的范围。Scrapy 提供了大量内置资源,用于电子邮件、选择器、项目等,并可用于从简单到基于 API 的内容提取。

在本章中,我们将学习以下内容:

  • 使用 Beautiful Soup 进行网络抓取

  • 使用 Scrapy 进行网络抓取

  • 部署网络爬虫(学习如何使用www.scrapinghub.com部署抓取代码)

技术要求

需要一个网络浏览器(Google Chrome 或 Mozilla Firefox),我们将使用此应用程序和列出的 Python 库:

  • 最新的 Python 3.7或 Python 3.0(已安装)

  • 所需的 Python 库如下:

  • lxml

  • requestsurllib

  • bs4beautifulsoup4

  • scrapy

有关设置或安装,请参阅第二章,Python 和 Web - 使用 urllib 和 Requests设置事项部分。

代码文件可在 GitHub 上找到:github.com/PacktPublishing/Hands-On-Web-Scraping-with-Python/tree/master/Chapter05

使用 Beautiful Soup 进行网络抓取

Web scraping 是从 Web 文档中提取数据的过程。对于数据收集或从 Web 文档中提取数据,识别和遍历元素(HTML、XML)是基本要求。Web 文档由各种类型的元素构建,可以单独存在或嵌套在一起。

解析是从任何给定的 Web 内容中分解、暴露或识别具有内容的组件的活动。这种活动增强了搜索和收集所需元素的内容的功能。获取、解析和遍历 Web 文档,以查找所需的数据或内容,是基本的抓取任务。

在第三章中,使用 LXML、XPath 和 CSS 选择器,我们探索了 lxml 进行类似的任务,并使用 XPath 和 CSS 选择器进行数据提取。lxml 也用于抓取和解析,因为它具有内存高效的特性和可扩展的库。

在下一小节中,我们将学习和探索 Python bs4库(用于 Beautiful Soup)的特性。

Beautiful Soup 简介

Beautiful Soup 通常被识别为解析库,也被称为用于解析 Web 文档的 HTML 解析器或 XML。它生成类似于 lxml(ElementTree)的解析树,用于识别和遍历元素以提取数据和进行网络抓取。

Beautiful Soup 提供了完整的解析相关功能,可以使用lxmlhtmllib。一系列简单易用的方法,以及用于导航、搜索和解析相关活动的属性,使 Beautiful Soup 成为其他 Python 库中的首选。

可以使用 Beautiful Soup 构造函数手动处理文档编码,但除非构造函数指定,否则 Beautiful Soup 会自动处理与编码相关的任务。

Beautiful Soup 的一个显著特点是,它可以用来解析损坏的 HTML 或具有不完整或缺失标签的文件。有关 Beautiful Soup 的更多信息,请访问www.crummy.com/software/BeautifulSoup

现在让我们探索并学习使用 Beautiful Soup 进行数据提取过程的一些主要工具和方法。

探索美丽汤

Python 的bs4库包含一个用于解析的BeautifulSoup类。有关 Beautiful Soup 和安装该库的更多详细信息,请参阅www.crummy.com/software/BeautifulSoup/上的官方文档。在成功安装库后,我们可以使用 Python IDE 获取如下屏幕截图中显示的详细信息:

成功安装带有详细信息的 bs4

此外,简单(命名)和可解释的方法集合以及编码支持使其在开发人员中更受欢迎。

让我们从bs4中导入BeautifulSoupSoupStrainer,如下所示:

from bs4 import BeautifulSoup
from bs4 import SoupStrainer #,BeautifulSoup

我们将使用以下片段或html_doc中显示的 HTML 作为示例,来探索 Beautiful Soup 的一些基本特性。还可以使用requestsurllib获取任何选择的 URL 的响应,以在真实的抓取案例中用于内容:

html_doc="""<html><head><title>The Dormouse's story</title></head> <body> <p class="title"><b>The Dormouse's story</b></p> <p class="story">Once upon a time there were three little sisters; and their names were <a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>, <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>; and they lived at the bottom of a well.</p> <p class="story">...</p> <h1>Secret agents</h1> <ul>
 <li data-id="10784">Jason Walters, 003: Found dead in "A View to a Kill".</li> <li data-id="97865">Alex Trevelyan, 006: Agent turned terrorist leader; James' nemesis in "Goldeneye".</li> <li data-id="45732">James Bond, 007: The main man; shaken but not stirred.</li> </ul> </body> </html>"""

要继续解析和访问 Beautiful Soup 的方法和属性,通常需要创建一个 Beautiful Soup 对象,通常称为 soup 对象。关于构造函数中提供的字符串或标记内容的类型,下面列出了创建 Beautiful Soup 对象的一些示例,以及前面提到的参数:

  • soup = Beautifulsoup(html_markup)

  • soup = Beautifulsoup(html_markup, 'lxml')

  • soup = Beautifulsoup(html_markup, 'lxml', parse_from=SoupStrainer("a"))

  • soup = Beautifulsoup(html_markup, 'html.parser')

  • soup = Beautifulsoup(html_markup, 'html5lib')

  • soup = Beautifulsoup(xml_markup, 'xml')

  • soup = Beautifulsoup(some_markup, from_encoding='ISO-8859-8')

  • soup = Beautifulsoup(some_markup, exclude_encodings=['ISO-8859-7'])

Beautiful Soup 构造函数起着重要作用,我们将在这里探索一些重要的参数:

  • markup:传递给构造函数的第一个参数接受要解析的字符串或对象。

  • features:解析器的名称或要用于markup的标记类型。解析器可以是lxmllxml-xmlhtml.parserhtml5lib。同样,可以使用的标记类型包括htmlhtml5xml。可以使用不同类型的支持解析器与 Beautiful Soup。如果我们只想解析一些 HTML,我们可以简单地将标记传递给 Beautiful Soup,它将相应地使用安装的适当解析器。有关解析器及其安装的更多信息,请访问www.crummy.com/software/BeautifulSoup/bs4/doc/#installing-a-parser

  • parse_only:接受一个bs4.SoupStrainer对象,即只有与SoupStrainer对象匹配的文档部分将用于解析。在只有部分文档需要解析时,这非常有用,考虑到代码的有效性和与内存相关的问题。有关SoupStrainer的更多信息,请访问www.crummy.com/software/BeautifulSoup/bs4/doc/#parsing-only-part-of-a-document

  • from_encoding:用于解析标记的字符串指示正确编码。如果 Beautiful Soup 使用错误的编码,通常会提供这个。

  • exclude_encodings:指示 Beautiful Soup 使用的错误编码的字符串列表。

在使用 Beautiful Soup 时,响应时间是一个重要因素。由于 Beautiful Soup 使用解析器(lxmlhtml.parserhtml5lib),因此总是存在额外的时间消耗的问题。建议始终使用解析器以在各个平台和系统上获得类似的结果。此外,为了加快速度,建议使用lxml作为 Beautiful Soup 的解析器。

对于这种特殊情况,我们将使用lxml作为解析器创建soupA对象,以及SoupStrainer对象tagsA(仅解析<a>,即 HTML 的元素或锚标签)。我们可以使用SoupStrainer获取要解析的部分内容,这在处理大量内容时非常有用。

soupA,Beautiful Soup 的一个对象,呈现了SoupStrainer对象tagsA中找到的所有<a>元素,如下面的代码中所使用的;如输出所示,只收集了<a>标签,或者解析的文档是使用lxml解析的SoupStrainer对象parsed

tagsA = SoupStrainer("a")
soupA = BeautifulSoup(html_doc,'lxml',parse_only=tagsA)
 print(type(soupA))
<class 'bs4.BeautifulSoup'>

print(soupA)
<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a><a class="sister" href="http://example.com/lacie" id="link2">Lacie</a><a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>

来自网站的 HTML 内容可能并不总是以干净的字符串格式呈现。阅读以段落而不是逐行代码呈现的页面内容将是困难且耗时的。

Beautiful Soup 的prettify()函数返回一个 Unicode 字符串,呈现为干净、格式化的结构,易于阅读,并且以树结构标识元素,如下面的代码所示;prettify()函数还接受编码参数:

print(soupA.prettify())

<a class="sister" href="http://example.com/elsie" id="link1">
 Elsie
</a>
<a class="sister" href="http://example.com/lacie" id="link2">
 Lacie
</a>
<a class="sister" href="http://example.com/tillie" id="link3">
 Tillie
</a>

解析树中的基于文档的元素(如 HTML 标签)可以具有具有预定义值的各种属性。元素属性是重要的资源,因为它们在元素内提供了标识和内容。在遍历树时,验证元素是否包含某些属性可能很方便。

例如,如下面的代码所示,HTML<a>元素包含classhrefid属性,每个属性都带有预定义的值,如下面的片段所示:

<a class="sister" href="http://example.com/lacie" id="link2">

Beautiful Soup 的has_attr()函数返回所选元素的搜索属性名称的布尔响应,如下所示:

  • 对于name属性返回False

  • 对于class属性返回True

我们可以使用has_attr()函数来确认文档中是否存在指定名称的属性键,如下所示:

print(soupA.a.has_attr('class'))
True

print(soupA.a.has_attr('name'))
False

通过对 Beautiful Soup 进行基本介绍并在本节中探讨了一些方法,我们现在将继续搜索、遍历和迭代解析树,寻找即将到来的部分中的元素和它们的内容。

搜索、遍历和迭代

Beautiful Soup 提供了许多方法和属性来遍历和搜索解析树中的元素。这些方法通常以与它们执行的任务描述相似的方式命名。还有许多属性和方法可以链接在一起,用于获得类似的结果。

find()函数返回与搜索条件或解析元素匹配的第一个子元素。在爬取上下文中查找元素和提取细节非常有用,但仅适用于单个结果。还可以传递其他参数给find()函数,以识别确切的元素,如下所示:

  • attrs:一个带有键值对的字典

  • text:带有元素文本

  • name:HTML 标签名称

让我们在代码中使用不同的允许参数来实现find()函数:

print(soupA.find("a")) #print(soupA.find(name="a"))
<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a> print(soupA.find("a",attrs={'class':'sister'}))
<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

print(soupA.find("a",attrs={'class':'sister'},text="Lacie"))
<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>

print(soupA.find("a",attrs={'id':'link3'}))
<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>

print(soupA.find('a',id="link2"))
<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>

以下是在前面的示例中实现的代码的简短描述列表:

  • find("a")或 find(name="a"):搜索 HTML<a>元素或提供的标签名称,a返回soupA中找到的第一个<a>的存在

  • find("a",attrs={'class':'sister'}):搜索带有属性键为class和值为sister的元素<a>

  • 使用find("a",attrs={'class':'sister'}, text="Lacie"):搜索具有class属性键和sister值以及文本为Lacie值的<a>元素

  • find("a",attrs={'id':'link3'}):搜索具有id属性键和link3值的<a>元素

  • find("a",id="link2"):搜索具有id属性和link2值的<a>元素

find_all()函数的工作方式类似于find()函数,还有额外的attrstext作为参数,并返回满足条件或name属性的多个匹配元素的列表,如下所示:

#find all <a> can also be written as #print(soupA.find_all(name="a")) print(soupA.find_all("a"))  [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

#find all <a>, but return only 2 of them
print(soupA.find_all("a",limit=2)) #attrs, text

[<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

limit参数接受数字值,控制使用find_all()函数返回的元素的总数。

可以将字符串、字符串列表、正则表达式对象或这些内容之一提供给nametext属性作为attrs参数的值,如下面代码中所示:

print(soupA.find("a",text=re.compile(r'cie'))) #import re
<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
 print(soupA.find_all("a",attrs={'id':re.compile(r'3')}))
[<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

print(soupA.find_all(re.compile(r'a'))) [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>] 

find_all()函数内置支持全局属性,例如类名,以及名称,如下所示:

soup = BeautifulSoup(html_doc,'lxml')
 print(soup.find_all("p","story")) #class=story
[<p class="story">Once upon a time there were three little sisters; and their names were
<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> and
<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>, <p class="story">...</p>]

print(soup.find_all("p","title")) #soup.find_all("p",attrs={'class':"title"})
[<p class="title"><b>The Dormouse's story</b></p>]

多个nameattrs值也可以通过列表传递,如下面的语法所示:

  • soup.find_all("p",attrs={'class':["title","story"]}):查找所有具有titlestory值的类属性的<p>元素

  • soup.find_all(["p","li"]):从 soup 对象中查找所有<p><li>元素

可以在下面的代码中观察到前面的语法:

print(soup.find_all("p",attrs={'class':["title","story"]}))
[<p class="title"><b>The Dormouse's story</b></p>,
<p class="story">Once upon a...
<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,....
<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>, <p class="story">...</p>]

print(soup.find_all(["p","li"]))
[<p class="title"><b>The Dormouse's story</b></p>,
<p class="story">Once...<a class="sister" href="http://example.com/elsie"...., 
<p class="story">...</p>, 
<li data-id="10784">Jason Walters, 003:....</li>,<li....., 
<li data-id="45732">James Bond, 007: The main man; shaken but not stirred.</li>]

我们还可以使用元素文本来搜索和列出内容。类似于text参数的string参数用于这种情况;它也可以与任何标签名称一起使用或不使用,如下面的代码所示:

print(soup.find_all(string="Elsie")) #text="Elsie"
['Elsie']

print(soup.find_all(text=re.compile(r'Elsie'))) #import re
['Elsie']

print(soup.find_all("a",string="Lacie")) #text="Lacie"
[<a class="sister" href="http://example.com/elsie" id="link2">Lacie</a>]

也可以使用find_all()函数进行元素迭代。如下面的代码所示,我们正在检索<ul>元素内找到的所有<li>元素,并打印它们的标签名称、属性数据、ID 和文本:

for li in soup.ul.find_all('li'):
    print(li.name, ' > ',li.get('data-id'),' > ', li.text)

li > 10784 > Jason Walters, 003: Found dead in "A View to a Kill".
li > 97865 > Alex Trevelyan, 006: Agent turned terrorist leader; James' nemesis in "Goldeneye".
li > 45732 > James Bond, 007: The main man; shaken but not stirred.

可以使用get()函数检索元素的value属性。还可以使用has_attr()函数检查属性的存在。

元素遍历也可以只使用标签名称,并且可以使用或不使用find()find_all()函数,如下面的代码所示:

print(soupA.a) #tag a
<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

print(soup.li) #tag li
<li data-id="10784">Jason Walters, 003: Found dead in "A View to a Kill".</li>

print(soup.p)
<p class="title"><b>The Dormouse's story</b></p>

print(soup.p.b) #tag p and b
<b>The Dormouse's story</b>

print(soup.ul.find('li',attrs={'data-id':'45732'}))
<li data-id="45732">James Bond, 007: The main man; shaken but not stirred.</li>

可以使用textstring属性或get_text()方法与元素一起用于提取它们的文本,同时遍历用于搜索内容的元素中也有textstring参数,如下面的代码所示:

print(soup.ul.find('li',attrs={'data-id':'45732'}).text)
James Bond, 007: The main man; shaken but not stirred.

print(soup.p.text) #get_text()
The Dormouse's story

print(soup.li.text)
Jason Walters, 003: Found dead in "A View to a Kill".

print(soup.p.string)
The Dormouse's story

在本节中,我们探索了使用元素进行搜索和遍历,并实现了重要函数,如find()find_all()函数以及它们的适当参数和条件。

在接下来的部分,我们将根据解析树中的位置探索元素。

使用子元素和父元素

对于解析的文档,可以使用contentschildrendescendants元素遍历子元素或子元素:

  • contents在列表中收集满足条件的子元素。

  • children用于具有直接子元素的迭代。

  • descendantscontentschildren元素的工作方式略有不同。它允许迭代所有子元素,而不仅仅是直接子元素,也就是说,元素标签和标签内的内容实际上是两个独立的子元素。

前面的列表显示了也可以用于迭代的特性。以下代码演示了如何使用这些特性并输出:

print(list(soup.find('p','story').children))
['Once upon a time there were three little sisters; and their names were\n', <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>, ',\n', <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, ' and\n', <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>, ';\nand they lived at the bottom of a well.']

print(list(soup.find('p','story').contents))
['Once upon a time there were three little sisters; and their names were\n', <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>, ',\n', <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, ' and\n', <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>, ';\nand they lived at the bottom of a well.']

print(list(soup.find('p','story').descendants))
['Once upon a time there were three little sisters; and their names were\n', <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>, 'Elsie', ',\n', <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, 'Lacie', ' and\n', <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>, 'Tillie', ';\nand they lived at the bottom of a well.']

可以使用name属性获取所选的childrendescendants标签名称。解析的字符串和\n函数(换行符)返回为None,可以在下面的代码中进行过滤:

#using List Comprehension Technique
print([a.name for a in soup.find('p','story').children])
[None, 'a', None, 'a', None, 'a', None]

print([{'tag':a.name,'text':a.text,'class':a.get('class')} for a in soup.find('p','story').children if a.name!=None])
[{'tag': 'a', 'text': 'Elsie', 'class': ['sister']}, {'tag': 'a', 'text': 'Lacie', 'class': ['sister']}, {'tag': 'a', 'text': 'Tillie', 'class': ['sister']}]

print([a.name for a in soup.find('p','story').descendants])
[None, 'a', None, None, 'a', None, None, 'a', None, None]

print(list(filter(None,[a.name for a in soup.find('p','story').descendants])))
['a', 'a', 'a']

find()find_all()函数类似,我们还可以使用findChild()findChildren()函数来遍历子元素。findChild()函数用于检索单个子元素,而findChildren()函数检索子元素的列表,如下面的代码所示:

print(soup.find('p','story').findChildren())
[<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

print(soup.find('p','story').findChild()) #soup.find('p','story').find()
<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

children元素类似,parent元素返回了搜索条件找到的父对象。这里的主要区别是parent元素返回树中的单个父对象,如下面的代码所示:

#print parent element of <a> with class=sister
print(soup.find('a','sister').parent)
<p class="story">Once upon a time there were three little sisters; and their names were
<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> and
<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

#print parent element name of <a> with class=sister
print(soup.find('a','sister').parent.name)
p

#print text from parent element of <a> with class=sister
print(soup.find('a','sister').parent.text)
Once upon a time there were three little sisters; and their names were
Elsie,
Lacie and
Tillie;
and they lived at the bottom of a well.

使用parents元素可以克服返回单个父元素的限制;这将返回多个现有的父元素,并匹配在find()函数中提供的搜索条件,如下面的代码中所示,通常用于迭代:

for element in soup.find('a','sister').parents:
    print(element.name)

p
body
html #complete HTML
[document]  #soup object

如前面的输出所示,[document]指的是 soup 对象,html指的是在 soup 中找到的完整 HTML 块。Beautiful Soup 对象本身创建的是一个解析元素。

与用于遍历子元素的函数类似,父元素也可以使用findParent()findParents()搜索函数进行遍历和检索。findParent()函数遍历到直接父元素,而findParents()函数返回为提供的条件找到的所有父元素。

还必须注意,子元素和父元素的遍历函数是与find()函数一起使用的,其中提供了必要的参数和条件,如下面的代码所示:

#find single Parent for selected <a> with class=sister 
print(soup.find('a','sister').findParent())

<p class="story">Once upon a time there were three little sisters; and their names were
<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> and
<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

#find Parents for selected <a> with class=sister 
print(soup.find('a','sister').findParents())

[<p class="story">Once upon a time there were three little sisters; and their names were
<a class="sister".........Tillie</a>;and they lived at the bottom of a well.</p>,
<body><p class="title"><b>The Dormouse's story</b></p>
<p class="story">Once upon........... <li data-id="45732">James Bond, 007: The main man; shaken but not stirred.</li> </ul> </body>, 
<html><head><title>The Dormouse's story</title></head><body><p class="title"><b>The Dormouse's story</b></p> ........... </ul> </body></html>,
<html><head><title>The Dormouse's story</title></head><body><p class="title"><b>The Dormouse's story</b></p>...........</body></html>]

我们使用了各种函数来探索遍历和搜索子元素和父元素。在下一节中,我们将探索并使用解析树中的位置元素。

使用 next 和 previous

与在树中遍历解析的子元素和父元素类似,Beautiful Soup 还支持遍历和迭代位于提供的条件之前和之后的元素。

属性nextnext_element返回所选条件的立即解析内容。我们还可以将nextnext_element函数附加到一起创建遍历的代码链,如下面的代码所示:

print(soup.find('p','story').next)
Once upon a time there were three little sisters; and their names were

print(soup.find('p','story').next.next)
<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

print(soup.find('p','story').next_element)
Once upon a time there were three little sisters; and their names were

print(soup.find('p','story').next_element.next_element)
<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

print(soup.find('p','story').next_element.next_element.next_element)
Elsie

nextnext_elements函数类似,还存在着返回先前或之前解析元素结果的遍历结果的属性,例如previousprevious_element,与nextnext_element函数相比,它们在工作时是相反的。

如下面的代码所示,previousprevious_element也可以附加到自身以创建一个遍历系列:

print(soup.find('p','story').previous) #returns empty or new-line. print(soup.find('p','title').next.next.next) #returns empty or newline similar to code above

print(soup.find('p','story').previous.previous)
The Dormouse's story

print(soup.find('p','story').previous_element) #returns empty or new-line. 
print(soup.find('p','story').previous_element.previous_element)
The Dormouse's story

print(soup.find('p','story').previous_element.previous_element.previous_element)
<b>The Dormouse's story</b>

现在我们将nextnext_elementpreviousprevious_element元素组合在一起进行遍历,如下所示:

print(soup.find('p','title').next.next.previous.previous)

<p class="title"><b>The Dormouse's story</b></p>

使用next_elementprevious_element的迭代特性是通过next_elementsprevious_elements获得的。这些迭代器用于移动到下一个或上一个解析内容,如下所示:

for element in soup.find('ul').next_elements:
    print(element)

<li data-id="10784">Jason Walters, 003: Found dead in "A View to a Kill".</li>
Jason Walters, 003: Found dead in "A View to a Kill".

<li data-id="97865">Alex Trevelyan, 006: Agent ............. "Goldeneye".</li>
Alex Trevelyan, 006: Agent turned terrorist leader; James' nemesis in "Goldeneye".

<li data-id="45732">James Bond, 007: The main man; shaken but not stirred.</li>
James Bond, 007: The main man; shaken but not stirred.

find_next()函数实现了next_elements,但只返回在nextnext_element元素之后找到的单个元素。使用find_next()函数的优势在于我们可以为元素实现额外的搜索逻辑。

下面的代码演示了find_next()函数的使用,带有和不带有搜索条件;它还显示了next元素和next_elements的输出,以便比较实际的用法,如下所示:

print(soup.find('p','story').next)
Once upon a time there were three little sisters; and their names were

print(soup.find('p','story').next_element)
Once upon a time there were three little sisters; and their names were

print(soup.find('p','story').find_next()) #element after next_element
<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

print(soup.find('p','story').find_next('h1'))
<h1>Secret agents</h1>

find_all_next()函数的工作方式与find_next()函数类似,但返回所有下一个元素。它也被用作find_next()函数的迭代版本。可以使用额外的搜索条件和参数,如limit,来搜索和控制返回的结果,如下面的代码所示:

print(soup.find('p','story').find_all_next())
[<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>, <p class="story">...</p>, <h1>Secret agents</h1>, <ul>
<li data-id="10784">Jason Walters, 003: Found dead in "A View to a Kill".</li>
<li data-id="97865">Alex Trevelyan, 006: Agent turned terrorist leader; James' nemesis in "Goldeneye".</li>
<li data-id="45732">James Bond, 007: The main man; shaken but not stirred.</li>
</ul>, <li data-id="10784">Jason Walters, 003: Found dead in "A View to a Kill".</li>, <li data-id="97865">Alex Trevelyan, 006: Agent turned terrorist leader; James' nemesis in "Goldeneye".</li>, <li data-id="45732">James Bond, 007: The main man; shaken but not stirred.</li>]

print(soup.find('p','story').find_all_next('li',limit=2))
[<li data-id="10784">Jason Walters, 003: Found dead in "A View to a Kill".</li>, <li data-id="97865">Alex Trevelyan, 006: Agent turned terrorist leader; James' nemesis in "Goldeneye".</li>]

find_previous()函数实现了previous_elements,但只返回在previousprevious_element之前找到的单个元素。它还比previous_elements具有优势,因为我们可以为元素实现额外的搜索逻辑。下面的代码演示了find_previous()函数和previous函数的用法:

print(soup.find('ul').previous.previous.previous)
<h1>Secret agents</h1>

print(soup.find('ul').find_previous())
<h1>Secret agents</h1>

print(soup.find('ul').find_previous('p','title'))
<p class="title"><b>The Dormouse's story</b></p>

find_all_previous()函数是find_previous()的迭代版本;它返回满足可用条件的所有先前元素,如下面的代码所示:

print(soup.find('ul').find_all_previous('p'))

[<p class="story">...</p>, <p class="story">Once upon a time there were three little sisters; and their names were
<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> and
<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>, <p class="title"><b>The Dormouse's story</b></p>]

next_siblingprevious_sibling是沿着解析树寻找下一个和上一个兄弟姐妹的另一种方式。兄弟姐妹是指出现在相同级别或在解析树中找到的元素,或者共享相同父元素的元素。下面的代码说明了next_siblingprevious_sibling元素的用法:

print(soup.find('p','title').next_sibling) #returns empty or new-line

print(soup.find('p','title').next_sibling.next_sibling) #print(soup.find('p','title').next_sibling.next)
<p class="story">Once upon a time there were three little sisters; and their names were
<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> and
<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

print(soup.find('ul').previous_sibling) #returns empty or new-line

print(soup.find('ul').previous_sibling.previous_sibling)
<h1>Secret agents</h1>

迭代也可以使用兄弟姐妹,使用next_siblingsprevious_siblings元素,如下面的代码所示:

#using List Comprehension 
title = [ele.name for ele in soup.find('p','title').next_siblings]
print(list(filter(None,title)))
['p', 'p', 'h1', 'ul']

ul = [ele.name for ele in soup.find('ul').previous_siblings]
print(list(filter(None,ul)))
['h1', 'p', 'p', 'p']

类似于find_next()find_all_next()函数用于下一个元素,还有可用于兄弟姐妹的函数,即

find_next_sibling()find_next_siblings()函数。这些函数实现了next_siblings函数来迭代和搜索可用的兄弟姐妹。如下面的代码所示,find_next_sibling()函数返回单个元素,而find_next_siblings()函数返回所有匹配的兄弟姐妹:

#find next <p> siblings for selected <p> with class=title
print(soup.find('p','title').find_next_siblings('p'))
[<p class="story">Once upon a time there were three little sisters; and their names were
<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> and
<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>, <p class="story">...</p>]

#find single or next sibling for selected <h1>
print(soup.find('h1').find_next_sibling())
<ul>
<li data-id="10784">Jason Walters, 003: Found dead in "A View to a Kill".</li>
<li data-id="97865">Alex Trevelyan, 006: ............in "Goldeneye".</li>
<li data-id="45732">James Bond, 007: The main man; shaken but not stirred.</li>
</ul>

#find single or next sibling <li> for selected <h1>
print(soup.find('h1').find_next_sibling('li'))
None

find_previous_sibling()find_previous_siblings()函数的工作方式与find_next_sibling()find_next_siblings()函数类似,但结果是通过previous_siblings函数跟踪的元素。还可以应用额外的搜索条件和结果控制参数limit到迭代版本,例如find_previous_siblings()函数。

如下面的代码所示,find_previous_sibling()函数返回单个兄弟元素,而find_previous_siblings()函数返回先前满足给定条件的所有兄弟元素:

#find first previous sibling to <ul>
print(soup.find('ul').find_previous_sibling())
<h1>Secret agents</h1>

#find all previous siblings to <ul>
print(soup.find('ul').find_previous_siblings())

[<h1>Secret agents</h1>, <p class="story">...</p>, <p class="story">Once upon a time there were three little sisters; and their names were
<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> and
<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>, <p class="title"><b>The Dormouse's story</b></p>]

我们已经探索了在本节中探讨的函数和属性中搜索和遍历解析树的各种方法。

以下是一些提示列表,可以帮助记住和规划使用 Beautiful Soup 进行搜索和遍历活动:

  • find函数开头的函数名称用于搜索和迭代提供条件和参数:

  • find函数的复数版本用于迭代,例如findChildren()findParents()元素

  • find函数的单数版本返回单个元素,例如find()findChild()findParent()函数

  • find_all开头的函数名称返回所有匹配的元素,并用于使用提供的条件和参数进行搜索和迭代,例如find_all()find_all_next()find_all_previous()函数

  • 具有复数名称的属性用于迭代目的,例如next_elementsprevious_elementsparentschildrencontentsdescendantsnext_siblingsprevious_siblings元素

  • 具有单数名称的属性返回单个元素,也可以附加在一起形成遍历代码链,例如parentnextpreviousnext_elementprevious_elementnext_siblingprevious_sibling函数

使用 CSS 选择器

我们在前面的部分中使用了大量的属性和函数,寻找所需的元素和它们的内容。Beautiful Soup 还支持 CSS 选择器(使用库 SoupSieve 在facelessuser.github.io/soupsieve/selectors/),这增强了它的使用,并允许开发人员编写有效和高效的代码来遍历解析树。

CSS 选择器(CSS 查询或 CSS 选择器查询)是 CSS 使用的定义模式,用于选择 HTML 元素,可以按元素名称或使用全局属性(IDClass)进行选择。有关 CSS 选择器的更多信息,请参考第三章,使用 LXML、XPath 和 CSS 选择器XPath 和 CSS 选择器简介部分。

对于 Beautiful Soup,select()函数用于执行 CSS 选择器。我们可以通过定义 CSS 选择器来执行元素的搜索、遍历和迭代。select()函数是独立实现的,即它没有与 Beautiful Soup 中找到的其他函数和属性扩展,从而创建了一系列代码。select()函数返回与提供的 CSS 选择器匹配的元素列表。此外,使用 CSS 选择器的代码长度相对于前面部分用于类似目的的代码来说也是相当短的。

我们将使用select()来处理 CSS 选择器的几个示例。

示例 1 - 列出具有 data-id 属性的
  • 元素
  • 在下面的示例中,我们将使用select()函数列出具有data-id属性的<li>元素:

    print(soup.select('li[data-id]'))
    [<li data-id="10784">Jason Walters, 003: Found dead in "A View to a Kill".</li>, <li data-id="97865">Alex Trevelyan, 006: Agent turned terrorist leader; James' nemesis in "Goldeneye".</li>, <li data-id="45732">James Bond, 007: The main man; shaken but not stirred.</li>]
    

    如前面的代码所示,li[data-id]选择器查询具有名为data-id的属性键的<li>元素。data-id的值为空,这允许遍历所有具有data-id<li>。结果以对象列表的形式获得,可以应用索引来获取确切的元素,如下面的代码所示:

    print(soup.select('ul li[data-id]')[1]) #fetch index 1 only from resulted List
    <li data-id="97865">Alex Trevelyan, 006: Agent turned terrorist leader; James' nemesis in "Goldeneye".</li>
    

    如果我们希望提取 CSS 查询结果中的第一个匹配项,我们可以使用列表索引,即0(零),或者在以下代码中看到的select()函数的位置上使用select_one()函数。select_one()函数返回对象的字符串,而不是列表:

    print(soup.select_one('li[data-id]'))
    <li data-id="10784">Jason Walters, 003: Found dead in "A View to a Kill".</li>
    

    示例 2 - 遍历元素

    CSS 选择器有各种组合符号,如+,>,空格字符等,显示元素之间的关系。在以下示例代码中使用了一些这样的组合符号:

    print(soup.select('p.story > a.sister'))#Selects all <a> with class='sister' that are direct child to <p> with class="story"
    [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
    
    print(soup.select('p b'))#Selects <b> inside <p> [<b>The Dormouse's story</b>]
    
    print(soup.select('p + h1'))#Selects immediate <h1> after <p>
    [<h1>Secret agents</h1>]
    
    print(soup.select('p.story + h1'))#Selects immediate <h1> after <p> with class 'story' [<h1>Secret agents</h1>] print(soup.select('p.title + h1'))#Selects immediate <h1> after <p> with class 'title' []
    

    示例 3 - 根据属性值搜索元素

    在 Beautiful Soup 中有各种查找元素的方法,比如使用以find开头的函数或在 CSS 选择器中使用属性。可以使用 CSS 选择器中的*来搜索属性键,如下面的代码所示:

    print(soup.select('a[href*="example.com"]'))
    [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
    
    print(soup.select('a[id*="link"]'))
    [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
    

    我们正在搜索具有文本example.com<a>元素,该文本可能存在于href属性的值中。此外,我们正在搜索包含带有文本链接的属性 ID 的<a>元素。

    有了对 CSS 选择器的基本了解,我们可以在 Beautiful Soup 中使用它来实现各种目的。当处理元素时,使用select()函数非常有效,但我们可能会遇到一些限制,比如从获取的元素中提取文本或内容。

    我们在前面的部分介绍和探讨了 Beautiful Soup 的元素。为了总结这个概念,我们将在接下来的部分创建一个爬虫示例。

    构建网络爬虫

    在本节中,我们将构建一个网络爬虫,以演示基于实际内容的爬取,目标是网页内容。

    我们将从toscrape.com/上爬取名言,并从quotes.toscrape.com/上找到作者的名言。爬虫将从前五个列表页面收集名言和作者信息,并将数据写入 CSV 文件。我们还将探索单个作者页面,并提取有关作者的信息。

    首先,基本规划和识别我们愿意从中收集信息的字段,请参考第三章,使用 LXML、XPath 和 CSS 选择器使用 Web 浏览器开发者工具访问 Web 内容部分:

    ''' Listing Quotes from first 5 or less pages found from 'http://quotes.toscrape.com/' '''   import requests
    import re
    from bs4 import BeautifulSoup
    import csv
    
    sourceUrl = 'http://quotes.toscrape.com/' keys = ['quote_tags','author_url','author_name','born_date','born_location','quote_title']
    

    在上述代码中,有几个库和对象列在此处并在此处描述:

    • sourceUrl:表示要为类别网页抓取的数据而抓取的主页面的 URL

    • keys:Python 列表包含在向外部文件写入记录时将使用的列名

    • requests:导入此库以使用在带引用列表的页面 URL 上发出 HTTP 请求并接收响应

    • csv:此库将用于将抓取的数据写入外部 CSV 文件

    • bs4:用于实现和使用 Beautiful Soup 的库

    CSV 文件的第一行包含列名。我们需要在向 CSV 文件中附加实际内容的记录之前写入这些列。

    read_url() 函数,如下面的代码中所示,将用于使用requests函数发出请求并接收响应。此函数将接受一个url参数用于页面:

    def read_url(url):
        """Read given Url, Returns requests object for page content"""
      response = requests.get(url)
        return response.text
    

    dataSet是一个句柄,用于管理外部文件quotes.csvcsv.writer()文件句柄用于访问基于 CSV 的属性。writerow()函数传递了键,用于将包含列表键中的列名的行写入到外部文件中,如下所示:

    if __name__ == '__main__':
        dataSet = open('quotes.csv', 'w', newline='', encoding='utf-8')
        dataWriter = csv.writer(dataSet)
    
        # Write a Header or Column_names to CSV
      dataWriter.writerow(keys)
    
        #load details for provided URL
        get_details(sourceUrl, dataWriter)
      dataSet.close()
    

    正在实现的get_details()函数正在编写用于分页和抓取逻辑。read_url()函数将提供动态生成的页面 URL 以管理分页,如下所示:

    def get_details(page, dataWriter):
        """Get 'response' for first 5 pages, parse it and collect data for 'keys' headers"""
      nextPage = True
      pageNo = 1
      while (nextPage and pageNo <= 5):
            response = read_url(page + 'page/' + str(pageNo))
            soup = BeautifulSoup(response, 'lxml')
    
            rows = soup.find_all('div', 'quote')
            if (len(rows) > 0):
                print("Page ",pageNo," Total Quotes Found ",len(rows))
                for row in rows:
                    if row.find('span',attrs={'itemprop':'text'}):
                        title = row.find(attrs={'itemprop':'text'}).text.strip()
                        author = row.find(attrs={'itemprop':'author'}).text.strip()
                        authorLink = row.find('a',href=re.compile(r'/author/')).get('href')
                        tags = row.find('div','tags').find(itemprop="keywords").get('content')
                        print(title, ' : ', author,' : ',authorLink, ' : ',tags)
    
                        if authorLink:
                            authorLink = 'http://quotes.toscrape.com' + authorLink
                            linkDetail = read_url(authorLink)
                            soupInner = BeautifulSoup(linkDetail, 'lxml')
                            born_date = soupInner.find('span','author-born-date').text.strip()
                            born_location = soupInner.find('span','author-born-location').text.strip()
                            # Write a list of values in file
      dataWriter.writerow(
                            [tags,authorLink,author,born_date,born_location.replace('in ',''),title])
    
                nextPage = True
      pageNo += 1
      else:
                print("Quotes Not Listed!")
    

    如下面的代码中所示,使用lxml解析read_url()函数中的response元素以获取soup元素。使用 soup 获取的行列出了单页中所有的引用(即包含单个引用详细信息的元素块)在<div class="quote">函数中找到,并将被迭代以抓取quote_tagsauthor_urlauthor_name等个别项目的数据:

    带引用元素的页面源代码

    接收到的各个项目将被抓取、清理并收集到一个列表中,保持其列名的顺序,并使用csv库和文件句柄访问的writerow()函数将其写入文件(将值列表附加到文件)。

    quotes.csv数据文件将包含如下截图中所见的抓取数据:

    http://quotes.toscrape.com/抓取的数据行

    在本节中,我们探讨了使用 Beautiful Soup 进行遍历和搜索的各种方法。在接下来的部分中,我们将使用 Scrapy,一个网络爬虫框架。

    使用 Scrapy 进行网页抓取

    到目前为止,我们在本书中已经使用和探索了各种库和技术进行网页抓取。最新的库可以适应新的概念,并以更有效、多样和简单的方式实现这些技术;Scrapy 就是其中之一。

    在本节中,我们将介绍并使用 Scrapy(一个用 Python 编写的开源网络爬虫框架)。有关 Scrapy 的更详细信息,请访问官方文档docs.scrapy.org/en/latest/

    在本节中,我们将实现抓取功能并构建一个演示有用概念的项目。

    Scrapy 简介

    Scrapy 是一个用 Python 编写的网络爬虫框架,用于以有效和最小的编码方式爬取网站。根据 Scrapy 的官方网站(scrapy.org/)的说法,它是“一个用于从网站中提取所需数据的开源和协作框架。以一种快速、简单但可扩展的方式。”

    Scrapy 提供了一个完整的框架,用于部署具有内置工具的爬虫。Scrapy 最初是为网页抓取而设计的;随着其流行和发展,它也用于从 API 中提取数据。基于 Scrapy 的网络爬虫也易于管理和维护,因为其结构。总的来说,Scrapy 为处理网页抓取的项目提供了基于项目的范围。

    以下是一些使 Scrapy 成为开发人员喜爱的功能和显著点:

    • Scrapy 提供了内置支持,用于使用 XPath、CSS 选择器和正则表达式解析、遍历和提取数据。

    • 爬虫被安排和异步管理,允许同时爬取多个链接。

    • 它自动化了 HTTP 方法和操作,也就是说,不需要手动导入诸如requestsurllib之类的库来编写代码。Scrapy 使用其内置库处理请求和响应。

    • 有内置支持的 feed 导出、管道(项目、文件、图像和媒体),即以 JSON、CSV、XML 和数据库导出、下载和存储数据。

    • 中间件的可用性和大量内置扩展可以处理 cookie、会话、身份验证、robots.txt、日志、使用统计、电子邮件处理等。

    • Scrapy 驱动的项目由易于识别的组件和文件组成,可以用基本的 Python 技能处理,还有更多。

    请参阅 Scrapy 的官方文档docs.scrapy.org/en/latest/intro/overview.html进行深入和详细的概述。

    通过对 Scrapy 的基本介绍,我们现在开始在接下来的章节中设置项目并更详细地探索框架。

    设置项目

    在进行项目设置之前,我们需要在系统上成功安装了scrapy的 Python 库。有关设置或安装,请参阅第二章,Python 和 Web-使用 urllib 和 Requests设置事项部分,或者有关 Scrapy 安装的更多详细信息,请参阅官方安装指南docs.scrapy.org/en/latest/intro/overview.html

    安装成功后,我们可以使用 Python IDE 获得以下截图中显示的细节:

    成功安装 Scrapy 并显示细节

    通过成功安装scrapy库,还可以使用scrapy命令行工具。这个命令行工具包含一些命令,在项目的各个阶段使用,从创建项目到完全运行。

    要开始创建一个项目,让我们按照以下步骤进行:

    1. 打开终端或命令行界面

    2. 创建一个文件夹(ScrapyProjects),如下截图所示,或选择一个放置 Scrapy 项目的文件夹

    3. 在选择的文件夹中,运行或执行scrapy命令

    4. 将出现一个可用命令及其简要详情的列表,类似于以下截图:

    Scrapy 的可用命令列表

    我们将创建一个Quotes项目,从toscrape.com/获取与网页抓取相关的作者引用,访问存在的前五页或更少的信息,使用 URL quotes.toscrape.com/

    我们现在将开始Quotes项目。从命令提示符中运行或执行scrapy startproject Quotes命令,如下截图所示:

    开始一个项目(使用命令:scrapy startproject Quotes

    如果成功,上述命令将创建一个名为Quotes的新文件夹(即项目根目录),并包含如下截图所示的其他文件和子文件夹:

    项目文件夹 ScrapyProjects\Quotes 的内容

    项目成功创建后,让我们来探索项目文件夹中的各个组件:

    • scrapy.cfg是一个配置文件,其中包含部署的默认项目相关设置,可以进行添加。

    • 子文件夹将找到与项目目录同名的Quotes,实际上是一个 Python 模块。我们将在这个模块中找到其他的 Python 文件和其他资源。

    项目文件夹 ScrapyProjects\Quotes\Quotes 的内容

    如前面的截图所示,模块包含在spiders文件夹和items.pypipelines.pysettings.py Python 文件中。Quotes模块中的内容在以下列表中具有特定的实现:

    • spiders:这个文件夹将包含用 Python 编写的蜘蛛类或蜘蛛。蜘蛛是包含用于抓取的代码的类。每个单独的蜘蛛类都指定了特定的抓取活动。

    • items.py:这个 Python 文件包含项目容器,即继承scrapy. Items的 Python 类文件,用于收集抓取的数据并在蜘蛛中使用。项目通常被声明为携带值,并从主项目中的其他资源获得内置支持。项目就像一个 Python 字典对象,其中键是scrapy.item.Field的字段或对象,将保存特定的值。

    尽管默认项目为项目相关任务创建了items.py,但在蜘蛛中使用它并不是强制的。我们可以使用任何列表或收集数据值,并以我们自己的方式处理,比如将它们写入文件,将它们附加到列表等。

    • pipelines.py:这部分在数据被抓取后执行。抓取的项目被发送到管道执行某些操作。它还决定是否处理接收到的抓取项目或丢弃它们。

    • settings.py:这是最重要的文件,可以在其中调整项目的设置。根据项目的偏好,我们可以调整设置。请参考 Scrapy 的官方文档scrapy2.readthedocs.io/en/latest/topics/settings.html

    在本节中,我们已成功使用 Scrapy 创建了一个项目和所需的文件。这些文件将如下节所述被使用和更新。

    生成一个蜘蛛

    我们需要生成一个蜘蛛来收集数据。蜘蛛将执行爬行活动。在ScrapyProjects\Quotes\Quotes文件夹中存在一个名为spiders的空默认文件夹。

    ScrapyProjects\Quotes项目文件夹中运行或执行scrapy genspider quotes quotes.toscrape.com命令。

    成功执行该命令将在ScrapyProjects\Quotes\Quotes\spiders\路径下创建一个quotes.py文件,即一个蜘蛛。生成的QuotesSpider类继承自scrapy.Spider,在QuotesSpider中还有一些必需的属性和函数,如下代码所示:

    import scrapy
    
    class QuotesSpider(scrapy.Spider):
        name = "quotes"
        allowed_domains = ["quotes.toscrape.com"]
        start_urls = (
            'http://www.quotes.toscrape.com/',
        )
    
        def parse(self, response):
            pass
    

    QuotesSpider蜘蛛类包含自动生成的属性,用于特定任务,如下列表所示:

    • name:这个变量保存值,即蜘蛛 quotes 的名称,如前面的代码所示。名称标识了蜘蛛,并可以用于访问它。名称的值是通过命令行指令提供的,比如在genspider之后的第一个参数scrapy genspider quotes

    • allowed_domains:创建的 Spider 允许在allowed_domains中列出的域内爬行。传递的最后一个参数是quotes.toscrape.com参数,生成 Spider 实际上是一个将列在allowed_domains列表中的域名。

    • 传递给allowed_domains的域名将为start_urls生成 URL。如果存在 URL 重定向的可能性,则需要在allowed_domains中提及这些 URL 域名。

    • start_urls:这些包含 Spider 实际处理的 URL 列表。找到或提供给allowed_domains的域名将自动添加到此列表中,并且可以手动添加或更新。Scrapy 生成start_urls的 URL 添加了 HTTP 协议。在某些情况下,我们可能还需要手动更改或修复 URL,例如,需要删除添加到域名的www。更新后的start_urls将如下代码所示:

    start_urls = ( 'http://quotes.toscrape.com/',)
    
    • parse():此函数实现了与数据提取或处理相关的逻辑。parse()充当了抓取活动的主控制器和起点。为主项目创建的 Spider 将开始处理提供的 URL 或start_urls,或者在parse()内部。实现了与 XPath 和 CSS 选择器相关的表达式和代码,并且提取的值也被添加到 item(即来自item.py文件的QuotesItem)。

    我们还可以通过执行以下命令来验证 Spider 的成功创建:

    • scrapy list

    • scrapy list spide*r*

    这两个命令都将列出 Spider 的名称,该名称在spiders文件夹中找到,如下面的屏幕截图所示:

    从命令提示符中列出 Spider

    在这一部分,我们为我们的抓取任务生成了一个名为quotes的 Spider。在接下来的部分中,我们将创建与 Spider 一起工作并帮助收集数据的 Item 字段。

    创建一个 item

    继续进行抓取任务和项目文件夹,我们将找到一个名为item.py或 item 的文件,其中包含 Python 类QuotesItem。该 item 也是由 Scrapy 在发出scrapy startproject Quotes命令时自动生成的。QuotesItem类继承了scrapy.Item,具有内置属性和方法,如Field。在 Scrapy 中,ItemQuotesItem代表了一个用于收集值的容器,如下面的代码所示,包括引用、标签等,这些将作为我们使用parse()函数获取的值的键。相同字段的值将在找到的页面上被提取和收集。

    item 被视为 Python 字典,提供的字段作为键,其提取的值作为值。在 Spider 中声明字段并在 Spider 中使用它们是有效的,但不是强制使用item.py,如下面的示例所示:

    class QuotesItem(scrapy.Item):
        # define the fields for your item here like:
        # name = scrapy.Field()
    
        quote = scrapy.Field()
        tags = scrapy.Field()
        author = scrapy.Field()
        author_link = scrapy.Field()
    
        pass
    

    当 Spider 内部需要 item 时,我们需要导入QuotesItem,如下面的代码所示,并通过创建对象并访问声明的字段,即quotetagsauthor等来处理它:

    #inside Spider 'quotes.py'
    from Quotes.items import QuotesItem
    ....
    #inside parse()
    item = QuotesItem() #create an object 'item' and access the fields declared.
    
    item['quote'] = .......
    item['tags'] = .......
    item['author'] = ......
    item['author_link'] = ......
    ......
    

    在这一部分,我们声明了我们愿意从网站中检索数据的item字段。在接下来的部分中,我们将探索不同的数据提取方法,并将它们与项目字段相关联。

    提取数据

    有了生成的 Spider 和声明所需字段的 item,我们现在将继续提取特定项目字段所需的值或数据。可以使用 XPath、CSS 选择器和正则表达式应用与提取相关的逻辑,我们还可以实现 Python 相关的库,如bs4(Beautiful Soup)、pyquery等。

    通过为 Spider 设置适当的start_urls和项目(QuotesItem)来进行爬取,我们现在可以使用parse()和在docs.scrapy.org/en/latest/topics/selectors.html中使用选择器进行提取逻辑。

    使用 XPath

    Spider 内的parse()函数是实现所有抓取数据的逻辑过程的地方。如下所示,我们在此 Spider 中使用 XPath 表达式来提取QuotesItem中所需字段的值。

    有关 XPath 和使用基于浏览器的开发工具获取 XPath 查询的更多信息,请参阅第三章,使用 LXML、XPath 和 CSS 选择器使用 DevTools 的 XPath 和 CSS 选择器部分。同样,有关pyquery Python 库的更多信息,请参阅第四章,使用 pyquery - 一个 Python 库进行抓取

    如下一段代码片段所示,从QuotesItem中使用item对象收集单个字段相关数据,并最终使用 Python 关键字yield进行收集和迭代。parse()实际上是一个返回QuotesItem中的item对象的生成器。

    Python 关键字yield用于返回一个生成器。生成器是返回可迭代对象的函数。Python 函数可以使用yield代替return来作为生成器处理。

    parse()有一个额外的参数response;这是 Scrapy 返回的一个scrapy.http.response.html.HtmlResponse对象,其中包含所访问或爬取的 URL 的页面内容。获取的响应可以与 XPath 和 CSS 选择器一起用于进一步的抓取活动:

    '''
    Using XPath
    ''' def parse(self, response):
     print("Response Type >>> ", type(response))
     rows = response.xpath("//div[@class='quote']") #root element
    
     print("Quotes Count >> ", rows.__len__())
     for row in rows:
         item = QuotesItem()
    
         item['tags'] =     row.xpath('div[@class="tags"]/meta[@itemprop="keywords"]/@content').extract_first().strip()
         item['author'] = row.xpath('//span/small[@itemprop="author"]/text()').extract_first()
         item['quote'] = row.xpath('span[@itemprop="text"]/text()').extract_first()
         item['author_link'] = row.xpath('//a[contains(@href,"/author/")]/@href').extract_first()
    
         if len(item['author_link'])>0:
             item['author_link'] = 'http://quotes.toscrape.com'+item['author_link']
    
         yield item
    

    如下所示,XPath 表达式被应用于响应,使用xpath()表达式并用作response.xpath()。提供给response.xpath()的 XPath 表达式或查询被解析为行,即包含所需字段的元素块。

    获取的行将通过提供 XPath 查询并使用此处列出的其他函数进行迭代,以提取单个元素值:

    • extract(): 提取与提供的表达式匹配的所有元素。

    • extract_first(): 仅提取与提供的表达式匹配的第一个元素。

    • strip():清除字符串开头和结尾的空白字符。我们需要小心使用此函数来处理提取的内容,如果结果不是字符串类型,例如NoneTypeList等,可能会导致错误。

    在本节中,我们使用 XPath 收集了引用列表的详细信息;在下一节中,我们将使用 CSS 选择器来完成相同的过程。

    使用 CSS 选择器

    在本节中,我们将使用 CSS 选择器及其扩展,如::text::attr,以及extract()strip()。与response.xpath()类似,可以使用response.css()来运行 CSS 选择器。css()选择器使用提供的表达式匹配元素:

    '''
    Using CSS Selectors
    '''
    def parse(self, response):
        print("Response Type >>> ", type(response))
        rows = response.css("div.quote") #root element
    
        for row in rows:
            item = QuotesItem()
    
            item['tags'] = row.css('div.tags > meta[itemprop="keywords"]::attr("content")').extract_first()
            item['author'] = row.css('small[itemprop="author"]::text').extract_first()
            item['quote'] = row.css('span[itemprop="text"]::text').extract_first()
            item['author_link'] = row.css('a:contains("(about)")::attr(href)').extract_first()
    
            if len(item['author_link'])>0:
                item['author_link'] = 'http://quotes.toscrape.com'+item['author_link']
    
            yield item   
    

    如前面的代码所示,rows代表具有post-item类的单个元素,用于获取Item字段。

    有关 CSS 选择器和使用基于浏览器的开发工具获取 CSS 选择器的更多信息,请参阅第三章,使用 LXML、XPath 和 CSS 选择器CSS 选择器部分和使用 DevTools 的 XPath 和 CSS 选择器部分。

    有关选择器及其属性的更详细信息,请参阅docs.scrapy.org/en/latest/topics/selectors.html上的 Scrapy 官方文档。在接下来的部分中,我们将学习如何从多个页面中抓取数据。

    来自多个页面的数据

    在前面的部分中,我们尝试对start_urls中的 URL 进行数据抓取,即quotes.toscrape.com/。还要注意的是,这个特定的 URL 只会返回第一页的引用列表。

    引用列表分布在多个页面上,我们需要访问每一页来收集信息。下面的列表中找到了分页链接的模式:

    parse()中使用的 XPath 和 CSS 选择器,如前一节中的代码所示,将从第一页或第 1 页中抓取数据。跨页面找到的分页链接可以通过将链接传递给 Spider 中的parse()并使用scrapy.Requestcallback参数来请求和提取。

    如下面的代码所示,在第 1 页上找到的第 2 页的链接被提取并传递给scrapy.Request,发出对nextPage的请求并使用parse()来产生项目字段。类似地,迭代会一直进行,直到下一页或nextPage的链接存在为止:

    def parse(self, response):
        print("Response Type >>> ", type(response))
        rows = response.css("div.quote")
    
        for row in rows:
            item = QuotesItem()
            ......
            ......
            yield item
    
        #using CSS
        nextPage = response.css("ul.pager > li.next > a::attr(href)").extract_first() 
        #using XPath
        #nextPage = response.xpath("//ul[@class='pager']//li[@class='next']/a/@href").extract_first()
    
        if nextPage:
            print("Next Page URL: ",nextPage)
            #nextPage obtained from either XPath or CSS can be used.
      yield scrapy.Request('http://quotes.toscrape.com'+nextPage,callback=self.parse)
    
     print('Completed')
    

    我们还可以通过仅对start_urls进行更改来获得基于分页的结果,如下面的代码所示。使用这个过程不需要像前面的代码中使用的nextPagescrapy.Request

    要爬取的 URL 可以在start_url中列出,并且通过parse()递归实现,如下面的代码所示:

    ''' To be used for pagination purpose: include the URL to be used by parse() ''' start_urls = (
     'http://quotes.toscrape.com/', 'http://quotes.toscrape.com/page/1/', 'http://quotes.toscrape.com/page/2/', )
    

    我们还可以使用 Python 的列表推导技术获取 URL 列表。下面的代码中使用的range()函数接受参数的开始和结束,即 1 和 4,将得到 1、2 和 3 这些数字:

    start_urls = ['http://quotes.toscrape.com/page/%s' % page for page in xrange(1, 6)] '''
    Results to: 
    [http://quotes.toscrape.com/page/1,
    http://quotes.toscrape.com/page/2,
    http://quotes.toscrape.com/page/3,
    http://quotes.toscrape.com/page/4,
    http://quotes.toscrape.com/page/5,]
    '''
    

    在下一节中,我们将运行爬虫 quotes 并将项目导出到外部文件中,使用提取逻辑以及分页和声明的项目。

    运行和导出

    我们需要运行一个 Spider,并在提供的 URL 中查找项目字段的数据。我们可以通过在命令行中发出scrapy crawl quotes命令或如下截图中所示的方式来开始运行 Spider:

    运行 Spider(scrapy crawl quotes)

    在命令中提供了 Scrapy 参数crawl和 Spider 名称(quotes)。成功运行该命令将得到有关 Scrapy、机器人、Spider、爬取统计和 HTTP 方法的信息,并将列出项目数据作为字典。

    在执行 Spider 时,我们将收到各种形式的信息,例如INFO/DEBUG/scrapy统计数据等,如下面的代码中所示:

    ...[scrapy] INFO: Scrapy 1.0.3 started (bot: Quotes)
    ...[scrapy] INFO: Optional features available: ssl, http11, boto
    ...[scrapy] INFO: Overridden settings: {'NEWSPIDER_MODULE': 'Quotes.spiders', 'SPIDER_MODULES':     ['Quoyes.spiders'], 'BOT_NAME': 'Quotes'} ....... ...[scrapy] INFO: Enabled item pipelines:
    ...[scrapy] INFO: Spider opened
    ...[scrapy] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
    ...[scrapy] DEBUG: Telnet console listening on 127.0.0.1:6023
    ...[scrapy] DEBUG: Redirecting (301) to <GET http://quotes.toscrape.com/> from <GET http://quotes.toscrape.com/> 
    [scrapy] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/1/> (referer: None)
    ('Response Type >>> ', <class 'scrapy.http.response.html.HtmlResponse'>).......
    .......
    ('Response Type >>> ', <class 'scrapy.http.response.html.HtmlResponse'>)
    ...[scrapy] DEBUG: Scraped from <200 http://quotes.toscrape.com/>
    {'author': u'J.K. Rowling',
    .......
    ...[scrapy] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/5/>
    {'author': u'James Baldwin',
     'author_link': u'http://quotes.toscrape.com/author/James-Baldwin',
    .....
    ('Next Page URL: ', u'/page/6/')
    .......
    .......
    Completed
    ...[scrapy] INFO: Closing spider (finished)  
    

    Scrapy 的统计数据如下:

    [scrapy] INFO: Dumping Scrapy stats:
    {'downloader/request_bytes': 3316,
     'downloader/request_count': 13,
     'downloader/request_method_count/GET': 13,
     'downloader/response_bytes': 28699,
     'downloader/response_count': 13,
     'downloader/response_status_count/200': 11,
     'downloader/response_status_count/301': 2,
     'dupefilter/filtered': 1,
     'finish_reason': 'finished',
     'finish_time': datetime.datetime(.....
     'item_scraped_count': 110,
     'log_count/DEBUG': 126,
     'log_count/ERROR': 2,
     'log_count/INFO': 8,
     'log_count/WARNING': 1,
     'request_depth_max': 8,
     'response_received_count': 11,
     'scheduler/dequeued': 13,
     'scheduler/dequeued/memory': 13,
     'scheduler/enqueued': 13,
     'scheduler/enqueued/memory': 13,
     'start_time': datetime.datetime(....
    ..... [scrapy] INFO: Spider closed (finished)
    

    我们还可以运行 Spider 并将找到的项目或抓取的数据保存到外部文件中。数据被导出或存储在文件中,以便于访问、使用和分享管理。

    使用 Scrapy,我们可以使用爬行命令将抓取的数据导出到外部文件,如下面的列表中所示:

    • 要将数据提取到 CSV 文件中,我们可以使用C:\ScrapyProjects\Quotes> scrapy crawl quotes -o quotes.csv命令,如下面的截图所示:

    来自文件 quotes.csv 的内容

    • 要将数据提取到 JSON 文件格式中,我们可以使用C:\ScrapyProjects\Quotes> scrapy crawl quotes -o quotes.json命令,如下所示:

    来自文件 quotes.json 的内容

    在主项目文件夹中将生成-o参数后跟随的文件名。有关 feed 导出的更详细信息和可以用于导出数据的文件类型,请参阅官方 Scrapy 文档docs.scrapy.org/en/latest/topics/feed-exports.html

    在本节中,我们学习了 Scrapy 并使用它创建了一个爬虫来抓取数据并将抓取的数据导出到外部文件。在下一节中,我们将在网络上部署爬虫。

    部署网络爬虫

    在线部署网络爬虫或在实时服务器上部署将显著提高爬取活动的效果,具有速度、更新的技术、网络空间、随时使用等优势。在线部署之前需要进行本地测试和确认。我们需要拥有或购买网络空间,与网络托管公司或云服务器合作。

    Scrapy Cloud 位于scrapinghub.com/scrapy-cloud,来自scrapinghub.com/的 Scrapinghub 是部署和管理 Scrapy Spider 的最佳平台之一。Scrapy Cloud 提供了一个简单而交互式的界面来部署 Scrapy,并且是免费的,以下是一些额外功能:

    • 编码/管理和运行 Spider

    • 将 Spider 部署到云端

    • 下载和分享数据

    • API 访问与资源管理

    以下是使用 Scrapy Cloud 部署项目的步骤:

    1. 打开网络浏览器并转到scrapinghub.com/

    2. 从导航菜单中选择“产品”,并选择 SCRAPY CLOUD,如下面的屏幕截图所示:

    Scrapinghub 产品

    1. 登录或注册页面加载自scrapinghub.com/scrapy-cloud(或打开登录页面:app.scrapinghub.com/account/login/):

    从 scraping hub 登录和注册页面

    1. 完成注册和登录后,用户将获得一个交互式仪表板,并有一个“创建项目”的选项,如下面的屏幕截图所示:

    用户仪表板

    1. 单击“创建项目”将弹出一个窗口,如下面的屏幕截图所示:

    从 Scrapy Cloud 创建新项目

    1. 创建一个项目,如屏幕截图所示,并选择部署 Spider 的技术 SCRAPY;点击“创建”。

    2. 将加载带有 Scrapy Cloud 项目的仪表板,列出新创建的项目,如下面的屏幕截图所示:

    Scrapy Cloud 项目列表,带有“创建项目”选项

    1. 要部署创建项目的代码,请从 Scrapy Cloud 项目列表中选择项目。

    2. 项目仪表板将加载各种选项。选择“代码和部署”选项:

    带有各种选项的项目仪表板

    1. 使用命令行或 GitHub 部署代码。

    2. 成功部署将列出 Spider,如下面的屏幕截图所示:

    代码部署后 Spider 的列表

    1. 单击列出的 Spider,将显示详细信息和可用选项,如下面的屏幕截图所示:

    Spider 详情

    1. 点击“运行”开始爬取所选的 Spider,如下所示:

    爬虫运行窗口

    1. 点击“运行”使用默认选项。

    2. 爬取作业将列在如下屏幕截图所示。我们可以浏览“已完成的作业”以获取有关项目、请求、错误、日志等的详细信息:

    Spider 的作业详情

    1. 在浏览已完成作业的项目时,可以使用筛选器、数据导出和下载等选项,以及有关请求、日志、统计等的爬取作业详细信息。单击列出的特定 Spider 可以加载更多信息:

    从 Spider 列出项目

    使用前面列出的操作,我们可以成功地使用 Scraping hub 部署 Scrapy Spider。

    在本节中,我们使用和探索了 Scraping hub 来部署 Scrapy Spider。

    总结

    选择合适的库和框架取决于项目的范围。用户可以自由选择库并体验在线过程。

    在本章中,我们使用和探索了使用 Beautiful Soup 遍历 web 文档的各个方面,并探索了一个用于爬虫活动的框架:Scrapy。Scrapy 提供了一个完整的框架来开发爬虫,并且可以有效地使用 XPath 和 CSS 选择器来支持数据导出。Scrapy 项目也可以使用 Scraping hub 部署,以体验部署 Spider 的实时性能,并享受 Scraping hub(Scrapy Cloud)提供的功能。

    在下一章中,我们将探索更多有关从网页中抓取数据的信息。

    进一步阅读

    第三部分:高级概念

    在本节中,您将学习如何抓取安全网站,以及处理 HTML 表单和 Web cookies。您还将探索面向目标数据的基于 Web 的 API,并使用基于 Web 的测试框架,如 Selenium。

    本节包括以下章节:

    • 第六章,处理安全 Web

    • 第七章,使用基于 Web 的 API 提取数据

    • 第八章,使用 Selenium 抓取 Web

    • 第九章,使用正则表达式提取数据

    第六章:处理安全网络

    到目前为止,我们已经了解了可以用来访问和抓取网络内容的网络开发技术、数据查找技术和 Python 库。

    现今存在各种形式的基于网络的安全措施,用于保护我们免受未经身份验证的使用和对敏感网络内容的未经授权访问。许多工具和技术被网站应用;有些针对用户行为,而有些针对网站内容及其可用性。

    安全网络(或基于网络的安全功能)被认为是由网站实施并被希望使用或查看网站内容的最终用户所利用的技术之一。我们将从网络抓取的角度涵盖一些处理这些功能的基本概念。

    在本章中,我们将学习以下主题:

    • 安全网络简介

    • HTML <form>处理

    • 处理用户身份验证

    • 处理 cookie 和会话

    技术要求

    本章需要一个网络浏览器(Google Chrome 或 Mozilla Firefox)。我们将使用以下 Python 库:

    • requests

    • pyquery

    如果这些库在您当前的 Python 设置中不存在,请参阅第二章,Python 和网络 - 使用 urllib 和 Requests设置事物部分,获取有关其安装和设置的更多信息。

    本章的代码文件可在本书的 GitHub 存储库中找到:github.com/PacktPublishing/Hands-On-Web-Scraping-with-Python/tree/master/Chapter06

    安全网络简介

    实施基于网络的安全功能(或用于维护安全访问状态的功能)以访问信息的方式正在日益增长。随着网络技术的不断发展,网站和网络应用程序部署基本或高度复杂的安全机制。

    安全的网络内容在爬取和抓取的角度上通常具有挑战性。在本节中,您将了解一些基本的基于安全的概念。我们将在接下来的章节中探讨这些概念及其实施。

    接下来的章节将讨论一些安全功能概念或容易受到安全威胁的概念。这些概念可以独立或协作地在网站中使用一些基础工具或措施来实施。

    表单处理

    这也被称为 HTML <form>处理、表单处理或表单提交。这种方法处理和处理 HTML <form>内的数据。

    HTML <form><form>标签内的元素,如<input><option><button><textarea>等,通常用于收集和提交数据。请访问 W3School HTML 表单(www.w3schools.com/html/html_forms.asp)获取 HTML 表单的实际示例和详细信息。

    HTTP 方法或请求方法,如GETPOSTPUT等,用于在网页之间访问或提交数据。有关 HTTP 的更多信息,请访问www.w3.org/Protocols/

    从安全角度来看,HTML <form> 可以包含动态和隐藏或系统生成的值,用于管理验证、为字段提供值,或在表单提交期间执行基于安全的实现。具有诸如<input type="hidden"...>的字段的表单在页面上对用户可能不可见。在这种情况下,用户必须从页面源代码或基于浏览器的开发者工具获取帮助。

    一个带有表单的网页可能在某些字段中显示并要求输入,并且可以在后端或源代码中包含一些额外的字段,其中可能包含用户或系统信息。这些信息在幕后被收集和处理,用于基于网页的分析、营销、用户和系统识别、安全管理等。

    有关表单处理的更多信息,请参阅第三章,使用 LXML、XPath 和 CSS 选择器使用网页浏览器开发者工具访问网页内容部分。

    Cookies 和会话

    要访问由浏览网站设置的 cookie 和会话值,请参阅第一章,网页抓取基础知识开发者工具部分的数据查找技术部分。现在,让我们了解一下 cookie 和会话是什么。

    Cookies

    Cookie 是由网站在您的系统或计算机上生成和存储的数据。Cookie 中的数据有助于识别用户对网站的网络请求。Cookie 中存储的数据以键:值对的形式存储。存储在 cookie 中的数据有助于网站访问该数据,并以快速交互的形式传输某些保存的值。

    Cookie 还允许网站跟踪用户资料、他们的网页习惯等,并利用这些信息进行索引、页面广告和营销活动。

    基于 cookie 的数据可以持续一个会话(即从加载网页到关闭浏览器的时间)形成所谓的会话 cookie,或者持续几天、几周或几个月,这被称为永久或存储的 cookie。Cookie 还可以包含以秒为单位的过期值,一旦该值表示的时间段过去,cookie 就会过期或从系统中删除。

    有关 cookie 的更多信息,请参阅第一章,网页抓取基础知识了解网页开发和技术部分的HTTP部分。您也可以访问www.aboutcookies.org/www.allaboutcookies.org/获取更多信息。

    会话

    会话是强制两个系统之间基于状态的通信的属性。会话用于临时存储用户信息,并在用户退出浏览器或离开网站时被删除。

    会话用于维护安全活动。网站生成一个唯一的标识号,也称为会话 ID 或会话密钥,用于独立跟踪他们的用户或基于安全的特性。在大多数情况下,可以使用 cookie 来跟踪会话的可用性。

    用户认证

    用户认证涉及处理和管理基于用户的身份识别过程。网站通过其注册页面提供用户注册,并收集用户对所需或可用字段的输入。用户的详细信息被保存在安全的地方,如云端或基于服务器的数据库,或任何其他安全系统。

    注册用户经过验证,被允许从他们的系统登录和退出,并通过他们的用户名、密码和电子邮件地址进行识别。

    表单处理、cookies、会话管理和其他基于安全性的措施可以单独或协同部署用于这个过程。

    在上一章中,我们探讨并解决了基于信息可用性、访问网页、应用各种 HTTP 方法等各种情景,以及在网页抓取过程中可能实施或面临的各种措施和情况。本章的各节涉及可以实施或在网页抓取过程中可能面临的各种措施和情况。

    HTML
    处理

    在本节中,我们将处理表单处理或表单提交,以便从toscrape.com(ViewState)搜索活动。ViewState 是基于 AJAX 的过滤表单。

    这个特定的表单提交是通过 AJAX(www.w3schools.com/js/js_ajax_intro.asp)在多个步骤中执行的。有关 AJAX 的更多信息,请访问W3Schools AJAX

    http://toscrape.com 中的引用部分具有各种端点

    让我们设置代码。需要导入pyqueryrequests库,并收集所需的 URL,以便可以使用它们。processRequests()函数,连同位置参数和命名参数,用于处理对所提供url的请求,使用基于params参数的 HTTP POSTGET 方法返回 PyQuery 对象作为响应。

    我们还对迭代authorTags感兴趣,并分别收集quoteAuthormessage。以类似的方式,可以提取从页面获得的任何信息:

    from pyquery import PyQuery as pq
    import requests
    mainurl = "http://toscrape.com/" searchurl = "http://quotes.toscrape.com/search.aspx" filterurl = "http://quotes.toscrape.com/filter.aspx" quoteurl = "http://quotes.toscrape.com/" authorTags = [('Albert Einstein', 'success'), ('Thomas A. Edison', 'inspirational')]
    
    def processRequests(url, params={}, customheaders={}):
        if len(params) > 0:
            response = requests.post(url, data=params, headers=customheaders)
        else:
            response = requests.get(url)   return pq(response.text)
    
    if __name__ == '__main__':
        for authorTag in authorTags:
            authorName,tagName= authorTag
    

    以下屏幕截图显示了在前面的代码中定义的searchurl页面的内容。存在两个单独的下拉菜单,分别用于作者和他们的标签的选项:

    http://quotes.toscrape.com/search.aspx 带有作者和标签的searchurl

    让我们加载searchurl,如下面的代码所示,并从作者下拉菜单中选择一个作者。使用 AJAX 生成<option>标签,以供作者的选定<option>

    请参阅第三章,使用 LXML、XPath 和 CSS 选择器使用 Web 浏览器开发工具访问 Web 内容部分,以及第一章,Web 抓取基础知识数据查找技术开发人员工具部分。

    #Step 1: load searchURL searchResponse = processRequests(searchurl)
    author = searchResponse.find('select#author option:contains("' + authorName + '")').attr('value')
    viewstate = searchResponse.find('input#__VIEWSTATE').attr('value')
    tag = searchResponse.find('select#tag option').text()
    
    print("Author: ", author)
    print("ViewState: ", viewstate)
    print("Tag: ", tag)
    

    如您所见,使用 HTTP GET 调用processRequests()函数到searchurl,并将返回一个 PyQuery 对象作为响应。从searchResponse中,让我们收集必要的表单字段。收集诸如authorviewstatetag之类的字段,并在每次迭代中获得的字段的值显示在以下输出中:

    Author: Albert Einstein
    ViewState: NTA2MjI4NmE1Y2Q3NGFhMzhjZTgxMzM4ZWU0NjU4MmUsQWxiZXJ0IEVpbnN0ZWluLEouSy4gUm93bGluZyxKYW5lIEF1c3Rlbi............BDdW1taW5ncyxLaGFsZWQgSG9zc2VpbmksSGFycGVyIExlZSxNYWRlbGVpbmUgTCdFbmdsZQ==
    Tag: ----------
    
    Author: Thomas A. Edison
    ViewState: ZjNhZTUwZDYzY2YyNDZlZmE5ODY0YTI5OWRhNDAyMDYsQWxiZXJ0IEVpbnN0ZWluLEouSy4gUm93bGluZyxKYW5lIEF1c3Rlbi............BDdW1taW5ncyxLaGFsZWQgSG9zc2VpbmksSGFycGVyIExlZSxNYWRlbGVpbmUgTCdFbmdsZQ==
    Tag: ----------
    

    从前面的输出中,我们可以看到viewstate (<input id="__VIEWSTATE"..>)authorTags的两次迭代中包含唯一值。

    ViewState是由网站生成的用于识别页面的各个状态的唯一和随机值,通常作为隐藏的<input>值。这种<form>值存在于大多数使用<form>和内置 ASP 或 ASP.NET 技术的网站中。ViewState值在客户端上使用,它保留或保持了<form>元素的值,以及页面的身份。使用ViewState是与状态管理相关的技术之一。有关更多信息,请访问来自 C#Corner 的文章,网址为www.c-sharpcorner.com/article/Asp-Net-state-management-techniques/

    ViewState的值对于获取所选作者的<option>标签是必不可少的。正如我们在下面的代码中所看到的,params是使用authortag__VIEWSTATE创建的,并通过 HTTP POSTcustomheaders提交到filterurl,通过获取filterResponse。以下代码显示了当filterurl加载了作者和默认标签时会发生什么:

    #Step 2: load filterurl with author and default tag params = {'author': author, 'tag': tag, '__VIEWSTATE': viewstate}
    customheaders = {
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
        'Content-Type': 'application/x-www-form-urlencoded',
        'Referer': searchurl
    }
    
    filterResponse = processRequests(filterurl,params,customheaders)
    viewstate = filterResponse.find('input#__VIEWSTATE').attr('value')
    tagSuccess = filterResponse.find('select#tag option:contains("' + tagName + '")').attr('value')
    submitButton = filterResponse.find('input[name="submit_button"]').attr('value')
     print("Author: ", author)
    print("ViewState: ", viewstate)
    print("Tag: ", tagSuccess)
    print("Submit: ", submitButton)
    

    迭代前面的代码将产生以下输出:

    • http://quotes.toscrape.com/filter.aspx 页面上选择了作者(托马斯·爱迪生)和标签(鼓舞人心):
    Author: Thomas A. Edison
    ViewState: ZjNhZTUwZDYzY2YyNDZlZmE5ODY0YTI5OWRhNDAyMDYsQWxiZXJ0IEVpbnN0ZWluLEouSy4gUm93bGluZyxKYW5lIEF1c3Rlbi............BDdW1taW5ncyxLaGFsZWQgSG9zc2VpbmksSGFycGVyIExlZSxNYWRlbGVpbmUgTCdFbmdsZSwtLS0tLS0tLS0t
    Tag: inspirational
    Submit: Search
    
    • http://quotes.toscrape.com/filter.aspx 页面上选择了作者(阿尔伯特·爱因斯坦)和标签(成功):
    Author: Albert Einstein
    ViewState: NTA2MjI4NmE1Y2Q3NGFhMzhjZTgxMzM4ZWU0NjU4MmUsQWxiZXJ0IEVpbnN0ZWluLEouSy4gUm93bGluZyxKYW5lIEF1c3Rlbi............BDdW1taW5ncyxLaGFsZWQgSG9zc2VpbmksSGFycGVyIExlZSxNYWRlbGVpbmUgTCdFbmdsZSwtLS0tLS0tLS0t
    Tag: success
    Submit: Search
    

    现在我们已经获得了每个authorTags的所有过滤<form>参数,最后一步是提交这些参数,即paramsfilterurl,使用HTTP POST并提取结果信息:

    #Step 3: load filterurl with author and defined tag params = {'author': author, 'tag': tagSuccess, 'submit_button': submitButton, '__VIEWSTATE': viewstate}  customheaders = {
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
    'Content-Type': 'application/x-www-form-urlencoded',
    'Referer': filterurl
    }
    
    finalResponse = processRequests(filterurl,params, customheaders)
    
    #Step 4: Extract results quote = finalResponse.find('div.quote span.content').text()
    
    quoteAuthor = finalResponse.find('div.quote span.author').text()
    message = finalResponse.find('div.quote span.tag').text()
    print("Author: ", quoteAuthor, "\nMessage: ", message)
    

    正如我们所看到的,finalResponse是由processRequests()返回的 PyQuery 对象,并被解析以获取quotequoteAuthormessage,如下面的屏幕截图所示:

    http://quotes.toscrape.com/filter.aspx ,结果为作者和标签

    使用前面的代码进行第一次迭代的输出,包括AuthorMessage,如下所示:

    Author: Albert Einstein 
    Message: success
    

    以下是第二次迭代的屏幕截图:

    http://quotes.toscrape.com/filter.aspx ,结果为作者和标签

    使用前面的代码进行第二次迭代的输出,包括AuthorMessage,如下所示:

    Author: Thomas A. Edison 
    Message: inspirational
    

    在前面的代码中显示了带有搜索和过滤操作的表单处理,以及使用隐藏字段。ViewState值由系统在后台使用,以识别所选选项并过滤与其关联的标签,从而得到作者的引用。

    最终表单提交的 HTTP POST参数总数为四个,而页面上只显示或允许与两个选项交互。如果对值进行任何更改,例如viewstate,或者如果viewstateparams中丢失,将导致空引号,如下面的代码所示:

    #params={'author':author,'tag':tagSuccess,'submit_button':submitButton,'__VIEWSTATE':viewstate}
    params={'author':author,'tag':tagSuccess,'submit_button':submitButton,'__VIEWSTATE':viewstate+"TEST"}
    #params={'author':author,'tag':tagSuccess,'submit_button':submitButton}
    ......
    finalResponse = processRequests(filterurl,params, customheaders)
    ......
    print("Author: ", quoteAuthor, "\nMessage: ", message)
    
    *Quote:* 
    *Author:* 
    *Message:*
    

    表单提交不仅取决于从页面上可见的<form>元素中选择的必需参数,还可能存在隐藏的值和动态生成的状态表示,应该对其进行有效处理以获得成功的输出。

    在下一节中,我们将处理表单提交和用户身份验证。

    处理用户身份验证

    在本节中,我们将探讨用于处理基本用户身份验证的任务,该任务可从testing-ground.scraping.pro/login获得。用户身份验证通常使用一组唯一的信息进行处理,例如用户名、密码、电子邮件等,以在网站上识别用户。

    本节中的代码涉及登录和更改登录凭据,以及从页面获取相应的消息。

    如下面的屏幕截图所示,HTML <form>存在两个<input>框,用于接受用户名和密码(即登录凭据),这些是登录所需的。登录凭据是私密和安全的信息,但对于这个特定的测试站点,这些值是可见的,预定义的,并提供的,即Username = "admin"Password = "12345"

    登录页面

    使用这些凭据在testing-ground.scraping.pro/login上进行登录处理,我们需要找到页面上用于处理输入凭据的<form>属性,即actionmethod。正如我们所看到的,HTTP POST方法将被应用于在testing-ground.scraping.pro/login?mode=login上执行表单提交:

    检查<form>元素

    让我们继续设置代码。需要导入pyqueryrequests库,并收集所需的 URL,以便可以使用它们:

    from pyquery import PyQuery as pq
    import requests
    mainUrl = "http://testing-ground.scraping.pro" loginUrl = "http://testing-ground.scraping.pro/login" logoutUrl = "http://testing-ground.scraping.pro/login?mode=logout" postUrl="http://testing-ground.scraping.pro/login?mode=login"
    

    如下面的代码所示,responseCookies()函数将接受从requests.get()获得的响应对象,然后打印头信息和 cookies 信息。同样,processParams()函数接受基于<form>的参数,将被发布,并打印从页面获得的消息:

    def responseCookies(response):
        headers = response.headers
        cookies = response.cookies
        print("Headers: ", headers)
        print("Cookies: ", cookies)
    
    def processParams(params):
        response = requests.post(postUrl, data=params)
        responseB = pq(response.text)
        message = responseB.find('div#case_login h3').text()
        print("Confirm Login : ",message)
    
    if __name__ == '__main__': 
        requests.get(logoutUrl)
    
        response = requests.get(mainUrl)
        responseCookies(response)
    
        response = requests.get(loginUrl)
        responseCookies(response)
    

    现在,让我们请求logoutUrl来清除 cookies 和会话(如果存在)。或者,对于一个全新的过程,我们可以分别请求mainUrlloginUrl,并检查从responseCookies()接收到的消息。以下是输出:

    Headers:{'Vary':'Accept-Encoding','Content-Type':'text/html','Connection':'Keep-Alive', ..........., 'Content-Encoding':'gzip','X-Powered-By':'PHP/5.4.4-14+deb7u12'}
    Cookies: <RequestsCookieJar[]>
    
    Headers:{'Vary':'Accept-Encoding','Content-Type':'text/html','Connection':'Keep-Alive',.............., 'Set-Cookie':'tdsess=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT',........., 'Keep-Alive':'timeout=5, max=100','X-Powered-By':'PHP/5.4.4-14+deb7u12'}
    Cookies: <RequestsCookieJar[]>
    

    如前面的输出所示,mainUrlloginUrl的 cookies 为空,并且除了来自loginUrl的值为“tdsess = deleted; expires = Thu, 01-Jan-1970 00:00:01 GMT”的Set-Cookie之外,没有其他唯一的标头对可用。

    现在,responseAloginUrl``<form>元素属性名称已被收集为usernamepassword,此信息将用于创建paramsCorrectparamsIncorrect参数字符串,然后将其发布到postUrl

    responseA = pq(response.text)
    username = responseA.find('input[id="usr"]').attr('name')
    password = responseA.find('input[id="pwd"]').attr('name')
    
    #Welcome : Success paramsCorrect = {username: 'admin', password: '12345'} #Success print(paramsCorrect)
    processParams(paramsCorrect)
    

    使用提供的paramsCorrect参数字符串成功提交表单将导致以下输出:

    {'pwd': '12345', 'usr': 'admin'}
    Confirm Login : WELCOME :)
    

    前面的输出是从postUrl的响应中提取的,在这个测试案例中实际上是一个重定向页面,URL 为testing-ground.scraping.pro/login?mode=welcome

    使用有效的登录凭据成功提交表单

    让我们继续使用表单提交,但使用无效的凭据。 paramsIncorrect短语包含“密码”的无效值:

     paramsIncorrect = {username: 'admin', password: '123456'} #Access Denied
      print(paramsIncorrect)
     processParams(paramsIncorrect)
    

    上述代码将导致以下输出:

    {'pwd': '123456', 'usr': 'admin'}
    Confirm Login : ACCESS DENIED!
    

    前面的输出也可以在loginUrl本身找到,这次不会发生重定向:

    访问被拒绝!(使用错误的凭据处理)

    正如您所看到的,用户身份验证和表单提交是相辅相成的。通过使用正确的登录凭据,并能够使用 Python 处理表单提交过程,我们可以获得成功的输出,或者处理从网站返回的相关输出。

    在下一节中,我们将通过处理包含会话的 cookie 来执行表单提交和用户身份验证。

    处理 cookie 和会话

    在本节中,我们将处理用户身份验证的表单处理,并为quotes.toscrape.com/logintoscrape.com管理 cookie 和会话。

    为了登录,您需要使用 CSRF 令牌登录(任何用户名/密码都可以使用)。

    让我们设置代码。需要导入pyqueryrequests库,并收集并使用所需的 URL。使用“getCustomHeaders()”函数,以及cookieHeader参数,用于为 URL 请求标头设置 cookie 值。使用“responseCookies()”函数,以及response参数,显示headerscookies,并从cookies返回Set-Cookie值:

    from pyquery import PyQuery as pq
    import requests
    mainUrl = "http://toscrape.com/" loginUrl = "http://quotes.toscrape.com/login"  quoteUrl = "http://quotes.toscrape.com/"   def getCustomHeaders(cookieHeader):
        return {
            'Host': 'quotes.toscrape.com',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
            'Referer': 'http://quotes.toscrape.com/login',
            'Content-Type': 'application/x-www-form-urlencoded', 
            'Cookie': cookieHeader
        }
    
    def responseCookies(response):
        headers = response.headers
        cookies = response.cookies
        print("Headers: ", headers)
        print("Cookies: ", cookies)
        return headers['Set-Cookie']
    
    if __name__ == '__main__':
    

    有关 HTTP 和 HTTP 标头的更多信息,请访问第一章,网络抓取基础知识了解 Web 开发和技术HTTP部分。有关 cookie 的更多详细信息,请访问www.aboutcookies.org/allaboutcookies.org

    现在,让我们分别加载mainUrlloginUrl

    requests.get(mainUrl)
    response = requests.get(loginUrl)
    
    

    以下屏幕截图显示了使用loginUrl时登录页面的外观:

    http://quotes.toscrape.com/login 登录页面

    一旦加载了loginUrl,我们可以检查或使用基于浏览器的开发人员工具来查找请求标头,并确认是否存在任何 cookie。我们收到以下输出:

    来自浏览器开发人员工具的网络面板文档标头选项卡

    以下代码接受来自response的 cookie,并用于标头:

    setCookie = responseCookies(response)
    print("Set-Cookie: ",setCookie)
    

    正如我们从前面的屏幕截图中看到的,请求标头包含以“sessio = ....”开头的值为“Cookie”的key,也称为会话 ID。此信息在response.headersresponse.cookies中找到,并且responseCookies()函数在打印详细信息之前从response.headers返回 cookie 值:

    Headers: {'Set-Cookie': session=eyJjc3JmX3Rva2VuIjoicUlPVGNnQ2FKZmJaS3NOdmlIREFWbVdvWGtMakJkVXl1U3BScmVZTWhRd0d6dEZueFBsRSJ9.D68Log.3ANox76h0whpTRjkqNo7JRgCtWI; HttpOnly; Path=/',...,'Content-Encoding':'gzip','Content-Type':'text/html; charset=utf-8',......}
    
    Cookies: <RequestsCookieJar[<Cookie session=eyJjc3JmX3Rva2VuIjoicUlPVGNnQ2FKZmJaS3NOdmlIREFWbVdvWGtMakJkVXl1U3BScmVZTWhRd0d6dEZueFBsRSJ9.D68Log.3ANox76h0whpTRjkqNo7JRgCtWI for quotes.toscrape.com/>]>
    
    Set-Cookie: session=eyJjc3JmX3Rva2VuIjoicUlPVGNnQ2FKZmJaS3NOdmlIREFWbVdvWGtMakJkVXl1U3BScmVZTWhRd0d6dEZueFBsRSJ9.D68Log.3ANox76h0whpTRjkqNo7JRgCtWI; HttpOnly; Path=/
    

    requests.post()短语使用 HTTP POST请求到loginURL,并使用已设置的paramscustomHeaderscustomHeaders是使用我们之前收到的setCookie值创建的:

    现在我们已经收到了基于 cookie 的会话值,我们需要维护这个值,以便进行成功的登录过程。

    Cookies: www.aboutcookies.org/ , www.allaboutcookies.org/

    在下一章中,我们将使用 Python 编程语言与 Web API 进行数据提取交互。

    浏览器开发者工具中的元素面板与页面源

    以下截图显示了成功的身份验证和验证信息:

    在用户和网站之间保持安全措施是一项具有挑战性和危险性的任务。存在不同的安全问题需要加以管理。网络上存在各种新概念,需要有效合法地处理,以便进行网络抓取活动。

    浏览器开发者工具:developers.google.com/web/tools/chrome-devtools/, developer.mozilla.org/son/docs/Tools

    responseA = pq(response.text)
    csrf_token = responseA.find('input[name="csrf_token"]').attr('value')
    username = responseA.find('input[id="username"]').attr('name')
    password = responseA.find('input[id="password"]').attr('name')
    
    params = {username: 'test', password: 'test', 'csrf_token': csrf_token}
    print(params)
    

    让我们收集基于<form>的字段以及有关表单提交的更多信息:

    {'password':'test','username':'test','csrf_token':'jJgAHDQykMBnCFsPIZOoqdbflYRzXtSuiEmwKeGavVWxpNLUhrcT'}
    

    通过<form>元素的name属性作为键和默认值构建要通过表单操作提交的参数,并分别需要接收值作为它们的值。

    进一步阅读

    customHeaders = getCustomHeaders(setCookie)
    response = requests.post(loginUrl, data=params, headers=customHeaders)
    setCookie = responseCookies(response)
    #print("Set-Cookie: ",setCookie)
    
    responseB = pq(response.text)
    logoutText = responseB.find('a[href*="logout"]').text()
    logoutLink = responseB.find('a[href*="logout"]').attr('href')
    
    print("Current Page : ",response.url)
    print("Confirm Login : ", responseB.find('.row h2').text())
    print("Logout Info : ", logoutText," & ",logoutLink)
    

    Current Page : http://quotes.toscrape.com/
    Confirm Login : Top Ten tags
    Logout Info : Logout & /logout
    

    最后,我们收到了成功的输出,以及重定向的 URL 和有关注销的信息:

    在这个例子中,usernamepassword是开放的字符串值,test已经被用于两者:

    没有key命名为Cookie的空customHeaderscustomHeaders将无法成功进行身份验证。同样,csrf_token也是必需的参数。即使提供了所需的key:value信息对,发布的、更新的或空的csrf_token也将无法成功进行身份验证。

    总结

    在本章中,我们探讨了一些与安全问题相关的基本措施和技术,这些问题经常出现,对于网络抓取来说是具有挑战性的。

    AJAX: api.jquery.com/jquery.ajax/, www.w3schools.com/js/js_ajax_intro.asp

    跨站请求伪造CSRF)或会话劫持是一种安全措施,用于识别用户和网站之间的每个单独请求。通常,CSRF_TOKEN或令牌用于管理这样的机制。当用户向网站发出请求时,网站会生成一个随机字符串的令牌。处理网站的任何形式的 HTTP 请求都需要令牌值。每个成功请求的令牌值都会发生变化。包含令牌值的 HTML <form>可以使用已更新或已删除的令牌进行处理,但网站不会接受这些令牌。

    quotes.toscrape.com/验证的成功身份验证信息

    第七章:使用基于 Web 的 API 进行数据提取

    基于 Web 的 API 允许用户与网络上的信息进行交互。API 直接处理格式化模式易于使用和维护的数据。有些 API 在向用户提供数据之前还需要用户身份验证。本章将介绍使用 Python 和一些 Web API 与可用 API 进行交互和提取数据。通常,API 以可交换的文档格式(如 JSON、CSV 和 XML)提供数据。

    在本章中,我们将涵盖以下主题:

    • Web API 简介

    • 使用 Python 编程语言访问 Web API

    • 通过 Web API 处理和提取数据

    技术要求

    本章需要使用 Web 浏览器(Google Chrome 或 Mozilla Firefox)。我们将使用以下 Python 库:

    • requests

    • json

    • collections

    如果这些库在您当前的 Python 设置中不存在,请参考第二章,Python 和 Web-使用 urllib 和 Requests,在设置事项部分了解如何下载它们。

    本章的代码文件可在本书的 GitHub 存储库中找到:github.com/PacktPublishing/Hands-On-Web-Scraping-with-Python/tree/master/Chapter07

    Web API 简介

    基于 Web 的应用程序编程信息基于 Web 的 API是网站提供的接口,用于返回接收到的请求的信息。Web API(或 API)实际上是网站为用户或第三方 Web 应用程序或自动化脚本提供的 Web 服务,以便共享和交换信息。

    通常,这是通过 Web 浏览器处理的用户界面(UI),用于从已向网站或 Web 服务器发出的请求中检索特定信息。具有任何类型大量信息的网站可以为其用户提供 Web API,以便进行信息共享。

    在软件应用领域,API 以其一组设施(如方法和库)而闻名,可用于进一步增强、构建或开发应用程序。这也被称为开发者 API。

    Web API 不依赖于任何编程语言。它们使得以原始格式轻松访问基于 Web 的信息,并通常以 JSON、XML 或 CSV 格式返回结构化响应。

    它们遵循 HTTP 原则(请求和响应循环),但只接受预定义格式的请求和参数集以生成响应。在安全方面,许多 API 还提供身份验证工具,如 API 密钥,这是向网站发出请求所必需的。

    REST 和 SOAP

    API 是由基于软件架构或原则的 Web 服务器提供的服务。简单对象访问协议SOAP)和表述状态转移REST)是访问 Web 服务的方法。虽然 REST 是一种架构,但 SOAP 是基于 Web 标准的协议。我们将在接下来的部分中处理 REST API。

    REST

    REST(www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm)是一种基于一组定义和解决网络原则的软件架构风格。REST 是一种软件架构,而不是一组标准。REST 使用标准的 HTTP 协议和方法,如GETPOSTPUTDELETE来提供服务。它是无状态的、多层的,也支持缓存。

    Web API 通常被归类为 RESTful Web 服务;它们为用户和其他资源提供通信接口。RESTful Web 服务(REST API 或 Web API)(restfulapi.net/)是 Web 提供的适应 REST 架构的服务。

    通过 REST 提供的服务无需适应新的标准、开发或框架。大多数情况下,它将使用 GET 请求,以及已发出到 API 的查询字符串,搜索其响应。通常会跟踪 HTTP 状态码(restfulapi.net/http-status-codes/)(404、200、304)以确定 API 的响应。响应也可以以 JSON、XML 和 CSV 等各种格式获取。

    在选择 REST 和 SOAP 之间,REST 在处理方面比 SOAP 更容易和高效,并且被许多网站提供给公众。

    SOAP

    SOAP(www.w3.org/TR/soap/is)是由 W3C 指定的一组标准,也是 Web 服务中与 REST 相对应的选择。SOAP 使用 HTTP 和 SMTP(简单邮件传输协议),用于在互联网上交换文档,以及通过远程过程。

    SOAP 使用 XML 作为消息服务,也被称为基于 XML 的协议。SOAP 请求包含描述发送到服务器的方法和参数的 XML 文档(带有信封和正文)。服务器将执行接收到的方法,以及参数,并将 SOAP 响应发送回发起请求的程序。

    SOAP 具有高度的可扩展性,并包括内置的错误处理。它还与其他协议(如 SMTP)一起工作。SOAP 也独立于平台和编程语言,并且主要在分布式企业环境中实现。

    Web API 的好处

    信息需求与其在网络上的可用性一天比一天增长。信息来源、其可用性、设施和共享和交换技术已成为全球需求。API 是首选的数据来源之一,可用于检索数据。

    API 不仅是通过 Web 浏览器与用户进行通信的一种方式-您还可以使用系统。API 允许系统和设备之间的通信,例如移动设备,尽管它们的基础系统或编程语言不同。许多移动应用程序会向某些 API 发出请求,并显示从响应中检索到的相关信息。API 不仅是用于检索数据的简单服务;它们用于交换和处理信息,甚至在不同平台和服务之间进行系统间通信。

    从网络抓取的角度来看,通过 API 可用的响应或数据优于使用抓取脚本检索的数据。这是由于以下原因:

    • API 返回的数据完全特定于正在执行的请求,以及已应用于它的过滤器或参数。

    • 使用 Python 库(如 BeautifulSoup、pyquery 和 lxml)解析 HTML 或 XML 并不总是必需的。

    • 数据的格式是结构化的,易于处理。

    • 数据清理和处理最终列表将更容易或可能不需要。

    • 与编码、分析网页并应用 XPath 和 CSS 选择器来检索数据相比,处理时间会显著减少。

    • 它们易于处理。

    在完全从抓取的角度转向 Web API 之前,还有一些因素需要考虑,包括以下内容:

    • 并非所有网站都向用户提供访问 Web API 的权限。

    • API 的响应是特定于预定义参数集的。这可能限制基于需求可以进行的确切请求,并限制立即获取的数据的可用性。

    • 返回的响应受限于一定的数量,例如每个请求返回的记录数以及允许的最大请求数量。

    • 尽管数据将以结构化格式可用,但它可能分布在键值对中,这可能需要一些额外的合并任务。

    鉴于这些观点,我们可以看到 web API 是从网站获取信息的首选选择。

    访问 web API 和数据格式

    在本节中,我们将探讨在 web 上可用的各种 API,向它们发送请求并接收响应,然后解释它们如何通过 Python 编程语言工作。

    让我们考虑以下示例 URL,https://www.someexampledomain.com。它提供的 API 带有参数,定位器和身份验证。通过使用这些,我们可以访问以下资源:

    • https://api.someexampledomain.com 

    • https://api.someexampledomain.com/resource?key1=value1&key2=value2

    • https://api.someexampledomain.com/resource?api_key=ACCESS_KEY&key1=value1&key2=value2

    • https://api.someexampledomain.com/resource/v1/2019/01

    参数或键值对的集合实际上是由 web 提供的预定义变量集。通常,API 提供有关其用法、HTTP 方法、可用键和类型或允许键接收的值的基本指南或文档,以及有关 API 支持的功能的其他信息,如下图所示:

    来自 https://sunrise-sunset.org/api 的 API 详细信息和链接

    最终用户和系统只能使用提供者允许的 API 功能和功能。

    以下是一些实际 API 链接和示例调用,显示了 URL 中使用的格式和参数:

    参数,如keyapi_keyapiKeyapi-key,是为了安全和跟踪措施而需要的,并且在处理任何 API 请求之前需要获得。

    本节中的 API 链接和示例调用与它们所列出的资源相关联。例如,api.twitter.com/1.1/search/tweets.json?q=nasa&result_type=populardeveloper.twitter.com/en/docs/tweets/search/api-reference/get-search-tweets上列出。

    使用 web 浏览器向 web API 发出请求

    获取通过查询字符串应用的参数信息和获取 API 密钥(如果需要)是获得 API 访问权限的初步步骤。与由 Google、Twitter 和 Facebook 提供的开发者 API 相比,大多数公共或免费 API 都非常简单易懂。

    API 请求可以通过 Web 浏览器进行。但是,在这一部分,我们将尝试展示访问 API 时可能遇到的一些常见情况,同时展示 RESTful API 的一些重要属性。

    案例 1 - 访问简单的 API(请求和响应)

    在这一部分,我们将使用以下 URL:api.sunrise-sunset.org/json?lat=27.717245&lng=85.323959&date=2019-03-04

    让我们通过一个简单的 API 处理一个请求,以获取尼泊尔加德满都的日出和日落时间(以 UTC 时间为准)。查询字符串需要为所选位置的lat(纬度)、lng(经度)和date提供值。如下面的截图所示,我们获得的响应是以 JSON 格式(使用浏览器扩展格式化)返回的,通过使用基于浏览器的开发者工具验证了成功的请求方法和 HTTP 状态码(200,即OK成功):

    来自api.sunrise-sunset.org/json?lat=27.717245&lng=85.323959&date=2019-03-04的响应状态码

    响应以原始格式或 JSON 格式返回,如下面的代码所示。当正常获取 JSON 响应时,可以使用 Python 的json库进行处理。在下面的代码中,API 请求已经使用requests库进行处理。requests提供了处理 HTTP 的各种功能;例如,可以使用status_code获取 HTTP 状态码。可以使用headers获取头信息。在这里,我们对status_codeheaders特别感兴趣,特别是Content-Type,以便我们可以计划进一步处理和可能需要使用的库:

    import requests
    url = 'https://api.sunrise-sunset.org/json?lat=27.7172&lng=85.3239&date=2019-03-04'   results = requests.get(url) #request url
    print("Status Code: ", results.status_code)
    print("Headers-ContentType: ", results.headers['Content-Type'])
    print("Headers: ", results.headers)
    
    jsonResult = results.json() #read JSON content
    print("Type JSON Results",type(jsonResult))
    print(jsonResult)
    print("SunRise & Sunset: ",jsonResult['results']['sunrise']," & ",jsonResult['results']['sunset'])
    

    如我们所见,status_code200(即OK),Content-Type是 JSON 类型。这给了我们确认,我们可以使用与 JSON 相关的库继续前进。但是,在这种情况下,我们使用了requests库中的json()函数,这减少了我们对额外库的依赖,并将响应对象转换为dict对象。通过收到的dict,我们可以使用key:value对访问所需的元素:

    Type Results <class 'requests.models.Response'>
    Status Code: 200
    Headers-ContentType: application/json
    
    Headers: {'Access-Control-Allow-Origin':'*','Content-Type':'application/json','Vary':'Accept-Encoding', 'Server':'nginx','Connection':'keep-alive','Content-Encoding':'gzip','Transfer-Encoding':'chunked','Date': 'Mon, 04 Mar 2019 07:48:29 GMT'}
    
    Type JSON Results <class 'dict'>
    
    {'status':'OK','results':{'civil_twilight_end':'12:44:16 PM','astronomical_twilight_end':'1:38:31 PM', 'civil_twilight_begin':'12:16:32 AM','sunrise':'12:39:54 AM',......,'sunset':'12:20:54 PM','solar_noon': '6:30:24 AM','day_length':'11:41:00'}}
    
    SunRise & Sunset: 12:39:54 AM & 12:20:54 PM** 
    

    案例 2 - 展示 API 的状态码和信息响应

    在这一部分,我们将使用以下 URL:api.twitter.com/1.1/search/tweets.json?q=

    在这一部分,我们将处理来自 Twitter 的 API 请求。要请求的 URL 是api.twitter.com/1.1/search/tweets.json?q=。通过使用这个 URL,我们可以很容易地确定查询字符串q是空的,Twitter API 期望的值没有提供。完整的 URL 应该是类似于api.twitter.com/1.1/search/tweets.json?q=nasa&result_type=popular

    返回的响应是不完整的 API 调用,如下面的截图所示,还有 HTTP 状态码(400Bad Request)以及 API 返回的消息,指出了“message”:“Bad Authentication data”的错误。有关 Twitter API 的搜索选项的更多信息,请参阅developer.twitter.com/en/docs/tweets/search/api-reference/get-search-tweets

    向 Twitter API 发出的不完整请求

    Twitter API 返回的响应实际上是信息,而不是错误。这种信息性的响应使 API 在被其他资源使用时更具可伸缩性和易于调试。这也是 RESTful web 服务的一个受欢迎的特性。这种信息可以通过部署 API 参数和其他要求轻松地克服。

    以下代码将使用空查询字符串向 Twitter 发出请求并识别响应:

    import requests
    import json
    url = 'https://api.twitter.com/1.1/search/tweets.json?q='  
    results = requests.get(url)
    print("Status Code: ", results.status_code)
    print("Headers: Content-Type: ", results.headers['Content-Type'])
    
    jsonResult = results.content    #jsonResult = results.json() print(jsonResult)
    
    jsonFinal = json.loads(jsonResult.decode())
    print(jsonFinal) #print(json.loads(requests.get(url).content.decode()))   if results.status_code==400:
     print(jsonFinal['errors'][0]['message'])
    else:
     pass
    

    前面的代码使用json Python 库加载了使用loads()函数获得的解码jsonResult。我们也可以像在案例 1 中那样使用requests中的json()jsonFinal现在是一个 Python 字典对象,可以被探索,以便我们可以找到它的'key:value'。最终输出如下:

    Status Code: 400
    Headers: Content-Type: application/json; charset=utf-8
    
    b'{"errors":[{"code":215,"message":"Bad Authentication data."}]}'
    {'errors': [{'message': 'Bad Authentication data.', 'code': 215}]}
    
    Bad Authentication data.
    

    案例 3 - 展示 RESTful API 缓存功能

    在本节中,我们将使用以下 URL:api.github.com/

    GitHUb(github.com/)是开发人员及其代码存储库的地方。GitHub API 在开发人员中非常有名,他们都来自不同的编程背景。正如我们在下面的截图中所看到的,响应是以 JSON 格式获得的。由于返回的 HTTP 状态码是200,即OK成功,因此请求是成功的:

    来自 https://api.github.com 的响应,HTTP 状态码为 200

    如您所见,我们对api.github.com进行了基本调用。返回的内容包含 API 的链接,以及一些参数供特定调用使用,例如{/gist_id}{/target}{query}

    让我们再次向 API 发送请求,但这次参数值没有任何更改或更新。我们将收到的内容与之前的响应类似,但 HTTP状态码将有所不同;也就是说,与 200OK相比,我们将获得304 未修改

    https://api.github.com 的 HTTP 状态码 304

    这个 HTTP 状态码(304未修改)展示了 REST 的缓存功能。由于响应没有任何更新或更新的内容,客户端缓存功能开始发挥作用。这有助于处理时间,以及带宽时间和使用。缓存是 RESTful web 服务的重要属性之一。以下是 Python 代码,显示了 RESTful API 的缓存属性,通过传递外部标头,这些标头被提供给headers参数,同时使用requests.get()发出请求获得:

    import requests
    url = 'https://api.github.com'  #First Request results = requests.get(url)
    print("Status Code: ", results.status_code)
    print("Headers: ", results.headers)
    
    #Second Request with 'headers'
    etag = results.headers['ETag']
    print("ETag: ",etag)
    
    results = requests.get(url, headers={'If-None-Match': etag})
    print("Status Code: ", results.status_code)
    

    requests在代码中两次调用url。我们还可以看到第二个请求已经提供了etag作为头信息,即If-None-Match。这个特定的头部检查使用ETag键作为 HTTP 响应头获得的响应头。ETag用于跟踪目的,通常标识存在的资源。这展示了缓存能力。有关ETag的更多信息,请参阅developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag

    ETag是从results.headers中收集的,并且随着获得 HTTP状态码:304的第二个请求一起转发。以下代码显示了输出:

    Status Code: 200
    Headers: Content-Type: application/json; charset=utf-8
    Headers: {'X-GitHub-Request-Id': 'A195:073C:37F223:79CCB0:5C8144B4', 'Status': '200 OK','ETag': 'W/"7dc470913f1fe9bb6c7355b50a0737bc"', 'Content-Encoding': 'gzip','Date': 'Thu, 07 Mar 2019 16:20:05 GMT',........, 'Content-Type': 'application/json; charset=utf-8', ....., 'Server': 'GitHub.com'}
    
    ETag: W/"7dc470913f1fe9bb6c7355b50a0737bc"
    Status Code: 304
    

    在本节中,我们已经学习了各种 API,通过使用功能访问它们,并演示了与网页抓取方法相关的一些重要概念。在下一节中,我们将使用 API 来抓取数据。

    使用 API 进行网页抓取

    在这一部分,我们将请求 API 并通过它们收集所需的数据。从技术上讲,通过 API 获取的数据并不类似于进行爬取活动,因为我们不能仅从 API 中提取所需的数据并进一步处理它。

    示例 1 - 搜索和收集大学名称和 URL

    在这个例子中,我们将使用 HIPO 提供的 API(hipolabs.com/)来搜索大学:universities.hipolabs.com/search?name=Wales

    这个 API 使用一个名为name的查询参数,它将寻找大学名称。我们还将提供一个额外的参数country,其中包括美国和英国等国家名称。可以从以下 URL 请求此 API,更多信息可以在github.com/hipo/university-domains-list找到:

    让我们导入所需的库并使用readUrl()函数来请求 API 并返回 JSON 响应,如下面的代码所示:

    import requests
    import json
    dataSet = []
     def readUrl(search):
        results = requests.get(url+search)
        print("Status Code: ", results.status_code)
        print("Headers: Content-Type: ", results.headers['Content-Type'])
      return results.json()
    

    通过返回的 JSON 响应,可以使用我们找到的键和索引检索所需的值,如下面的屏幕截图所示:

    从 API 中获取的 JSON(格式化)

    nameurl被遍历并附加到dataSet中:

    url = 'http://universities.hipolabs.com/search?name=' jsonResult = readUrl('Wales') # print(jsonResult)  for university in jsonResult:
        name = university['name']
        url = university['web_pages'][0]
        dataSet.append([name,url])
     print("Total Universities Found: ",len(dataSet))
    print(dataSet)
    

    最终输出如下:

    Status Code: 200 Headers: Content-Type: application/json Total Universities Found: 10 [['University of Wales', 'http://www.wales.ac.uk/'], ['University of Wales Institute, Cardiff', 'http://www.uwic.ac.uk/'], ......., ['University of Wales, Lampeter', 'http://www.lamp.ac.uk/'], ['University of Wales, Bangor', 'http://www.bangor.ac.uk/']]  
    

    示例 2 - 从 GitHub 事件中获取信息

    在这个例子中,我们将收集关于type(事件类型)、created_at(事件创建日期)、id(事件标识代码)和repo(存储库名称)的信息。我们将使用以下 URL:api.github.com/events

    GitHub“事件”列出了过去 90 天内执行的公共活动。这些事件以页面形式提供,每页 30 个项目,最多显示 300 个。事件中存在各种部分,所有这些部分都揭示了关于actorrepoorgcreated_attype等的描述。

    有关更多详细信息,请参阅以下链接:developer.github.com/v3/activity/events/

    以下是我们将要使用的代码:

    if __name__ == "__main__":
        eventTypes=[] 
        #IssueCommentEvent,WatchEvent,PullRequestReviewCommentEvent,CreateEvent
      for page in range(1, 4): #First 3 pages
            events = readUrl('events?page=' + str(page))
      for event in events:
                id = event['id']
                type = event['type']
                actor = event['actor']['display_login']
                repoUrl = event['repo']['url']
                createdAt = event['created_at']
                eventTypes.append(type)
                dataSet.append([id, type, createdAt, repoUrl, actor])
    
        eventInfo = dict(Counter(eventTypes))
    
        print("Individual Event Counts:", eventInfo)
        print("CreateEvent Counts:", eventInfo['CreateEvent'])
        print("DeleteEvent Counts:", eventInfo['DeleteEvent'])
    
    print("Total Events Found: ", len(dataSet))
    print(dataSet)
    

    上述代码给出了以下输出:

    Status Code: 200
    Headers: Content-Type: application/json; charset=utf-8
    ................
    Status Code: 200
    Headers: Content-Type: application/json; charset=utf-8
    
    Individual Event Counts: {'IssueCommentEvent': 8, 'PushEvent': 42, 'CreateEvent': 12, 'WatchEvent': 9, 'PullRequestEvent': 10, 'IssuesEvent': 2, 'DeleteEvent': 2, 'PublicEvent': 2, 'MemberEvent': 2, 'PullRequestReviewCommentEvent': 1}
    
    CreateEvent Counts: 12
    DeleteEvent Counts: 2
    Total Events Found: 90
    
    [['9206862975','PushEvent','2019-03-08T14:53:46Z','https://api.github.com/repos/CornerYoung/MDN','CornerYoung'],'https://api.github.com/repos/OUP/INTEGRATION-ANSIBLE','peter-masters'],.....................,'2019-03-08T14:53:47Z','https://api.github.com/repos/learn-co-curriculum/hs-zhw-shoes-layout','maxwellbenton']]
    

    collections Python 模块中的Counter类用于获取eventTypes中元素的个体计数:

    from collections import Counter
    

    总结

    API 提供了几个好处,我们在本章中都已经涵盖了。RESTful Web 服务的需求正在增长,并且将来会比以往更多地促进数据请求和响应。结构化、易访问、基于参数的过滤器使 API 更方便使用,并且在节省时间方面表现出色。

    在下一章中,我们将学习 Selenium 以及如何使用它从网络上爬取数据。

    进一步阅读

    第八章:使用 Selenium 进行 Web 抓取

    到目前为止,我们已经学习了如何使用多种数据查找技术,并通过实现各种 Python 库来访问 Web 内容进行 Web 抓取。

    Selenium 是一个 Web 应用程序测试框架,它自动化浏览操作,并可用于简单和复杂的 Web 抓取活动。 Selenium 提供了一个 Web 浏览器作为接口或自动化工具。使用 JavaScript、cookies、脚本等的动态或安全 Web 内容可以通过 Selenium 的帮助加载、测试,甚至抓取。

    关于 Selenium 框架有很多东西要学习。在本章中,我们将介绍与 Web 抓取相关的框架的主要概念。

    本章将涵盖以下主题:

    • Selenium 简介

    • 使用 Selenium 进行 Web 抓取

    技术要求

    本章需要一个 Web 浏览器(Google Chrome 或 Mozilla Firefox),我们将使用以下 Python 库:

    • selenium(Python 库)

    • re

    如果您当前的 Python 设置中没有这些库,则可以通过参考第二章中的设置事物部分来设置或安装它们。

    除了提到的 Python 库和 Web 浏览器之外,我们还将使用 WebDriver for Google Chrome。

    代码文件可在github.com/PacktPublishing/Hands-On-Web-Scraping-with-Python/tree/master/Chapter08上找到。

    Selenium 简介

    正如我所提到的,Selenium 是一个可以用于 Web 抓取活动的 Web 应用程序框架。它也可以用作浏览器自动化工具。

    与 Web 应用程序相关的任务或活动的自动化,例如以下列表中的任务,涉及在没有人类直接参与的情况下执行这些任务:

    • 浏览

    • 点击链接

    • 保存屏幕截图

    • 下载图像

    • 填写 HTML <form> 模板和许多其他活动

    Selenium 提供了一个 Web 浏览器作为接口或自动化工具。通过浏览操作的自动化,Selenium 也可以用于 Web 抓取。使用 JavaScript、cookies、脚本等的动态或安全 Web 服务可以通过 Selenium 的帮助加载、测试,甚至爬取和抓取。

    Selenium 是开源的,可以跨多个平台访问。可以使用各种 Web 浏览器进行测试,这些浏览器使用可用于编程语言(如 Java 和 Python)的库。使用库创建脚本与 Selenium 交互以执行基于浏览器的自动化。

    尽管在应用程序测试中使用 Selenium 在爬行和抓取等操作方面具有许多优势,但它也有其缺点,例如时间和内存消耗。 Selenium 是可扩展和有效的,但在执行其操作时速度较慢,并且消耗大量内存空间。

    有关 Selenium 的更详细信息,请访问www.seleniumhq.org/

    在接下来的部分中,我们将设置 Selenium WebDriver 并使用 Python 库进行设置,该库可以在selenium-python.readthedocs.io/找到。

    Selenium 是一个 Web 测试框架,而 Selenium (pypi.org/project/selenium/)是一个绑定 Selenium WebDriver 或用于创建与 Selenium 交互的脚本的 Python 库。

    应用程序测试是为了确保应用程序满足要求,并检测错误和错误以确保产品质量而进行的。它可以通过手动(借助用户的帮助)或使用自动化工具(如 Selenium)进行。在互联网上发布应用程序之前,会对基于 Web 的应用程序进行测试。

    Selenium 项目

    Selenium 由多个组件或工具组成,也被称为 Selenium 项目,使其成为一个完整的基于 web 的应用程序测试框架。我们现在将看一些这些 Selenium 项目的主要组件。

    Selenium WebDriver

    Selenium WebDriver 是 Selenium 的一个组件,用于自动化浏览器。通过提供各种语言绑定,如 Java、Python、JavaScript 等,使用第三方驱动程序,如 Google Chrome 驱动程序、Mozilla Gecko 驱动程序和 Opera(github.com/mozilla/geckodriver/)来提供命令来进行浏览器自动化。Selenium WebDriver 不依赖于任何其他软件或服务器。

    WebDriver 是一个面向对象的 API,具有更新的功能,克服并解决了之前 Selenium 版本和 Selenium Remote Control (RC) 的限制。请访问 Selenium WebDriver 网页(www.seleniumhq.org/projects/webdriver/)获取更多信息。

    Selenium RC

    Selenium RC 是一个用 Java 编程的服务器。它使用 HTTP 接受浏览器的命令,用于测试复杂的基于 AJAX 的 web 应用程序。

    Selenium RC 在发布 Selenium 2(Selenium 版本 2)后已正式弃用。然而,WebDriver 包含了 Selenium RC 的主要功能。请访问www.seleniumhq.org/projects/remote-control/ 获取更多信息。

    Selenium Grid

    Selenium Grid 也是一个服务器,允许测试在多台机器上并行运行,跨多个浏览器和操作系统,分发系统负载并减少性能问题,如时间消耗。

    复杂的测试用于同时处理 Selenium RC 和 Selenium Grid。自 2.0 版本发布以来,Selenium 服务器现在内置支持 WebDriver、Selenium RC 和 Selenium Grid。请访问 Selenium Grid 网页(www.seleniumhq.org/projects/grid/)获取更多信息。

    Selenium IDE

    一个开源的 Selenium 集成开发环境 (IDE) 用于使用 Selenium 构建测试用例。它基本上是一个网页浏览器扩展,具有诸如记录和通过图形用户 界面 (GUI) 回放网页自动化等功能。

    以下是 Selenium IDE 的一些关键特性:

    • 可扩展且易于调试

    • 韧性测试

    • 跨浏览器支持

    • 可以创建可以运行命令并支持控制流结构的脚本

    请访问 Selenium IDE 网页(www.seleniumhq.org/selenium-ide/)获取更多信息和安装程序。请访问 Selenium 项目网页(www.seleniumhq.org/projects/)获取有关 Selenium 组件的更多信息。

    现在我们知道了 Selenium 的用途和一些主要组件,让我们看看如何安装和使用 Selenium WebDriver 进行一般测试。

    设置事物

    为了成功实现使用 Selenium 进行浏览器自动化和应用程序测试,需要设置 WebDriver。让我们通过以下步骤来设置 Google Chrome 的 WebDriver:

    1. 访问www.seleniumhq.org/

     SeleniumHQ 浏览器自动化主页

    1. 点击下载(或浏览至www.seleniumhq.org/download/)。

    2. 在第三方驱动程序、绑定和插件部分,点击 Google Chrome Driver(或浏览至sites.google.com/a/chromium.org/chromedriver/):

     第三方驱动程序,Selenium

    1. 从 ChromeDriver - WebDriver for Chrome (sites.google.com/a/chromium.org/chromedriver),下载适用于平台的最新稳定版本的 ChromeDriver:

    ChromeDriver 列表

    1. 解压下载的chromedriver*.zip。应该出现一个名为chromedriver.exe的应用程序文件。我们可以将.exe文件放在包含代码的主文件夹中。

    我们将在整个章节中使用谷歌浏览器和 ChromeDriver;有关使用其他浏览器的详细信息,或有关 Selenium 的更多信息,请访问 SeleniumHQ。有关安装的更多信息,请参阅selenium-python.readthedocs.io/installation.html

    现在我们已经完成了 WebDriver 和 Selenium Python 库的设置,让我们通过 Python IDE 验证这个设置。如下面的屏幕截图所示,selenium包含webdriver模块,包括ChromeAndroidFirefoxIeOpera等子模块。当前版本是3.14.1

    打印 selenium.webdriver 版本

    我们将使用 Selenium 与谷歌浏览器,因此让我们探索webdriverChrome的内容:

    从 Selenium WebDriver 探索 Chrome。

    如前面的屏幕截图所示,有许多函数将被调用和用于实现浏览器自动化。您还可以看到许多函数名称以find_element*开头,类似于我们在早期章节中用于爬取活动的遍历和解析函数。

    在下一节中,我们将学习关于selenium.webdriver

    探索 Selenium

    在本节中,我们将使用和介绍webdriverwebdriver.Chrome的各种属性,同时查看一些真实案例。接下来的章节将说明 Selenium 的使用并探索其主要属性。

    访问浏览器属性

    在本节中,我们将演示使用 Selenium 和 Chrome WebDriver 加载谷歌浏览器的 URL 并访问某些基于浏览器的功能。

    首先,让我们从selenium中导入webdriver并设置到chromedriver.exe的路径,让我们称之为chromedriver_path。创建的路径将需要加载谷歌浏览器。根据应用程序位置,应提及chromedriver.exe的完整路径,并且对于成功实施是必需的:

    from selenium import webdriver
    import re
    
    #setting up path to 'chromedriver.exe'
    chromedriver_path='chromedriver' #C:\\Users\\....\\...\chromedriver.exe 
    

    selenium.webdriver用于实现各种浏览器,在本例中是谷歌浏览器。webdriver.Chrome()短语提供了 Chrome WebDriver 的路径,以便chromedriver_path用于执行。

    短语driverwebdriver.chrome.webdriver.WebDriver类的对象,使用webdriver.Chrome()创建,现在将提供对webdriver的各种属性和属性的访问:

    driver = webdriver.Chrome(executable_path=chromedriver_path)
    

    chromedriver.exe将在此实例或在driver对象创建时实例化。终端屏幕和空白的新窗口将加载谷歌浏览器,如下面的屏幕截图所示:

    终端屏幕和空白浏览器页面

    如果您在执行到目前为止的代码时遇到任何错误,请按照以下步骤执行代码:

    1. 获取最新的 ChromeDriver 并替换现有的 ChromeDriver

    2. 更新和验证chromedriver_pathPATH

    然后使用get()函数从webdriver为谷歌浏览器提供一个 URL。

    get()短语接受要在浏览器上加载的 URL。让我们将www.python.org作为get()的参数;浏览器将开始加载 URL,如下面的屏幕截图所示:

    driver.get('https://www.python.org')
    

    如您在下面的截图中所见,地址栏下方显示了一个通知,其中包含消息Chrome is being controlled by automated test software。这条消息也确认了selenium.webdriver活动的成功执行,并且可以提供进一步的代码来操作或自动化加载的页面:

    Chrome 浏览器加载了 https://www.python.org

    在页面成功加载后,我们可以使用driver访问和探索其属性。为了说明这一点,让我们从 HTML <title>标签中提取或打印标题,并打印当前可访问的 URL:

    print("Title: ",driver.title) #print <title> text
    Title:  Welcome to Python.org
    
    print("Current Page URL: ",driver.current_url) #print current url, loaded in the browser
    Current Page URL:  https://www.python.org/
    

    如前面的代码所示,可以使用driver.title获取页面标题,使用driver.current_url找到当前页面的 URL。current_url短语可用于验证在加载初始 URL 后是否发生了任何 URL 重定向。让我们使用 Python 库research()保存页面截图:

    #check if pattern matches the current url loaded
    
    if re.search(r'python.org',driver.current_url):
        driver.save_screenshot("pythonorg.png") #save screenshot with provided name
        print("Python Screenshot Saved!")
    

    save_screenshot()短语以文件名作为图像的参数,并创建一个 PNG 图像。图像将保存在当前代码位置;也可以提供完整的目标或所需路径。

    为了进一步探索,让我们从www.python.org收集网页 cookies。使用get_cookies()短语来检索 cookies,如下所示:

    #get cookie information
    cookies = driver.get_cookies() 
    print("Cookies obtained from python.org")
    print(cookies)
    
    Cookies obtained from python.org
    [{'domain': '.python.org', 'expiry': 1619415025, 'httpOnly': False, 'name': '__utma', 'path': '/', 'secure': False, 'value': '32101439.1226541417.1556343026.1556343026.1556343026.1'},........ {'domain': '.python.org', 'expiry': 1556343625, 'httpOnly': False, 'name': '__utmt', 'path': '/', 'secure': False, 'value': '1'}]
    

    可以使用driver.page_source获取页面源。

    要手动获取页面源,请右键单击页面,然后单击“查看页面源”,或按Ctrl + U

    print(driver.page_source) #page source
    

    可以使用driver.refresh()重新加载或刷新页面。

    要手动刷新页面源,请右键单击页面,然后单击“重新加载”,或按Ctrl + R

    driver.refresh() #reload or refresh the browser
    

    使用前面代码中的driver访问的功能,让我们继续加载、截图和访问www.google.com的 cookies,使用以下代码:

    driver.get('https://www.google.com')
    print("Title: ",driver.title)
    print("Current Page URL: ",driver.current_url)
    
    if re.search(r'google.com',driver.current_url):
        driver.save_screenshot("google.png")
        print("Google Screenshot Saved!")
    
    cookies = driver.get_cookies()
    

    使用google.com执行的操作将在用于访问python.org的同一浏览器窗口上进行。有了这个,我们现在可以使用浏览器历史记录执行操作(即,我们将使用 Web 浏览器中可用的“返回”和“前进”按钮),并检索 URL,如下面的代码所示:

    print("Current Page URL: ",driver.current_url)
    
    driver.back() #History back action
    print("Page URL (Back): ",driver.current_url)
    
    driver.forward() #History forward action
    print("Page URL (Forward): ",driver.current_url)
    

    在上述代码中,back()将浏览器返回到上一页,而forward()将其沿着浏览器历史向前移动一步。收到的输出如下:

    Current Page URL: https://www.google.com/
    Page URL (Back): https://www.python.org/
    Page URL (Forward): https://www.google.com/
    

    在成功执行代码后,建议您关闭并退出驱动程序以释放系统资源。我们可以使用以下功能执行终止操作:

    driver.close() #close browser
    driver.quit()  #quit webdriver
    

    上述代码包含以下两个短语:

    • close()终止加载的浏览器窗口

    • quit()结束 WebDriver 应用程序

    到目前为止,在本节中我们执行的完整代码如下:

    from selenium import webdriver
    import re
    chrome_path='chromedriver'
    driver = webdriver.Chrome(executable_path=chrome_path)  #print(type(driver))
    driver.get('https://www.python.org')  
    print("Title: ",driver.title)
    print("Current Page URL: ",driver.current_url)
    
    if re.search(r'python.org',driver.current_url):
        driver.save_screenshot("pythonorg.png")
        print("Python Screenshot Saved!")
    cookies = driver.get_cookies()
    
    print(driver.page_source)
    driver.refresh()
    
    driver.get('https://www.google.com')
    print("Title: ",driver.title)
    print("Current Page URL: ",driver.current_url)
    if re.search(r'google.com',driver.current_url):
        driver.save_screenshot("google.png")
        print("Google Screenshot Saved!")
    cookies = driver.get_cookies()
    
    print("Current Page URL: ",driver.current_url)
    driver.back()
    print("Page URL (Back): ",driver.current_url)
    driver.forward()
    print("Page URL (Forward): ",driver.current_url)
    
    driver.close()
    driver.quit()
    

    上述代码演示了selenium.webdriver及其各种属性的使用。在下一节中,我们将演示webdriver和网页元素(网页中的元素)的使用。

    定位网页元素

    在本节中,我们将在automationpractice.com上进行搜索,以获取与搜索查询匹配的产品列表,演示selenium.webdriver的使用。网页元素是列在网页上或在页面源中找到的元素。我们还看一下一个名为WebElement的类,它被用作selenium.webdriver.remote.webelement.WebElement

    自动化实践网站(automationpractice.com/)是来自www.seleniumframework.com的一个示例电子商务网站,您可以用来练习。

    首先,让我们从selenium中导入webdriver,设置chromedriver.exe的路径,创建webdriver的对象——也就是在前一节访问浏览器属性中实现的driver,并加载 URL,automationpractice.com

    driver.get('http://automationpractice.com')
    

    新的 Google Chrome 窗口将加载提供的 URL。如下图所示,找到位于购物车上方的搜索(输入)框:

    http://automationpractice.com 检查元素(搜索框)

    要继续通过脚本搜索,我们需要识别具有 HTML <input>的元素。请参阅第三章中的使用 Web 浏览器开发者工具访问 Web 内容部分,使用 LXML、XPath 和 CSS 选择器

    在我们的情况下,搜索框可以通过前面截图中显示的属性来识别,甚至可以使用 XPath 或 CSS 选择器:

    • id="search_query_top"

    • name="search_query"

    • class="search_query"

    selenium.webdriver提供了许多定位器(用于定位元素的方法),可以方便地应用于遇到的情况。

    定位器返回单个、多个或 WebElement 实例的列表,写作selenium.webdriver.remote.webelement.WebElement。以下是一些定位器以及简要描述:

    • find_element_by_id(): 通过其id属性来查找元素。此方法返回单个 WebElement。

    • find_element_by_name(): 通过其name属性来查找单个元素。可以使用find_elements_by_name()来找到或定位多个 WebElement。

    • find_element_by_tag_name(): 通过其 HTML 标签的名称来查找单个元素。可以使用find_elements_by_tag_name()来定位多个 WebElement。

    • find_element_by_class_name(): 通过其class属性来查找单个元素。可以使用find_elements_by_class_name()来定位多个 WebElement。

    • find_element_by_link_text(): 通过链接文本标识的链接来查找单个元素。可以使用find_elements_by_link_text()来定位多个 WebElement。

    • find_element_by_partial_link_text(): 通过元素携带的部分文本来查找单个元素的链接。可以使用find_elements_by_partial_link_text()来定位多个 WebElement。

    • find_element_by_xpath(): 通过提供 XPath 表达式来查找单个元素。可以使用find_elements_by_xpath()来定位多个 WebElement。

    • find_element_by_css_selector(): 通过提供 CSS 选择器来查找单个元素。可以使用find_elements_by_css_selector()来定位多个 WebElement。

    现在,让我们使用find_element_by_id()来找到输入框:

    searchBox = driver.find_element_by_id('search_query_top')
    #searchBox = driver.find_element_by_xpath('//*[@id="search_query_top"]')
    #searchBox = driver.find_element_by_css_selector('#search_query_top')
    

    如前面的代码所示,searchBox可以使用任何方便的定位器来定位,这些定位器都提供了它们各自的参数。

    获得的 WebElement 可以访问以下属性和一般方法,以及许多其他方法:

    • get_attribute(): 返回提供的键参数的属性值,例如valueidnameclass

    • tag_name: 返回特定 WebElement 的 HTML 标签名称。

    • text: 返回 WebElement 的文本。

    • clear(): 这会清除 HTML 表单元素的文本。

    • send_keys(): 用于填充文本并提供键效果,例如按下ENTERBACKSPACEDELETE,可从selenium.webdriver.common模块中的selenium.webdriver.common.keys模块中获得,应用于 HTML 表单元素。

    • click(): 执行单击操作到 WebElement。用于 HTML 元素,如提交按钮。

    在下面的代码中,我们将使用前面列出的searchBox中的函数和属性:

    print("Type :",type(searchBox))
    <class 'selenium.webdriver.remote.webelement.WebElement'>
    
    print("Attribute Value :",searchBox.get_attribute("value")) #is empty
    Attribute Value *:* 
    
    print("Attribute Class :",searchBox.get_attribute("class"))
    Attribute Class : search_query form-control ac_input
    
    print("Tag Name :",searchBox.tag_name)
    Tag Name : input
    

    让我们清除searchBox内的文本并输入要搜索的文本Dress。我们还需要提交位于searchBox右侧的按钮,并点击它以使用 WebElement 方法click()执行搜索:

    searchBox.clear() 
    searchBox.send_keys("Dress")
    submitButton = driver.find_element_by_name("submit_search")
    submitButton.click()
    

    浏览器将处理提交的文本Dress的搜索操作并加载结果页面。

    现在搜索操作完成,为了验证成功的搜索,我们将使用以下代码提取有关产品数量和计数的信息:

    #find text or provided class name
    resultsShowing = driver.find_element_by_class_name("product-count")
    print("Results Showing: ",resultsShowing.text) 
    
    Results Showing: Showing 1-7 of 7 items
    
    #find results text using XPath
    resultsFound = driver.find_element_by_xpath('//*[@id="center_column"]//span[@class="heading-counter"]')
    print("Results Found: ",resultsFound.text)
    
    Results Found: 7 results have been found.
    

    通过找到的项目数量和产品数量,这传达了我们搜索过程的成功信息。现在,我们可以继续使用 XPath、CSS 选择器等查找产品:

    #Using XPath
    products = driver.find_elements_by_xpath('//*[@id="center_column"]//a[@class="product-name"]')
    
    #Using CSS Selector
    #products = driver.find_elements_by_css_selector('ul.product_list li.ajax_block_product a.product-name')
    
    foundProducts=[]
    for product in products:
        foundProducts.append([product.text,product.get_attribute("href")])
    

    从前面的代码中,对获得的products进行迭代,并将单个项目添加到 Python 列表foundProducts中。product是 WebElement 对象,换句话说,是selenium.webdriver.remote.webelement.WebElement,而属性是使用textget_attribute()收集的:

    print(foundProducts) 
    
    [['Printed Summer Dress',
    'http://automationpractice.com/index.php?id_product=5&controller=product&search_query=Dress&results=7'],
    ['Printed Dress',
    'http://automationpractice.com/index.php?id_product=4&controller=product&search_query=Dress&results=7'],
    ['Printed Summer Dress',
    'http://automationpractice.com/index.php?id_product=6&controller=product&search_query=Dress&results=7'],
    ['Printed Chiffon Dress',
    'http://automationpractice.com/index.php?id_product=7&controller=product&search_query=Dress&results=7'],['PrintedDress',
    'http://automationpractice.com/index.php?id_product=3&controller=product&search_query=Dress&results=7'],
    ['Faded Short Sleeve T-shirts',
    'http://automationpractice.com/index.php?id_product=1&controller=product&search_query=Dress&results=7'],['Blouse',
    'http://automationpractice.com/index.php?id_product=2&controller=product&search_query=Dress&results=7']]
    

    在本节中,我们探索了selenium.webdriver中用于处理浏览器、使用 HTML 表单、读取页面内容等的各种属性和方法。请访问selenium-python.readthedocs.io了解有关 Python Selenium 及其模块的更详细信息。在下一节中,我们将使用本节中使用的大部分方法来从网页中抓取信息。

    使用 Selenium 进行网页抓取

    Selenium 用于测试 Web 应用程序。它主要用于使用各种基于编程语言的库和浏览器驱动程序执行浏览器自动化。正如我们在前面的探索 Selenium部分中看到的,我们可以使用 Selenium 导航和定位页面中的元素,并执行爬取和抓取相关的活动。

    让我们看一些使用 Selenium 从网页中抓取内容的示例。

    示例 1 - 抓取产品信息

    在这个例子中,我们将继续使用探索 Selenium部分获得的foundProducts的搜索结果。

    我们将从foundProducts中找到的每个单独的产品链接中提取一些特定信息,列举如下:

    • product_name:产品名称

    • product_price:列出的价格

    • image_url:产品主要图片的 URL

    • item_condition:产品的状态

    • product_description:产品的简短描述

    使用driver.get()加载foundProducts中的每个单独的产品链接:

    dataSet=[]
    if len(foundProducts)>0:
       for foundProduct in foundProducts:
           driver.get(foundProduct[1])
    
           product_url = driver.current_url
           product_name = driver.find_element_by_xpath('//*[@id="center_column"]//h1[@itemprop="name"]').text
           short_description = driver.find_element_by_xpath('//*[@id="short_description_content"]').text
           product_price = driver.find_element_by_xpath('//*[@id="our_price_display"]').text
           image_url = driver.find_element_by_xpath('//*[@id="bigpic"]').get_attribute('src')
           condition = driver.find_element_by_xpath('//*[@id="product_condition"]/span').text
           dataSet.append([product_name,product_price,condition,short_description,image_url,product_url])
    
    print(dataSet)
    

    使用 XPath 获取要提取的目标字段或信息,并将其附加到dataSet。请参考第三章中的使用 Web 浏览器开发者工具访问 Web 内容部分,使用 LXML、XPath 和 CSS 选择器

    dataSet中获取的输出如下:

    [['Printed Summer Dress','$28.98','New','Long printed dress with thin adjustable straps. V-neckline and wiring under the bust with ruffles at the bottom of the dress.', 'http://automationpractice.com/img/p/1/2/12-large_default.jpg', 'http://automationpractice.com/index.php?id_product=5&controller=product&search_query=Dress&results=7'],
    ['Printed Dress','$50.99','New','Printed evening dress with straight sleeves with black .............,
    ['Blouse','$27.00','New','Short sleeved blouse with feminine draped sleeve detail.', 'http://automationpractice.com/img/p/7/7-large_default.jpg','http://automationpractice.com/index.php?id_product=2&controller=product&search_query=Dress&results=7']]
    

    最后,使用close()quit()保持系统资源空闲。此示例的完整代码如下:

    from selenium import webdriver
    chrome_path='chromedriver' driver = webdriver.Chrome(executable_path=chrome_path)
    driver.get('http://automationpractice.com')
    
    searchBox = driver.find_element_by_id('search_query_top')
    searchBox.clear()
    searchBox.send_keys("Dress")
    submitButton = driver.find_element_by_name("submit_search")
    submitButton.click()
    
    resultsShowing = driver.find_element_by_class_name("product-count")
    resultsFound = driver.find_element_by_xpath('//*[@id="center_column"]//span[@class="heading-counter"]')
    
    products = driver.find_elements_by_xpath('//*[@id="center_column"]//a[@class="product-name"]')
    foundProducts=[]
    for product in products:
        foundProducts.append([product.text,product.get_attribute("href")])
    
    dataSet=[]
    if len(foundProducts)>0:
       for foundProduct in foundProducts:
           driver.get(foundProduct[1])
           product_url = driver.current_url
           product_name = driver.find_element_by_xpath('//*[@id="center_column"]//h1[@itemprop="name"]').text
           short_description = driver.find_element_by_xpath('//*[@id="short_description_content"]').text
           product_price = driver.find_element_by_xpath('//*[@id="our_price_display"]').text
           image_url = driver.find_element_by_xpath('//*[@id="bigpic"]').get_attribute('src')
           condition = driver.find_element_by_xpath('//*[@id="product_condition"]/span').text
           dataSet.append([product_name,product_price,condition,short_description,image_url,product_url])
    
    driver.close()
    driver.quit()
    

    在这个例子中,我们执行了基于 HTML <form>的操作,并从每个单独的页面中提取所需的细节。表单处理是在测试 Web 应用程序期间执行的主要任务之一。

    示例 2 - 抓取书籍信息

    在这个例子中,我们将自动化浏览器来处理主 URL 提供的类别和分页链接。我们有兴趣从books.toscrape.com/index.html跨多个页面提取食品和饮料类别的详细信息。

    类别中的单个页面包含产品(书籍)的列表,其中包含以下某些信息:

    • title:书籍的标题

    • titleLarge:列出的书籍标题(完整标题,作为title属性的值找到)

    • price:列出的书籍价格

    • stock:与列出的书籍相关的库存信息

    • image:书籍图片的 URL

    • starRating:评级(找到的星星数量)

    • url:列出每本书的 URL。

    在第三章中还展示了一个类似的例子,使用 LXML、XPath 和 CSS 选择器中的使用 LXML 进行网页抓取部分,名称为示例 2 - 使用 XPath 循环并从多个页面抓取数据。在那里,我们使用了 Python 库lxml

    导入selenium.webdriver并设置 Chrome 驱动程序路径后,让我们开始加载books.toscrape.com/index.html。当主页面加载时,我们将看到各种类别依次列出。

    目标类别包含文本“食品和饮料”,可以使用find_element_by_link_text()找到(我们可以使用任何适用的find_element...方法来找到特定类别)。找到的元素进一步使用click()进行处理 - 即点击返回的元素。此操作将在浏览器中加载特定类别的 URL:

    driver.get('http://books.toscrape.com/index.html')
    
    driver.find_element_by_link_text("Food and Drink").click()
    print("Current Page URL: ", driver.current_url)
    totalBooks = driver.find_element_by_xpath("//*[@id='default']//form/strong[1]")
    print("Found: ", totalBooks.text)
    

    为了处理在迭代过程中找到的多个页面,将从selenium.common.exceptions导入NoSuchElementException

    from selenium.common.exceptions import NoSuchElementException
    

    由于我们将使用分页按钮 next,NoSuchElementException将有助于处理如果没有找到进一步的 next 或页面的情况。

    如下代码所示,分页选项 next 位于页面中,并使用click()操作进行处理。此操作将加载它包含的 URL 到浏览器中,并且迭代将继续直到在页面中找不到或找到 next,被代码中的except块捕获:

    try:
     #Check for Pagination with text 'next'  driver.find_element_by_link_text('next').click()
        continue except NoSuchElementException:
        page = False
    

    此示例的完整代码如下所示:

    from selenium import webdriver
    from selenium.common.exceptions import NoSuchElementException
    chrome_path = 'chromedriver' driver = webdriver.Chrome(executable_path=chrome_path)
    driver.get('http://books.toscrape.com/index.html')
    
    dataSet = []
    driver.find_element_by_link_text("Food and Drink").click()
    totalBooks = driver.find_element_by_xpath("//*[@id='default']//form/strong[1]")
    
    page = True while page:
        listings = driver.find_elements_by_xpath("//*[@id='default']//ol/li[position()>0]")
        for listing in listings:
            url=listing.find_element_by_xpath(".//article[contains(@class,'product_pod')]/h3/a"). get_attribute('href')
            title=listing.find_element_by_xpath(".//article[contains(@class,'product_pod')]/h3/a").text
            titleLarge=listing.find_element_by_xpath(".//article[contains(@class,'product_pod')]/h3/a"). get_attribute('title')
            price=listing.find_element_by_xpath(".//article/div[2]/p[contains(@class,'price_color')]").text
            stock=listing.find_element_by_xpath(".//article/div[2]/p[2][contains(@class,'availability')]"). text
            image=listing.find_element_by_xpath(".//article/div[1][contains(@class,'image_container')]/a/img") .get_attribute('src')
            starRating=listing.find_element_by_xpath(".//article/p[contains(@class,'star-rating')]"). get_attribute('class')
            dataSet.append([titleLarge,title,price,stock,image,starRating.replace('star-rating ',''),url])
    
        try:
      driver.find_element_by_link_text('next').click()
            continue
     except NoSuchElementException:
            page = False 
    driver.close()
    driver.quit()
    

    最后,在迭代完成后,dataSet将包含所有页面的列表数据,如下所示:

    [['Foolproof Preserving: A Guide to Small Batch Jams, Jellies, Pickles, Condiments, and More: A Foolproof Guide to Making Small Batch Jams, Jellies, Pickles, Condiments, and More', 'Foolproof Preserving: A Guide ...','£30.52','In stock', 'http://books.toscrape.com/media/cache/9f/59/9f59f01fa916a7bb8f0b28a4012179a4.jpg','Three','http://books.toscrape.com/catalogue/foolproof-preserving-a-guide-to-small-batch-jams-jellies-pickles-condiments-and-more-a-foolproof-guide-to-making-small-batch-jams-jellies-pickles-condiments-and-more_978/index.html'], ['The Pioneer Woman Cooks: Dinnertime: Comfort Classics, Freezer Food, 16-Minute Meals, and Other Delicious Ways to Solve Supper!', 'The Pioneer Woman Cooks: ...', '£56.41', 'In stock', 'http://books.toscrape.com/media/cache/b7/f4/b7f4843dbe062d44be1ffcfa16b2faa4.jpg', 'One', 'http://books.toscrape.com/catalogue/the-pioneer-woman-cooks-dinnertime-comfort-classics-freezer-food-16-minute-meals-and-other-delicious-ways-to-solve-supper_943/index.html'],................, 
    ['Hungry Girl Clean & Hungry: Easy All-Natural Recipes for Healthy Eating in the Real World', 'Hungry Girl Clean & ...', '£33.14', 'In stock', 'http://books.toscrape.com/media/cache/6f/c4/6fc450625cd672e871a6176f74909be2.jpg', 'Three', 'http://books.toscrape.com/catalogue/hungry-girl-clean-hungry-easy-all-natural-recipes-for-healthy-eating-in-the-real-world_171/index.html']]
    

    在本节中,我们探索了来自selenium.webdriver的方法和属性,并将其用于网页抓取活动。

    摘要

    在本章中,我们学习了关于 Selenium 以及使用 Python 库进行浏览器自动化、网页内容抓取、基于浏览器的活动和 HTML <form> 处理。 Selenium 可以用于处理多种活动,这是 Selenium 相对于 Python 专用库(如lxmlpyquerybs4scrapy)的主要优势之一。

    在下一章中,我们将学习更多关于使用正则表达式进行网页抓取的技术。

    进一步阅读

    第九章:使用正则表达式提取数据

    如果您当前的 Python 设置中不存在这些库,请参考第二章,Python 和 Web - 使用 urllib 和 Requests设置事项部分,了解有关其安装和设置的更多信息。到目前为止,我们已经学习了关于 Web 技术、数据查找技术以及如何使用 Python 库访问 Web 内容的知识。

    正则表达式Regexregex)实际上是使用预定义命令和格式构建的模式,以匹配所需内容。在数据提取过程中,当没有特定的布局或标记模式可供选择时,正则表达式提供了很大的价值,并且可以与 XPath、CSS 选择器等其他技术一起应用。

    复杂的网页内容和一般文本或字符格式的数据可能需要使用正则表达式来完成匹配和提取等活动,还包括函数替换、拆分等。

    在本章中,我们将学习以下主题:

    • 正则表达式概述

    • 使用正则表达式提取数据

    技术要求

    本章需要一个 Web 浏览器(Google Chrome 或 Mozilla Firefox)。我们将使用以下 Python 库:

    • 请求

    • re

    • bs4

    如果您当前的 Python 设置中不存在这些库,请参考第二章,Python 和 Web - 使用 urllib 和 Requests设置事项部分,了解有关其安装和设置的更多信息。

    本章的代码文件可在本书的 GitHub 存储库中找到:github.com/PacktPublishing/Hands-On-Web-Scraping-with-Python/tree/master/Chapter09

    那些已经使用re的人可以参考使用正则表达式提取数据部分。

    正则表达式概述

    正则表达式用于匹配文本或字符串中找到的模式。正则表达式可以用于根据需要对文本或网页内容进行测试和查找模式。正则表达式包含各种定义模式和特殊符号的方法,例如转义代码,以应用一些预定义规则。有关正则表达式的更多信息,请参考进一步阅读部分。

    有各种情况下,正则表达式可以非常有效和快速地获得所需的结果。正则表达式可以仅应用于内容(文本或网页源代码),并且可以用于针对不易使用 XPath、CSS 选择器、BS4PyQuery 等提取的特定信息模式。

    有时,可能会出现需要同时使用正则表达式和 XPath 或 CSS 选择器才能获得所需输出的情况。然后可以使用正则表达式对输出进行测试,以查找模式或清理和管理数据。代码编辑器、文档编写器和阅读器还提供了嵌入式基于正则表达式的实用工具。

    正则表达式可以应用于任何包含正确或不正确格式的文本或字符字符串、HTML 源代码等。正则表达式可以用于各种应用,例如以下内容:

    • 基于特定模式的内容

    • 页面链接

    • 图像标题和链接

    • 链接内的文本

    • 匹配和验证电子邮件地址

    • 从地址字符串中匹配邮政编码或邮政编码

    • 验证电话号码等

    使用搜索、查找、拆分、替换、匹配和迭代等工具,无论是否有其他技术干扰,都可以适用。

    在接下来的章节中,我们将使用re Python 模块并探索其方法,然后将其应用于正则表达式。

    正则表达式和 Python

    re是一个标准的 Python 库,用于处理正则表达式。每个默认的 Python 安装都包含re库。如果该库不存在,请参考第二章,Python 和 Web - 使用 urllib 和 Requests**, 设置事物部分,了解如何设置它。

    >>> 在代码中表示使用 Python IDE。它接受给定的代码或指令,并在下一行显示输出。

    让我们开始通过 Python IDE 导入re并使用dir()函数列出其属性:

    >>> import re
    >>> print(dir(re)) #listing features from re
    

    以下是前面命令的输出:

    ['A', 'ASCII', 'DEBUG', 'DOTALL', 'I', 'IGNORECASE', 'L', 'LOCALE', 'M', 'MULTILINE', 'S', 'Scanner', 'T', 'TEMPLATE', 'U', 'UNICODE', 'VERBOSE', 'X', '_MAXCACHE', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', '__versio n__', '_alphanum_bytes', '_alphanum_str', '_cache', '_cache_repl', '_compile', '_compile_repl', '_expand', '_locale', '_pattern_type', '_pickle', '_subx', 'compile', 'copyreg', 'error', 'escape', 'findall', 'finditer', 'fullmatch', 'match', 'purge', 'search', 'split', 'sre_compile', 'sre_parse', 'sub', 'subn', 'sys', 'template']
    

    从前面的输出中可以看出,在re中有各种可用的函数。我们将从内容提取的角度使用其中的一些函数,并通过使用以下示例来解释正则表达式的基础知识:

    >>> sentence = """Brief information about Jobs in Python. Programming and Scripting experience in some language (such as Python R, MATLAB, SAS, Mathematica, Java, C, C++, VB, JavaScript or FORTRAN) is expected. Participants should be comfortable with basic programming concepts like variables, loops, and functions."""
    

    我们之前声明的sentence包含有关 Python 工作和工作描述的简要信息。我们将使用这个句子来解释基本的正则表达式功能。

    split()函数将字符串分解并返回由空格字符默认分隔的单词列表。我们也可以使用re.split()来拆分字符串对象。在这种情况下,split()接受正则表达式模式来拆分句子,例如re.split(r'\s+',sentence)

    >>> splitSentence = sentence.split() #split sentence or re.split(r'\s',sentence) >>> print("Length of Sentence: ",len(sentence), '& splitSentence: ',len(splitSentence))
    Length of Sentence: 297 & splitSentence: 42 >>> print(splitSentence) #List of words obtained using split() 
    ['Brief', 'information', 'about', 'Jobs', 'in', 'Python.', 'Programming', 'and', 'Scripting', 'experience', 'in', 'some', 'language', '(such', 'as', 'Python', 'R,', 'MATLAB,', 'SAS,', 'Mathematica,', 'Java,', 'C,', 'C++,', 'VB,', 'JavaScript', 'or', 'FORTRAN)', 'is', 'expected.', 'Participants', 'should', 'be', 'comfortable', 'with', 'basic', 'programming', 'concepts', 'like', 'variables,', 'loops,', 'and', 'functions.']
    

    使用前面的代码获取并打印sentence的长度和 Python 的splitSentence列表对象的长度。这些元素和字符的计数将有助于比较从以下示例返回的答案:

    >>> matches = re.findall(r"([A-Z+]+)\,",sentence) #finding pattern with [A-Z+] and comma behind >>> print("Findall found total ",len(matches)," Matches >> ",matches) **Findall found total  6  Matches >>  ['R', 'MATLAB', 'SAS', 'C', 'C++', 'VB']** >>> matches = re.findall(r"([A-Z]+)\,",sentence) #finding pattern with [A-Z] and comma behind >>> print("Findall found total ",len(matches)," Matches >> ",matches) Findall found total 5 Matches >> ['R', 'MATLAB', 'SAS', 'C', 'VB']
    

    re.findall()接受要搜索的模式和要查找的与提供的模式相关的内容。通常,模式可以直接作为参数提供给函数,并且作为原始字符串前面带有r,例如r'([A-Z]+)',或包含原始字符串的变量。

    在前面的代码中,我们可以看到类似的模式,提供了一些额外的字符,但它们的输出不同。以下是一些这些模式的一般解释:

    • [A-Z]:模式中的方括号匹配一组字符,并且区分大小写。在这里,它匹配从AZ的字符,但不匹配az的字符。我们可以提供一组字符,例如[A-Za-z0-9],它匹配从AZaz的任何字符,以及从09的数字字符。如果需要,可以在集合中传递其他字符,例如[A-Z+]+字符可以与AZ的字符一起存在,例如 C++或 C。

    • (): 模式中的圆括号包含匹配的值组。

    • +(用于重复):在字符集之外找到时,它匹配模式的一个或多个出现。[A-Z]+将匹配至少一个或多个AZ的字符组合,例如,前面代码中的RMATLAB。还有一些用于指定重复或出现次数的其他字符,也称为正则表达式量词:

    • * 匹配零次或多次模式

    • ? 匹配模式的零次或一次出现

    • {m,n} 分别匹配重复的最小m和最大n次数:

    • {2,5}:最少 2 次或最多 5 次

    • {2,}:最少 2 次或更多

    • {,5}:最多 5 次

    • {3}:3 次出现

    • \,(逗号):在正则表达式中,除了[A-Za-z0-9]之外的字符通常被写为转义字符,以便提及特定的字符(\,代表逗号,\.代表句号,\?代表问号等)。

    正则表达式量词也分为以下几类:

    • 贪婪量词:这些量词尽可能多地匹配任何元素。

    • 懒惰或非贪婪量词:这些量词尽可能少地匹配任何元素。通常,通过在贪婪量词后添加?将其转换为懒惰量词。

    诸如 ([A-Z+]+)\, 的模式匹配从 A 到 Z 和 + 中至少一个或多个字符,后跟,。在前面的代码中的sentence中,我们可以找到RMATLABSASMathematicaJavaCC++VBJavaScript(还有FORTRAN),即名称后跟,(但不适用于FORTRAN的情况;这就是为什么它在提供的模式的输出中被排除的原因)。

    在下面的代码中,我们试图匹配在sentence中找到的FORTRAN,并使用先前在代码中尝试的模式进行省略:

    >>> matches = re.findall(r"\s*([\sorA-Z+]+)\)",sentence) #r'\s*([A-Z]+)\)' matches 'FORTRAN' 
    >>> print("Findall found total ",len(matches)," Matches >> ",matches)
    
    Findall found total  1  Matches >>  ['or FORTRAN']
    
    >>> fortran = matches[0] # 'or FORTRAN'
    >>> if re.match(r'or',fortran): 
     fortran = re.sub(r'or\s*','',fortran) #substitute 'or ' with empty string >>> print(fortran)
    
    FORTRAN
    
    >>> if re.search(r'^F.*N$',fortran):  #using beginning and end of line searching pattern 
     print("True")
     True
    

    如前面的代码块所示,Python 库re具有各种函数,如下所示:

    • re.match(): 这匹配提供的模式在字符串的开头,并返回匹配的对象。

    • re.sub(): 这会找到一个模式并用提供的字符串替换它。它类似于文本中的查找和替换。

    • re.search(): 这在字符串中匹配模式并返回找到的匹配对象。

    • \s: 这表示空格制表符换行符。在这里,[\sorA-Z+]+\)匹配一个或多个字符,包括A-Zor\s+,后跟\)(右括号)。在正则表达式中还有一些其他转义代码,如下所示:

    • \d: 匹配数字

    • \D: 匹配非数字

    • \s: 匹配空白

    • \S: 匹配非空白

    • \w: 匹配字母数字字符

    • \W: 匹配非字母数字字符

    • \b: 匹配单词边界

    • \B: 匹配非单词边界

    • ^: 这匹配字符串的开头。

    注意:r'[^a-z]'(插入符号或^)在字符集内使用时起否定作用。这意味着除了排除[a-z]

    • $: 这匹配字符串的结尾。

    • |: 这在模式中实现逻辑表达式OR。例如,r'a|b'将匹配任何真实表达式,即ab

    以下代码显示了一些这些正则表达式模式和findall()函数的使用,以及它们的输出:

    >>> matches  = re.findall(r'\s(MAT.*?)\,',sentence,flags=re.IGNORECASE)
    >>> print("(MAT.*?)\,: ",matches)  #r'(?i)\s(MAT.*?)\,' can also be used
     (MAT.*?)\,: ['MATLAB', 'Mathematica']   >>> matches = re.findall(r'\s(MAT.*?)\,',sentence) #findall with 'MAT' case-sensitive
    >>> print("(MAT.*?)\,: ",matches)
     (MAT.*?)\,: ['MATLAB']   >>> matches = re.findall(r'\s(C.*?)\,',sentence)
    >>> print("\s(C.*?)\,: ",matches)
     \s(C.*?)\,: ['C', 'C++']
    

    在前面的代码中找到了以下函数:

    • re 函数还支持可选的flags 参数。这些标志也有缩写形式(i代表re.IGNORECASEs代表re.DOTALLM代表re.MULTILINE)。它们可以通过在表达式开头包含它们来在模式中使用。例如,r'(?i)\s(MAT.*?)\,将返回[MATLAB, Mathematica]。以下是在代码中找到的一些其他re函数:

    • re.IGNORECASE : 忽略提供的模式中发现的大小写敏感性

    • re.DOTALL : 允许. (句号)匹配换行符,并且适用于包含多行的字符串

    • re.MULTILINE : 与多行字符串一起使用,并搜索包括换行符("\n")在内的模式

    • . 或句号: 这匹配任何单个字符,但不包括换行符("\n")。它通常与重复字符一起在模式中使用。句号或. 需要在字符串中匹配,并且应该使用\.

    >>> matchesOne = re.split(r"\W+",sentence)  #split by word, \w (word characters, \W - nonword) >>> print("Regular Split '\W+' found total: ",len(matchesOne ),"\n",matchesOne)  Regular Split '\W+' found total: 43 
    ['Brief', 'information', 'about', 'Jobs', 'in', 'Python', 'Programming', 'and', 'Scripting', 'experience', 'in', 'some', 'language', 'such', 'as', 'Python', 'R', 'MATLAB', 'SAS', 'Mathematica', 'Java', 'C', 'C', 'VB', 'JavaScript', 'or', 'FORTRAN', 'is', 'expected', 'Participants', 'should', 'be', 'comfortable', 'with', 'basic', 'programming', 'concepts', 'like', 'variables', 'loops', 'and', 'functions', ''] >>> matchesTwo = re.split(r"\s",sentence) #split by space
    >>> print("Regular Split '\s' found total: ",len(matchesTwo),"\n", matchesTwo) **Regular Split '\s' found total: 42** 
    ['Brief', 'information', 'about', 'Jobs', 'in', 'Python.', 'Programming', 'and', 'Scripting', 'experience', 'in', 'some', 'language', '(such', 'as', 'Python', 'R,', 'MATLAB,', 'SAS,', 'Mathematica,', 'Java,', 'C,', 'C++,', 'VB,', 'JavaScript', 'or', 'FORTRAN)', 'is', 'expected.', 'Participants', 'should', 'be', 'comfortable', 'with', 'basic', 'programming', 'concepts', 'like', 'variables,', 'loops,', 'and', 'functions.']
    
    • re.split(): 这根据模式拆分提供的内容并返回带有结果的列表。还有一个split(),它可以与字符串一起使用以使用默认或提供的字符进行分割。它的使用方式与本节中稍早的splitSentence类似。

    建议您比较此部分中matchesOnematchesTwo的结果

    在下面的代码中,我们尝试应用 datetime 属性中找到的值的正则表达式模式。定义的模式将被编译,然后用于在代码块中搜索:

    >>> timeDate= '''<time datetime="2019-02-11T18:00:00+00:00"></time> <time datetime="2018-02-11T13:59:00+00:00"></time> <time datetime="2019-02-06T13:44:00.000002+00:00"></time> <time datetime="2019-02-05T17:39:00.000001+00:00"></time> <time datetime="2019-02-04T12:53:00+00:00"></time>''' >>> pattern = r'(20\d+)([-]+)(0[1-9]|1[012])([-]+)(0[1-9]|[12][0-9]|3[01])' >>> recompiled = re.compile(pattern)  # <class '_sre.SRE_Pattern'>
    >>> dateMatches = recompiled.search(timeDate)
    
    • re.compile(): 用于编译正则表达式模式并接收模式对象(_sre.SRE_Pattern)。接收到的对象可以与其他正则表达式功能一起使用。

    可以通过使用group()方法单独探索组匹配,如下面的代码所示:

    >>> print("Group : ",dateMatches.group()) 
    Group : 2019-02-11
     >>> print("Groups : ",dateMatches.groups())
    Groups : ('2019', '-', '02', '-', '11')
     >>> print("Group 1 : ",dateMatches.group(1))
    Group 1 : 2019
     >>> print("Group 5 : ",dateMatches.group(5))
    Group 5 : 11
    

    正如我们所看到的,尽管该模式已经针对多行 timeDate 进行了搜索,但结果是一个单独的分组;也可以使用索引返回单个分组。一个与 re 相关的匹配对象包含了 groups()group() 函数;groups(0) 的结果与 groups() 相同。groups() 中的单个元素将需要从 1 开始的索引。

    • re.finditer(): 用于迭代在提供的内容中找到的模式或模式对象的结果匹配。它返回一个从 re.match() 中找到的匹配(_sre.SRE_Match)对象。

    re.match() 返回一个包含在代码示例中使用的各种函数和属性的对象。这些如下:

    • start(): 返回与表达式匹配的起始字符索引

    • end(): 返回与表达式匹配的结束字符索引

    • span(): 返回匹配表达式的起始和结束字符索引

    • lastindex: 返回最后匹配表达式的索引

    • groupdict(): 返回匹配组字典与模式字符串和匹配值

    • groups(): 返回所有匹配的元素

    • group(): 返回一个单独的分组,并可以通过分组名称访问

    • lastgroup: 返回最后一个组的名称

    >>> for match in re.finditer(pattern, timeDate): # <class '_sre.SRE_Match'>
     #for match in re.finditer(recompiled, timeDate):
     s = match.start()
     e = match.end()
     l = match.lastindex
     g = match.groups()
    
     print('Found {} at {}:{}, groups{} lastindex:{}'.format(timeDate[s:e], s, e,g,l))
    
    Found 2019-02-11 at 16:26, groups('2019', '-', '02', '-', '11') lastindex:5
    Found 2018-02-11 at 67:77, groups('2018', '-', '02', '-', '11') lastindex:5
    Found 2019-02-06 at 118:128, groups('2019', '-', '02', '-', '06') lastindex:5
    Found 2019-02-05 at 176:186, groups('2019', '-', '02', '-', '05') lastindex:5
    Found 2019-02-04 at 234:244, groups('2019', '-', '02', '-', '04') lastindex:5
    

    模式也可以为它们所在的组指定字符串名称;例如,r'(?P<year>[0-9]{4})' 匹配 year 组。在正则表达式中使用基于组的模式可以帮助我们更准确地读取模式并管理输出;这意味着我们不必担心索引。

    让我们考虑模式 pDate(实现 group(), groupdict(), start(), end(), lastgroup, 和 lastindex)与一个分组名称和代码,分别展示日期和时间的输出:

    >>> pDate = r'(?P<year>[0-9]{4})(?P<sep>[-])(?P<month>0[1-9]|1[012])-(?P<day>0[1-9]|[12][0-9]|3[01])' >>> recompiled = re.compile(pDate) #compiles the pattern >>> for match in re.finditer(recompiled,timeDate): #apply pattern on timeDate
     s = match.start()
     e = match.end()
     l = match.lastindex
    
     print("Group ALL or 0: ",match.groups(0)) #or match.groups() that is all
     print("Group Year: ",match.group('year')) #return year
     print("Group Month: ",match.group('month')) #return month
     print("Group Day: ",match.group('day')) #return day
    
     print("Group Delimiter: ",match.group('sep')) #return seperator
     print('Found {} at {}:{}, lastindex: {}'.format(timeDate[s:e], s, e,l))
    
     print('year :',match.groupdict()['year']) #accessing groupdict()
     print('day :',match.groupdict()['day'])
    
     print('lastgroup :',match.lastgroup) #lastgroup name
    

    前面的代码将产生以下输出:

    Group ALL or 0: ('2019', '-', '02', '11')
    Group Year: 2019
    Group Month: 02
    Group Day: 11
    Group Delimiter: -
    Found 2019-02-11 at 16:26, lastindex: 4
    year : 2019
    day : 11
    lastgroup : day
    

    以下代码显示了使用 pTime(实现 span()):

    >>> pTime = r'(?P<hour>[0-9]{2})(?P<sep>[:])(?P<min>[0-9]{2}):(?P<sec_mil>[0-9.:+]+)'
    >>> recompiled = re.compile(pTime)
    
    >>> for match in re.finditer(recompiled,timeDate):
     print("Group String: ",match.group()) #groups
     print("Group ALL or 0: ",match.groups())
    
     print("Group Span: ",match.span()) #using span()
     print("Group Span 1: ",match.span(1))
     print("Group Span 4: ",match.span(4))
    
     print('hour :',match.groupdict()['hour']) #accessing groupdict()
     print('minute :',match.groupdict()['min'])
     print('second :',match.groupdict()['sec_mil'])
    
     print('lastgroup :',match.lastgroup) #lastgroup name
    

    前面的代码将产生以下输出:

    Group String: 12:53:00+00:00
    Group ALL or 0: ('12', ':', '53', '00+00:00')
    Group Span: (245, 259)
    Group Span 1: (245, 247)
    Group Span 4: (251, 259)
    hour : 12
    minute : 53
    second : 00+00:00
    lastgroup : sec_mil
    

    在本节中,我们已经介绍了正则表达式的一般概述和 re Python 库的特性,以及一些实际示例。请参考进一步阅读部分以获取有关正则表达式的更多信息。在下一节中,我们将应用正则表达式来从基于 web 的内容中提取数据。

    使用正则表达式提取数据

    现在我们已经介绍了基础知识并概述了正则表达式,我们将使用正则表达式以类似于使用 XPath、CSS 选择器、pyquerybs4 等的方式批量抓取(提取)数据,通过选择在正则表达式、XPath、pyquery 等之间的实现来满足网页访问的要求和可行性以及内容的可用性。

    并不总是要求内容应该是无结构的才能应用正则表达式并提取数据。正则表达式可以用于结构化和非结构化的网页内容,以提取所需的数据。在本节中,我们将探讨一些示例,同时使用正则表达式及其各种属性。

    示例 1 - 提取基于 HTML 的内容

    在这个例子中,我们将使用来自 regexHTML.html 文件的 HTML 内容,并应用正则表达式模式来提取以下信息:

    • HTML 元素

    • 元素的属性(key 和 values

    • 元素的内容

    这个例子将为您提供一个如何处理网页内容中存在的各种元素、值等以及如何应用正则表达式来提取内容的概述。我们将在接下来的代码中应用以下步骤来处理 HTML 和类似内容:

    <html>
    <head>
       <title>Welcome to Web Scraping: Example</title>
       <style type="text/css">
            ....
       </style>
    </head>
    <body>
        <h1 style="color:orange;">Welcome to Web Scraping</h1>
         Links:
        <a href="https://www.google.com" style="color:red;">Google</a>   <a class="classOne" href="https://www.yahoo.com">Yahoo</a>   <a id="idOne" href="https://www.wikipedia.org" style="color:blue;">Wikipedia</a>
        <div>
            <p id="mainContent" class="content">
                <i>Paragraph contents</i>
                <img src="mylogo.png" id="pageLogo" class="logo"/>
            </p>
            <p class="content" id="subContent">
                <i style="color:red">Sub paragraph content</i>
                <h1 itemprop="subheading">Sub heading Content!</h1>
            </p>
        </div>
    </body>
    </html>
    

    前面的代码是我们将要使用的 HTML 页面源代码。这里的内容是结构化的,我们可以用多种方式处理它。

    在下面的代码中,我们将使用以下函数:

    • read_file(): 这将读取 HTML 文件并返回页面源代码以供进一步处理。

    • applyPattern(): 这个函数接受一个pattern参数,即用于查找内容的正则表达式模式,它使用re.findall()应用于 HTML 源代码,并打印诸如搜索元素列表和它们的计数之类的信息。

    首先,让我们导入rebs4

    import re
    from bs4 import BeautifulSoup
    
    def read_file():
       ''' Read and return content from file (.html). '''  content = open("regexHTML.html", "r")
        pageSource = content.read()
        return pageSource
    
    def applyPattern(pattern):
    '''Applies regex pattern provided to Source and prints count and contents'''
        elements = re.findall(pattern, page) #apply pattern to source
        print("Pattern r'{}' ,Found total: {}".format(pattern,len(elements)))
        print(elements) #print all found tags
        return   if __name__ == "__main__":
        page = read_file() #read HTML file 
    

    在这里,page是从 HTML 文件中使用read_file()读取的 HTML 页面源。我们还在前面的代码中导入了BeautifulSoup,以提取单独的 HTML 标签名称,并通过使用soup.find_all()和我们将应用的正则表达式模式来比较代码的实现和结果:

    soup = BeautifulSoup(page, 'lxml')
    print([element.name for element in soup.find_all()])
    ['html', 'head', 'title', 'style', 'body', 'h1', 'a', 'a', 'a', 'div', 'p', 'i', 'img', 'p', 'i', 'h1']
    

    为了找到page中存在的所有 HTML 标签,我们使用了find_all()方法,soup作为BeautifulSoup的对象,使用lxml解析器。

    有关 Beautiful Soup 的更多信息,请访问第五章,使用 Scrapy 和 Beautiful Soup 进行 Web 抓取使用 Beautiful Soup 进行 Web 抓取部分。

    在这里,我们正在查找所有没有任何属性的 HTML 标签名称。\w+匹配任何一个或多个字符的单词:

    applyPattern(r'<(\w+)>') #Finding Elements without attributes 
    Pattern r'<(\w+)>' ,Found total: 6
    ['html', 'head', 'title', 'body', 'div', 'i']
    

    可以使用空格字符\s来查找所有不以>结尾或包含某些属性的 HTML 标签或元素:

    applyPattern(r'<(\w+)\s') #Finding Elements with attributes 
    Pattern r'<(\w+)\s' ,Found total: 10
    ['style', 'h1', 'a', 'a', 'a', 'p', 'img', 'p', 'i', 'h1']
    

    现在,通过结合所有这些模式,我们正在列出在页面源中找到的所有 HTML 标签。通过使用soup.find_all()name属性,前面的代码也得到了相同的结果:

    applyPattern(r'<(\w+)\s?') #Finding all HTML element
    
    Pattern r'<(\w+)\s?' ,Found total: 16
    ['html', 'head', 'title', 'style', 'body', 'h1', 'a', 'a', 'a', 'div', 'p', 'i', 'img', 'p', 'i', 'h1']
    

    让我们找到 HTML 元素中的属性名称:

    applyPattern(r'<\w+\s+(.*?)=') #Finding attributes name Pattern r'<\w+\s+(.*?)=' ,Found total: 10
    ['type', 'style', 'href', 'class', 'id', 'id', 'src', 'class', 'style', 'itemprop']
    

    正如我们所看到的,只列出了 10 个属性。在 HTML 源代码中,一些标签包含多个属性,比如<a href="https://www.google.com" style="color:red;">Google</a>,只有使用提供的模式找到了第一个属性。

    让我们纠正这一点。我们可以使用r'(\w+)='模式选择紧跟着=字符的单词,这将导致返回页面源中找到的所有属性:

    applyPattern(r'(\w+)=') #Finding names of all attributes Pattern r'(\w+)=' ,Found total: 18
    ['type', 'style', 'href', 'style', 'class', 'href', 'id', 'href', 'style', 'id', 'class', 'src', 'id', 'class', 'class', 'id', 'style', 'itemprop']
    

    同样,让我们找到我们找到的属性的所有值。以下代码列出了属性的值,并比较了我们之前列出的18个属性。只找到了9个值。使用的模式r'=\"(\w+)\"'只会找到单词字符。一些属性值包含非单词字符,比如<a href="https://www.google.com" style="color:red;">

    applyPattern(r'=\"(\w+)\"')
    
    Pattern r'=\"(\w+)\"' ,Found total: 9
    ['classOne', 'idOne', 'mainContent', 'content', 'pageLogo', 'logo', 'content', 'subContent', 'subheading']
    

    通过使用我们分析的适当模式列出了完整的属性值。内容属性值还包含非单词字符,如;/:.。在正则表达式中,我们可以单独包含这些字符,但这种方法可能并不适用于所有情况。

    在这种情况下,包括\w和非空白字符\S的模式非常合适,即r'=\"([\w\S]+)\"

    applyPattern(r'=\"([\w\S]+)\"')
    
    Pattern r'=\"([\w\S]+)\"' ,Found total: 18
    ['text/css', 'color:orange;', 'https://www.google.com', 'color:red;', 'classOne', 'https://www.yahoo.com', 'idOne', 'https://www.wikipedia.org', 'color:blue;', 'mainContent', 'content', 'mylogo.png', 'pageLogo', 'logo', 'content', 'subContent', 'color:red', 'subheading']
    

    最后,让我们收集在 HTML 标签的开头和结尾之间找到的所有文本:

    applyPattern(r'\>(.*)\<')
    Pattern r'\>(.*)\<' ,Found total: 8
    ['Welcome to Web Scraping: Example', 'Welcome to Web Scraping', 'Google', 'Yahoo', 'Wikipedia', 'Paragraph contents', 'Sub paragraph content', 'Sub heading Content!']  
    

    在对内容应用正则表达式时,必须进行内容类型和要提取的值的初步分析。这将有助于在一次尝试中获得所需的结果。

    示例 2 - 提取经销商位置

    在这个例子中,我们将从godfreysfeed.com/dealersandlocations.php提取内容。这个网站包含经销商位置信息,如下面的屏幕截图所示:

    import re
    import requests
     def read_url(url):
    '''
    Handles URL Request and Response
    Loads the URL provided using requests and returns the text of page source
    '''
      pageSource = requests.get(url).text
        return pageSource
    
    if __name__ == "__main__":
    

    在本节和其他示例中,我们将使用rerequests库来检索页面源代码,即pageSource。在这里,我们将使用read_url()函数来实现。

    页面包含 HTML<form>元素,以便我们可以根据输入的zipcode搜索经销商。还有一个带有标记的地理地图:

    Godfreysfeed 经销商首页

    您可以使用zipcode进行表单提交,也可以从地图中提取内容。

    通过分析页面源,我们将发现没有包含经销商信息的 HTML 元素。实现 Regex 非常适合这种情况。在这里,经销商的信息是在 JavaScript 代码中找到的,其中包含latLnginfoWindowContent等变量,如下截图所示:

    Godfreysfeed 经销商页面源

    我们现在将继续加载所需 URL 的页面源,并实现 Regex 来查找数据:

    dataSet=list() #collecting data extracted
    sourceUrl = 'http://godfreysfeed.com/dealersandlocations.php' page = read_url(sourceUrl) #load sourceUrl and return the page source
    

    通过从read_url()获取的页面源,让我们进行基本分析并构建一个模式来收集纬度和经度信息。我们需要两个不同的模式来分别获取经销商的地址和坐标值。从这两个模式的输出可以合并以获得最终结果:

    #Defining pattern matching latitude and longitude as found in page.
    pLatLng= r'var latLng = new google.maps.LatLng\((?P<lat>.*)\,\s*(?P<lng>.*)\)\;'
    
    #applying pattern to page source latlngs = re.findall(pLatLng,page) 
    print("Findall found total *LatLngs:* ", len(latlngs))
    
    #Print coordinates found
    print(latlngs)
    

    通过使用pLatLng模式,共找到了55个坐标值:

    Findall found total LatLngs: 55 
    [('33.2509855','-84.2633946'),('31.0426107','-84.8821949'),('34.8761989','-83.9582412'),('32.43158','-81.749293'),('33.8192864','-83.4387722'),('34.2959968','-83.0062267'),
    ('32.6537561','-83.7596295'),('31.462497','-82.5866503'),('33.7340136','-82.7472304')
    ,................................................................., 
    ('32.5444125','-82.8945945'),('32.7302168','-82.7117232'),('34.0082425','-81.7729772'),
    ('34.6639864', '-82.5126743'),('31.525261','-83.06603'),('34.2068698','-83.4689814'),
    ('32.9765932','-84.98978'),('34.0412765','-83.2001394'),('33.3066615','-83.6976187'), 
    ('31.3441482','-83.3002373'),('30.02116','-82.329495'),('34.58403','-83.760829')]
    

    现在我们已经得到了经销商的坐标,让我们找出经销商的名称、地址等信息:

    #Defining pattern to find dealer from page.
    pDealers = r'infoWindowContent = infoWindowContent\+\s*\"(.*?)\"\;'
    
    #applying dealers pattern to page source dealers = re.findall(pDealers, page)
    print("Findall found total Address: ", len(dealers))
    
    #Print dealers information found
    print(dealers)
    

    还有55个基于地址的信息,是通过使用pDealers模式找到的。请注意,经销商的内容是以 HTML 格式呈现的,需要进一步实现 Regex 以获取诸如nameaddresscity等个别标题:

    Findall found total Address: 55
    
    ["<strong><span style='color:#e5011c;'>Akins Feed & Seed</span></strong><br><strong>206 N Hill Street </strong><br><strong>Griffin, GA</strong><br><strong>30223</strong><br><br>", "<strong><span style='color:#e5011c;'>Alf&apos;s Farm and Garden</span></strong><br><strong>101 East 1st Street</strong><br><strong>Donalsonville, GA</strong><br><strong>39845</strong><br><br>", "<strong><span style='color:#e5011c;'>American Cowboy Shop</span></strong><br><strong>513 D Murphy Hwy</strong><br><strong>Blairsville, GA</strong><br><strong>30512</strong><br><br>",................................... ....................................,"<strong><span style='color:#e5011c;'>White Co. Farmers Exchange </span></strong><br><strong>951 S Main St</strong><br><strong>Cleveland, GA</strong><br><strong>30528 </strong><br><br>"]
    

    现在我们已经得到了latlngsdealers的结果,让我们收集经销商地址的各个部分。经销商的原始数据包含一些 HTML 标签,已被用于拆分和清理经销商的地址信息。由于re.findall()返回 Python 列表,索引也可以用于检索地址组件:

    d=0 #maintaining loop counter for dealer in dealers:
        dealerInfo = re.split(r'<br>',re.sub(r'<br><br>','',dealer))
    
        #extract individual item from dealerInfo
        name = re.findall(r'\'>(.*?)</span',dealerInfo[0])[0]
        address = re.findall(r'>(.*)<',dealerInfo[1])[0]
        city = re.findall(r'>(.*),\s*(.*)<',dealerInfo[2])[0][0]
        state = re.findall(r'>(.*),\s*(.*)<',dealerInfo[2])[0][1]
        zip = re.findall(r'>(.*)<',dealerInfo[3])[0]
        lat = latlngs[d][0]
        lng = latlngs[d][1]
        d+=1
    
        #appending items to dataset
      dataSet.append([name,address,city,state,zip,lat,lng])
     print(dataSet)  #[[name,address, city, state, zip, lat,lng],]
    

    最后,dataSet将包含从dealerslatlngs中合并的单个经销商信息:

    [['Akins Feed & Seed', '206 N Hill Street', 'Griffin', 'GA', '30223', '33.2509855', '-84.2633946'], ['Alf&apos;s Farm and Garden', '101 East 1st Street', 'Donalsonville', 'GA', '39845', '31.0426107', '-84.8821949'],...................................., 
    ['Twisted Fitterz', '10329 Nashville Enigma Rd', 'Alapaha', 'GA', '31622', '31.3441482', '-83.3002373'], 
    ['Westside Feed II', '230 SE 7th Avenue', 'Lake Butler', 'FL', '32054', '30.02116', '-82.329495'],
    ['White Co. Farmers Exchange', '951 S Main St', 'Cleveland', 'GA', '30528', '34.58403', '-83.760829']]
    

    在这个例子中,我们尝试使用不同的模式提取数据,并从提供的 URL 中检索了经销商的信息。

    示例 3 - 提取 XML 内容

    在这个例子中,我们将从sitemap.xml文件中提取内容,可以从webscraping.com/sitemap.xml下载:

    来自 https://webscraping.com 的 sitemap.xml 文件

    通过分析 XML 内容,我们可以看到不同类型的 URL 存在于子节点中,即<loc>。我们将从这些 URL 中提取以下内容:

    从代码中获取的博客标题和类别标题是从 URL 或实际可用的内容的表示中检索出来的。实际标题可能会有所不同。

    首先,让我们导入re Python 库并读取文件内容,以及创建一些 Python 列表以收集相关数据:

    import re
    
    filename = 'sitemap.xml' dataSetBlog = [] # collect Blog title information from URLs except 'category' dataSetBlogURL = [] # collects Blog URLs dataSetCategory = [] # collect Category title dataSetCategoryURL = [] # collect Category URLs   page = open(filename, 'r').read()
    

    从 XML 内容,也就是page中,我们需要找到 URL 模式。代码中使用的pattern匹配并返回<loc>节点内的所有 URL。urlPatterns<class 'list'>)是一个包含搜索 URL 的 Python 列表对象,可以迭代收集和处理所需的信息:

    #Pattern to be searched, found inside <loc>(.*)</loc>
    pattern = r"loc>(.*)</loc" urlPatterns = re.findall(pattern, page) #finding pattern on page
    
    for url in urlPatterns: #iterating individual url inside urlPatterns
    

    现在,让我们匹配一个url,比如webscraping.com/blog/Google-App-Engine-limitations/,其中包含一个blog字符串,并将其附加到dataSetBlogURL。还有一些其他 URL,比如webscraping.com/blog/8/,在我们提取blogTitle时将被忽略。

    此外,任何作为文本等于categoryblogTitle都将被忽略。r'blog/([A-Za-z0-9\-]+)模式匹配包含-字符的字母和数字值:

    if re.match(r'.*blog', url): #Blog related
        dataSetBlogURL.append(url)
     if re.match(r'[\w\-]', url):
            blogTitle = re.findall(r'blog/([A-Za-z0-9\-]+)', url)
    
            if len(blogTitle) > 0 and not re.match('(category)', blogTitle[0]):
                #blogTitle is a List, so index is applied.
                dataSetBlog.append(blogTitle[0]) 
    

    以下是dataSetBlogURL的输出:

    print("Blogs URL: ", len(dataSetBlogURL))
    print(dataSetBlogURL)
    
    Blogs URL: 80
    ['https://webscraping.com/blog', 'https://webscraping.com/blog/10/', 
    'https://webscraping.com/blog/11/', .......,
    'https://webscraping.com/blog/category/screenshot', 'https://webscraping.com/blog/category/sitescraper', 'https://webscraping.com/blog/category/sqlite', 'https://webscraping.com/blog/category/user-agent', 'https://webscraping.com/blog/category/web2py', 'https://webscraping.com/blog/category/webkit', 'https://webscraping.com/blog/category/website/', 'https://webscraping.com/blog/category/xpath']
    

    dataSetBlog将包含以下标题(URL 部分)。将set()方法应用于dataSetBlog时,将从dataSetBlog返回唯一元素。如下所示,dataSetBlog中没有重复的标题:

    print**("Blogs Title: ", len(dataSetBlog))
    print("Unique Blog Count: ", len(set(dataSetBlog)))
    print(dataSetBlog)
    #print(set(dataSetBlog)) #returns unique element from List similar to dataSetBlog.
    
    Blogs Title: 24
    Unique Blog Count: 24
     ['Android-Apps-Update', 'Apple-Apps-Update', 'Automating-CAPTCHAs', 'Automating-webkit', 'Bitcoin', 'Client-Feedback', 'Fixed-fee-or-hourly', 'Google-Storage', 'Google-interview', 'How-to-use-proxies', 'I-love-AJAX', 'Image-efficiencies', 'Luminati', 'Reverse-Geocode', 'Services', 'Solving-CAPTCHA', 'Startup', 'UPC-Database-Update', 'User-agents', 'Web-Scrapping', 'What-is-CSV', 'What-is-web-scraping', 'Why-Python', 'Why-web']
    

    现在,让我们通过使用category来提取与 URL 相关的信息。r'.*category'正则表达式模式匹配迭代中的url,并将其收集或附加到datasetCategoryURL。从与r'category/([\w\s\-]+)模式匹配的url中提取categoryTitle,并将其添加到dataSetCategory

    if re.match(r'.*category', url): #Category Related
        dataSetCategoryURL.append(url)
        categoryTitle = re.findall(r'category/([\w\s\-]+)', url)
        dataSetCategory.append(categoryTitle[0])
    
    print("Category URL Count: ", len(dataSetCategoryURL))
    print(dataSetCategoryURL)
    

    dataSetCategoryURL将产生以下值:

    Category URL Count: 43
    ['https://webscraping.com/blog/category/ajax', 'https://webscraping.com/blog/category/android/', 'https://webscraping.com/blog/category/big picture', 'https://webscraping.com/blog/category/business/', 'https://webscraping.com/blog/category/cache', 'https://webscraping.com/blog/category/captcha', ..................................., 'https://webscraping.com/blog/category/sitescraper', 'https://webscraping.com/blog/category/sqlite', 'https://webscraping.com/blog/category/user-agent', 'https://webscraping.com/blog/category/web2py', 'https://webscraping.com/blog/category/webkit', 'https://webscraping.com/blog/category/website/', 'https://webscraping.com/blog/category/xpath']
    

    最后,以下输出显示了从dataSetCategory中检索到的标题,以及其计数:

    print("Category Title Count: ", len(dataSetCategory))
    print("Unique Category Count: ", len(set(dataSetCategory)))
    print(dataSetCategory)
    #returns unique element from List similar to dataSetCategory.
    #print(set(dataSetCategory)) 
    
    Category Title Count: 43
    Unique Category Count: 43 
    ['ajax', 'android', 'big picture', 'business', 'cache', 'captcha', 'chickenfoot', 'concurrent', 'cookies', 'crawling', 'database', 'efficiency', 'elance', 'example', 'flash', 'freelancing', 'gae', 'google', 'html', 'image', 'ip', 'ir', 'javascript', 'learn', 'linux', 'lxml', 'mobile', 'mobile apps', 'ocr', 'opensource', 'proxies', 'python', 'qt', 'regex', 'scrapy', 'screenshot', 'sitescraper', 'sqlite', 'user-agent', 'web2py', 'webkit', 'website', 'xpath']
    

    从这些示例中,我们可以看到,通过使用正则表达式,我们可以编写针对来自网页、HTML 或 XML 等来源的特定数据的模式。

    搜索、分割和迭代等正则表达式功能可以通过re Python 库中的各种函数来实现。尽管正则表达式可以应用于任何类型的内容,但首选非结构化内容。使用 XPath 和 CSS 选择器时,首选带有属性的结构化网页内容。

    摘要

    在本章中,我们学习了正则表达式及其在re Python 库中的实现。

    到目前为止,我们已经了解了各种基于抓取的工具和技术。当涉及到提取任务时,正则表达式可以提供更多的灵活性,并且可以与其他工具一起使用。

    在下一章中,我们将学习进一步的步骤和主题,这些对于学习环境可能是有益的,比如管理抓取的数据,可视化和分析,以及机器学习和数据挖掘的介绍,以及探索一些相关资源。

    进一步阅读

    第四部分:结论

    在本节中,您将了解一些适用于收集或抓取数据的主题,并了解一些值得从信息和职业角度了解的高级概念。

    本节包括以下章节:

    • 第十章,下一步

    第十章:下一步

    到目前为止,我们已经通过使用 Python 编程语言探索了有关网页抓取的各种工具和技术。

    网页抓取或网络收集是为了从网站中提取和收集数据。网页抓取在模型开发方面非常有用,因为需要实时收集真实、与主题相关和准确的数据。这是可取的,因为与实施数据集相比,需要的时间更少。收集的数据以各种格式存储,如 JSON、CSV、XML 等,写入数据库以供以后使用,并且也作为数据集在线提供。

    网站还提供带有用户界面的 Web API,用于与网络上的信息进行交互。这些数据可以用于计算机科学、管理、医学等领域的研究、分析、营销、机器学习(ML)模型、信息构建、知识发现等。我们还可以对通过 API 和公开或免费提供的数据集获得的数据进行分析,并生成结果,但这个过程不被归类为网页抓取。

    在本章中,我们将学习与收集或抓取的数据相关的主题,并了解一些值得从信息和职业角度了解的高级概念:

    • 管理抓取的数据

    • 使用 pandas 和 matplotlib 进行分析和可视化

    • ML

    • 数据挖掘

    • 接下来是什么?

    技术要求

    需要使用网络浏览器(Google Chrome 或 Mozilla Firefox)。在本章中,我们将使用以下 Python 库:

    • pandas

    • matplotlib

    • csv

    • json

    如果这些库在您当前的 Python 设置中不存在,请参考第二章,Python 和 Web - 使用 urllib 和 Requests,在设置事项部分,获取安装和设置它们的说明。

    本章的代码文件可在本书的 GitHub 存储库中找到:github.com/PacktPublishing/Hands-On-Web-Scraping-with-Python/tree/master/Chapter10

    管理抓取的数据

    在本节中,我们将探索一些工具,并了解如何处理和管理我们从某些网站上抓取或提取的数据。

    使用抓取脚本从网站收集的数据称为原始数据。这些数据可能需要进行一些额外的任务,然后才能进一步处理,以便我们可以对其进行深入的了解。因此,原始数据应该经过验证和处理(如果需要),可以通过以下方式进行:

    • 清理:顾名思义,此步骤用于删除不需要的信息,例如空格和空白字符以及不需要的文本部分。以下代码显示了在先前章节的示例中使用的一些相关步骤,例如第九章,使用正则表达式提取数据,和第三章,使用 LXML、XPath 和 CSS 选择器。在许多地方使用sub()(即re.sub())、strip()replace()等函数,也可以用于清理的目的:
    dealerInfo = re.split(r'<br>', re.sub(r'<br><br>', '', dealer))
    
    stock = list(map(lambda stock:stock.strip(),availability))
    
    availability = stockPath(row)[0].strip()
    
    article['lastUpdated'] = article['lastUpdated'].replace('This page was last edited on', '')
    
    title = row.find(attrs={'itemprop':'text'}).text.strip()
    
    re.sub(r'or\s*','',fortran)
    
    dealerInfo = re.split(r'<br>',re.sub(r'<br><br>','',dealer))
    
    • 格式化:此步骤用于从数据中获取所需的格式。例如,我们可能需要在收到的价格中获得固定的小数位,我们可能需要将大浮点值转换或四舍五入为固定的小数位,将大字符串拆分为较小的单元等,然后将它们写入数据集。还可能出现将十进制数或整数提取为字符串并需要格式化的情况。通常,转换数据类型和呈现数据被视为格式化:
    >>> price = 1234.567801
    >>> newprice = round(price,2)
    >>> print(newprice)
    1234.57
    
    >>> totalsum="200.35"
    >>> print(type(totalsum))
    <class 'str'>
    
    #For large precision use: https://docs.python.org/2/library/decimal.html
    >>> totalsum = float(totalsum) 
    >>> print(type(totalsum))
    <class 'float'>
    
    >>> totalsum
    200.35
    >>> ratings = 5.5
    >>> print(int(rating))
    5
    

    这些额外的步骤也可以在提取特定数据的同时在脚本中执行,并且已经在本书中的示例中完成。在许多情况下,清理和格式化是一起进行的,或者是并行进行的。

    写入文件

    在整本书中,我们需要提取数据行。您可能已经注意到,在大多数示例中,我们使用了一个数据集(用于收集数据的 Python 列表对象),该数据集附加了 Python 列表中的各种字段,如下面的代码所示(从本书的各个示例中收集):

    dataSet.append([year,month,day,game_date,team1,team1_score,team2,team2_score,game_status])
    ..
    dataSet.append([title,price,availability,image.replace('../../../..',baseUrl),rating.replace('star-rating ','')])
    ...
    dataSet.append([link, atype, adate, title, excerpt,",".join(categories)])
    ...
    dataSet.append([titleLarge, title, price, stock, image, starRating.replace('star-rating ', ''), url])
    

    有了这样的数据集,我们可以将这些信息写入外部文件,也可以写入数据库。在将数据集写入文件之前,需要列名来描述数据集中的数据。考虑以下代码,其中keys是一个单独的列表,包含一个字符串标题,即列的名称,将其附加到数据集的相应列表项中:

    keys = ['year','month','day','game_date','team1', 'team1_score', 'team2', 'team2_score', 'game_status']
    ......
    dataSet.append([year,month,day,game_date,team1,team1_score,team2,team2_score,game_status])
    

    让我们考虑以下示例,其中包含要使用的列的colNames,以及清理和格式化数据的dataSet

    import csv
    import json
    
    colNames = ['Title','Price','Stock','Rating']
    dataSet= [['Rip it Up and ...', 35.02, 'In stock', 5],['Our Band Could Be ...', 57.25, 'In stock', 4],
        ['How Music Works', 37.32, 'In stock', 2],['Love Is a Mix ...', 18.03, 'Out of stock',1],
        ['Please Kill Me: The ...', 31.19, 'In stock', 4],["Kill 'Em and Leave: ...", 45.0, 'In stock',5],
        ['Chronicles, Vol. 1', 52.60, 'Out of stock',2],['This Is Your Brain ...', 38.4, 'In stock',1],
        ['Orchestra of Exiles: The ...', 12.36, 'In stock',3],['No One Here Gets ...', 20.02, 'In stock',5],
       ['Life', 31.58, 'In stock',5],['Old Records Never Die: ...', 55.66, 'Out of Stock',2],
        ['Forever Rockers (The Rocker ...', 28.80, 'In stock',3]]
    

    现在我们将上述dataSet写入 CSV 文件。CSV 文件的第一行应始终包含列名。在本例中,我们将使用colNames作为列名:

    fileCsv = open('bookdetails.csv', 'w', newline='', encoding='utf-8')
    writer = csv.writer(fileCsv) #csv.writer object created
    
    writer.writerow(colNames)  #write columns from colNames
    for data in dataSet:       #iterate through dataSet and write to file
        writer.writerow(data)
    
    fileCsv.close() #closes the file handler
    

    上述代码将导致bookdetails.csv文件,其内容如下:

    Title,Price,Stock,Rating Rip it Up and ...,35.02,In stock,5 Our Band Could Be ...,57.25,In stock,4 ........... Life,31.58,In stock,5 Old Records Never Die: ...,55.66,Out of Stock,2 Forever Rockers (The Rocker ...,28.8,In stock,3
    

    同样,让我们创建一个包含colNamesdataSets的 JSON 文件。JSON 类似于 Python 字典,其中每个数据或值都具有一个键;也就是说,它存在于键值对中:

    finalDataSet=list() #empty DataSet 
    for data in dataSet:
        finalDataSet.append(dict(zip(colNames,data))) 
    
    print(finalDataSet)
    
    [{'Price': 35.02, 'Stock': 'In stock', 'Title': 'Rip it Up and ...', 'Rating': 5}, {'Price': 57.25, 'Stock': 'In stock', ..........'Title': 'Old Records Never Die: ...', 'Rating': 2}, {'Price': 28.8, 'Stock': 'In stock', 'Title': 'Forever Rockers (The Rocker ...', 'Rating': 3}]
    

    正如我们所看到的,finalDataSet是通过从dataSet中添加数据并使用zip() Python 函数形成的。zip()将列表中的每个单独元素组合在一起。然后将这个压缩对象转换为 Python 字典。例如,考虑以下代码:

    #first iteration from loop above dict(zip(colNames,data)) will generate
    {'Rating': 5, 'Title': 'Rip it Up and ...', 'Price': 35.02, 'Stock': 'In stock'}
    

    现在,有了可用的finalDataSet,我们可以使用json模块的dump()函数将数据转储或添加到 JSON 文件中:

    with open('bookdetails.json', 'w') as jsonfile:
        json.dump(finalDataSet,jsonfile)
    

    上述代码将导致bookdetails.json文件。其内容如下:

    [
      {
        "Price": 35.02,
        "Stock": "In stock",
        "Title": "Rip it Up and ...",
        "Rating": 5
      },
      ................
      {
        "Price": 28.8,
        "Stock": "In stock",
        "Title": "Forever Rockers (The Rocker ...",
        "Rating": 3
      }
    ]
    

    在本节中,我们已经介绍了管理原始数据的基本步骤。我们获得的文件可以在各种独立系统之间轻松共享和交换,用作 ML 的模型,并且可以作为应用程序中的数据源导入。此外,我们还可以使用数据库管理系统DBMS)如 MySQL、PostgreSQL 等来存储数据,并使用必要的 Python 库执行结构化查询语言SQL)。

    使用 pandas 和 matplotlib 进行分析和可视化

    在本节中,我们将探讨使用 pandas 分析数据和使用 matplotlib 绘制通用图表的一些基本概念。

    pandas 是近年来最受欢迎的数据分析库之一。数据分析和可视化是主要任务,可以借助 pandas 和其他库(如 matplotlib)来完成。

    有关 pandas 和 matplotlib 的更多详细信息和文档,请访问它们的官方网站pandas.pydata.org/matplotlib.org/

    pandas 也被称为原始电子表格,并支持数学、统计和查询类型的语句,并允许您从各种文件中读取和写入。它也受到开发人员和分析师的欢迎,因为它具有易于使用的函数和属性,可以帮助您处理以行和列结构存在的数据:

    使用 Python IDE 探索 pandas

    在本节中,我们将从bookdetails.csv文件中读取数据,并使用该文件的数据进行分析和可视化。让我们导入所需的库,即 pandas 和matplotlib.pyplot。我们将分别使用pdplt别名,并从文件中读取数据:

    import pandas as pd
    import matplotlib.pyplot as plt
    
    dataSet = pd.read_csv('bookdetails.csv') #loads the file content as dataframe.
    
    print(type(dataSet)) #<class 'pandas.core.frame.DataFrame'>
    

    正如我们所看到的,read_csv()函数从 CSV 文件中读取内容并生成一个 DataFrame 对象。pandas 还通过使用read_html()read_excel()read_json()read_sql_table()等函数支持各种数据文件。

    在这里,dataSet是 pandas DataFrame 的一个对象。DataFrame 表示具有行、列和索引的二维表格结构。DataFrame 支持针对行和列中的数据的查询级别分析、条件语句、过滤、分组等操作:

    print(dataSet)
    

    以下屏幕截图显示了现在在dataSet中可用的内容:

    来自 CSV 文件的数据集内容

    行索引也显示出来,所有的行索引都以0(零)开头。可以使用describe()函数获得一般的统计输出:

    print(dataSet.describe()) 
    #print(dataSet.describe('price') will only generate values for column price
    
          Price      Rating
    count 13.000000  13.000000
    mean  35.633077  3.230769
    std   14.239014  1.535895
    min   12.360000  1.000000
    25%   28.800000  2.000000
    50%   35.020000  3.000000
    75%   45.000000  5.000000
    max   57.250000  5.000000
    

    正如我们所看到的,默认情况下,describe()选择适用于统计函数的列,并返回以下函数的计算结果:

    • count: 行数

    • mean: 相关列的平均值

    • min: 找到的最小值

    • max: 找到的最大值

    • std: 计算的标准偏差

    • 25%: 返回第 25 个百分位数

    • 50%: 返回第 50 个百分位数

    • 75%: 返回第 75 个百分位数

    在以下代码中,我们选择了一个名为Price的单独列作为price_group。可以使用dataSet.columns列出数据集中的所有列。可以使用以下格式选择多个列dataSet[['Price','Rating']]

    print(dataSet.columns)
    Index(['Title', 'Price', 'Stock', 'Rating'], dtype='object')
    
    print(sum(dataSet['Price']))
    463.23
    
    print(sum(dataSet['Rating']))
    42
    
    print(dataSet['Price'][0:5])
    0 35.02
    1 57.25
    2 37.32
    3 18.03
    4 31.19
    Name: Price, dtype: float64
    

    以下代码显示了Price列的单独数据:

    #dataSet[['Price','Rating']] will select both column
    price_group = dataSet[['Price']] #selecting 'Price' column only.
    print(price_group) 
    
    Index(['Title', 'Price', 'Stock', 'Rating'], dtype='object')
      Price
    0 35.02
    1 57.25
    2 37.32
    .....
    11 55.66
    12 28.80
    

    pandas DataFrame 也接受对列使用条件或过滤操作。如您所见,筛选应用于Rating,其值为>=4.0,并且只返回TitlePrice

     print(dataSet[dataSet['Rating']>=4.0][['Title','Price']])
    
      Title                  Price
    0 Rip it Up and ...      35.02
    1 Our Band Could Be ...  57.25
    4 Please Kill Me: The ...31.19
    5 Kill 'Em and Leave: ...45.00
    9 No One Here Gets ...   20.02
    10 Life                  31.58
    

    同样,也可以应用基于字符串的过滤。包含Out文本的Stock被过滤,输出返回满足Out文本的所有列。contains()函数接受正则表达式和字符串:

    print(dataSet[dataSet.Stock.str.contains(r'Out')])
    
       Title                     Price Stock        Rating
    3  Love Is a Mix ...         18.03 Out of stock 1
    6  Chronicles, Vol. 1        52.60 Out of stock 2
    11 Old Records Never Die: ...55.66 Out of Stock 2#will return only column 'Price'
    #print(dataSet[dataSet.Stock.str.contains(r'Out')]['Price'])
    
    

    between()函数提供了与Rating相关的值,以过滤和返回书籍的Title

    print(dataSet[dataSet.Rating.between(3.5,4.5)]['Title'])
    
    1 Our Band Could Be ...
    4 Please Kill Me: The ...
    

    由于我们有price_group数据,我们可以使用show()函数在数据上调用plot()函数:

     bar_plot = price_group.plot()  #default plot
     bar_plot.set_xlabel("No of Books") #set X axis: label
     bar_plot.set_ylabel("Price") #set Y axis: label
     plt.show() #displays the plot or chart created
    

    上述代码将生成一个带有默认属性的线图,如颜色和图例位置:

    Price 列的默认线图

    我们还可以更改图表的类型,即线图、柱状图等。

    访问 matplotlib matplotlib.org/gallery/index.html 了解更多有关各种功能图表类型及其附加属性的信息。

    在以下代码中,kind='bar'覆盖了默认的线型:

    bar_plot = price_group.plot(kind='bar') #kind='bar'
    bar_plot.set_xlabel("No of Books")  #Label for X-Axis
    bar_plot.set_ylabel("Price") #label for Y-Axis
    plt.show() 
    

    上述代码生成了以下柱状图:

    Price 列的柱状图

    到目前为止,我们已经使用了基本的图表类型和单个列。在以下代码中,我们将使用PriceRating值绘制柱状图:

    price_group = dataSet[['Price','Rating']]  #obtain both columns
    #title: generates a title for plot
    bar_plot = price_group.plot(kind='bar',title="Book Price ad Rating")
    bar_plot.set_xlabel("No of Books")
    bar_plot.set_ylabel("Price")
    plt.show()
    

    我们收到以下输出:

    具有 Price 和 Rating 列的柱状图

    到目前为止,我们已成功绘制了线图和柱状图。以下代码为Price列的前六个项目绘制了一个饼图,并使用dataSet中可用的前六个Title标签它们:

    prices = dataSet['Price'][0:6] #Price from first 6 items
    labels = dataSet['Title'][0:6] #Book Titles from first 6 items
    legends,ax1 = plt.pie(prices, labels=labels, shadow=True, startangle=45)
    plt.legend(legends, prices, loc="best") #legend built using Prices
    plt.show() 
    

    来自Price的值被用作图例。我们收到以下输出:

    带有 Price 和 Title 列数据的饼图

    在使用 pandas 和 matplotlib 方面还有很多可以探索的地方。在本节中,我们展示了这两个库中可用的基本功能。现在,我们将看看机器学习。

    机器学习

    机器学习是人工智能的一个分支,涉及研究数学和统计算法,以处理和开发能够从数据中学习的自动化系统,减少人类干预。机器学习的预测和决策模型依赖于数据。网络抓取是使数据可用于机器学习模型的资源之一。

    如今,许多推荐引擎实现了机器学习,以便实时提供营销广告和推荐,比如谷歌的 AdSense 和 AdWords。机器学习中实施的过程类似于数据挖掘和预测建模。这两个概念都在浏览数据并根据要求修改程序的行为时寻找模式。因此,机器学习在探索商业、营销、零售、股票价格、视频监控、人脸识别、医学诊断、天气预测、在线客户支持、在线欺诈检测等领域时是一个方便的工具。

    随着新的和改进的机器学习算法、数据捕获方法以及更快的计算机和网络,机器学习领域正在加速发展。

    机器学习和人工智能

    人工智能是一个广泛的范畴,涵盖了诸多主题,如神经网络、专家系统、机器人技术、模糊逻辑等等。机器学习是人工智能的一个子集,它探索了构建一个能够自主学习的机器的理念,从而超越了对不断推测的需求。因此,机器学习已经取得了实现人工智能的重大突破。

    机器学习包括使用多种算法,从而使软件能够提供准确的结果。从一组解析数据中进行有用的预测是机器学习概念的目标。机器学习的最主要优势是它可以不知疲倦地学习和预测,而无需硬编码的软件体系。训练包括将大量数据集作为输入。这使得算法能够学习、处理和进行预测,然后将预测结果作为输出。

    在衡量任何模型的潜力时,会采用几个重要参数。准确性是其中之一,也是衡量任何开发模型成功的重要参数。在机器学习中,80%的准确性就是成功。如果模型的准确性达到 80%,那么我们就节省了 80%的时间,提高了生产率。然而,如果数据不平衡,准确性并不总是评估分类模型的最佳指标。

    总的来说,准确性被称为一种直观的度量。在使用准确性时,对假阳性和假阴性分配了相等的成本。对于不平衡的数据(比如 94%属于一种情况,6%属于另一种情况),有许多降低成本的好方法;做一个模糊的预测,即每个实例都属于多数类,证明整体准确性为 94%,然后完成任务。同样,如果我们讨论的是一种罕见且致命的疾病,问题就会出现。未能正确检查患病者的疾病的成本高于将健康个体推向更多检查的成本。

    总之,没有最佳的度量标准。两个人选择不同的度量标准来达到他们的目标是很常见的。

    Python 和机器学习

    荷兰程序员(Guido Van Rossum)将 Python 作为他的副业项目推出,但没有意识到它会加速他的成功。Python 在开发人员中被广泛采用,当涉及到快速原型设计时尤其如此。它因其可读性、多功能性和易用性而在所有机器学习工具中备受欢迎。

    作为机器学习工程师、计算机视觉工程师、数据科学家或数据工程师,我们必须在线性代数和微积分的概念中游刃有余,一旦深入研究,这些概念往往变得复杂。然而,Python 通过其快速实现来解救我们,从而绕过了最大努力的障碍。对这一理念的快速验证使得 Python 编程语言更加受欢迎。

    对于 ML 来说,数据就是一切。原始数据是非结构化的、庞大的、不完整的,并且存在缺失值。数据清洗是 ML 中最关键的步骤之一,这样我们才能继续处理我们的数据。Python 中有许多重要的库,使 ML 的实施变得更简单。Python 中的各种开源存储库帮助改变现有的方法。Web 抓取是这些方法之一,它处理存在于网络上的数据,然后进一步处理为 ML 模型的输入。

    以下是一些最常见和广泛使用的库,如果我们决定使用 Python 和 ML,值得一看:

    • scikit-learn:用于处理经典 ML 算法

    • NumPy(数值 Python):设计用于科学计算

    • SciPy:包含线性代数、优化、积分和统计模块

    • pandas:用于数据聚合、操作和可视化

    • matplotlibSeaborn:用于数据可视化

    • BokehPlotly:用于交互式可视化

    • TensorFlowTheano:用于深度学习

    • Beautiful Soup, LXML, PyQueryScrapy:用于从 HTML 和 XML 文档中提取数据

    一旦我们对 Python 有了基本的了解,就可以导入并实施这些库。或者,我们也可以从头开始应用这些功能,这是大多数开发人员所做的。

    Python 在编写和调试代码方面比其他编程语言节省时间。这正是 AI 和 ML 程序员想要的:专注于理解架构方面,而不是花费所有时间在调试上。因此,Python 可以很容易地被不太懂编程的人处理,因为它提供了人类水平的可读性的语法。

    除了 Python,还有其他几种用于 ML 的工具,如 Microsoft Excel、SAS、MATLAB 和 R。由于缺乏足够的社区服务和无法处理大型数据集,这些工具经常被忽视。MATLAB 还提供了用于图像处理和分析的复杂库和包。与 Python 相比,执行时间适中,功能仅限于原型设计,而不是部署。

    R 是另一个用于统计分析的工具。Python 通过提供各种开发工具来执行数据操作,可以与其他系统协作。然而,R 只能处理特定形式的数据集,因此预定义的函数需要预定义的输入。R 为数据提供了一个原始的基础,而 Python 允许我们探索数据。

    ML 算法的类型

    一般来说,有三种 ML 算法,如下所示:

    • 监督学习:

    • 分类

    • 回归

    • 无监督学习:

    • 关联

    • 聚类

    • 强化学习

    监督学习

    监督学习是观察或指导执行某事。输入给模型的是我们想要做出的预测。标记的数据是对特定输入实例的明确预测。监督学习需要标记的数据,这需要一些专业知识。然而,这些条件并不总是满足的。我们并不总是拥有标记的数据集。例如,欺诈预测是一个迅速发展的领域,攻击者不断寻找可用的漏洞。这些新攻击不可能在带有标记攻击的数据集下得到维护。

    数学上,输入到输出的映射函数可以表示为Y = f(X)。这里,Y是输出变量,X是输入变量。

    分类

    分类根据其属性确定或分类模型,并且根据成员类别来确定新观察结果所属的流派的过程,这是事先已知的。这是一种根据一个或多个自变量确定因变量属于哪个类别的技术。分类问题中的输出变量是一个组或类别。

    一些例子包括信用评分(根据收入和储蓄区分高风险和低风险)、医学诊断(预测疾病风险)、网络广告(预测用户是否会点击广告)等。

    分类模型的能力可以通过模型评估程序和模型评估指标来确定。

    模型评估程序

    模型评估程序可以帮助您找出模型对样本数据的适应程度:

    • 训练和测试数据:训练数据用于训练模型,使其适应参数。测试数据是一个掩盖的数据集,需要进行预测。

    • 训练和测试分离:通常情况下,当数据被分离时,大部分数据用于训练,而一小部分数据用于测试。

    • K 折交叉验证:创建 K 个训练和测试分离,并将它们平均在一起。该过程比训练和测试分离运行 k 倍慢。

    模型评估指标

    模型评估指标用于量化模型的性能。以下指标可用于衡量分类预测模型的能力。

    评估指标是通过以下方式进行管理:

    • 混淆矩阵:这是一个 2x2 矩阵,也称为错误矩阵。它有助于描述算法的性能,通常是监督学习算法,通过分类准确度、分类错误、灵敏度、精确度和预测。指标的选择取决于业务目标。因此,有必要确定根据要求是否可以减少假阳性或假阴性。

    • 逻辑回归:逻辑回归是一种用于分析数据集的统计模型。它有几个独立变量负责确定输出。输出用双倍体变量(涉及两种可能的结果)来衡量。逻辑回归的目标是找到最佳拟合模型,描述双倍体变量(因变量)和一组独立变量(预测变量)之间的关系。因此,它也被称为预测学习模型。

    • 朴素贝叶斯:这是基于条件概率的概念工作,由贝叶斯定理给出。贝叶斯定理根据可能与事件相关的先验知识计算事件的条件概率。这种方法广泛应用于人脸识别、医学诊断、新闻分类等。朴素贝叶斯分类器基于贝叶斯定理,可以计算A给定B的条件概率如下:

    P(A | B) = ( P(B | A) * P( A ))/ P( B  )
    Given:
    P(A | B) = Conditional probability of A given B
    P(B | A) = Conditional probability of B given A
    P( A )= Probability of occurrence of event A
    P( B  )= Probability of occurrence of event B
    
    • 决策树:决策树是一种监督学习模型,最终结果可以以树的形式呈现。决策树包括叶节点、决策节点和根节点。决策节点有两个或更多的分支,而叶节点代表分类或决策。决策树进一步将数据集分解为更小的子集,从而逐步发展相关的树。它易于理解,可以轻松处理分类和数值数据集。

    • 随机森林算法:这个算法是一种易于使用且即使没有超参数调整也能提供出色结果的监督式机器学习算法。由于其简单性,它可以用于回归和分类任务。它可以处理更大的数据集以保持缺失值。与回归相比,该算法被认为是执行与分类相关任务最好的。

    • 神经网络:虽然我们已经有了线性和分类算法,但神经网络是许多机器学习问题的最先进技术。神经网络由单元组成,即神经元,它们排列成层。它们负责将输入向量转换为某种输出。每个单元接受输入,应用函数,并将输出传递到下一层。通常,对该算法应用非线性函数。

    • 支持向量机(SVM)算法:SVM 学习算法是一种监督式机器学习模型。它用于分类和回归分析,并被广泛认为是一个受限制的优化问题。SVM 可以通过核技巧(线性、径向基函数、多项式和 Sigmoid)变得更加强大。然而,SVM 方法的局限性在于核的选择。

    回归

    回归是一种有助于估计变量之间关系的统计测量。一般来说,分类侧重于标签的预测,而回归侧重于数量的预测。回归在金融、投资和其他领域中被管理者用来估值他们的资产。在同一条线上,它试图确定因变量和一系列其他变化的自变量之间关系的强度;例如,商品价格与经营这些商品的企业之间的关系。

    回归模型具有两个主要特征。回归问题中的输出变量是实数或数量性质的。模型的创建考虑了过去的数据。从数学上讲,预测模型将输入变量(X)映射到连续的输出变量(Y)。连续的输出变量是整数或浮点值。

    回归预测模型的能力可以通过计算均方根误差(RMSE)来衡量。例如,总共,回归预测模型做出了两次预测,即 1.5 和 3.3,而预期值分别为 1.0 和 3.0。因此,RMSE 可以计算如下:

    RMSE = sqrt(average(error²))
    RMSE = sqrt(((1.0 - 1.5)² + (3.0 - 3.3)²) / 2)
    RMSE = sqrt((0.25 + 0.09) / 2)
    RMSE = sqrt(0.17)
    RMSE = 0.412
    

    无监督学习

    无监督学习是一类机器学习技术,其中作为输入的数据没有标签。此外,只提供输入变量(X),没有对应的输出变量(Y)。在无监督学习中,算法被留在孤立中自行学习和探索,没有真正的早期期望。这种缺乏标记教会我们关于使用表示或嵌入重建输入数据。在数据挖掘和特征提取方面非常有益。

    无监督学习可以帮助您发现隐藏的趋势和模式。一些现实世界的例子包括预测或理解手写数字、纳米摄像头制造技术、普朗克量子光谱等。

    从数学上讲,无监督学习具有没有相应输出值的输入值(X)。与监督学习相比,无监督学习的任务处理非常复杂。无监督学习的实现可以在自动或自动驾驶汽车、面部识别程序、专家系统、生物信息学等领域找到。

    关联和聚类是无监督学习的两个部分。

    关联

    这是一种用于在大型数据集中发现新模式的技术。关联被认为是根据新闻价值程度从数据集中识别强规则的。在对数据进行长时间分析时,会生成更多的新规则。

    关联规则在市场篮分析中被广泛应用。这种技术有助于确定购买产品对之间的关联强度以及在观察中的共同发生频率。

    市场篮分析是零售商用来发现商品之间关联的建模技术之一。该理论围绕着这样一个事实展开,即如果我们购买某些商品,我们更有可能购买类似的商品。

    在数学上,它表示为P(A|B),其中购买A的人也购买B。也可以写成如果{A},那么{B}。换句话说,如果 A 发生的概率,那么 B 也会发生的概率。例如,P(牛奶 | 面包 ) = 0.7

    聚类

    簇是属于同一标签的对象的集合,被视为一个整体。聚类是将对象分组到其相应类别的技术。这包括将多个对象分类到它们的特定组中,如果它属于同一组,则其关联度最大,否则最小。

    最流行的聚类算法之一是 k 均值聚类算法。该算法要求预定义的 k 值。K 代表我们想要将数据分成的簇的数量。当簇是超球形时,如二维空间中的圆或三维空间中的球时,才能获得真正的性能。

    聚类的主要优势在于它帮助你从数据中找出独特、有用的特征,并且它对变化具有灵活性。

    强化学习

    强化学习是机器学习的一部分,它处理采取必要行动以增加特定情况奖励的问题。它利用多个软件和机器来找到特定情况的最佳路径。

    强化学习与监督学习不同。在监督学习中,提供带有标签的训练数据,基于这些数据进行训练。在强化学习的情况下,强化代理人做出决定来解决分配给他们的任务。

    强化学习有两种类型:

    • 正强化:最大化性能并维持更长时间的变化

    • 负强化:最小化性能并维持更短时间的变化

    数据挖掘

    从大型数据集或数据库中发现隐藏或预测信息的过程被称为数据挖掘。数据挖掘是一种在数据上进行的分析形式,以发现新的模式和事实。这些事实被用来发现知识,也被认为是朝着数据库知识发现KDD)的一步。

    通常结合人工智能、机器学习、统计学、数据库管理系统等各种过程和步骤来寻找新的模式。随着数据量和机器学习算法的增长,总是有发现数据库中新的或隐藏事实的趋势。发现或搜索到的事实和模式随后被用来预测特定结果,并且也可以应用于统计学、数据可视化、营销、管理、医学、决策系统等许多领域。

    数据分析和数据挖掘经常被比较或并列讨论。数据挖掘被认为是数据分析过程的一部分。在进行数据分析时,我们需要一些预定义的假设,因为这是组织数据以开发模型并确定一些见解的过程。在应用实践方面,数据挖掘主要是针对结构化数据进行的,而数据分析可以针对结构化、非结构化或半结构化数据进行。

    数据挖掘基于科学和数学方法,而数据分析使用分析模型和智能系统。从远处看,数据分析和数据挖掘都是数据科学的子集,数据挖掘实施预测算法来发现模式,而数据分析实施活动来从数据集中获得一些见解。

    数据挖掘的一个主要好处是能够在短时间内处理大量数据。它还可以在新平台或现有平台上实施,预测隐藏的模式或帮助发现它们,帮助决策、知识发现等等。

    数据挖掘的任务

    一般来说,数据挖掘任务分为两种类型,也称为数据挖掘分析或数据挖掘建模。如下所示,两者都可以进一步分类:

    • 预测:

    • 分类

    • 回归

    • 预测

    • 描述性:

      • 聚类
    • 总结

    • 关联规则

    预测

    这使用统计分析将数据转化为有价值的信息。它预测可能发生情况的未来结果。通过分析当前和历史事实生成输出的与预测相关的技术属于这种模型。

    分类

    这是最常见的挖掘技术之一,在处理样本之前对其进行分类和归类以找到事实。有关分类和模型评估程序的更多信息,请参阅ML 算法类型部分。

    回归

    这种技术用于预测、预测和分析信息趋势和变量之间的关系。有关回归的更多信息,请参阅ML 算法类型部分。

    预测

    这种技术分析过去的事件,并通过使用其他数据挖掘技术(如聚类、分类等)的参考来预测可能缺失或未来的值。

    描述性

    也称为数据处理的初步阶段,它使用商业智能和许多其他系统。这种形式的分析是有限的,因为它只分析过去的数据,并且通常提供有关已经发生的事情的信息。

    聚类

    聚类是一种用于识别彼此相似的数据的技术。有关聚类的更多信息,请参阅ML 算法类型部分。

    总结

    这提供了数据集的更紧凑表示,并包括可视化和报告生成。大多数关于销售和营销的管理报告使用这种技术。

    关联规则

    有关关联的更多信息,请参阅ML 算法类型部分。

    接下来是什么?

    Web 抓取是动态的、要求高的,也是一项具有挑战性的任务。在进行此任务之前,我们需要遵守法律的角度,这是在网站的服务条款(ToS)和隐私政策中提出的。Python 编程,以其支持性、简单的语法、简短可读的代码形式以及库和工具的可用性,是用于 Web 抓取的最佳语言之一。

    然而,挑战依然存在,通用脚本可能无法满足需求。有时,抓取任务可能需要大量资源,个人 PC 或笔记本电脑在考虑时间、机器资源等方面可能不值得实施。有许多功能和程序可以使抓取任务变得更加复杂和具有挑战性。让我们来看看其中一些:

    • 采用不断增长的基于网络的安全措施

    • 动态加载数据和脚本语言的参与使得抓取变得复杂

    • 存在 CAPTCHA,可以在www.captcha.net/找到

    • 阻止用户的 IP 地址(用于同时请求)

    • 阻止来自世界某些地区的请求(使用和切换代理可能会有所帮助)

    对于这种情况,我们可以从正在进行与抓取相关工作的组织那里获得帮助。这些组织可以通过收取一定费用并为我们提供一个网络界面来帮助我们满足数据需求。这样的公司可以在谷歌上搜索“网络抓取服务”或“网络抓取软件”来寻找。还有各种基于浏览器的扩展程序可供搜索“抓取扩展”来找到。

    总结

    在本章中,我们探讨并学习了使用文件进行数据管理、分析和可视化的基本概念,使用了 pandas 和 matplotlib。我们还介绍了机器学习和数据挖掘,并探讨了一些相关资源,这些资源对进一步学习和职业发展可能有帮助。

    通过本章,我们完成了本书!网络抓取是一个广泛的主题,直接或间接与许多技术和开发技术相关。在整本书中,我们通过使用 Python 编程语言学习了这一领域的许多概念。我们还可以探索与网络抓取相关的更多主题,如机器学习、数据挖掘、网络抓取、人工智能和 Python 编程。从知识和职业发展的角度来看,这些主题都值得探索。

    进一步阅读

    posted @ 2024-05-04 21:27  绝不原创的飞龙  阅读(38)  评论(0编辑  收藏  举报