Skip to content

Latest commit

 

History

History
407 lines (302 loc) · 20 KB

File metadata and controls

407 lines (302 loc) · 20 KB

三、爬虫礼仪

在开始编写太多代码之前,在开始运行 web scraper 时,您需要记住几点。重要的是要记住,为了让每个人都能相处,我们都必须成为互联网的好公民。记住这一点,有许多工具和最佳实践可供遵循,以确保在向外部 web 服务器添加负载时做到公平和尊重。超出这些指导原则可能会使您的刮板面临被 web 服务器阻止的风险,或者在极端情况下,您可能会发现自己陷入法律困境。

在本章中,我们将介绍以下主题:

  • 什么是 robots.txt 文件?
  • 什么是用户代理字符串?
  • 你如何控制你的网络刮板?
  • 如何使用缓存?

什么是 robots.txt 文件?

网站上的大多数页面都可以被网络爬虫和机器人免费访问。允许这样做的一些原因是为了被搜索引擎索引或允许内容管理员发现页面。Googlebot 是大多数网站非常乐意提供内容访问权限的工具之一。然而,有些网站可能不希望所有内容都显示在谷歌搜索结果中。想象一下,如果你能用谷歌搜索一个人,并立即获得他们所有的社交媒体档案,包括联系信息和地址。这对个人来说是个坏消息,对网站托管公司来说肯定不是一个好的隐私政策。为了控制对网站不同部分的访问,您需要配置一个robots.txt文件。

robots.txt文件通常位于/robots.txt资源中网站的根目录下。此文件包含谁可以访问此网站中哪些页面的定义。这是通过描述一个与User-Agent字符串匹配的机器人,并指定允许和不允许的路径来实现的。AllowDisallow语句中也支持通配符。以下是来自 Twitter 的robots.txt文件示例:

User-agent: *
Disallow: /

这是您将遇到的限制性最强的robots.txt文件。声明称,任何网络爬虫都无法访问twitter.com的任何部分。违反此规则将使你的刮板有被 Twitter 服务器列入黑名单的风险。另一方面,像 Medium 这样的网站更为宽容。这是他们的robots.txt文件:

User-Agent: *
Disallow: /m/
Disallow: /me/
Disallow: /@me$
Disallow: /@me/
Disallow: /*/edit$
Disallow: /*/*/edit$
Allow: /_/
Allow: /_/api/users/*/meta
Allow: /_/api/users/*/profile/stream
Allow: /_/api/posts/*/responses
Allow: /_/api/posts/*/responsesStream
Allow: /_/api/posts/*/related
Sitemap: https://medium.com/sitemap/sitemap.xml

仔细观察,您可以看到以下指令不允许编辑配置文件:

  • Disallow: /*/edit$
  • Disallow: /*/*/edit$

Disallow: /m/也不允许使用与登录和注册相关的页面,这些页面可用于自动创建帐户。

如果您重视刮板,请不要访问这些页面。Allow语句为 in/_/路由中的路径以及一些api相关资源提供了明确的权限。在这里定义的范围之外,如果没有明确的Disallow语句,那么您的 scraper 有权访问该信息。就媒体而言,这包括所有公开发表的文章,以及关于作者和出版物的公开信息。这个robots.txt文件还包括一个sitemap,这是一个 XML 编码的文件,列出了网站上所有可用的页面。你可以把它看作是一个巨大的索引,非常方便。

robots.txt文件的另一个示例显示了站点如何为不同的User-Agent实例定义规则。以下robots.txt文件来自阿迪达斯:

