diff --git a/_scraping/ch1.md b/_scraping/ch1.md new file mode 100644 index 0000000..e7b25c0 --- /dev/null +++ b/_scraping/ch1.md @@ -0,0 +1,426 @@ +--- +layout: post +title: "第一章:How the Internet Works" +author: "lili" +mathjax: true +sticky: false +excerpt_separator: +tags: + - python + - scraping +--- + + +**目录** +* TOC +{:toc} + +我这一生中遇到的真正了解互联网工作原理的人寥寥无几,而我显然不在其中。我们大多数人只是凭借一套能够让我们恰到好处地使用互联网的心理抽象来应付。即使对于程序员来说,这些抽象也可能仅仅扩展到他们在职业生涯中曾经解决过的某个特别棘手的问题所需的程度。 + +由于页面篇幅的限制和作者知识的局限,本章也不得不依赖这些抽象。它描述了互联网和 Web 应用程序的机制,尽可能涵盖了爬取网页所需的内容(或许还多一点点)。在某种意义上,本章描述了网络爬虫所运行的世界:习俗、做法、协议和标准,这些将在整本书中不断被提及。 + +当你在网络浏览器的地址栏中输入 URL 并按下回车键时,交互式文本、图像和媒体就会像魔法般地出现。这种魔法每天都在为数十亿人身上发生。他们访问着相同的网站,使用着相同的应用程序——经常获取到专门为他们定制的媒体和文本。 + +这数十亿人使用着不同类型的设备和软件应用程序,这些程序是由不同的开发人员在不同的(通常是竞争的!)公司编写的。令人惊讶的是,没有一个全能的监管机构通过法律力量来规范互联网并协调其发展。相反,互联网的不同部分是由一些随着时间推移逐渐形成的不同组织治理的,这些组织在一定程度上是临时和自愿加入的。 + +当然,选择不遵循这些组织发布的标准可能会导致你对互联网的贡献根本……无法运作。如果你的网站无法在流行的网络浏览器中显示,人们很可能不会访问它。如果你的路由器发送的数据无法被其他任何路由器解析,那些数据将被忽略。 + +网络爬取本质上是用你自己设计的应用程序替代网络浏览器的行为。正因为如此,理解网络浏览器所依赖的标准和框架是非常重要的。作为一个网络爬虫,你必须既要模仿,又有时要颠覆预期的互联网习俗和做法。 + +## 网络 + +在电话系统的早期,每部电话都通过一根实物电线连接到一个中央交换机板。如果你想打电话给附近的朋友,你拿起电话,要求接线员帮你接通,接线员通过插头和插孔物理地创建一条你和朋友之间的专线连接。 + +长途电话费用昂贵,而且可能需要几分钟才能接通。从波士顿拨打到西雅图的长途电话会涉及全美各地的接线员协调,创建一条直接连接你电话和接收者电话的长电线。 + +如今,我们不再通过临时专线打电话,而是可以通过一张持久的电线网络从家里拨打视频电话到世界任何地方。这些电线不会告诉数据该去哪里,数据自己引导自己,这个过程被称为分组交换(packet switching)。尽管多年来许多技术对我们所称的“互联网”有所贡献,但真正单独启动这一切的技术是分组交换。 + +在分组交换网络中,要发送的消息被分成离散的有序分组,每个分组都有自己的发送者和接收者地址。这些分组根据地址动态路由到网络上的任何目的地。与必须盲目穿过从接收者到发送者的单一专线不同,这些分组可以走任何网络选择的路径。事实上,同一消息传输中的分组可能会通过网络中的不同路线,在到达时由接收计算机重新排序。 + +如果说旧的电话网络像是一条索道——将乘客从山顶的一个目的地带到山底的另一个目的地——那么分组交换网络就像是一条高速公路系统,来往多个目的地的汽车都可以使用同样的道路。 + +现代的分组交换网络通常使用开放系统互连(OSI)模型来描述,该模型由七层路由、编码和错误处理组成: + +* 物理层 +* 数据链路层 +* 网络层 +* 传输层 +* 会话层 +* 表示层 +* 应用层 + +大多数 Web 应用程序开发人员的工作完全在第七层,即应用层。这也是本书中花费最多时间的一层。然而,在爬取网页时,了解其他层次的概念知识也是重要的。例如,第17章讨论的 TLS 指纹识别(TLS fingerprinting)是一种涉及传输层的 Web 爬虫检测方法。 + +此外,了解所有的数据封装和传输层次有助于解决 Web 应用程序和 Web 爬虫中的错误。 + + + +### 物理层 + +物理层指定了信息如何通过你家中的以太网电缆(或任何本地网络)以电信号的形式进行物理传输。它定义了编码1和0的电压水平,以及这些电压脉冲的速度。它还定义了通过蓝牙和 WiFi 传输的无线电波如何被解释。 + +这一层不涉及任何编程或数字指令,而是纯粹基于物理和电气标准。 + +### 数据链路层 + +数据链路层规定了信息如何在本地网络的两个节点之间传输,例如,在你的计算机和路由器之间。它定义了一次传输的开始和结束,并在传输丢失或出现错误时提供纠错功能。 + +在这一层,数据包被包装在一个额外的“数字信封”中,包含路由信息,并被称为帧(frame)。当帧中的信息不再需要时,它被解封并作为数据包发送到网络中。 + +需要注意的是,在数据链路层,网络上的所有设备都在同时接收相同的数据——没有实际的“切换”或控制数据的去向。然而,数据未被发送到的设备通常会忽略数据,直到它们接收到为它们发送的数据。 + +### 网络层 + +网络层是分组交换发生的地方,因此也是“互联网”发生的地方。这一层允许你的计算机的数据包通过路由器转发并到达其直接网络之外的设备。 + +网络层涉及传输控制协议/互联网协议(TCP/IP)中的互联网协议(IP)。IP 是我们获取 IP 地址的地方。例如,我在全球互联网的 IP 地址目前是 173.48.178.92。这允许世界上的任何计算机向我发送数据,我也可以从我的地址向任何其他地址发送数据。 + +### 传输层 + +第4层,即传输层,关注的是将运行在一台计算机上的特定服务或应用程序连接到另一台计算机上运行的特定应用程序,而不仅仅是连接计算机本身。它还负责数据流中的任何错误纠正或重试。 + +例如,TCP 非常严格,会不断请求丢失的包,直到所有包都正确接收。TCP 通常用于文件传输,其中所有包必须按正确顺序正确接收,以使文件正常工作。 + +相比之下,用户数据报协议(UDP)则会高兴地跳过丢失的包,以保持数据流动。它通常用于视频会议或音频会议,其中传输质量的暂时下降比对话中的延迟更可取。 + +因为你的计算机上的不同应用程序在同一时间可能有不同的数据可靠性需求(例如,一边打电话一边下载文件),传输层也是端口号起作用的地方。操作系统为运行在你计算机上的每个应用程序或服务分配一个特定的端口,通过该端口发送和接收数据。 + +这个端口通常写成 IP 地址后面的一个数字,用冒号分隔。例如,71.245.238.173:8080 表示操作系统分配给 IP 地址为 71.245.238.173 的计算机上的端口 8080 的应用程序。 + + +### 会话层 + +会话层负责在两个应用程序之间开启和关闭会话。这个会话允许保存关于哪些数据已发送和未发送、以及计算机与谁通信的状态信息。会话通常在完成数据请求所需的时间内保持打开状态,然后关闭。 + +会话层允许在短暂崩溃或断开连接的情况下重试传输。 + +**会话与会话** + +OSI 模型的会话层中的会话与 Web 开发人员通常谈论的会话和会话数据不同。Web 应用程序中的会话变量是应用层的一个概念,由 Web 浏览器软件实现。应用层中的会话变量会在浏览器中保存,直到需要或用户关闭浏览器窗口为止。在 OSI 模型的会话层中,会话通常只持续传输单个文件所需的时间。 + +### 表示层 + +表示层将传入的数据从字符字符串转换为应用程序可以理解和使用的格式。它还负责字符编码和数据压缩。表示层关心的是接收到的数据显示的是 PNG 文件还是 HTML 文件,并相应地将该文件交给应用层。 + +### 应用层 + +应用层解释由表示层编码的数据,并将其适当地用于应用程序。我喜欢将表示层看作是负责转换和识别事物,而应用层则负责“执行”事物。例如,HTTP 及其方法和状态是应用层协议。更平凡的 JSON 和 HTML(因为它们是定义数据如何编码的文件类型)是表示层协议。 + + + +## HTML + +Web 浏览器的主要功能是显示 HTML(超文本标记语言)文档。HTML 文档是以 .html 或较少见的 .htm 结尾的文件。 + +像文本文件一样,HTML 文件使用纯文本字符编码,通常是 ASCII(详见第 147 页的“文本编码和全球互联网”)。这意味着它们可以用任何文本编辑器打开和阅读。 + +以下是一个简单的 HTML 文件示例: + +```html + + + A Simple Webpage + + + +

Hello, World!

+ + +``` + +HTML 文件是一种特殊类型的 XML(可扩展标记语言)文件。每个以 \< 开头并以 > 结尾的字符串称为标签。 + +XML 标准定义了像 \ 这样的起始标签和像 \ 这样的结束标签。起始和结束标签之间是标签的内容。 + +在某些情况下,标签不需要任何内容,这时你可能会看到一个自闭合标签。自闭合标签或空元素标签的样子如下: + +```html +

+``` + +标签还可以包含属性,形式为 attributeKey="attribute value",例如: + +```html +

+Lorem ipsum dolor sit amet, consectetur adipiscing elit +
+``` + +这里,div 标签具有 class 属性,其值为 main-content。 + +HTML 元素包括一个带有一些可选属性的起始标签,一些内容和一个结束标签。一个元素还可以包含多个其他元素,在这种情况下它们是嵌套元素。 + +虽然 XML 定义了标签、内容、属性和值的基本概念,但 HTML 定义了这些标签可以和不能是什么,它们可以和不能包含什么,以及浏览器必须如何解释和显示它们。 + +例如,HTML 标准定义了 class 属性和 id 属性的用法,它们通常用于组织和控制 HTML 元素的显示: + +```html +

Some Title

+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit +
+``` + +通常,页面上的多个元素可以包含相同的 class 值;然而,在该页面上 id 字段的任何值必须是唯一的。因此,多个元素可以具有 class content,但只能有一个元素具有 id main-title。 + +HTML 文档中的元素在 Web 浏览器中的显示方式完全取决于 Web 浏览器作为软件的编程方式。如果一个 Web 浏览器被编程为与另一个 Web 浏览器不同地显示元素,这将导致不同 Web 浏览器的用户体验不一致。 + +因此,协调 HTML 标签的具体作用并将其编写为统一标准非常重要。HTML 标准目前由万维网联盟(W3C)控制。所有 HTML 标签的当前规范可以在 https://html.spec.whatwg.org/multipage/ 找到。 + +然而,如果你从未接触过 HTML,正式的 W3C HTML 标准可能不是学习 HTML 的最佳途径。网页抓取的大部分工作涉及读取和解释在网上找到的原始 HTML 文件。如果你从未处理过 HTML,我强烈推荐《HTML & CSS: The Good Parts》这样的书来熟悉一些更常见的 HTML 标签。 + + + +## CSS + +层叠样式表(CSS)定义了网页上 HTML 元素的外观。CSS 定义了布局、颜色、位置、大小和其他属性,使一个带有浏览器默认样式的无聊的 HTML 页面变得更加吸引现代网页浏览者。 + +使用前面的 HTML 示例: + +```html + + + A Simple Webpage + + + +

Hello, World!

+ + +``` + +相应的 CSS 可能是: + +```css +h1 { + font-size: 20px; + color: green; +} +``` + +这个 CSS 将 h1 标签的内容字体大小设置为 20 像素,并将其显示为绿色文本。 + +CSS 中的 h1 部分被称为选择器或 CSS 选择器。这个 CSS 选择器表明大括号内的 CSS 将应用于任何 h1 标签的内容。 + +CSS 选择器还可以编写为仅应用于具有特定 class 或 id 属性的元素。例如,使用以下 HTML: + +```html +

Some Title

