Go 网页抓取导航详解

到目前为止,本书的重点是检索单个网页的信息。尽管这是 web 抓取的基础,但它并不涵盖大多数用例。更有可能的是,您需要访问多个网页或网站,以便收集所有信息以满足您的需求。这可能需要通过列表或 URL 直接访问许多已知网站,或者跟随在某些页面中发现的指向更多未知位置的链接。有许多不同的方式在网络上导航你的刮板。

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

正如您在本书的许多示例中所看到的,有一些 HTML 元素由包含引用不同 URL 的href属性的<a>标记表示。这些标签称为锚定标签,是如何在网页上生成链接的。在 web 浏览器中,这些链接通常具有不同的字体颜色,通常为蓝色,并带有下划线。作为 web 浏览器中的用户,如果您想查看某个链接,通常只需单击该链接,就会被重定向到 URL。作为 web 刮板,通常不需要单击操作。相反,您可以向href属性本身中的 URL 发送GET请求。

如果发现href属性缺少http://https://前缀和主机名,则必须使用当前网页的前缀和主机名。

第 4 章解析 HTML中,我们使用了一个示例,从 Packt 发布网站检索最新版本的标题和价格。您可以通过链接到每本书的主网页来收集关于每本书的更多信息。在下面的代码示例中,我们将添加导航以实现这一点:

package main

import (
  "fmt"
  "strings"
  "time"
  "github.com/PuerkitoBio/goquery"
)