User-agent: *
Disallow: /*null*
Disallow: /*Cart-MiniAddProduct
Disallow: /jp/apps/shoplocator*
Disallow: /com/apps/claimfreedom*
Disallow: /us/help-topics-affiliates.html
Disallow: /on/Demandware.store/Sites-adidas-US-Site/en_US/
User-Agent: bingbot
Crawl-delay: 1
Sitemap: https://www.adidas.com/on/demandware.static/-/Sites-CustomerFileStore/default/adidas-US/en_US/sitemaps/adidas-US-sitemap.xml
Sitemap: https://www.adidas.com/on/demandware.static/-/Sites-CustomerFileStore/default/adidas-MLT/en_PT/sitemaps/adidas-MLT-sitemap.xml

本例明确禁止所有 web scraper 访问几个路径,并对bingbot进行了特别说明。bingbot必须尊重1秒的Crawl-delay,这意味着它不能每秒访问任何页面超过一次。Crawl-delays非常重要,需要注意,因为它们将定义您发出 web 请求的速度。违反此规则可能会为您的 web scraper 生成更多错误,或者可能会被永久阻止。

什么是用户代理字符串?

当 HTTP 客户端向 web 服务器发出请求时,它们会识别自己是谁。这同样适用于 web scraper 和普通浏览器。你有没有想过为什么一个网站知道你是 Windows 或 Mac 用户?此信息包含在您的User-Agent字符串中。以下是 Linux 计算机上 Firefox 浏览器的User-Agent字符串示例:

Mozilla/5.0 (X11; Linux x86_64; rv:57.0) Gecko/20100101 Firefox/57.0

您可以看到,此字符串标识 web 浏览器的系列、名称和版本以及操作系统。此字符串将与来自此浏览器的每个请求一起发送到请求标头内,例如:

GET /index.html HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:57.0) Gecko/20100101 Firefox/57.0

并非所有的User-Agent字符串都包含这么多信息。非 web 浏览器的 HTTP 客户端通常要小得多。以下是一些例子:

  • 卷曲:curl/7.47.0
  • Go:Go-http-client/1.1
  • 爪哇:Apache-HttpClient/4.5.2
  • 谷歌机器人(用于图像):Googlebot-Image/1.0

User-Agent字符串是介绍您的机器人并负责遵守robots.txt文件中设置的规则的好方法。通过使用此机制,您将对任何违规行为负责。

实例

有一些开源工具可以帮助解析robots.txt文件,并根据这些文件验证网站 URL,以查看您是否具有访问权限。我推荐的一个项目可以在 GitHub 上通过用户temoto调用robotstxt。要下载此库,请在终端中运行以下命令:

go get github.com/temoto/robotstxt

这里提到的$GOPATH是您在第一章中安装 Go 编程语言时设置的介绍了网页抓取和 Go。这是带有src/ bin/pkg/ directories的目录。

这将在您的机器上的$GOPATH/src/github/temoto/robotstxt安装库。如果您愿意,您可以阅读代码,看看它是如何工作的。为了这本书,我们将在我们自己的项目中使用这个库。在您的$GOPATH/src文件夹中,创建一个名为robotsexample的新文件夹。在robotsexample文件夹中创建一个main.go文件。下面的main.go代码向您展示了如何使用temoto/robotstxt软件包的简单示例:

package main

import (
  "net/http"

  "github.com/temoto/robotstxt"
)

func main() {
  // Get the contents of robots.txt from packtpub.com
  resp, err := http.Get("https://www.packtpub.com/robots.txt")
  if err != nil {
    panic(err)
  }
  // Process the response using temoto/robotstxt
  data, err := robotstxt.FromResponse(resp)
  if err != nil {
    panic(err)
  }
  // Look for the definition in the robots.txt file that matches the default Go User-Agent string
  grp := data.FindGroup("Go-http-client/1.1")
  if grp != nil {
    testUrls := []string{
      // These paths are all permissable
      "/all",
      "/all?search=Go",
      "/bundles",

      // These paths are not
      "/contact/",
      "/search/",
      "/user/password/",
    }

    for _, url := range testUrls {
      print("checking " + url + "...")

      // Test the path against the User-Agent group
      if grp.Test(url) == true {
        println("OK")
      } else {
        println("X")
      }
    }
  }
}

本例使用range操作符对每个循环使用 Go。range运算符返回两个变量,第一个是iterationindex(我们通过将其分配给_来忽略),第二个是该索引处的值。

此代码针对robots.txt文件检查六个不同的路径 https://www.packtpub.com/ ,使用 Go HTTP 客户端的默认User-Agent字符串。如果允许User-Agent访问页面,Test()方法返回true。如果返回false,则您的刮板不应访问网站的此部分。

如何节流你的爬虫

良好的网络抓取礼仪的一部分是确保您不会在目标 web 服务器上施加太多负载。这意味着限制您在特定时间段内提出的请求数量。对于较小的服务器,这一点尤其正确,因为它们的资源池非常有限。作为一个好的经验法则,你应该只访问你认为会改变的同一个网页。例如,如果你在看每日交易,你可能每天只需要刮一次。对于从同一个网站上抓取多个页面,您应该首先遵循robots.txt文件中的Crawl-Delay。如果没有指定Crawl-Delay,那么您应该在每页之后手动将请求延迟一秒钟。

有许多不同的方法可以将延迟合并到爬虫程序中,从手动将程序置于睡眠状态到使用外部队列和工作线程。本节将解释一些基本技术。在讨论 Go 编程语言并发模型时,我们将重新讨论更复杂的示例。

向 web scraper 添加限制的最简单方法是跟踪请求的时间戳,并确保所用时间大于所需速率。例如,如果您以每5秒一页的速度进行刮纸,它将如下所示:

package main

import (
  "fmt"
  "net/http"
  "time"
)

func main() {
  // Tracks the timestamp of the last request to the webserver
  var lastRequestTime time.Time

  // The maximum number of requests we will make to the webserver
  maximumNumberOfRequests := 5

  // Our scrape rate at 1 page per 5 seconds
  pageDelay := 5 * time.Second

  for i := 0; i < maximumNumberOfRequests; i++ {
    // Calculate the time difference since our last request
    elapsedTime := time.Now().Sub(lastRequestTime)
    fmt.Printf("Elapsed Time: %.2f (s)\n", elapsedTime.Seconds())
    //Check if there has been enough time
    if elapsedTime < pageDelay {
      // Sleep the difference between the pageDelay and elapsedTime
      var timeDiff time.Duration = pageDelay - elapsedTime
      fmt.Printf("Sleeping for %.2f (s)\n", timeDiff.Seconds())
      time.Sleep(pageDelay - elapsedTime)
    }

    // Just for this example, we are not processing the response
    println("GET example.com/index.html")
    _, err := http.Get("http://www.example.com/index.html")
    if err != nil {
      panic(err)
    }

    // Update the last request time
    lastRequestTime = time.Now()
  }
}

本例在定义变量时有许多:=的实例。这是一种同时声明和实例化变量的简写方法。它取代了需要说的以下内容:

var a string a = "value"

相反,它变成了: a := "value"

在本例中,我们向发出请求 http://www.example.com/index.html 每五秒一次。我们知道距离上次请求已经有多长时间了,因为我们更新了lastRequestTime变量,并在发出每个请求之前检查它。这是所有你需要刮一个网站,即使你刮多个网页。

如果你正在抓取多个网站,你需要将每个网站的lastRequestTime分为一个变量。最简单的方法是使用map,Go 的键值结构,其中键是主机,值是最后一个请求的时间戳。这将用类似以下内容替换定义:

var lastRequestMap map[string]time.Time = map[string]time.Time{
  "example.com": time.Time{},
  "packtpub.com": time.Time{},
}

我们的for循环也会稍微改变,并将地图的值设置为当前的抓取时间,但仅对于网站,我们正在抓取。例如,如果我们以交替方式刮页面,它可能看起来像这样:

// Check if "i" is an even number
if i%2 == 0 {
  // Use the Packt Publishing site and elapsed time
  webpage = packtPage
  elapsedTime = time.Now().Sub(lastRequestMap["packtpub.com"])
} else {
  // Use the example.com elapsed time
  elapsedTime = time.Now().Sub(lastRequestMap["example.com"])
}

最后,要使用最后一个已知的请求时间更新映射,我们将使用类似的块:

// Update the last request time
if i%2 == 0 {
  // Use the Packt Publishing elapsed time
  lastRequestMap["packtpub.com"] = time.Now()
} else {
  // Use the example.com elapsed time
  lastRequestMap["example.com"] = time.Now()
}

您可以在 GitHub 上找到此示例的完整源代码。

如果您查看终端中的输出,您将看到对任一站点的第一次请求都没有延迟,现在每个睡眠时间都略少于 5 秒。这表明爬虫独立地尊重每个站点的速率。

如何使用缓存

最后一个可以让你的 scraper 受益的技术,以及减少网站负载的技术,是只在内容改变时请求新内容。如果您的 scraper 正在从 web 服务器下载相同的旧内容,那么您将无法获得任何新信息,web 服务器正在做不必要的工作。出于这个原因,大多数 web 服务器实现了一些技术,为客户机提供有关缓存的说明。

一个支持缓存的网站将向客户端提供关于它可以存储什么以及存储多长时间的信息。这是通过响应头完成的,例如Cache-ControlEtagDateExpiresVary。您的 web scraper 应该了解这些指令,以避免向 web 服务器发出不必要的请求,从而节省您和服务器、时间和计算资源。让我们看看我们的 T5http://www.example.com/index.html 再次响应,如下所示:

HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: max-age=604800
Content-Type: text/html; charset=UTF-8
Date: Mon, 29 Oct 2018 13:31:23 GMT
Etag: "1541025663"
Expires: Mon, 05 Nov 2018 13:31:23 GMT
Last-Modified: Fri, 09 Aug 2013 23:54:35 GMT
Server: ECS (dca/53DB)
Vary: Accept-Encoding
X-Cache: HIT
Content-Length: 1270
...

本例中不包括响应主体。

有几个响应头用于传递缓存指令,您应该遵循这些指令,以提高 web scraper 的效率。这些标题将通知您要缓存哪些信息、缓存多长时间,以及一些其他有用的信息,以使您的生活更轻松。

缓存控制

Cache-Control标题用于指示此内容是否可缓存,以及可缓存多长时间。此标题的一些常用值如下所示:

  • no-cache
  • no-store
  • must-revalidated
  • max-age=<seconds>
  • public

存在诸如no-cacheno-storemust-revalidate等缓存指令,以防止客户端缓存响应。有时,服务器知道此页面上的内容经常更改,或者依赖于其无法控制的源。如果没有发送这些指令,您应该能够使用提供的max-age指令缓存响应。这定义了你应该认为这个内容是新鲜的秒数。在这段时间之后,响应被认为是过时的,应该向服务器发出新的请求。

在上一个示例的响应中,服务器发送一个Cache-Control头:

Cache-Control: max-age=604800

这表示您应该将此页面缓存最多604880秒(七天)。

到期

Expires头是定义缓存信息保留时间的另一种方式。此标题定义了内容被视为过时并应刷新的确切日期和时间。此时间应与Cache-Control标题中的max-age指令一致(如果提供)。

在我们的示例中,Expires报头根据Date报头匹配 7 天到期,该报头定义了服务器接收请求的时间:

Date: Mon, 29 Oct 2018 13:31:23 GMT
Expires: Mon, 05 Nov 2018 13:31:23 GMT

埃塔格

Etag在保存缓存信息方面也很重要。这是此页面的唯一键,仅当页面内容更改时才会更改。缓存过期后,您可以使用此标记与服务器检查是否确实存在新内容,而无需下载新副本。这是通过发送包含Etag值的If-None-Match头来实现的。发生这种情况时,服务器将检查当前资源上的Etag是否与If-None-Match头中的Etag匹配。如果匹配,则没有更新,服务器响应状态代码为 304 Not Modified,并带有一些头以扩展缓存。以下是304响应的示例:

HTTP/1.1 304 Not Modified
Accept-Ranges: bytes
Cache-Control: max-age=604800
Date: Fri, 02 Nov 2018 14:37:16 GMT
Etag: "1541025663"
Expires: Fri, 09 Nov 2018 14:37:16 GMT
Last-Modified: Fri, 09 Aug 2013 23:54:35 GMT
Server: ECS (dca/53DB)
Vary: Accept-Encoding
X-Cache: HIT

在本例中,服务器验证Etag并提供一个新的Expires时间,从第二个请求完成时起,该时间仍然与max-age匹配。这样,您仍然可以节省时间,不需要通过网络读取更多数据。您仍然可以使用缓存页面来满足您的需要。

在 Go 中缓存内容

缓存页面的存储和检索可以手动使用本地文件系统或数据库来保存数据和缓存信息。还有一些开源工具可以帮助简化这项技术。其中一个项目是 GitHub 用户gregjoneshttpcache

httpcache遵循互联网标准管理机构互联网工程任务组IETF规定的缓存要求。该库提供了一个模块,可以从本地计算机存储和检索网页,还提供了一个插件供 Go HTTP 客户端自动处理所有与缓存相关的 HTTP 请求和响应头。它还提供了多个存储后端,您可以在其中存储缓存的信息,例如 Redis、Memcached 和 LevelDB。这将允许您在不同的机器上运行 web scraper,但连接到相同的缓存信息。

随着 scraper 规模的增长,您需要设计一个分布式体系结构,这样的特性对于确保时间和资源不会浪费在重复的工作上至关重要。所有爬虫之间的稳定沟通是关键!

让我们看一个例子,使用首先,在终端中输入以下命令安装httpcache,如下所示:

  • go get github.com/gregjones/httpcache
  • go get github.com/peterbourgon/diskv

httpcache使用diskv项目将网页存储在本地机器上。

在您的$GOPATH/src中,创建一个名为cache的文件夹,其中包含一个main.go。为您的main.go文件使用以下代码:

package main

import (
  "io/ioutil"

  "github.com/gregjones/httpcache"
  "github.com/gregjones/httpcache/diskcache"
)

func main() {
  // Set up the local disk cache
  storage := diskcache.New("./cache")
  cache := httpcache.NewTransport(storage)

  // Set this to true to inform us if the responses are being read from a cache
  cache.MarkCachedResponses = true
  cachedClient := cache.Client()

  // Make the initial request
  println("Caching: http://www.example.com/index.html")
  resp, err := cachedClient.Get("http://www.example.com/index.html")
  if err != nil {
    panic(err)
  }

  // httpcache requires you to read the body in order to cache the response
  ioutil.ReadAll(resp.Body)
  resp.Body.Close()

  // Request index.html again
  println("Requesting: http://www.example.com/index.html")
  resp, err = cachedClient.Get("http://www.example.com/index.html")
  if err != nil {
    panic(err)
  }

  // Look for the flag added by httpcache to show the result is read from the cache
  _, ok = resp.Header["X-From-Cache"]
  if ok {
    println("Result was pulled from the cache!")
  }
}

此程序使用本地磁盘缓存存储来自的响应 http://www.example.com/index.html 。在引擎盖下,它读取所有与缓存相关的头,以确定是否可以存储页面,并将过期日期与数据一起包括在内。在第二个请求中,httpcache检查内容是否过期,并返回缓存数据,而不是发出另一个 HTTP 请求。它还添加了一个额外的头文件X-From-Cache,以指示这是从缓存中读取的。如果页面已过期,它将发出带有If-None-Match头的 HTTP 请求并处理响应,包括在响应未修改的情况下更新缓存。

使用自动设置为处理缓存内容的客户端将使您的 scraper 运行得更快,并降低您的 web scraper 被标记为坏公民的可能性。当这与尊重网站的robots.txt文件和适当限制您的请求相结合时,您就可以自信地勉强应付,因为您知道自己是 web 社区中值得尊敬的成员。

总结

在本章中,您学习了尊重他人在网络上爬行的基本礼仪。你学会了什么是robots.txt文件,以及遵守它的重要性。您还学习了如何使用User-Agent字符串正确表示自己。还介绍了如何通过节流和缓存控制刮板。有了这些技能,您就离构建一个功能齐全的 web 刮板又近了一步。

第 4 章解析 HTML中,我们将了解如何使用各种技术从 HTML 页面中提取信息。