+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit +
+``` + +相应的 CSS 可能是: + +```css +h1#main-title { + font-size: 20px; +} + +div.content { + color: green; +} +``` + +\#用于指示 id 属性的值,而 . 用于指示 class 属性的值。 + +如果标签的值不重要,则可以完全省略标签名。例如,这个 CSS 将使任何具有 content 类的元素内容变为绿色: + +```css +.content { +color: green; +} +``` + + +CSS 数据可以包含在 HTML 本身中,也可以在扩展名为 .css 的单独 CSS 文件中。HTML 文件中的 CSS 放置在 HTML 文档头部的 \ +... +``` + +更常见的是,CSS 使用 link 标签导入到文档头部: + +```html + + + +... +``` + +作为一个网页抓取器,你不常需要编写样式表来美化 HTML。然而,能够读取和识别 HTML 页面如何被 CSS 转换是很重要的,这样你才能将你在网页浏览器中看到的内容与代码中看到的内容联系起来。 + +例如,当 HTML 元素没有出现在页面上时,你可能会感到困惑。当你阅读元素应用的 CSS 时,你会看到: + +```css +.mystery-element { + display: none; +} +``` + +这将元素的 display 属性设置为 none,从页面中隐藏它。 + +如果你以前从未接触过 CSS,你可能不需要深入研究它来抓取网页,但你应该熟悉它的语法,并注意本书中提到的 CSS 规则。 + + + + +## JavaScript + +当客户端向网络服务器请求特定的网页时,网络服务器会执行一些代码来创建发送回的网页。这些代码称为服务器端代码,可以简单到检索静态 HTML 文件并发送它,也可以是用 Python(最好的语言)、Java、PHP 或任何常见的服务器端编程语言编写的复杂应用程序。 + +最终,这些服务器端代码创建了一些数据流,发送到浏览器并显示。但是,如果你想要一些交互或行为——比如文本更改或拖放元素——发生而不需要返回服务器运行更多代码,你需要使用客户端代码。 + +客户端代码是由网络服务器发送的但实际由客户端浏览器执行的任何代码。在互联网早期(2000年之前),客户端代码是用多种语言编写的。例如,你可能还记得 Java 小程序和 Flash 应用程序。但是 JavaScript 作为客户端代码的唯一选项出现是有一个简单的原因:它是浏览器本身支持的唯一语言,无需下载和更新单独的软件(如 Adobe Flash Player)就可以运行程序。 + +JavaScript 起源于90年代中期,作为 Netscape Navigator 中的一个新功能。它很快被 Internet Explorer 接受,成为当时两大主流网络浏览器的标准。 + +尽管名字相似,JavaScript 与服务器端编程语言 Java 几乎没有任何关系。除了一小部分表面上的语法相似之外,它们是完全不同的语言。 + +1996年,Netscape(JavaScript 的创建者)和 Sun Microsystems(Java 的创建者)签订了许可协议,允许 Netscape 使用名称“JavaScript”,预示着两种语言之间进一步合作。然而,这种合作从未发生过,自那时以来一直是一个令人困惑的误称。 + +尽管作为一个已经消亡的网络浏览器的脚本语言,JavaScript 的开始有些不确定,但现在它是世界上最流行的编程语言。它的流行程度得益于它也可以用于服务器端,使用 Node.js。但它的流行程度肯定也得益于它是唯一的客户端编程语言。 + +JavaScript 被嵌入到 HTML 页面中使用 \ +``` + +或者可以使用 src 属性引用在单独文件中的 JavaScript: + +```html + +``` + + +与 HTML 和 CSS 不同,你在抓取网页时可能不需要阅读或编写 JavaScript,但至少了解一下它的语法会很有用。有时它可能包含有用的数据。例如: + +```javascript + +``` + +这里,使用关键字 const(代表“常量”)声明了一个 JavaScript 变量,并设置为一个包含一些数据的 JSON 格式化字符串,可以被网页抓取器直接解析。 + +JSON(JavaScript 对象表示法)是一种包含人类可读数据的文本格式,可以被网页抓取器轻松解析,也是在网络上普遍存在的。我将在第15章中进一步讨论它。 + +你可能也会看到 JavaScript 向完全不同的来源请求数据: + +```javascript + +``` + +这里,JavaScript 正在创建一个对 http://example.com/data.json 的请求,并在接收到响应后将其记录到控制台中(关于“控制台”的更多信息将在下一节中讨论)。 + +JavaScript 最初是为了在原本静态的网络中提供动态互动和动画而创建的。然而,今天,并不是所有的动态行为都是由 JavaScript 创建的。HTML 和 CSS 也有一些功能,使它们可以在页面上更改内容。 + +例如,CSS 关键帧动画可以让元素在用户单击或悬停在该元素上时移动、更改颜色、更改大小或进行其他转换。 + +认识到网站的(通常是字面上的)运动部分是如何组合在一起的,可以帮助你在尝试定位数据时避免无用功。 + +## 观察网站使用开发者工具 + +就像珠宝商的放大镜或心脏病医生的听诊器一样,你的浏览器开发者工具对于网络抓取至关重要。要从网站收集数据,你必须了解它是如何组成的。开发者工具就能展示给你这些信息。 + +在本书中,我将使用 Google Chrome 中显示的开发者工具。然而,Firefox、Microsoft Edge 和其他浏览器中的开发者工具都非常相似。 + +要访问你的浏览器菜单中的开发者工具,请按照以下说明操作: + +**Chrome** + +查看→ 开发者 → 开发者工具 + +**Safari** + +Safari → 首选项 → 高级 → 勾选“在菜单栏中显示‘开发’菜单” +然后,使用“开发”菜单:开发 → 显示网页检查器 + +**Microsoft Edge** + +使用菜单:工具 → 开发者 → 开发者工具 + +**Firefox** + +工具 → 浏览器工具 → Web 开发者工具 + +在所有浏览器中,打开开发者工具的键盘快捷键是相同的,取决于你的操作系统。 + +**Mac** + +Option + Command + I + +**Windows** + +CTRL + Shift + I + +在进行网络抓取时,你可能会大部分时间都在网络选项卡(如图1-1所示)和元素选项卡中。 + +![](/img/scraping/ch1/1.png) +*图1-1. Chrome开发者工具显示来自维基百科的页面加载* + +网络选项卡显示页面加载时页面发出的所有请求。如果你之前没有使用过它,你可能会感到惊讶!复杂页面加载时通常会发出数十甚至数百个请求以获取资产。在某些情况下,页面甚至可能会在你停留期间持续不断地发送请求。例如,它们可能正在向行动跟踪软件发送数据,或者轮询更新。 + +**在网络选项卡中看不到任何内容?** + +请注意,开发者工具必须在页面发出请求时打开,才能捕获这些请求。如果你加载页面时没有打开开发者选项卡,然后决定检查它,你可能需要刷新页面以重新加载并查看它发出的请求。 + +如果你在网络选项卡中单击一个单独的网络请求,你将看到与该请求相关的所有数据。这个网络请求检查工具的布局在不同的浏览器中略有不同,但通常允许你看到: + +* 请求发送的 URL +* 使用的 HTTP 方法 +* 响应状态 +* 与请求相关的所有头和 Cookie +* 负载 +* 响应 + +这些信息对于编写网络抓取器以复制这些请求以获取页面所获取的数据非常有用。 + +元素选项卡用于检查 HTML 文件的结构和内容。它非常方便,可以检查页面上特定数据的 HTML 标签周围,并编写抓取器来获取它。 + +![](/img/scraping/ch1/2.png) +*图1-2. 右键单击任何文本或数据,并选择检查以查看在“元素”选项卡中围绕该数据的元素* + +当你在元素选项卡中悬停在每个 HTML 元素的文本上时,你会看到浏览器中相应的元素在视觉上高亮显示。使用这个工具是探索页面和更深入了解其结构的好方法。 + +![](/img/scraping/ch1/3.png) +*图1-3. 在HTML中悬停在元素上会突出显示页面上相应的结构* + +你不需要成为互联网、网络或编程的专家就可以开始抓取网络。然而,了解这些组件如何组合以及你的浏览器的开发者工具如何显示这些组件的基础知识是必不可少的。 + + + + + + + + + + + + + + + + + + + + + diff --git a/_scraping/ch2.md b/_scraping/ch2.md new file mode 100644 index 0000000..6aa91b7 --- /dev/null +++ b/_scraping/ch2.md @@ -0,0 +1,354 @@ +--- +layout: post +title: "第二章:The Legalities and Ethics of Web Scraping" +author: "lili" +mathjax: true +sticky: false +excerpt_separator: +tags: + - python + - scraping +--- + + +**目录** +* TOC +{:toc} + +2010年,软件工程师Pete Warden构建了一个网络爬虫来收集来自Facebook的数据。他从大约2亿Facebook用户那里收集了数据——姓名、位置信息、朋友和兴趣。当然,Facebook注意到了并向他发送了停止侵权的信函,他遵守了。当被问及为何遵守时,他说:“大数据?便宜。律师?不便宜。” + +在本章中,您将了解与网络爬虫相关的美国法律(以及一些国际法律),并学习如何分析特定网络爬虫情境的合法性和伦理问题。 + +在阅读以下内容之前,请考虑一个显而易见的事实:我是软件工程师,不是律师。不要将您在这里或书中任何其他章节中阅读的内容解释为专业法律建议,也不要因此而采取行动。尽管我相信自己能够知识渊博地讨论网络爬虫的合法性和伦理,但在进行任何法律上有歧义的网络爬虫项目之前,您应咨询律师(而不是软件工程师)。 + +本章的目标是为您提供一个框架,以便您能够理解和讨论网络爬虫合法性的各个方面,如知识产权、未经授权的计算机访问和服务器使用,但这不应替代实际的法律建议。 + +## 商标、版权、专利,天哪! + +现在是知识产权速成课程时间!知识产权有三种基本类型:商标(有时用TM或®符号表示)、版权(常用©表示)和专利(常用专利号或不使用任何标志)。 + +专利仅用于声明对发明的所有权。您不能为图像、文本或任何信息本身申请专利。尽管某些专利(如软件专利)比我们所认为的“发明”更不具体,但请记住,专利的是事物(或技术),而不是构成软件的数据。除非您是从抓取的图表中构建东西,或者有人为网络爬虫的方法申请了专利,否则您不太可能因网络爬虫而无意中侵犯专利。 + +商标也不太可能成为问题,但仍需考虑。根据美国专利商标局的说法: + +商标是用来识别和区分一个方的商品来源与他方商品来源的文字、短语、符号和/或设计。服务标记是用来识别和区分服务来源而非商品的文字、短语、符号和/或设计。术语“商标”通常用于指代商标和服务标记。 + +除了您想到的文字和符号外,其他描述性属性也可以注册为商标。例如,容器的形状(如可口可乐瓶)或甚至颜色(最著名的是Owens Corning的粉红豹玻璃纤维绝缘材料的粉红色)。 + +与专利不同,商标的所有权在很大程度上取决于使用的上下文。例如,如果我希望发布一篇附带可口可乐标志图片的博客文章,我可以这么做,只要我不暗示我的博客文章是由可口可乐赞助或发布的。如果我想制造一种新软饮料,并在包装上展示相同的可口可乐标志,那显然是商标侵权。同样,虽然我可以将我的新软饮料包装成粉红豹粉红色,但我不能用这种颜色制造家庭绝缘产品。 + +这使我们进入“合理使用”话题,这通常在版权法的背景下讨论,但也适用于商标。作为对品牌的参考储存或显示商标是可以的。以可能误导消费者的方式使用商标是不可以的。然而,“合理使用”的概念不适用于专利。例如,一个行业中的专利发明不能在另一个行业中应用,除非与专利持有者达成协议。 + +### 版权法 + +商标和专利有一个共同点,即它们必须正式注册才能被认可。与普遍的误解相反,这对于版权材料并不适用。什么使图像、文本、音乐等受版权保护?不是页面底部的“版权所有”警告或“已发布”与“未发布”材料之间的任何特殊之处。您创建的每一件材料在您将其带入存在的那一刻就自动受到版权法的保护。 + +《保护文学和艺术作品伯尔尼公约》以1886年首次通过的瑞士伯尔尼命名,是版权的国际标准。该公约基本上规定,所有成员国必须像对待本国公民的作品一样承认其他成员国公民作品的版权保护。实际上,这意味着,作为美国公民,您可以在美国因侵犯例如法国某人所写材料的版权而被追究责任(反之亦然)。 + +**版权注册** + +虽然确实版权保护自动适用,不需要任何形式的注册,但也可以在美国政府正式注册版权。这通常针对有价值的创意作品(如大型电影)进行,以便以后使任何诉讼更容易,并创建强有力的文件记录以证明谁拥有作品。然而,不要让这种版权注册的存在让您感到困惑——所有创意作品,除非特别属于公共领域,都是受版权保护的! + +显然,版权比商标或专利更令网络爬虫担忧。如果我从别人的博客抓取内容并发布到自己的博客,我很可能会面临诉讼。幸运的是,我有几个保护层,可能使我的博客爬虫项目具有辩护性,具体取决于其功能。 + +首先,版权保护仅适用于创意作品,不包括统计数据或事实。幸运的是,许多网络爬虫所追求的是统计数据和事实。收集各网站诗歌并在您自己的网站上展示这些诗歌的网络爬虫可能会违反版权法;然而,收集各网站上诗歌发布频率信息的网络爬虫则不会。诗歌本身是一种创意作品。按月发布在网站上的诗歌平均字数是事实数据,而不是创意作品。 + +逐字发布内容(而不是从原始抓取数据中汇总/计算的内容)可能不违反版权法,如果该数据是价格、公司高管姓名或其他事实性信息。 + +即使是受版权保护的内容也可以在一定程度上直接使用,1988年《数字千年版权法》对此有所规定。《数字千年版权法》很长,包含许多具体规则,涵盖从电子书到电话的各个方面。然而,其中两个主要观点可能与网络爬虫特别相关: + +* 根据“避风港”保护,如果您从一个您被引导相信只包含无版权材料的来源抓取材料,但用户提交了版权材料,只要您在被通知后移除该版权材料,您就受到保护。 +* 您不能绕过安全措施(如密码保护)来收集内容。 + +此外,《数字千年版权法》还承认《美国法典》第17条第107节中的合理使用原则,如果使用版权材料符合合理使用原则,则不应根据避风港保护发出删除通知。 + +简而言之,您不应在未经原作者或版权持有人许可的情况下直接发布版权材料。如果您将有自由访问权限的版权材料存储在自己的非公开数据库中进行分析,这是可以的。如果您将该数据库发布到您的网站供查看或下载,这是不可以的。如果您分析该数据库并发布关于字数、按多产度排列的作者列表或其他数据的元分析,这是可以的。如果您在元分析中附上一些精选的引文或简短的数据样本以说明您的观点,这也可能是可以的,但您可能需要查阅《美国法典》中的合理使用条款以确保无误。 + +**版权与人工智能** + +生成式人工智能,即基于现有创作作品集生成新“创作”作品的 AI 程序,在版权法中提出了独特的挑战。 + +如果生成式 AI 程序的输出与现有作品相似,则可能存在版权问题。许多案例已经作为先例来指导“相似”一词的含义,但根据国会研究服务处的说法: + +实质性相似性测试很难定义,并且在美国各地法院的定义各不相同。法院各种描述了该测试的要求,例如,作品必须具有“实质上相似的整体概念和感觉”或“整体外观和感觉”,或“普通的合理人会无法区分两者”。 + +现代复杂算法的问题在于,很难自动确定您的 AI 是否产生了一个令人兴奋且新颖的混搭作品,还是更为……直接的衍生作品。AI 可能无法将其输出标记为与特定输入“实质上相似”,甚至无法识别它用来生成其创作的具体输入!可能第一个迹象是收到停止信函或法庭传票。 + +除了生成式 AI 输出的版权侵权问题,未来的法院案件还在测试训练过程本身是否可能侵犯版权持有人的权利。 + +为了训练这些系统,几乎总是需要下载、存储和复制受版权保护的作品。虽然下载受版权保护的图像或文本看起来没什么大不了的,但这与下载受版权保护的电影没有太大区别——而你不会下载电影,对吧? + +有些人声称这是合理使用,他们没有以影响市场的方式发布或使用内容。 + +截至撰写本文时,OpenAI 正在美国专利商标局辩称,其使用大量受版权保护的材料构成合理使用。虽然这个论点主要是在 AI 生成算法的背景下提出的,但我怀疑其结果将适用于各种用途的网页抓取工具。 + +## 对动产的侵权 + +对动产的侵权与我们通常所理解的“擅自进入”法律有根本区别,它适用于动产,即法律术语中的动产,而非不动产或土地。当您的财产因某种方式受到干扰而无法访问或使用时,该法律适用。 + +在云计算时代,我们很容易不将网络服务器视为真实的、有形的资源。然而,服务器不仅由昂贵的组件构成,还需要存储、监控、冷却、清洁并提供大量电力。根据一些估计,全球约有10%的电力消耗来自计算机。如果查看您自己的电子设备不足以让您信服,想想谷歌庞大的服务器农场,所有这些服务器都需要连接到大型电站。 + +虽然服务器是昂贵的资源,但从法律角度来看,它们很有趣,因为网络管理员通常希望人们使用他们的资源(即访问他们的网站);他们只是不希望人们过度使用他们的资源。通过浏览器访问网站是可以的,但显然不能对其发起大规模的分布式拒绝服务(DDOS)攻击。 + +要使网络爬虫违反对动产的侵权,需要满足三个标准: + +* 缺乏同意 +因为网络服务器对所有人开放,它们通常也“同意”网络爬虫的访问。然而,许多网站的服务条款协议明确禁止使用爬虫。此外,任何发给您的停止信函都可能撤销这种同意。 + +* 实际损害 +服务器是昂贵的资源。除了服务器成本外,如果您的爬虫导致网站崩溃或限制其为其他用户服务的能力,这会增加您造成的“损害”。 + +* 故意性 +如果您在编写代码,您知道它的功能是什么!在为您的网络爬虫辩护时,声称缺乏故意性可能效果不佳。 + +要使对动产的侵权适用,必须满足所有这三个标准。然而,如果您违反了服务条款协议,但未造成实际损害,不要以为您可以免于法律诉讼。您可能非常有可能违反了版权法、《数字千年版权法案》(DMCA)、《计算机欺诈和滥用法案》(本章后面会详细讨论)或适用于网络爬虫的其他多种法律之一。 + +**限制您的爬虫** + +在过去,网络服务器比个人电脑强大得多。事实上,服务器的定义之一就是“大型计算机”。现在,情况有所变化。例如,我的个人电脑有一个 3.5 GHz 的处理器和 32 GB 的 RAM。而 AWS 的中型实例相对只有 4 GB 的 RAM 和大约 3 GHz 的处理能力。 + +有了良好的互联网连接和专用机器,即使是一台个人电脑也能对许多网站造成巨大负荷,甚至使其瘫痪或完全崩溃。除非有医疗紧急情况且唯一的解决办法是两秒内汇总 Joe Schmo 网站上的所有数据,否则没有理由对某个网站进行猛烈攻击。 + +“看着的爬虫永远不会完成。” 有时让爬虫在夜间运行比在下午或晚上运行要好,原因如下: + +* 如果您有大约 8 小时时间,即使每页速度缓慢到 2 秒,您也可以爬取超过 14,000 页。当时间不是问题时,您不会被诱惑去加快爬虫的速度。 +* 假设网站的目标受众在您的一般位置(对于远程目标受众相应调整),网站的流量负载在夜间可能会大大降低,这意味着您的爬取不会增加高峰时段的拥堵。 +* 您可以通过睡觉节省时间,而不是不断检查日志以获取新信息。想想早上醒来看到全新数据的兴奋吧! + +请考虑以下场景: + +* 您有一个网络爬虫,它遍历 Joe Schmo 的网站,汇总其部分或全部数据。 +* 您有一个网络爬虫,它遍历数百个小型网站,汇总其部分或全部数据。 +* 您有一个网络爬虫,它遍历一个非常大型的网站,例如维基百科。 + +在第一个场景中,最好让爬虫在夜间慢速运行。 + +在第二个场景中,最好以循环方式爬取每个网站,而不是一个接一个慢慢爬取。根据您爬取的网站数量,这意味着您可以以您的互联网连接和机器所能管理的最快速度收集数据,但对于每个远程服务器来说,负载是合理的。您可以通过编程实现这一点,要么使用多线程(每个线程爬取一个网站并暂停其自身的执行),要么使用 Python 列表来跟踪网站。 + +在第三个场景中,您的互联网连接和家庭机器对像维基百科这样的网站造成的负载可能不会被注意到或在意。然而,如果您使用分布式网络的机器,这显然是另一回事。请小心,并在可能的情况下询问公司代表。 + + + +## 计算机欺诈和滥用法案 + +20世纪80年代初,计算机开始从学术界进入商业领域。病毒和蠕虫首次被视为不只是一个不便(或甚至是一个有趣的爱好),而是一个可能造成经济损失的严重刑事问题。1983年,由马修·布罗德里克主演的电影《战争游戏》也将这个问题带到了公众和时任总统罗纳德·里根的视野中。作为回应,1986年《计算机欺诈和滥用法案》(CFAA)应运而生。 + +虽然您可能认为CFAA仅适用于那些释放病毒的恶意黑客的刻板印象,但该法案对网络爬虫也有很强的影响。想象一下,一个爬虫扫描网络寻找易猜测密码的登录表单,或者收集意外留在隐藏但公开位置的政府机密。根据CFAA,所有这些活动都是非法的(且理应如此)。 + +该法案定义了七项主要刑事犯罪,可以概括如下: + +* 明知未经授权访问美国政府拥有的计算机并从中获取信息。 +* 明知未经授权访问计算机,获取财务信息。 +* 明知未经授权访问美国政府拥有的计算机,影响政府对该计算机的使用。 +* 明知访问任何受保护的计算机,意图欺诈。 +* 明知未经授权访问计算机并对该计算机造成损害。 +* 共享或贩卖用于访问美国政府使用的计算机或影响州际或外国商业的计算机的密码或授权信息。 +* 试图通过对任何受保护的计算机造成损害或威胁造成损害来敲诈金钱或“任何有价值的东西”。 + +简而言之:远离受保护的计算机,不要访问您未被授权访问的计算机(包括网络服务器),尤其要远离政府或金融计算机。 + + + +## obots.txt 和服务条款 + +从法律角度来看,网站的服务条款和robots.txt文件处于一个有趣的领域。如果一个网站是公开访问的,网站管理员宣称哪些软件可以访问它,哪些软件不可以,这是有争议的。说“你可以使用浏览器查看这个网站,但不能使用你自己编写的程序查看”是有难度的。 + +大多数网站在每个页面的页脚都有服务条款(TOS)的链接。TOS不仅包含了针对网络爬虫和自动化访问的规则,还通常包括网站收集什么信息、如何处理这些信息的信息,以及网站提供的服务没有任何明示或暗示的担保的法律免责声明。 + +如果你对搜索引擎优化(SEO)或搜索引擎技术感兴趣,你可能听说过robots.txt文件。如果你访问任何一个大型网站并查找其robots.txt文件,你会在根目录下找到它:http://website.com/robots.txt。 + +robots.txt文件的语法是在1994年搜索引擎技术初期发展起来的。大约在那时,像AltaVista和DogPile这样扫描整个互联网的搜索引擎开始与像Yahoo!这样的按主题组织的网站列表竞争。互联网搜索的增长不仅意味着网络爬虫数量的激增,还意味着普通公民可以获取爬虫收集的信息。 + +虽然今天我们可能认为这种可用性是理所当然的,但一些网站管理员在他们发布在网站深层结构中的信息出现在主要搜索引擎的首页搜索结果时感到震惊。作为回应,robots.txt文件的语法,即机器人排除协议(Robots Exclusion Protocol)被开发出来。 + +与服务条款不同,服务条款通常用广泛的、非常人性化的语言谈论网络爬虫,而robots.txt可以被自动程序非常容易地解析和使用。虽然这看起来像是一次性解决不受欢迎的机器人问题的完美系统,但请记住: + +* robots.txt的语法没有官方的管理机构。它是一个常用且普遍遵循的约定,但没有任何东西可以阻止任何人创建自己的版本的robots.txt文件(除了没有机器人会识别或遵守它,直到它流行起来)。也就是说,它是一个被广泛接受的约定,主要是因为它相对简单,公司没有动力去发明自己的标准或改进它。 + +* 没有法律或技术手段可以强制执行robots.txt文件。它只是一个标志,表示“请不要访问网站的这些部分”。许多网络爬虫库都遵守robots.txt——尽管这通常是一个可以被覆盖的默认设置。抛开库的默认设置不谈,编写一个遵守robots.txt的网络爬虫实际上在技术上比编写一个完全忽略它的爬虫更具挑战性。毕竟,你需要读取、解析并将robots.txt的内容应用到你的代码逻辑中。 + +机器人排除协议的语法相当简单。与Python(和许多其他语言)一样,注释以#符号开头,以换行符结束,可以在文件的任何地方使用。 + +文件的第一行,除了任何注释,都是以User-agent:开头的,指定以下规则适用于哪个用户。接下来是一组规则,根据机器人是否被允许访问网站的那一部分,可以是Allow:或Disallow:。星号(*)表示通配符,可用于描述User-agent或URL。 + +如果一条规则与其后面的规则相矛盾,最后的规则优先。例如: + +``` +#Welcome to my robots.txt file! +User-agent: * +Disallow: * + +User-agent: Googlebot +Allow: * +Disallow: /private +``` + +在这种情况下,所有机器人都不允许访问该网站的任何部分,除了Googlebot,它被允许访问除/private目录之外的所有部分。 + +Twitter(现在也称为“X”)的robots.txt文件对Google、Yahoo!、Yandex(一个流行的俄罗斯搜索引擎)、Microsoft和其他不在任何先前类别中的机器人或搜索引擎有明确的指示。Google部分(看起来与所有其他类别的机器人允许的权限相同)如下所示: + +``` +#Google Search Engine Robot +User-agent: Googlebot +Allow: /?_escaped_fragment_ +Allow: /?lang= +Allow: /hashtag/*?src= +Allow: /search?q=%23 +Disallow: /search/realtime +Disallow: /search/users +Disallow: /search/*/grid +Disallow: /*? +Disallow: /*/followers +Disallow: /*/following +``` + +请注意,Twitter限制访问那些有API的站点部分。因为Twitter有一个管理良好的API(并且可以通过许可赚钱),所以公司最好禁止任何“自制API”通过独立爬取其站点来收集信息。 + +虽然一个文件告诉你的爬虫不能去哪里起初可能看起来有些限制,但对于网络爬虫开发来说,它可能是一个隐藏的福音。如果你发现一个robots.txt文件禁止爬取网站的某个部分,这基本上是网站管理员在说,他们对爬虫在网站的其他部分是没有问题的。毕竟,如果他们对其有问题,他们在编写robots.txt时就会限制访问。 + +例如,适用于一般网络爬虫(与搜索引擎相对)的Wikipedia的robots.txt文件部分非常宽松。它甚至包含了欢迎机器人的人类可读文本(这就是我们!),并且只阻止了少数页面的访问,例如登录页面、搜索页面和“随机文章”页面: + +``` +# +# Friendly, low-speed bots are welcome viewing article pages, but not +# dynamically generated pages please. +# +# Inktomi's "Slurp" can read a minimum delay between hits; if your bot supports +# such a thing using the 'Crawl-delay' or another instruction, please let us +# know. +# +# There is a special exception for API mobileview to allow dynamic mobile web & +# app views to load section content. +# These views aren't HTTP-cached but use parser cache aggressively and don't +# expose special: pages etc. +# +User-agent: * +Allow: /w/api.php?action=mobileview& +Disallow: /w/ +Disallow: /trap/ +Disallow: /wiki/Especial:Search +Disallow: /wiki/Especial%3ASearch +Disallow: /wiki/Special:Collection +Disallow: /wiki/Spezial:Sammlung +Disallow: /wiki/Special:Random +Disallow: /wiki/Special%3ARandom +Disallow: /wiki/Special:Search +Disallow: /wiki/Special%3ASearch +Disallow: /wiki/Spesial:Search +Disallow: /wiki/Spesial%3ASearch +Disallow: /wiki/Spezial:Search +Disallow: /wiki/Spezial%3ASearch +Disallow: /wiki/Specjalna:Search +Disallow: /wiki/Specjalna%3ASearch +Disallow: /wiki/Speciaal:Search +Disallow: /wiki/Speciaal%3ASearch +Disallow: /wiki/Speciaal:Random +Disallow: /wiki/Speciaal%3ARandom +Disallow: /wiki/Speciel:Search +Disallow: /wiki/Speciel%3ASearch +Disallow: /wiki/Speciale:Search +Disallow: /wiki/Speciale%3ASearch +Disallow: /wiki/Istimewa:Search +Disallow: /wiki/Istimewa%3ASearch +Disallow: /wiki/Toiminnot:Search +Disallow: /wiki/Toiminnot%3ASearch +``` + +是否选择编写遵守robots.txt的网络爬虫取决于你,但我强烈推荐这样做,特别是如果你有无差别爬取网络的爬虫。 + + + +## 三个网络爬虫案例 + +由于网络爬虫的应用范围如此广泛,有很多方式可以使你陷入法律困境。本节介绍了三个涉及某种通常适用于网络爬虫的法律的案例,以及这些法律在具体案例中的应用。 + +### eBay诉Bidder's Edge及对动产的侵害 + +1997年,Beanie Baby市场火爆,科技行业蓬勃发展,在线拍卖行成为互联网的新宠。一家公司叫Bidder's Edge成立,创建了一种新型的元拍卖网站。它不再强迫你在不同的拍卖网站之间来回比较价格,而是聚合所有当前拍卖的特定产品(比如,一款热门的Furby娃娃或一张《香料世界》CD)的数据,并指向价格最低的网站。 + +Bidder's Edge通过一支网络爬虫大军实现了这一点,这些爬虫不断向各拍卖网站的服务器发出请求以获取价格和产品信息。在所有拍卖网站中,eBay是最大的,而Bidder's Edge每天向eBay的服务器发送约10万次请求。即使按今天的标准,这也是很大的流量。根据eBay的说法,这占其当时总互联网流量的1.53%,显然eBay对此并不满意。 + +eBay向Bidder's Edge发出停止和终止信,并提供了其数据的许可,但双方未能达成许可协议,Bidder's Edge继续爬取eBay的网站。 + +eBay尝试阻止Bidder's Edge使用的IP地址,封锁了169个IP地址,但Bidder's Edge通过使用代理服务器(代表其他机器转发请求的服务器,但使用代理服务器自己的IP地址)绕过了这些封锁。可以想象,这对双方来说都是一个令人沮丧且不可持续的解决方案——Bidder's Edge不断寻找新的代理服务器并购买新的IP地址,而旧的IP地址被封锁,eBay则不得不维护庞大的防火墙列表(并在每次数据包检查中增加计算量大的IP地址比较开销)。 + +最终,在1999年12月,eBay以动产侵害的罪名起诉Bidder's Edge。由于eBay的服务器是其拥有的真实、具体的资源,而它不欣赏Bidder's Edge对其的异常使用,动产侵害似乎是理想的法律依据。事实上,在现代,动产侵害与网络爬虫诉讼密不可分,通常被认为是一种信息技术法律。 + +法院裁定,要让eBay通过动产侵害赢得诉讼,eBay必须证明两点: + +* Bidder's Edge知道其明确被禁止使用eBay的资源。 +* eBay因Bidder's Edge的行为遭受了经济损失。 + +鉴于eBay的停止和终止信的记录,加上IT记录显示的服务器使用情况和与服务器相关的实际成本,eBay相对容易证明这一点。当然,没有任何大型法庭案件会轻易结束:双方提起了反诉,许多律师得到了报酬,最终在2001年3月案件在庭外和解,金额未披露。 + +那么,这是否意味着任何未经授权使用他人服务器的行为都会自动构成动产侵害的违法行为?不一定。Bidder's Edge是一个极端案例;它使用了eBay如此多的资源,以至于公司不得不购买额外的服务器,支付更多的电费,甚至可能雇佣额外的人员。虽然1.53%的增加看起来不多,但在大公司中,这可能会累计成一笔可观的费用。 + +2003年,加利福尼亚州最高法院裁定了另一起案件,即Intel公司诉Hamidi案。在该案中,一名前Intel员工(Hamidi)向Intel员工发送了Intel不喜欢的电子邮件,邮件通过Intel的服务器传送。法院表示: + +>“Intel的主张失败了,不是因为通过互联网传输的电子邮件享有独特的豁免权,而是因为在加利福尼亚州,动产侵害罪——与前述的诉因不同——不能在没有证据表明对原告个人财产或其法律利益造成损害的情况下成立。” + +基本上,Intel未能证明Hamidi向所有员工发送的六封电子邮件的传输成本(有趣的是,每封邮件都包含从Hamidi的邮件列表中删除的选项——至少他还是很有礼貌的!)对Intel造成了任何经济损失。它没有剥夺Intel任何财产或其使用权。 + + + +### 美国诉奥恩海默案与《计算机欺诈和滥用法》 + +如果某信息通过网页浏览器可以轻松获取,那么以自动化方式获取相同信息不太可能让你陷入联邦调查局的困境。然而,对一个足够好奇的人来说,发现一个小的安全漏洞很容易,但当自动化爬虫介入时,这个小漏洞可能很快变成一个更大、更危险的问题。 + +2010年,安德鲁·奥恩海默(Andrew Auernheimer)和丹尼尔·斯皮特勒(Daniel Spitler)注意到了iPad的一个有趣功能:当你使用iPad访问AT&T的网站时,AT&T会将你重定向到一个包含你iPad唯一ID号的URL: + +``` +https://dcp2.att.com/OEPClient/openPage?ICCID=&IMEI= +``` + +该页面包含一个登录表单,表单上有与URL中ID号对应的用户的电子邮件地址。用户只需输入密码即可访问自己的账户。 + +尽管可能的iPad ID号数量庞大,但通过一个网络爬虫,还是可以遍历这些可能的ID号并收集电子邮件地址。通过提供这个便捷的登录功能,AT&T实际上将其客户的电子邮件地址公开给了网络。 + +奥恩海默和斯皮特勒创建了一个爬虫,收集了11.4万个电子邮件地址,其中包括名人、首席执行官和政府官员的私人电子邮件地址。奥恩海默(但不是斯皮特勒)随后将名单和获取方式发送给了Gawker媒体,后者在标题为“苹果最严重的安全漏洞:11.4万iPad用户信息泄露”的报道中披露了这一消息(但未公布名单)。 + +2011年6月,联邦调查局因收集电子邮件地址而搜查了奥恩海默的家,虽然他们最终因毒品指控逮捕了他。2012年11月,他被判犯有身份欺诈和共谋未经授权访问计算机罪,随后被判处41个月联邦监禁,并被命令支付7.3万美元的赔偿。 + +他的案件引起了民权律师奥林·克尔(Orin Kerr)的注意,他加入了奥恩海默的法律团队,并向第三巡回上诉法院提出上诉。2014年4月11日(法律程序可能需要相当长的时间),他们提出了这样的论点: + +奥恩海默在第一项指控上的定罪必须推翻,因为访问一个公开可用的网站不构成《计算机欺诈和滥用法》(18 U.S.C. § 1030(a)(2)(C))下的未经授权访问。AT&T选择不使用密码或任何其他保护措施来控制对其客户电子邮件地址的访问。AT&T主观上希望外人不要发现这些数据,或者奥恩海默夸张地将访问描述为“盗窃”是无关紧要的。公司配置其服务器以使信息对所有人可见,从而授权公众查看这些信息。通过AT&T的公共网站访问电子邮件地址在CFAA下是被授权的,因此不是犯罪。 + +虽然奥恩海默的定罪因场所不足而在上诉中被推翻,但第三巡回法院在其裁决的脚注中似乎对这一论点表示支持: + +虽然我们不需要解决奥恩海默的行为是否涉及此类侵入,但审判中没有证据表明账户爬虫曾经突破任何密码门或其他基于代码的屏障。账户爬虫只是访问了登录屏幕的公共部分,并抓取了AT&T无意中发布的信息。 + +虽然奥恩海默最终没有因《计算机欺诈和滥用法》而被定罪,但他的家被联邦调查局搜查,他花费了数千美元的法律费用,并在法庭和监狱中度过了三年。 + +作为网络爬虫使用者,我们可以从中汲取哪些教训以避免类似情况?或许一个好的开始是:不要做混蛋。 + +爬取任何形式的敏感信息,无论是个人数据(在本案中是电子邮件地址)、商业机密或政府机密,可能都是你不想在没有律师随时待命的情况下做的事情。即使是公开可用的信息,想一想:“普通计算机用户是否能够轻松访问这些信息,如果他们想查看的话?”或“这是公司希望用户看到的内容吗?” + +我多次致电公司报告其网络应用程序中的安全漏洞。这个方式效果显著:“你好,我是一名安全专家,发现了你们网站上的一个潜在漏洞。能否引导我找到相关人员报告并解决这个问题?”除了立即得到对你(白帽)黑客天才的认可外,你可能还会获得免费的订阅、现金奖励和其他好处! + +此外,奥恩海默将信息发布给Gawker媒体(而不是先通知AT&T)以及他对漏洞利用的炫耀行为,也使他成为AT&T律师的特别目标。 + +如果你在某个网站上发现安全漏洞,最好的做法是通知该网站的所有者,而不是媒体。你可能会想写一篇博客文章并向全世界宣布,特别是如果问题没有立即得到解决的话。然而,你需要记住,这是公司的责任,而不是你的。你能做的最好的事情就是把你的网络爬虫(如果适用的话,还有你的业务)从该网站上撤走! + + +### 菲尔德 vs Google: 版权和robots.txt + + + +布莱克·菲尔德(Blake Field)是一名律师,他以谷歌的网站缓存功能违反版权法为由提起了诉讼,因为在他从自己的网站上移除书籍后,谷歌显示了该书的副本。版权法允许原创作品的创作者对该作品的分发拥有控制权。菲尔德的论点是,谷歌的缓存(在他从网站上移除后)剥夺了他控制其分发的能力。 + +**谷歌网页缓存** + +当谷歌网络爬虫(也称为Googlebots)爬取网站时,它们会复制该网站并将其托管在互联网上。任何人都可以通过以下URL格式访问此缓存:http://webcache.googleusercontent.com/search?q=cache:http://pythonscraping.com +如果你正在搜索或爬取的网站不可用,你可以在那里查看是否存在可用的副本! + +了解谷歌的缓存功能但未采取行动并没有帮助菲尔德的案件。毕竟,他本可以通过添加robots.txt文件简单指示哪些页面应该被爬取,哪些不应该被爬取来阻止谷歌爬虫缓存他的网站。 + +更重要的是,法院发现《数字千年版权法》(DMCA)的安全港规定允许谷歌合法地缓存和显示菲尔德等网站:“服务提供商不应因在由其控制或为其服务的系统或网络上中间和临时存储材料而导致的版权侵权而被要求支付金钱赔偿。” + + + + + + + + diff --git a/_scraping/ch3.md b/_scraping/ch3.md new file mode 100644 index 0000000..4e704a2 --- /dev/null +++ b/_scraping/ch3.md @@ -0,0 +1,171 @@ +--- +layout: post +title: "第三章:Applications of Web Scraping" +author: "lili" +mathjax: true +sticky: false +excerpt_separator: +tags: + - python + - scraping +--- + + +**目录** +* TOC +{:toc} + +虽然网页抓取器几乎可以帮助任何企业,但通常真正的难题在于如何实现。像人工智能,或者说编程本身,你不能只是挥舞魔法棒就期望它改善你的利润。将网页抓取应用于你的业务需要真正的策略和仔细的规划,以便有效利用它。你需要识别特定的问题,弄清楚需要什么数据来解决这些问题,然后概述输入、输出和算法,这些算法将使你的网页抓取器生成这些数据。 + +## 项目分类 + +在规划网页抓取项目时,你应该考虑它如何适应几个类别之一。 + +你的网页抓取器是“广泛的”还是“有针对性的”?你可以编写模板来指导有针对性的网页抓取器,但广泛的抓取器需要不同的技术: + +* 你会抓取单个网站还是甚至是该网站内的一组固定页面?如果是这样,这就是一个非常有针对性的网页抓取项目。 +* 你需要抓取一组已知的网站吗?这仍然是一个相当有针对性的抓取器,但你可能需要为每个网站编写少量的自定义代码,并在网页抓取器的架构上投入更多时间。 +* 你是否在抓取大量未知网站并动态发现新目标?你会构建一个必须自动检测和假设网站结构的爬虫吗?你可能正在编写一个广泛或无目标的抓取器。 + +你需要一次性运行抓取器还是这将是一个持续的工作,不断重新获取数据或不断寻找要抓取的新页面? + +* 一次性网页抓取项目可以快速且便宜地编写。代码不必精美!该项目的最终结果是数据本身——你可能会交出一个Excel或CSV文件给业务部门,他们很满意。完成后代码可以丢弃。 + +* 任何涉及监控、重新扫描新数据或更新数据的项目,都需要更健壮的代码,能够维护。它可能还需要自己的监控基础设施,以检测何时遇到错误、无法运行或使用的时间或资源超出预期。 + +收集的数据是你的最终产品还是需要更深入的分析或操作? + +* 在简单的数据收集中,网页抓取器将数据直接存入数据库,或许进行一些简单的清理(例如,去除产品价格中的美元符号)。 +* 当需要更高级的分析时,你甚至可能不知道哪些数据是重要的。在这种情况下,你也必须更加考虑抓取器的架构。 + +我鼓励你考虑每个项目可能属于哪一类,以及该项目的范围如何需要修改以适应你的业务需求。 + + + + +## 电子商务 + +虽然我编写过各种从网页上收集有趣数据的网页抓取器,但我收到的最常见请求是从电子商务网站上收集产品和价格数据。 + +通常,这些请求来自拥有竞争性电子商务网站的人,或者是正在进行研究、计划推出新产品或进入新市场的人。在电子商务中,你可能首先想到的指标是“价格”。你想了解你的价格与竞争对手的价格相比如何。然而,还有很多其他可能的数据你可能想收集。 + +许多产品(但不是全部)都有各种尺寸、颜色和样式的变化。这些变化可能与不同的成本和可用性相关。记录每种产品的每个变化,以及每个主要产品列表,可能会有所帮助。请注意,对于每个变化,你可能会找到唯一的SKU(库存单位)识别码,该代码对于单个产品变化和电子商务网站是唯一的(Target 和 Walmart 对于每个产品变化有不同的SKU,但如果你以后回头查看,SKU会保持不变)。即使SKU在网站上并不立即可见,你很可能会在页面的HTML代码中或在填充网站产品数据的JavaScript API中找到它。 + +在抓取电子商务网站时,记录产品的可用数量也可能很重要。像SKU一样,单位数量可能在网站上并不立即可见。你可能会在HTML或网站使用的API中找到此信息。确保还记录产品缺货的情况!这对于评估市场需求以及影响你自己产品的定价(如果你有库存)可能很有用。 + +当产品打折时,你通常会在网站上清楚地看到折扣价和原价。确保分别记录这两个价格。通过跟踪一段时间内的销售情况,你可以分析竞争对手的促销和折扣策略。 + +产品评价和评分是另一个有用的信息来源。当然,你不能直接在自己的网站上显示来自竞争对手网站的产品评价文本。然而,分析这些评价的原始数据可以帮助你了解哪些产品受欢迎或正流行。 + + +## 营销 + +在线品牌管理和营销通常涉及大量数据的聚合。与其在社交媒体上滚动浏览或花费数小时搜索公司的名字,不如让网页抓取器做所有繁重的工作! + +恶意攻击者可以利用网页抓取器来“复制”网站,目的是销售假冒商品或欺骗潜在客户。幸运的是,网页抓取器也可以帮助打击这种行为,通过扫描搜索引擎结果,查找公司商标和其他知识产权的欺诈或不当使用。有些公司,如MarqVision,还将这些网页抓取器作为服务出售,让品牌可以外包抓取网页、检测欺诈和发布删除通知的过程。 + +另一方面,并非所有使用品牌商标的行为都是侵权。如果你的公司被提及用于评论或评估,你可能会想知道这些信息!网页抓取器可以聚合和跟踪公众对公司及其品牌的情感和看法。 + +在全网跟踪你的品牌时,不要忘记你的竞争对手!你可以考虑抓取那些评价过竞争产品或讨论过竞争品牌的人的信息,以便向他们提供折扣或试用促销。 + +当然,谈到营销和互联网,首先想到的往往是“社交媒体”。抓取社交媒体的好处在于,通常只有少数大型网站允许你编写有针对性的抓取器。这些网站包含数百万格式良好的帖子,具有类似的数据和属性(如点赞、分享和评论),可以轻松地在网站间进行比较。 + +社交媒体的缺点是获取数据可能会遇到障碍。一些网站,如Twitter,提供免费的或收费的API。其他社交媒体网站则通过技术和法律手段保护其数据。我建议你在抓取Facebook和LinkedIn等网站之前,咨询公司法律顾问。 + +跟踪关于与你品牌相关话题的帖子的指标(点赞、分享和评论)有助于识别热门话题或互动机会。根据内容长度、包含图像/媒体和语言使用等属性跟踪受欢迎程度,还可以识别哪些内容最能引起目标受众的共鸣。 + +如果让拥有数亿粉丝的人赞助你的产品超出了公司的预算,你可以考虑“微影响者”或“纳米影响者”——社交媒体存在感较小的用户,他们甚至可能不认为自己是影响者!在这里,构建一个网页抓取器以找到和定位那些经常发布与你品牌相关话题的账户将会很有帮助。 + +## 学术研究 + +虽然本章的大多数例子最终都是为了促进资本主义的发展,网页抓取器也被用于追求知识。网页抓取器常用于医学、社会学和心理学研究等多个领域。 + +例如,罗格斯大学开设了一门名为“计算社会科学”的课程,教授学生使用网页抓取来收集研究项目的数据。一些大学课程,如奥斯陆大学的“收集和分析大数据”,甚至将本书列入课程大纲! + +2017年,由美国国家卫生研究院支持的一个项目抓取了美国监狱囚犯的记录,以估算感染艾滋病毒的囚犯数量。该项目引发了一场广泛的伦理分析,权衡了这项研究的好处与囚犯隐私风险之间的关系。最终,研究继续进行,但我建议在使用网页抓取进行研究之前,特别是在医学领域,仔细审查项目的伦理问题。 + +另一项健康研究从《卫报》关于肥胖的新闻文章中抓取了数百条评论,并分析了这些评论的修辞。尽管规模小于其他研究项目,但值得考虑的是,网页抓取器也可以用于需要“小数据”和定性分析的项目。 + +以下是另一个利用网页抓取进行小众研究的例子。2016年,一项全面研究抓取并对每个加拿大社区学院的营销材料进行了定性分析。研究人员发现,现代设施和“非常规组织符号”是最受推崇的。 + +在经济学研究中,日本银行发布了一篇关于他们使用网页抓取获取“替代数据”的论文。所谓替代数据,是指银行通常使用的GDP统计数据和公司财务报告之外的数据。在这篇论文中,他们透露了网页抓取器是替代数据的一个来源,他们利用这些数据来调整价格指数。 + +## 产品构建 + +你是否有一个商业想法,并且只需要一个包含相对公共、常识性信息的数据库来启动?是否找不到价格合理且方便的现成信息来源?你可能需要一个网页抓取器。 + +网页抓取器可以快速提供数据,让你获得一个最低可行产品(MVP)进行发布。以下是一些网页抓取器可能是最佳解决方案的情况: + +**旅游网站,列出热门旅游景点和活动** + +在这种情况下,仅仅一个简单的地理信息数据库是不够的。你需要知道人们将会参观的是基督救世主,而不仅仅是里约热内卢,巴西。一个企业目录也不太合适。虽然人们可能对大英博物馆非常感兴趣,但街边的赛恩斯伯里超市并没有同样的吸引力。然而,许多旅游评论网站已经包含了关于热门旅游景点的信息。 + +**产品评论博客** + +抓取一份产品名称和关键词或描述的列表,然后使用你喜欢的生成型聊天AI来补充其余内容。 + +说到人工智能,那些模型需要数据——通常是大量的数据!无论你是希望预测趋势还是生成逼真的自然语言,网页抓取通常是获取产品训练数据集的最佳方式。 + +许多商业服务产品需要拥有严格保密的行业知识,这些知识可能昂贵或难以获得,例如工业材料供应商列表、特定领域专家的联系信息或各公司开放的职位。网页抓取器可以汇集在网上各处找到的这些信息,使你能够以相对较低的前期成本建立一个综合数据库。 + +## 旅行 + +无论你是想创办一家旅游相关的公司,还是非常热衷于在下次度假时省钱,旅游行业因其提供的多种网页抓取应用而值得特别关注。 + +酒店、航空公司和租车服务在各自的市场中几乎没有产品差异化,竞争对手众多。这意味着价格通常非常接近,并且会随着市场条件的变化而频繁波动。 + +尽管像Kayak和Trivago这样的网站现在可能已经足够大和强大,可以支付或获得API,但所有公司都是从某个地方开始的。网页抓取器可以成为启动新的旅游聚合网站的一个绝佳方式,通过从网络各处找到最优惠的交易来吸引用户。 + +即使你不打算创业,是否曾乘坐飞机或计划将来乘坐?如果你在寻找测试本书技能的想法,我强烈推荐写一个旅游网站抓取器作为第一个项目。大量的数据和这些数据随时间的波动提供了一些有趣的工程挑战。 + +旅游网站在反抓取防御方面也处于中间地带。它们希望被搜索引擎爬取和索引,并希望使其数据对用户友好且易于访问。然而,它们与其他旅游网站的竞争激烈,这可能需要使用本书后面介绍的一些高级技术。关注浏览器头信息和Cookies是一个好的起点。 + +如果你发现自己被某个旅游网站屏蔽,不确定如何通过Python访问其内容,请放心,可能还有其他旅游网站拥有相同的数据供你尝试。 + +## 销售 + +网页抓取器是获取销售线索的理想工具。如果你知道某个网站上有你的目标市场的联系信息来源,剩下的工作就简单了。无论你的领域多么小众都不重要。在我与销售客户的合作中,我抓取过青少年体育队教练、健身房老板、护肤品供应商等各种目标受众的名单,用于销售目的。 + +招聘行业(我认为是销售的一个子集)经常在双方利用网页抓取器。候选人资料和职位列表都会被抓取。由于LinkedIn的强力反抓取政策,插件如Instant Data Scraper或Dux-Soup常常被用来在浏览器中手动访问时抓取候选人资料。这使得招聘人员能够在抓取页面之前快速浏览候选人,确保他们适合职位描述。 + +像Yelp这样的目录可以帮助定制实体企业的搜索,比如“昂贵程度”、是否接受信用卡、提供外送或餐饮服务,或是否提供酒类。虽然Yelp主要以餐馆评论闻名,但它也有关于本地木匠、零售店、会计师、汽车修理店等详细信息。 + +像Yelp这样的网站不仅仅是向客户宣传企业——联系信息也可以用来进行销售介绍。同样,详细的过滤工具可以帮助你定制目标市场。 + +抓取员工目录或招聘网站也可以成为获取员工姓名和联系信息的宝贵来源,这有助于进行更有针对性的销售介绍。检查Google的结构化数据标签(见下一节,“SERP抓取”)是构建广泛网页抓取器的好策略,可以在抓取可靠、格式良好的联系信息的同时,定位多个网站。 + +本书中的几乎所有例子都是关于抓取网站的“内容”——它们呈现的人类可读信息。然而,即使是网站的底层代码也能透露很多信息。它使用什么内容管理系统?有没有关于其服务器端技术栈的线索?是否有任何客户聊天机器人或分析系统? + +了解潜在客户可能已经使用或需要的技术,对于销售和营销来说是非常有价值的。 + + +## SERP抓取 + +SERP(search engine results page),即搜索引擎结果页面抓取,是直接从搜索引擎结果中抓取有用数据,而无需访问链接页面的做法。搜索引擎结果的好处是其格式已知且一致,而搜索引擎链接的页面格式多样且未知,处理这些页面既繁琐又麻烦,能避免最好。 + +搜索引擎公司有专门的人员,他们的全部工作就是利用元数据分析、巧妙编程和AI工具从网站中提取页面摘要、统计数据和关键词。使用他们的结果,而不是尝试在内部复制这些结果,可以节省大量时间和金钱。 + +例如,如果你想获取过去40年所有主要美国体育联盟的排名信息,你可能会找到各种信息来源。http://nhl.com有一种格式的冰球排名,而http://nfl.com有另一种格式的橄榄球排名。然而,搜索“nba standings 2008”或“mlb standings 2004”会提供一致格式的结果,并可以深入查看该赛季的个别比赛得分和球员信息。 + +你可能还需要关于搜索结果本身的存在和排名信息,例如,跟踪哪些网站出现以及它们在某些搜索词中的顺序。这可以帮助监控你的品牌,并留意竞争对手。 + +如果你正在运行搜索引擎广告活动,或有意启动一个,你可以仅监控广告而不是所有搜索结果。同样,你可以跟踪哪些广告出现、它们的顺序,或许还可以了解这些结果随时间的变化。 + +确保不要只限于主要的搜索结果页面。例如,Google有Google Maps、Google Images、Google Shopping、Google Flights、Google News等。这些本质上都是不同类型内容的搜索引擎,可能与你的项目相关。 + +即使你没有从搜索引擎本身抓取数据,了解搜索引擎如何找到和标记它们在特殊搜索结果功能和增强显示的数据也可能有帮助。搜索引擎不会进行大量的猜测来确定如何显示数据;它们要求网页开发人员专门格式化内容,以便由像它们这样的第三方显示。 + +Google的结构化数据文档可以在此处找到。如果你在抓取网页时遇到这些数据,现在你就知道如何使用它们了。 + + + + + + + + + + + + + diff --git a/_scraping/ch4.md b/_scraping/ch4.md new file mode 100644 index 0000000..b358b7d --- /dev/null +++ b/_scraping/ch4.md @@ -0,0 +1,408 @@ +--- +layout: post +title: "第四章:Writing Your First Web Scraper" +author: "lili" +mathjax: true +sticky: false +excerpt_separator: +tags: + - python + - scraping +--- + + +**目录** +* TOC +{:toc} + +这一章的内容从发送GET请求(请求获取网页内容)开始,读取该页面的HTML输出,并进行一些简单的数据提取,以便隔离你所寻找的内容。在开始之前,让我们先来欣赏一下网页浏览器为我们做的一切小事情。刚开始网页抓取时,没有HTML格式化、CSS样式、JavaScript执行和图像渲染的网络可能会显得有点令人畏惧。在这一章中,我们将开始探讨如何在没有网页浏览器的帮助下格式化和解释这些基本数据。 + + +## 安装和使用Jupyter。 + + 这门课程的代码可以在https://github.com/REMitchell/python-scraping 上找到。 在大多数情况下,代码示例是Jupyter Notebook文件的形式,扩展名为.ipynb。 如果你还没有使用过它们,Jupyter Notebook是一种极好的方式,可以组织和处理许多小但相关的Python代码片段,如图4-1所示。 + + + + +![](/img/scraping/ch4/1.png) + +每段代码都包含在一个名为单元格的框中。你可以通过输入Shift + Enter或点击页面顶部的运行按钮来运行每个单元格中的代码。 + +Jupyter项目始于2014年的IPython(交互式Python)项目的一个衍生项目。这些笔记本设计用于在浏览器中以可访问和交互的方式运行Python代码,适合于教学和演示。 + +要安装Jupyter笔记本: + +``` +pip install notebook +``` + +在安装完成后,你应该可以访问jupyter命令,该命令将允许你启动Web服务器。导航至包含本书下载的练习文件的目录,并运行: + +``` +jupyter notebook +``` +这将在8888端口上启动Web服务器。如果你已经运行了一个Web浏览器,一个新的标签页应该会自动打开。如果没有,请将终端中显示的带有提供的令牌的URL复制到你的Web浏览器中。 + +## 连接 + +在这本书的第一部分中,我们深入探讨了互联网如何将数据包从浏览器发送到Web服务器,然后再次返回。当你打开一个浏览器,输入google.com并按下回车键时,正是这种情况发生了——数据以HTTP请求的形式从你的计算机传输出去,谷歌的Web服务器以HTML文件的形式回应,代表了google.com根目录下的数据。 + +但是,在这些数据包和帧的交换中,Web浏览器到底起到了什么作用呢?实际上,完全没有。事实上,ARPANET(第一个公共分组交换网络)比第一个Web浏览器Nexus要早至少20年。 + +是的,Web浏览器是一个有用的应用程序,用于创建这些信息包,并告诉你的操作系统发送它们,并将你收到的数据解释为漂亮的图片、声音、视频和文本。然而,Web浏览器只是代码,而代码可以被拆解,分解为其基本组件,重新编写、重复使用,并被制作成任何你想要的东西。Web浏览器可以告诉处理无线(或有线)接口的应用程序发送数据的处理器,但是你可以使用Python仅需三行代码来做同样的事情: + +``` +from urllib.request import urlopen +html = urlopen('http://pythonscraping.com/pages/page1.html') +print(html.read()) +``` + + +要运行这个代码,你可以使用GitHub仓库中第1章的IPython笔记本,或者你可以将其保存到本地作为scrapetest.py,并在终端中使用以下命令运行它: + +``` +python scrapetest.py +``` + + +请注意,如果你的计算机上同时安装了Python 2.x,并且同时运行两个Python版本,你可能需要显式地调用Python 3.x来运行这个命令: + +``` +python3 scrapetest.py +``` + +这个命令输出了位于URL http://pythonscraping.com/pages/page1.html 的页面1的完整HTML代码。更准确地说,这输出了在位于http://pythonscraping.com 的域名的服务器上的\/pages 目录中找到的HTML文件page1.html。 + +为什么将这些地址看作“文件”而不是“页面”很重要?大多数现代网页都与许多资源文件相关联。这些文件可能是图片文件、JavaScript文件、CSS文件,或者与您请求的页面相关联的任何其他内容。当Web浏览器遇到像\这样的标签时,浏览器知道它需要向服务器发出另一个请求,以获取位置cuteKitten.jpg处的数据,以便完全渲染页面供用户查看。 + +当然,你的Python脚本还没有逻辑去返回并请求多个文件(但现在);它只能读取你直接请求的单个HTML文件。 + +``` +from urllib.request import urlopen +``` + +这意味着它看起来像它的意思:它查看Python模块request(在urllib库中找到)并仅导入函数urlopen。 + +urllib是一个标准的Python库(这意味着你不需要安装任何额外的东西来运行这个示例),它包含用于在网络上请求数据、处理Cookie,甚至更改元数据(如头部和用户代理)的函数。我们将在整本书中广泛使用urllib,所以我建议你阅读一下该库的Python文档。 + +urlopen用于打开网络上的远程对象并读取它。由于它是一个相当通用的函数(它可以轻松地读取HTML文件、图像文件或任何其他文件流),我们将在整本书中经常使用它。 + + + +## 介绍 BeautifulSoup + +>Beautiful Soup, so rich and green, +>Waiting in a hot tureen! +>Who for such dainties would not stoop? +>Soup of the evening, beautiful Soup! + +BeautifulSoup 库的名字来源于刘易斯·卡罗尔在《爱丽丝梦游仙境》中的同名诗歌。在故事中,这首诗由一个叫假乌龟(Mock Turtle)的角色演唱(假乌龟本身是对维多利亚时期流行的假乌龟汤的双关语,这种汤不是用乌龟,而是用牛肉做的)。 + +像它在《仙境》中的同名角色一样,BeautifulSoup 尝试理解无意义的东西;它通过修复糟糕的 HTML 并向我们呈现易于遍历的代表 XML 结构的 Python 对象,帮助格式化和组织混乱的网页。 + +### 安装 BeautifulSoup + +因为 BeautifulSoup 库不是默认的 Python 库,所以必须安装。如果你已经有安装 Python 库的经验,请使用你喜欢的安装程序并跳到下一节 “运行 BeautifulSoup”(第46页)。对于那些没有安装过 Python 库(或需要复习)的读者来说,本书将使用这个通用方法来安装多个库,因此你可能希望将这一节作为未来的参考。 + +在整本书中,我们将使用 BeautifulSoup 4 库(也称为 BS4)。BeautifulSoup 4 的完整文档以及安装说明可以在 Crummy.com 上找到。 + +如果你花了很多时间编写 Python 代码,你可能已经使用过 Python 的包安装器(pip)。如果没有,我强烈建议你安装 pip 以便安装 BeautifulSoup 和本书中使用的其他 Python 包。根据你使用的 Python 安装程序,pip 可能已经安装在你的计算机上。要检查,请尝试: + +```bash +pip +``` + +这个命令应打印 pip 帮助文本到你的终端。如果命令未被识别,你可能需要安装 pip。pip 可以通过多种方式安装,例如在 Linux 上使用 apt-get 或在 macOS 上使用 brew。无论你的操作系统是什么,你也可以下载 pip 引导文件 https://bootstrap.pypa.io/get-pip.py,保存此文件为 get-pip.py,并使用 Python 运行它: + +```bash +python get-pip.py +``` + +再次注意,如果你的机器上同时安装了 Python 2.x 和 3.x,你可能需要显式调用 python3: + +```bash +python3 get-pip.py +``` + +最后,使用 pip 安装 BeautifulSoup: + +```bash +pip install bs4 +``` + +如果你有两个版本的 Python,以及两个版本的 pip,你可能需要调用 pip3 来安装 Python 3.x 版本的包: + +```bash +pip3 install bs4 +``` + +就这样!现在 BeautifulSoup 将被识别为你机器上的一个 Python 库。你可以通过打开一个 Python 终端并导入它来测试: + +```python +from bs4 import BeautifulSoup +``` + +导入应无错误完成。 + +**使用虚拟环境保持库的清晰** + +如果你打算进行多个 Python 项目,或者你需要一种简单的方式来捆绑所有相关库的项目,或者你担心已安装库之间的潜在冲突,你可以安装一个 Python 虚拟环境以保持一切分离并易于管理。 + +当你在没有虚拟环境的情况下安装 Python 库时,你是在全局安装它。这通常需要你是管理员,或者以 root 身份运行,并且 Python 库存在于机器上的每个用户和每个项目中。幸运的是,创建虚拟环境很简单: + +```bash +virtualenv scrapingEnv +``` + +这会创建一个名为 scrapingEnv 的新环境,你必须激活它才能使用: + +```bash +cd scrapingEnv/ +source bin/activate +``` + +激活环境后,你会在命令行提示符中看到该环境的名称,提醒你当前正在使用它。你安装的任何库或运行的脚本都将仅在该虚拟环境中。 + +在新创建的 scrapingEnv 环境中工作时,你可以安装并使用 BeautifulSoup;例如: + +```bash +(scrapingEnv)ryan$ pip install beautifulsoup4 +(scrapingEnv)ryan$ python +> from bs4 import BeautifulSoup +> +``` + +你可以使用 deactivate 命令离开该环境,之后你将无法访问在虚拟环境中安装的任何库: + +```bash +(scrapingEnv)ryan$ deactivate +ryan$ python +> from bs4 import BeautifulSoup +Traceback (most recent call last): +File "", line 1, in +ImportError: No module named 'bs4' +``` + +通过项目分离所有库也使得将整个环境文件夹压缩并发送给其他人变得容易。只要他们在其机器上安装了相同版本的 Python,你的代码就可以在虚拟环境中工作,而不需要他们自己安装任何库。 + +尽管我不会明确指示你在本书的所有示例中使用虚拟环境,但请记住,你可以在任何时候简单地通过预先激活它来应用虚拟环境。 + +### 运行 BeautifulSoup + +在 BeautifulSoup 库中最常用的对象是 BeautifulSoup 对象。让我们来看一下它的实际应用,修改本章开头的示例: + +```python +from urllib.request import urlopen +from bs4 import BeautifulSoup +html = urlopen('http://www.pythonscraping.com/pages/page1.html') +bs = BeautifulSoup(html.read(), 'html.parser') +print(bs.h1) +``` + +输出如下: + +``` +