func main() {
  doc, err := goquery.NewDocument("https://www.packtpub.com/latest-releases")
  if err != nil {
    panic(err)
  }

  println("Here are the latest releases!")
  println("-----------------------------")
  time.Sleep(1 * time.Second)
  doc.Find(`div.landing-page-row div[itemtype$="/Product"] a`).
    Each(func(i int, e *goquery.Selection) {
      var title, description, author, price string
      link, _ := e.Attr("href")
      link = "https://www.packtpub.com" + link

      bookPage, err := goquery.NewDocument(link)
      if err != nil {
        panic(err)
      }
      title = bookPage.Find("div.book-top-block-info h1").Text()
      description = strings.TrimSpace(bookPage.Find("div.book-top-
      block-info div.book-top-block-info-one-liner").Text())
      price = strings.TrimSpace(bookPage.Find("div.book-top-block-info 
      div.onlyDesktop div.book-top-pricing-main-ebook-price").Text())
      authorNodes := bookPage.Find("div.book-top-block-info div.book-
      top-block-info-authors")
       if len(authorNodes.Nodes) < 1 {
        return
      } 
      author = strings.TrimSpace(authorNodes.Nodes[0].FirstChild.Data)
      fmt.Printf("%s\nby: %s\n%s\n%s\n---------------------\n\n", 
      title, author, price, description)
      time.Sleep(1 * time.Second)
    })
}

如您所见,我们修改了Each()循环,以提取网页中列出的每个产品的链接。每个链接只包含到该书的相对路径,因此我们将前缀设置为https://www.packtpub.com 每个链接的字符串。接下来,我们使用构建的链接导航到页面本身,并获取所需的信息。在每一页的末尾,我们都会睡上1秒,这样我们的网络刮板就不会让服务器负担过重,遵守第 3 章网络刮板礼仪中学习的良好礼仪。

到目前为止,我们只能使用 HTTPGET请求从服务器请求信息。这些请求涵盖了您在构建自己的 web 刮板时将遇到的绝大多数 web 刮板任务。但是,有时您可能需要提交某种表单数据,以便检索您正在查找的信息。此表单数据可能需要搜索查询、登录屏幕或任何需要您在框中键入并单击提交按钮的页面。

对于简单的网站,这是使用 HTML<form>元素完成的,该元素包含一个或多个<input>元素和一个提交按钮。此<form>元素通常具有定义action(向何处发送<form>数据)和method(要使用的 HTTP 方法)的属性。默认情况下,web 页面将使用 HTTPGET请求来发送表单数据,但通常也会看到 HTTPPOST请求。

在下面的示例中,您将看到如何使用 HTML 表单的属性和元素来模拟表单提交。我们将使用位于的表格 https://hub.packtpub.com/ 网站,查找关于 Go 编程语言(通常称为 GoLang)的文章。在主页上 https://hub.packtpub.com ,页面左上角有一个搜索框,如下图所示:

通过右键单击搜索。。。框中,您应该能够使用浏览器的开发人员工具检查元素。这将显示页面的 HTML 源代码,显示此框位于 HTML 表单中。在 Google Chrome 中,它看起来类似于以下屏幕截图:

此表单使用 HTTPGET方法,并提交给https://hub.packtpub.com/ 终点。此表单的值取自<input>标记,使用name属性作为键,搜索框中的文本作为值。由于此表单使用GET作为方法,因此将键值对作为 URL 的查询部分发送到服务器。对于我们的示例,我们希望提交 GoLang 作为搜索查询。为此,当您单击按钮提交查询时,您的浏览器将向发送GET请求 https://hub.packtpub.com/?s=Golang

结果页面将包含与 Go 相关的所有文章。您可以删除标题、日期、作者等,以便保留 Go 文章的索引。通过定期提交此查询,您可以在新文章发布后立即发现它们。

我们在前面的示例中使用的表单使用GET作为方法。假设,如果使用POST方法,表单的提交方式会略有不同。您需要构建一个请求主体,而不是将值放在 URL 中。在下面的示例中,相同的表单和搜索查询将被构造为一个POST请求:

package main

import (
  "net/http"
  "net/url"
)

func main() {
  data := url.Values{}
  data.Set("s", "Golang")

  response, err := http.PostForm("https://hub.packtpub.com/", data)

  // ... Continue processing the response ...
}

在 Go 中,使用url.Values结构构建表单提交。在我们的例子中,您可以使用它设置表单-s=Golang的输入,并使用http.Post()函数提交表单。只有当表单使用POST作为其方法时,此技术才会有所帮助。

如果您正在构建一个跟随链接的 web 刮板,您可能需要知道您已经访问了哪些页面。很可能您正在访问的页面包含指向您已经访问过的页面的链接,将您发送到一个无限循环中。因此,在你的铲运机上建立一个记录历史的跟踪系统是非常重要的。

存储项目的唯一集合的最简单数据结构是集合。Go 标准库没有设置的数据结构,但可以使用map[string]interface{}{}进行模拟。

Go 中的interface{}是一个泛型对象,类似于java.lang.Object

在 Go 中,可以按如下方式定义地图:

visitedMap := map[string]interface{}{}

在本例中,我们将使用访问的 URL 作为键,并使用您想要的任何内容作为值。我们将只使用nil,因为只要有钥匙,我们就知道我们已经访问了该站点。添加我们访问过的站点只需插入 URL 作为键,nil作为值,如下代码块所示:

visitedMap["http://example.com/index.html"] = nil

当您尝试使用给定的键从映射中检索值时,Go 将返回两个值:键的值(如果存在)和布尔值(说明该键是否存在于映射中)。就我们而言,我们只关心后者。

我们将检查类似以下代码块中演示的现场访问:

_, ok := visitedMap["http://example.com/index.html"]

if ok {
  // ok == true, meaning the URL exists in the visitedMap
  // Skip this URL
} else {
  // ok == false, meaning the URL does not exist in the visitedMap
  // Make the HTTP Request and continue processing this page
  // ...
} 

既然您能够导航到不同的页面,并且能够避免陷入循环,那么在对网站进行爬网时,您还有一个更重要的选择。一般来说,通过以下链接覆盖所有页面有两种主要方法:广度优先和深度优先。假设您正在抓取一个包含 20 个链接的网页。当然,您将遵循页面上的第一个链接。在第二页上,还有十个链接。这就是你的决定:跟随第二页的第一个链接,或者回到第一页的第二个链接。

如果选择跟随第二页上的第一个链接,这将被视为深度优先爬网:

你的刮板将继续尽可能深入地跟踪链接以收集所有页面。对于产品,您可能会遵循一系列建议或类似项目。这可能会将您带到远离刮板原始起点的产品。另一方面,它也有助于快速建立一个更紧密的相关项目网络。在包含文章的网站上,深度优先爬网会很快将您送回时间,因为链接的页面很可能是对以前撰写的文章的引用。这将帮助您快速到达许多链接路径的原点。

第 6 章、*保护您的网页刮板中,我们将学习如何通过确保适当的边界来避免深度优先爬行的一些陷阱。*

*# 广度优先

如果您选择跟随第一页上的第二个链接,这将被视为广度优先爬网:

使用这种技术,您很可能会在原始搜索域中停留更长的时间。例如,如果你在一个有产品的网站上开始搜索鞋子,那么页面上的大多数链接都与鞋子有关。您将首先收集同一域中的链接。随着你在网站中的深入,推荐的物品可能会让你找到其他类型的衣服。广度优先爬网将帮助您更快地收集完整的页面集群。

没有正确或错误的技术来指导你的铲运机;这完全取决于你的具体需要。深度优先爬网将揭示特定主题的起源,而广度优先爬网将在发现新内容之前完成整个集群。如果这符合您的需求,您甚至可以使用多种技术的组合。

到目前为止,我们关注的是简单的网页,其中所需的所有信息仅在 HTML 文件中可用。对于更现代的网站来说,情况并非总是如此,它们包含 JavaScript 代码,负责在初始页面加载后加载额外信息。在许多网站中,当您执行搜索时,初始页面可能会显示一个空表,并在后台发出第二个请求以收集要显示的实际结果。为了做到这一点,您的 web 浏览器将运行用 JavaScript 编写的自定义代码。在这种情况下,使用标准 HTTP 客户端是不够的,您需要使用支持 JavaScript 执行的外部浏览器。

在 Go 中,由于一些标准协议,有许多选项可用于将 scraper 代码与 web 浏览器集成。WebDriver 协议是由 Selenium 开发的原始标准,大多数主要浏览器都支持它。该协议允许程序发送浏览器命令,例如加载网页、等待元素、单击按钮和捕获 HTML。这些命令对于从通过 JavaScript 加载项目的网页收集结果是必需的。其中一个支持 WebDriver 客户端协议的库是 GitHub 用户tebeka提供的selenium

在 Packt Publishing 网站上,书评是通过 JavaScript 加载的,第一次加载页面时不可见。此示例演示如何使用selenium包从 Packt Publishing 站点上的图书列表中获取评论

selenium包依赖四个外部依赖项才能正常运行:

  • Google Chrome 或 Mozilla Firefox web 浏览器
  • 分别与 Chrome 或 Firefox 兼容的 WebDriver
  • Selenium 服务器二进制文件
  • JAVA

所有这些依赖项都将在安装期间由selenium脚本下载,Java 除外。

请确保您的计算机上安装了 Java。如果没有,请从下载并安装官方版本 https://www.java.com/en/download/help/download_options.xml

首先,通过以下方式安装软件包:

go get github.com/tebeka/selenium

这将在您的GOPATH$GOPATH/src/github.com/tebeka/selenium安装selenium。此安装脚本依赖于许多其他软件包才能运行。可以使用以下命令安装它们:

go get cloud.google.com/go/storage
go get github.com/golang/glog
go get google.golang.org/api/option

接下来,我们安装代码示例所需的浏览器、驱动程序和selenium二进制文件。导航到selenium目录中的Vendor文件夹,运行以下命令完成安装:

go run init.go

既然selenium及其所有依赖项都已设置,您就可以在$GOPATH/src中创建一个新文件夹,其中包含一个main.go文件。让我们一步一步地浏览一下为了收集书评而需要编写的代码。首先,让我们看一下import声明:

package main

import (
  "github.com/tebeka/selenium"
)

如您所见,我们的程序只依赖selenium包来运行示例!接下来,我们可以看到main函数的开头,并定义几个重要变量:

func main() {

 // The paths to these binaries will be different on your machine!

  const (
    seleniumPath = "/home/vincent/Documents/workspace/Go/src/github.com/tebeka/selenium/vendor/selenium-server-standalone-3.14.0.jar"

  geckoDriverPath = "/home/vincent/Documents/workspace/Go/src/github.com/tebeka/selenium/vendor/geckodriver-v0.23.0-linux64"
  )

在这里,我们呈现selenium服务器可执行文件路径的常量,以及 Firefox WebDriver 的路径,称为geckodriver。如果您使用 Chrome 运行此示例,那么您将提供指向chromedriver的路径。所有这些文件都是由之前运行的init.go程序安装的,您的路径将与此处编写的路径不同。请务必更改这些以适应您的环境。函数的下一部分初始化selenium驱动程序:

  service, err := selenium.NewSeleniumService(
    seleniumPath, 
    8080, 
    selenium.GeckoDriver(geckoDriverPath))

  if err != nil {
    panic(err)
  }
  defer service.Stop()

  caps := selenium.Capabilities{"browserName": "firefox"}
  wd, err := selenium.NewRemote(caps, "http://localhost:8080/wd/hub")
  if err != nil {
    panic(err)
  }
  defer wd.Quit()

defer statements tell Go to run the following command at the end of the function. It is good practice to defer your cleanup statements so you don't forget to put them at the end of your function!

在这里,我们通过提供所需的可执行文件路径以及代码与selenium服务器通信的端口来创建selenium驱动程序。我们还通过调用NewRemote()获得与 WebDriver 的连接。wd对象是我们将用于向 Firefox 浏览器发送命令的 WebDriver 连接,如以下代码段所示:

  err = wd.Get("https://www.packtpub.com/networking-and-servers/mastering-go")
  if err != nil {
    panic(err)
  }

  var elems []selenium.WebElement
  wd.Wait(func(wd2 selenium.WebDriver) (bool, error) {
    elems, err = wd.FindElements(selenium.ByCSSSelector, "div.product-reviews-review div.review-body")
    if err != nil {
      return false, err
    } else {
      return len(elems) > 0, nil
    }
  })

  for _, review := range elems {
    body, err := review.Text()
    if err != nil {
      panic(err)
    }
    println(body)
  }
}

我们告诉浏览器加载 Mihalis Tsoukalos 的Mastering Go网页,然后等待我们的 CSS 查询返回多个结果。这将无限期地循环,直到出现评论。一旦我们发现了评论,我们就会打印每一篇评论的文本。

在本章中,我们介绍了如何在网站中导航 web scraper 的基本知识。我们研究了 web 链接的结构,以及如何使用 HTTPGET请求来模拟跟踪链接。我们研究了 HTTP 表单(如搜索框)如何生成 HTTP 请求。我们还看到了 HTTPGETPOST请求之间的区别,以及如何在 Go 中发送POST请求。我们还介绍了如何通过跟踪历史来避免循环。最后,讨论了广度优先和深度优先 web 爬行之间的差异,以及它们各自的优缺点。

第 6 章保护您的网络刮板中,我们将探讨如何确保您在网络上爬行时的安全。*

教程来源于Github,感谢apachecn大佬的无私奉献,致敬!

技术教程推荐

硅谷产品实战36讲 -〔曲晓音〕

Java核心技术面试精讲 -〔杨晓峰〕

大厂晋升指南 -〔李运华〕

实用密码学 -〔范学雷〕

操作系统实战45讲 -〔彭东〕

说透5G -〔杨四昌〕

云计算的必修小课 -〔吕蕴偲〕

手把手带你搭建推荐系统 -〔黄鸿波〕

AI绘画核心技术与实战 -〔南柯〕