An Interesting Title

+``` + +注意,这只返回了页面中找到的第一个 h1 标签实例。按照惯例,一个页面上应该只有一个 h1 标签,但在网络上,惯例常常被打破,所以你应该意识到这只会检索第一个 h1 标签实例,而不一定是你想要的那个。 + +在前面的网页抓取示例中,你导入了 urlopen 函数并调用了 html.read() 来获取页面的 HTML 内容。除了文本字符串外,BeautifulSoup 还可以直接使用 urlopen 返回的文件对象,而不需要先调用 .read(): + +```python +bs = BeautifulSoup(html, 'html.parser') +``` + +这些 HTML 内容随后被转换成了具有以下结构的 BeautifulSoup 对象: + +``` +html → ...... + head → A Useful Page<title></head> + title → <title>A Useful Page + body →

An Int...

Lorem ip...
+ h1 →

An Interesting Title

+ div →
Lorem Ipsum dolor...
+``` + +注意,你从页面中提取的 h1 标签在你的 BeautifulSoup 对象结构中嵌套了两层(html → body → h1)。然而,当你实际从对象中获取它时,你直接调用 h1 标签: + +```python +bs.h1 +``` + +实际上,以下任何函数调用都会产生相同的输出: + +```python +bs.html.body.h1 +bs.body.h1 +bs.html.h1 +``` + +当你创建一个 BeautifulSoup 对象时,会传入两个参数: + +```python +bs = BeautifulSoup(html.read(), 'html.parser') +``` + +第一个参数是对象基于的 HTML 字符串,第二个参数指定你希望 BeautifulSoup 使用的解析器。在大多数情况下,选择哪个解析器并没有太大区别。 + +html.parser 是一个包含在 Python 3 中的解析器,使用时不需要额外安装。除非另有要求,我们将在整本书中使用这个解析器。 + +另一个流行的解析器是 lxml,可以通过 pip 安装: + +```bash +pip install lxml +``` + +可以通过更改提供的解析器字符串来使用 lxml 与 BeautifulSoup: + +```python +bs = BeautifulSoup(html.read(), 'lxml') +``` + +lxml 相对于 html.parser 的优势在于它通常更擅长解析“凌乱”或格式错误的 HTML 代码。它非常宽容,能够修复诸如未闭合标签、标签嵌套错误以及缺失的头部或主体标签等问题。 + +lxml 也比 html.parser 快一些,尽管在网络抓取中速度并不一定是优势,因为网络本身的速度几乎总是最大的瓶颈。 + +**避免对网页抓取代码进行过度优化** + +优雅的算法很美,但在网页抓取中可能没有实际影响。几微秒的处理时间很可能会被网络请求所需的几秒钟(有时是实际的几秒钟)网络延迟所掩盖。 + +良好的网页抓取代码通常关注于健壮且易读的实现,而不是巧妙的处理优化。 + +lxml 的一个缺点是它需要单独安装,并依赖第三方 C 库来运行。与 html.parser 相比,这可能会对可移植性和易用性造成问题。 + +另一个流行的 HTML 解析器是 html5lib。与 lxml 类似,html5lib 是一个极其宽容的解析器,能更积极地修复损坏的 HTML。它也依赖于外部依赖项,并且比 lxml 和 html.parser 都慢。然而,如果你在处理凌乱或手写的 HTML 站点,它可能是一个不错的选择。 + +可以通过安装 html5lib 并将字符串 html5lib 传递给 BeautifulSoup 对象来使用: + +```python +bs = BeautifulSoup(html.read(), 'html5lib') +``` + +希望这个 BeautifulSoup 的简短介绍能让你了解这个库的强大和简单。只要有一个识别标签包围或靠近它,几乎可以从任何 HTML(或 XML)文件中提取任何信息。第五章将更深入地探讨更复杂的 BeautifulSoup 函数调用,并介绍正则表达式及其在 BeautifulSoup 中的使用,以便从网站中提取信息。 + +### 可靠连接与异常处理 + +网络是混乱的。数据格式不好,网站会崩溃,关闭标签会丢失。在进行网页抓取时,最令人沮丧的经历之一是,你运行抓取器时去睡觉,梦想着第二天你的数据库中会有大量的数据——结果却发现抓取器在遇到意外的数据格式后出错,并在你停止查看屏幕后不久停止执行。 + +在这种情况下,你可能会忍不住诅咒创建网站的开发人员(以及格式奇怪的数据),但实际上你应该责怪的是自己,因为你没有提前预料到异常情况! + +让我们来看一下抓取器的第一行代码,在 import 语句之后,并思考如何处理可能抛出的任何异常: + +```python +html = urlopen('http://www.pythonscraping.com/pages/page1.html') +``` + +在这行代码中有两件主要的事情可能出错: + +* 服务器上的页面找不到(或在检索时出现错误)。 +* 根本找不到服务器。 + +在第一种情况下,将返回 HTTP 错误。这个 HTTP 错误可能是“404 页面未找到”,“500 内部服务器错误”等。在所有这些情况下,urlopen 函数将抛出通用异常 HTTPError。你可以这样处理这个异常: + +``` +from urllib.request import urlopen +from urllib.error import HTTPError + +try: + html = urlopen('http://www.pythonscraping.com/pages/page1.html') +except HTTPError as e: + print(e) + # return null, break, or do some other "Plan B" +else: + # program continues. Note: If you return or break in the + # exception catch, you do not need to use the "else" statement +``` + +如果返回 HTTP 错误代码,程序现在会打印错误并在 else 语句下不执行其余的程序。 + +如果根本找不到服务器(例如,http://www.pythonscraping.com 崩溃,或 URL 输入错误),urlopen 将抛出一个 URLError。这表示根本无法访问服务器,并且由于远程服务器负责返回 HTTP 状态码,因此无法抛出 HTTPError,必须捕获更严重的 URLError。你可以添加一个检查来查看是否是这种情况: + +```python +from urllib.request import urlopen +from urllib.error import HTTPError +from urllib.error import URLError + +try: + html = urlopen("https://pythonscrapingthisurldoesnotexist.com") +except HTTPError as e: + print("The server returned an HTTP error") +except URLError as e: + print("The server could not be found!") +else: + print(html.read()) +``` + +当然,如果页面从服务器成功检索到,页面上的内容仍然可能不是你期望的。每次你访问 BeautifulSoup 对象中的一个标签时,最好添加一个检查以确保标签实际存在。如果你尝试访问不存在的标签,BeautifulSoup 将返回一个 None 对象。问题是,尝试在 None 对象上访问一个标签本身将导致抛出 AttributeError。 + +以下代码行(其中 nonExistentTag 是一个虚构的标签,而不是实际的 BeautifulSoup 函数名称): + +```python +print(bs.nonExistentTag) +``` + +返回一个 None 对象。这个对象是完全合理的,可以处理和检查的问题。如果你不检查它,而是继续尝试在 None 对象上调用另一个函数,则会出现问题,如下所示: + +```python +print(bs.nonExistentTag.someTag) +``` + +这将返回一个异常: + +``` +AttributeError: 'NoneType' object has no attribute 'someTag' +``` + +那么如何防止这两种情况发生呢?最简单的方法是显式检查这两种情况: + +```python +try: + badContent = bs.nonExistingTag.anotherTag +except AttributeError as e: + print('Tag was not found') +else: + if badContent == None: + print ('Tag was not found') +``` + + +对每个错误进行检查和处理一开始看起来很费力,但通过对代码进行一些重新组织,可以使其编写起来不那么困难(更重要的是,更容易阅读)。例如,这段代码是我们用稍微不同的方式编写的相同抓取器: + +```python +from urllib.request import urlopen +from urllib.error import HTTPError +from bs4 import BeautifulSoup + + +def getTitle(url): + try: + html = urlopen(url) + except HTTPError as e: + return None + try: + bsObj = BeautifulSoup(html.read(), "lxml") + title = bsObj.body.h1 + except AttributeError as e: + return None + return title + + +title = getTitle("http://www.pythonscraping.com/pages/page1.html") +if title == None: + print("Title could not be found") +else: + print(title) +``` + +在这个例子中,你创建了一个 getTitle 函数,它返回页面的标题,或者在检索时出现问题时返回一个 None 对象。在 getTitle 中,你检查 HTTPError,如前例所示,并将两个 BeautifulSoup 行封装在一个 try 语句中。如果服务器不存在,html 将是一个 None 对象,html.read() 将抛出一个 AttributeError。事实上,你可以在一个 try 语句中封装任意多行代码,或完全调用另一个函数,这在任何时候都可能抛出 AttributeError。 + +编写抓取器时,重要的是考虑代码的整体模式,以便同时处理异常并使其可读。你还可能需要大量重复使用代码。拥有诸如 getSiteHTML 和 getTitle 之类的通用函数(包括彻底的异常处理)使得快速且可靠地进行网页抓取变得容易。 + + + + + + + diff --git a/_scraping/ch5.md b/_scraping/ch5.md new file mode 100644 index 0000000..940110a --- /dev/null +++ b/_scraping/ch5.md @@ -0,0 +1,531 @@ +--- +layout: post +title: "第五章:Advanced HTML Parsing" +author: "lili" +mathjax: true +sticky: false +excerpt_separator: +tags: + - python + - scraping +--- + + +**目录** +* TOC +{:toc} + +当被问到他是如何雕刻出像大卫这样精妙的艺术作品时,米开朗基罗据说曾著名地回答道:“很简单,你只需将那些看起来不像大卫的石头剔除掉就行了。” 尽管网页抓取在大多数其他方面与大理石雕刻不同,但在从复杂的网页中提取所需信息时,你必须采取类似的态度。在本章中,我们将探索各种技术来剔除那些看起来不像你想要的内容,直到你得到所需的信息。复杂的HTML页面起初可能看起来令人生畏,但只需不断地剔除! + +## 再来一碗BeautifulSoup + +在第四章中,你快速了解了如何安装和运行BeautifulSoup,以及如何一次选择一个对象。在本节中,我们将讨论如何按属性搜索标签、处理标签列表以及遍历解析树。 + +几乎你遇到的每个网站都包含样式表。样式表的创建是为了让网页浏览器能够将HTML呈现为色彩丰富且美观的设计,供人类欣赏。你可能认为这一样式层对于网页抓取器来说完全可以忽略——但慢着!CSS实际上对网页抓取器大有帮助,因为它要求区分HTML元素以便于不同样式的设计。 + +CSS为网页开发者提供了一个动机,使他们添加本来可能会留有相同标记的HTML元素的标签。一些标签可能看起来像这样: + +```html + +``` + +其他标签看起来像这样: + +```html + +``` + +网页抓取器可以轻松地基于它们的类将这两个标签分开。例如,可以使用BeautifulSoup抓取所有红色文本而不是绿色文本。由于CSS依赖这些标识属性来正确地样式化网站,你几乎可以保证在大多数现代网站上会有大量的类和id属性。 + +让我们创建一个示例网页抓取器,抓取位于http://www.pythonscraping.com/pages/warandpeace.html的页面。 + +在这个页面上,故事中人物说的话是红色的,而人物的名字是绿色的。你可以在以下页面源码示例中看到引用相应CSS类的span标签: + +```html +Heavens! what a virulent attack! replied +the prince, not in the least disconcerted by this reception. +``` + +你可以通过使用类似于第四章中使用的程序抓取整个页面并创建一个BeautifulSoup对象: + +```python +from urllib.request import urlopen +from bs4 import BeautifulSoup +html = urlopen('http://www.pythonscraping.com/pages/warandpeace.html') +bs = BeautifulSoup(html.read(), 'html.parser') +``` + +使用这个BeautifulSoup对象,你可以使用find_all函数通过选择仅在标签内的文本来提取一个包含专有名词的Python列表(find_all是一个非常灵活的函数,你将在本书的后面部分经常使用): + +```python +nameList = bs.findAll('span', {'class': 'green'}) +for name in nameList: + print(name.get_text()) +``` + +运行时,它应该按照《战争与和平》中出现的顺序列出所有专有名词。它是如何工作的?之前,你调用bs.tagName来获取页面上该标签的第一个实例。现在,你调用bs.find_all(tagName, tagAttributes)来获取页面上所有该标签的列表,而不仅仅是第一个。 + +在获取名字列表后,程序遍历列表中的所有名字,并打印name.get_text()以便将内容与标签分开。 + +**何时使用get_text()以及何时保留标签** + +.get_text()会从你正在处理的文档中剥离所有标签,并返回一个只包含文本的Unicode字符串。例如,如果你正在处理包含许多超链接、段落和其他标签的大段文本,这些都会被剥离掉,你将得到一个没有标签的文本块。 + +请记住,在BeautifulSoup对象中寻找你所需内容比在文本块中要容易得多。调用.get_text()应该总是你在打印、存储或操作最终数据之前的最后一步。通常,你应尽量长时间保留文档的标签结构。 + + + +### 使用BeautifulSoup的find()和find_all() + +BeautifulSoup的find()和find_all()是你最有可能使用的两个函数。通过它们,你可以轻松地根据各种属性筛选HTML页面,找到所需标签的列表或单个标签。 + +这两个函数非常相似,从BeautifulSoup文档中的定义可以看出: + +``` +find_all(tag, attrs, recursive, text, limit, **kwargs) +find(tag, attrs, recursive, text, **kwargs) +``` + +很可能,你在95%的时间里只需要使用前两个参数:tag和attrs。然而,让我们详细看看所有参数。 + +**tag参数** 是你之前见过的;你可以传递一个标签的字符串名称,甚至是一个字符串标签名称的Python列表。例如,以下代码返回文档中所有标题标签的列表: + +```python +.find_all(['h1','h2','h3','h4','h5','h6']) +``` + +与tag参数不同,它可以是字符串或可迭代对象,而**attrs参数**必须是一个Python字典,包含属性和值。它匹配包含这些属性中任何一个的标签。例如,以下函数将在HTML文档中返回绿色和红色的span标签: + +```python +.find_all('span', {'class': ['green', 'red']}) +``` + +**recursive参数** 是一个布尔值。你想要在文档中深入到什么程度?如果recursive设置为True,find_all函数会在子元素、子元素的子元素等中查找匹配参数的标签。如果为False,它只会查找文档中的顶级标签。默认情况下,find_all递归工作(recursive设置为True)。通常,除非你非常清楚需要做什么且性能是一个问题,否则最好保持默认设置。 + +**text参数** 不同寻常,因为它基于标签的文本内容进行匹配,而不是标签本身的属性。例如,如果你想找到示例页面中“the prince”被标签包围的次数,可以用以下代码替换之前示例中的.find_all()函数: + +```python +nameList = bs.find_all(text='the prince') +print(len(nameList)) +``` + +输出为7。 + +**limit参数** 当然,仅在find_all方法中使用;find等同于将find_all的limit设置为1的调用。如果你只对从页面中检索前x个项目感兴趣,可以设置此参数。注意,这会按照它们在文档中出现的顺序给你页面上的前几个项目,而不一定是你想要的第一个。 + +**kwargs参数** 允许你向方法传递任何其他命名参数。find或find_all未识别的任何额外参数将用作标签属性匹配器。例如: + +```python +title = bs.find_all(id='title', class_='text') +``` + +这将返回第一个在class属性中包含“text”并且id属性为“title”的标签。请注意,根据惯例,id的每个值应在页面上只使用一次。因此,实际上,这样的一行代码可能并不特别有用,应等同于使用find函数: + +```python +title = bs.find(id='title') +``` + + +**关键词参数和Class** + +class是Python中的保留字,不能用作变量或参数名称。例如,如果你尝试以下调用,由于class的非标准用法,你会得到语法错误: + +```python +bs.find_all(class='green') +``` + +因此,BeautifulSoup要求你使用关键字参数_class而不是class。 + +你可能已经注意到,BeautifulSoup已经有一种基于属性和值查找标签的方法:attr参数。实际上,以下两行是相同的: + +```python +bs.find(id='text') +bs.find(attrs={'id':'text'}) +``` + +然而,第一行的语法更短,对于快速过滤需要特定属性的标签时,操作起来可能更容易。当过滤器变得更复杂,或者你需要在参数中传递属性值选项列表时,你可能会希望使用attrs参数: + +```python +bs.find(attrs={'class':['red', 'blue', 'green']}) +``` + + +### 其他BeautifulSoup对象 + +到目前为止,在本书中你已经见过BeautifulSoup库中的两种对象: + +**BeautifulSoup对象** +之前代码示例中作为变量bs出现的实例 + +**Tag对象** +通过调用BeautifulSoup对象上的find和find_all或深入查找检索到的列表中单独检索到的对象: + +```python +bs.div.h1 +``` + +然而,库中还有两个不太常用但仍然重要的对象: + +**NavigableString对象** +用于表示标签内的文本,而不是标签本身(某些函数对NavigableString进行操作并生成NavigableString,而不是标签对象)。 + +**Comment对象** +用于查找HTML注释中的注释标签,如。 + +在撰写本文时,这些是BeautifulSoup包中唯一的四种对象。这也是2004年BeautifulSoup包发布时的唯一四种对象,因此可用对象的数量在不久的将来不太可能改变。 + + + + + +### 树结构导航 + +find_all函数负责根据标签名称和属性查找标签。但如果你需要根据标签在文档中的位置来查找标签呢?这时树导航就派上用场了。在第4章中,你学习了如何在一个方向上导航BeautifulSoup树: + +```python +bs.tag.subTag.anotherSubTag +``` + +现在让我们看看如何在HTML树中向上、横向和斜向导航。你将使用我们那个代处理的在线购物网站(http://www.pythonscraping.com/pages/page3.html)作为抓取的示例页面,如图5-1所示。 + + +![](/img/scraping/ch5/1.png) +*图5.1 http://www.pythonscraping.com/pages/page3.html的截屏* + +这个页面的HTML结构,被映射成树(为了简洁省略了一些标签),看起来是这样的: + +``` +HTML +— body + — div.wrapper + — h1 + — div.content + — table#giftList + — tr + — th + — th + — th + — th + — tr.gift#gift1 + — td + — td + — span.excitingNote + — td + — td + — img + — ...table rows continue... + — div.footer +``` + + +你将在接下来的几节中使用这个相同的HTML结构作为示例。 + +#### 处理子元素和其他后代元素 + +在计算机科学和某些数学分支中,你经常听到对孩子们做的可怕事情:移动他们、存储他们、移除他们,甚至杀死他们。幸运的是,本节只关注选择他们! + +在BeautifulSoup库以及许多其他库中,孩子和后代之间有一个区分:就像在一个人类家谱中一样,孩子总是紧挨着父标签的下一级标签,而后代可以是父标签下面任意层级的标签。例如,tr标签是table标签的子元素,而tr、th、td、img和span都是table标签的后代(至少在我们的示例页面中)。所有的子元素都是后代,但不是所有的后代都是子元素。 + +一般来说,BeautifulSoup函数总是处理当前选定标签的后代。例如,bs.body.h1选择的是body标签的第一个h1后代标签。它不会查找位于body标签外的标签。 + +同样,bs.div.find_all('img')会找到文档中的第一个div标签,然后检索一个包含所有img标签(这些img标签是该div标签的后代)的列表。 + +如果你只想找到子元素,可以使用.children标签: + +```python +from urllib.request import urlopen +from bs4 import BeautifulSoup + +html = urlopen('http://www.pythonscraping.com/pages/page3.html') +bs = BeautifulSoup(html, 'html.parser') + +for child in bs.find('table',{'id':'giftList'}).children: + print(child) +``` + + +这段代码会打印出 giftList 表中的商品行列表,包括初始的列标签行。如果你用 descendants() 函数代替 children() 函数来编写这段代码,在表格中会找到并打印出大约二十多个标签,包括 img 标签、span 标签和单独的 td 标签。区分子元素和后代元素确实非常重要! + +#### 处理兄弟元素 + +BeautifulSoup 的 next_siblings() 函数让从表格中收集数据变得非常简单,尤其是那些有标题行的表格: + +```python +from urllib.request import urlopen +from bs4 import BeautifulSoup + +html = urlopen('http://www.pythonscraping.com/pages/page3.html') +bs = BeautifulSoup(html, 'html.parser') + +for sibling in bs.find('table', {'id':'giftList'}).tr.next_siblings: + print(sibling) +``` + +这段代码的输出是打印出产品表中的所有产品行,除了第一个标题行。为什么标题行会被跳过呢?对象不能和它们自己成为兄弟元素。每当你获取一个对象的兄弟元素时,对象本身不会包含在列表中。顾名思义,这个函数只调用后续的兄弟元素。如果你选择列表中间的一行,并调用 next_siblings,则只会返回后续的兄弟元素。因此,通过选择标题行并调用 next_siblings,你可以选择表格中的所有行,而不选择标题行本身。 + +**使选择更具体** + +上述代码同样适用于选择 bs.table.tr 或甚至 bs.tr 来选择表格的第一行。然而,在代码中,我尽量用较长的形式来写出所有内容: + +```python +bs.find('table', {'id': 'giftList'}).tr +``` + +即使看起来页面上只有一个表格(或其他目标标签),也很容易漏掉一些东西。此外,页面布局经常会发生变化。曾经是页面上第一个的标签,可能某天会变成页面上第二个或第三个同类标签。为了使抓取器更健壮,在进行标签选择时最好尽可能具体。利用标签属性(如果有的话)。 + +作为 next_siblings 的补充,previous_siblings 函数在你想获取一组兄弟标签列表的末尾的某个易于选择的标签时也很有帮助。 + +当然,还有 next_sibling 和 previous_sibling 函数,它们的功能几乎与 next_siblings 和 previous_siblings 相同,只不过返回的是单个标签,而不是它们的列表。 + +#### 处理父元素 + +在抓取页面时,你可能会发现需要查找标签的父元素的情况比查找它们的子元素或兄弟元素的情况要少。通常,当你查看 HTML 页面并希望爬取它们时,你从查看标签的顶层开始,然后弄清楚如何向下钻取到你想要的确切数据。然而,有时你会遇到一些需要 BeautifulSoup 的父元素查找函数 .parent 和 .parents 的特殊情况。例如: + +```python +from urllib.request import urlopen +from bs4 import BeautifulSoup + +html = urlopen('http://www.pythonscraping.com/pages/page3.html') +bs = BeautifulSoup(html, 'html.parser') +print(bs.find('img', + {'src':'../img/gifts/img1.jpg'}) + .parent.previous_sibling.get_text()) +``` + +这段代码将打印出位于 ../img/gifts/img1.jpg 图片位置所代表的对象的价格(在这个例子中,价格是 \\$15.00)。 + +这是如何工作的呢?以下图示代表了你正在处理的 HTML 页面部分的树结构,并标示了步骤: + +``` + + — td + — td + — td ③ + — "$15.00" ④ + — td ② + — ① +``` + +* ① 首先选择 src="../img/gifts/img1.jpg" 的图片标签。 +* ② 选择该标签的父标签(在这个例子中是 td 标签)。 +* ③ 选择 td 标签的 previous_sibling(在这个例子中是包含产品价格的 td 标签)。 +* ④ 选择该标签内的文本 "\\$15.00"。 + + +## 正则表达式 + +正如老计算机科学笑话所说:“假设你有一个问题,然后你决定用正则表达式来解决它。那么现在你有两个问题了。” + +不幸的是,正则表达式(通常缩写为 regex)通常通过一大堆随机符号的表格来教授,这些符号串在一起看起来像一堆无意义的东西。这往往让人望而却步,后来他们进入职场后,为了避免使用正则表达式,会编写不必要复杂的搜索和过滤函数。 + +在网页抓取时,正则表达式是一个非常宝贵的工具。幸运的是,正则表达式并不难快速上手,你可以通过看一些简单的例子并进行试验来学习它们。 + +正则表达式之所以被称为正则,是因为它们用于识别属于正则语言的字符串。这里的“语言”并不是指编程语言或自然语言(如英语或法语),而是数学意义上的“遵循某些规则的字符串集合”。 + +正则语言是由一组线性规则生成的一组字符串,可以简单地沿着候选字符串移动并将其与规则进行匹配。例如: + +* 一个或多个字母a(至少一个)。 +* 在后面精确地附加五个字母 b。 +* 再附加偶数个字母 c。 +* 在结尾写一个字母 d 或 e。 + +正则表达式可以明确地确定:“是的,你给我的这个字符串符合规则,”或“这个字符串不符合规则。”这在快速扫描大型文档以查找看起来像电话号码或电子邮件地址的字符串时非常方便。 + +符合上述规则的字符串有 aaaabbbbbccccd、aabbbbbcce 等。从数学上讲,符合这个模式的字符串有无限多个。 + +正则表达式只是表示这些规则的一种简写方式。例如,以下是刚才描述的一系列步骤的正则表达式: + +``` +aa*bbbbb(cc)*(d|e) +``` + +这个字符串乍一看可能有点令人望而生畏,但当你将其分解为各个组件时会变得更清晰: + +``` +aa* +``` + +写字母 a,后面跟一个星号(*),表示“任意数量的 a,包括 0 个。”通过这种方式,你可以保证至少写一个字母 a。 + +``` +bbbbb +``` + +这里没有什么特别之处——只是连续的五个 b。 + +``` +(cc)* +``` + +任何偶数个的东西都可以成对地组合,所以要强制执行关于偶数的规则,你可以写两个 c,用括号将它们括起来,并在后面写一个星号,这意味着你可以有任意数量的 c 对(注意这也可以意味着零对 c)。 + +``` +(d|e) +``` + +在两个表达式中间添加一个竖线(|),表示它可以是“这个东西或那个东西。”在这种情况下,你是在说“添加一个 d 或一个 e。”通过这种方式,你可以保证有且只有一个这两个字符中的一个。 + +**实验正则表达式** + +在学习如何编写正则表达式时,关键是要不断尝试,体会它们的工作方式。如果你不想打开代码编辑器,写几行代码并运行程序以查看正则表达式是否按预期工作,你可以访问像 [RegEx Pal](http://regexpal.com/) 这样的网站,实时测试你的正则表达式。 + +表 5-1 列出了常用的正则表达式符号,附有简要说明和示例。这个列表并不完整,正如之前提到的,你可能会遇到因语言不同而略有差异的符号。然而,这 12 个符号是 Python 中最常用的正则表达式,可以用来查找和收集几乎任何类型的字符串。 + +正则表达式的一个经典例子是用于识别电子邮件地址。虽然电子邮件地址的具体规则因邮件服务器而略有不同,但我们可以创建一些通用规则。每条规则对应的正则表达式如下表所示: +通过将所有规则连接起来,你可以得到以下正则表达式: + +``` +[A-Za-z0-9._+]+@[A-Za-z]+.(com|org|edu|net) +``` + +在尝试从头编写任何正则表达式时,最好首先列出步骤,具体说明你的目标字符串的样子。注意边界情况。例如,如果你正在识别电话号码,你是否考虑了国家代码和分机号码? + +**正则表达式:并非总是“正则”!** + +标准版本的正则表达式(本书中涵盖的内容,Python 和 BeautifulSoup 使用的正则表达式语法)基于 Perl 的语法。大多数现代编程语言使用这一版本或类似版本的语法。然而,请注意,如果你在其他语言中使用正则表达式,可能会遇到问题。即使是一些现代语言,如 Java,在处理正则表达式时也有细微差别。如有疑问,请查阅文档! + +## 正则表达式与BeautifulSoup + +如果之前关于正则表达式的部分似乎与本书的主题有些脱节,这里就是将它们联系在一起的地方。BeautifulSoup和正则表达式在网页抓取时密不可分。事实上,大多数接受字符串参数的函数(例如,find(id="aTagIdHere"))同样也可以接受正则表达式作为参数。 + +让我们看一些示例,抓取位于 http://www.python-scraping.com/pages/page3.html 的页面。 + +注意该网站有许多产品图片,其形式如下: + +```html + +``` + +如果你想获取所有产品图片的URL,一开始可能看起来相当简单:只需使用 .find_all("img") 抓取所有图片标签,对吧?但这里有一个问题。除了显而易见的“额外”图片(例如,徽标),现代网站通常还有隐藏图片、用于间距和对齐元素的空白图片,以及其他你可能未意识到的随机图片标签。当然,你不能指望页面上的唯一图片都是产品图片。 + +假设页面布局可能会改变,或者出于某种原因,你不想依赖图片在页面中的位置来找到正确的标签。当你试图抓取散布在网站各处的特定元素或数据时,可能会遇到这种情况。例如,特色产品图片可能会出现在某些页面顶部的特殊布局中,而在其他页面中则不会。 + +解决方案是寻找标签本身的某些识别特征。在这种情况下,你可以查看产品图片的文件路径: + +```python +from urllib.request import urlopen +from bs4 import BeautifulSoup +import re + +html = urlopen('http://www.pythonscraping.com/pages/page3.html') +bs = BeautifulSoup(html, 'html.parser') +images = bs.find_all('img', {'src':re.compile('\.\.\/img\/gifts/img.*\.jpg')}) +for image in images: + print(image['src']) +``` + +这将只打印相对路径以 ../img/gifts/img 开头并以 .jpg 结尾的图片路径,其输出为: + +``` +../img/gifts/img1.jpg +../img/gifts/img2.jpg +../img/gifts/img3.jpg +../img/gifts/img4.jpg +../img/gifts/img6.jpg +``` + +正则表达式可以作为任何参数插入到BeautifulSoup表达式中,使你在查找目标元素时拥有极大的灵活性。 + + +## 访问属性 + +到目前为止,你已经学会了如何访问和过滤标签以及访问它们内部的内容。然而,在网页抓取中,通常你不是在寻找标签的内容,而是在寻找它们的属性。这在像 a 这样的标签中尤其有用,其中它指向的URL包含在 href 属性中;或者像 img 标签中,目标图像包含在 src 属性中。 + +使用标签对象,可以通过调用以下方法自动访问属性的 Python 列表: + +```python +myTag.attrs +``` + +请记住,这实际上返回一个Python字典对象,使得这些属性的检索和操作变得简单。例如,可以使用以下代码找到图像的源位置: + +```python +myImgTag.attrs['src'] +``` + +## Lambda 表达式 + +Lambda 是一个高级学术术语,在编程中简单地指的是“一种简写函数的方式”。在Python中,我们可以编写一个返回数字平方的函数: + +```python +def square(n): + return n**2 +``` + +我们可以使用 lambda 表达式在一行中完成同样的事情: + +```python +square = lambda n: n**2 +``` + +这将变量 square 直接赋值给一个函数,它接受一个参数 n 并返回 n\*\*2。但并没有规定函数必须是“命名的”或者完全赋值给变量。我们可以直接将它们写成值: + +```python +>>> lambda r: r**2 + at 0x7f8f88223a60> +``` + +实际上,lambda 表达式是一个独立存在的函数,没有命名或分配给变量。在Python中,lambda 函数不能有多行代码(这只是Python的风格和审美问题,而不是计算机科学的基本规则)。 + +Lambda 表达式最常见的用法是作为参数传递给其他函数。BeautifulSoup允许将某些类型的函数作为参数传递给 find_all 函数。 + +唯一的限制是这些函数必须以标签对象为参数并返回一个布尔值。BeautifulSoup在这个函数中评估每个标签对象,返回评估为 True 的标签,而其余的则被丢弃。 + +例如,以下代码检索具有两个属性的所有标签: + +```python +bs.find_all(lambda tag: len(tag.attrs) == 2) +``` + +在这里,作为参数传递的函数是 len(tag.attrs) == 2。当这个条件为 True 时,find_all 函数将返回该标签。也就是说,它将找到具有两个属性的标签,例如: + +```html +
+ +``` + +Lambda 函数非常有用,甚至可以用它们来替换现有的BeautifulSoup函数: + +```python +bs.find_all(lambda tag: tag.get_text() == 'Or maybe he\'s only resting?') +``` + +这也可以不使用 lambda 函数来实现: + +```python +bs.find_all('', text='Or maybe he\'s only resting?') +``` + +然而,如果你记住 lambda 函数的语法以及如何访问标签属性,也许你将再也不需要记住任何其他BeautifulSoup语法了!因为提供的 lambda 函数可以是返回 True 或 False 值的任何函数,你甚至可以将它们与正则表达式结合起来,以找到具有匹配特定字符串模式的属性的标签。 + + + + +## 有时不一定需要一把锤子 + +当面对一团复杂的标签时,很容易陷入其中,使用多行语句尝试提取信息。然而,请记住,在这一章节中过度使用技术可能导致代码难以调试、脆弱或两者兼而有之。让我们看看一些可以完全避免需要高级 HTML 解析的方法。 + +假设你有一些目标内容。也许是一个姓名、统计数据或一段文本。也许它被埋在 20 层标签深度的 HTML 混乱中,找不到有用的标签或 HTML 属性。你可能决定冒险,尝试写出类似以下行的提取代码: + +```python +bs.find_all('table')[4].find_all('tr')[2].find('td').find_all('div')[1].find('a') +``` + +看起来并不怎么样。除了行的美感外,甚至网站管理员对网站进行的最小更改都可能完全破坏你的网络爬虫。如果网站的 web 开发者决定添加另一个表格或另一列数据呢?如果开发者在页面顶部添加另一个组件(带有几个 div 标签)呢?上述代码是不稳定的,依赖于网站结构永远不会改变。 + +那么你有哪些选择呢? + +* 寻找任何可以用于直接跳到文档中间、更接近你实际想要的内容的“地标”。方便的 CSS 属性是一个明显的地标,但你也可以创造性地使用 .find_all(text='some tag content') 通过标签内容来抓取标签。 +* 如果没有简单的方法来隔离你想要的标签或其任何父级标签,你能找到一个兄弟标签吗?使用 .parent 方法然后再向下钻取到目标标签。 +* 完全放弃这个文档,并寻找“打印此页”链接,或者查看站点的移动版本,它可能具有更好格式的 HTML(有关模拟成移动设备和接收移动站点版本的更多信息,请参阅第17章)。 +* 不要忽略\