使用GO构建网络刮板
#编程 #go #webscraping #backend

简介ð£

在本教程中,我们将在GO编程语言中使用Colly库来构建Web刮板的过程。此刮板将从特定网站提取食谱数据。我们将逐步介绍代码以了解每个组件。

什么是网络拖

顾名思义,一个网络cr徒是您可以用来scrape或从网站提取数据的工具。


对于这个项目,我决定刮擦意大利食谱网站Giallo Zafferano

入门

开始

  • ð»您选择的IDE-我使用VSCODE。

  • ðª一些小吃,因为所有这些食谱刮擦都会让您饿。

让我们来编码ð

安装去

首先,让我们设置GO项目。由于这是GO学习系列的第一篇文章,所以我将花几秒钟的时间通过安装GO。

如果您已经安装了它,请滚动浏览€

根据操作系统,您可以在Go documentation page上找到安装指南。如果您是MacOS用户,并且使用brew,则可以在终端中运行它:

brew install go

设置项目

为您的项目创建一个新目录,移动到目录,然后运行以下命令,您可以将单词webscraper替换为希望您的模块命名的任何内容。

go mod init webscraper

ðâ
go mod init命令在执行它的目录中初始化了一个新的GO模块。它创建了一个新的go.mod文件,该文件用于定义项目中使用的第三方软件包的依赖版和管理版本(ðkinkaikinda tike package.json,如果您使用的是node)。

现在让我们安装colly及其依赖项

go get github.com/gocolly/colly

此命令还将使用所有必需的依赖项以及创建go.sum文件更新go.mod文件。

ðâ
Colly是一个GO软件包,可让您编写网络刮刀和爬网。它建立在用于网络通信的GO NET/HTTP软件包的顶部,而goquery则提供了用于针对HTML元素的“类似jQuery的”语法。

刮板逻辑

我们准备开始。在您的目录中创建一个scraper.go文件,并设置您的main函数。

package main

import (
    // import Colly
    "github.com/gocolly/colly"
)

func main(){
}

ð§
如果您以前从未使用过,这看起来可能有些怪异。如果您问自己什么是主要的?该软件包来自哪里?然后查看我对此详细介绍的其他博客文章。

使用colly

该程序的大部分将由Colly和管理网络通信的收集器对象处理,并负责执行附件的回调

使用让我们在主要功能中初始化收集器:

collector := colly.NewCollector()

您可以自定义收集器的参数以满足不同的需求。我们将使用默认设置,但是如果您很好奇,可以查看Colly文档以获取更多信息。

首先,让我们从最简单的情况开始,看看我们的设置是否有效:我们想访问URL并在控制台中打印我们正在访问的URL。这是我想使用的URL:https://ricette.giallozafferano.it/Schiacciata-fiorentina.html

ðâ
该程序中的代码可以满足该URL的工作(以及该网站的其他食谱) - 它在任何其他URL上都无法使用,因此如果您遵循的话,请使用相同的URL。

使用命令行参数

在我在构建此刮板之前签出的每个教程中,它们在文件中抄袭了本地的URL,然后将其传递给collector拨打kude11



我没有发现该解决方案非常优雅,并且以学习和实验的名义,我查找了如何将命令行论证传递给GO应用程序,以便我可以将URL作为命令行参数传递,并刮擦不同来自同一网站的URL无需手动更改代码。

为了实现这一目标,我们正在使用os软件包,因此我们将其添加到导入中,并使用到目前为止所需的内容填写我们的主要功能:

  • 一个称为args的变量,它将存储我们在运行时通过的命令行参数。

  • 一个称为url的变量,我们从args对象分配了索引1的值。这是我们在命令行中存储的URL的地方。

  • colly collector对象。

package main

import (
    "os"
    // import Colly
    "github.com/gocolly/colly"
)

func main(){
    args := os.Args
    url := args[1]
    collector := colly.NewCollector()
}
ð 有关如何在GO中使用命令行参数的深入示例,您应该从“ go fim示例”网站:Go by Example: Command-Line Arguments中查看此页面。

现在,我们已经拥有了程序的构建块,让我们尝试一下,看看它是否可以通过为我们的收集器对象设置回调。就这样,您就知道,从现在开始我们将要写的所有内容都将放置在main功能中。对于我们的打印语句,我们使用的是fmt的软件包 - 它代表“格式化”,您可以在导入函数中导入它,如下面的代码块中所示。

package main

import (
    "fmt"
    "os"
    // import Colly
    "github.com/gocolly/colly"
)
func main() {
    args := os.Args
    url := args[1]
    collector := colly.NewCollector()

  // whenever the collector is about to make a new request
    collector.OnRequest(func(r *colly.Request) {
        // print the url of that request
        fmt.Println("Visiting", r.URL)
    })
    collector.OnResponse(func(r *colly.Response) {
        fmt.Println("Got a response from", r.Request.URL)
    })
    collector.OnError(func(r *colly.Response, e error) {
        fmt.Println("Blimey, an error occurred!:", e)
    })
    collector.Visit(url)

}

ð
如果您以前从未使用过回调,则可以简要说明那里发生的事情。我将以第一个回调为示例(OnRequest),但解释适用于每个回调。

  1. OnRequest是收集器对象的一种方法,每当收集器要提出新请求时,它会登记一个函数。

  2. 我们将其传递给一个匿名函数,将*colly.Request对象作为参数(表示即将提出的请求)。在此功能中,我们使用fmt.Println函数将消息“访问”以及当前正在命令行中处理的请求的URL打印出来。

本质上,每当收集器要提出请求时,此功能将被执行,并且会打印一条消息,表明收集器正在访问特定的URL。这对于调试和监视Web刮擦过程的进度可能很有用,使您可以查看收藏家访问了哪些URL。同样,OnResponseOnError方法将分别收到响应或提出错误时触发。

在您的终端中,运行

go run scraper.go https://ricette.giallozafferano.it/Schiacciata-fiorentina.html

,您应该看到以下打印回:

Visiting https://ricette.giallozafferano.it/Schiacciata-fiorentina.html
Got a response from https://ricette.giallozafferano.it/Schiacciata-fiorentina.html

ð - 现在来了

要编写我们的主要逻辑,我们将使用OnHTML回调。为了充分理解这一点,我建议您在Colly文档上度过一段美好的时光。当收集器遇到可以指定为 css-selector 的HTML元素时,我们在此回调中定义的功能将运行。在下面的示例中,当收集器找到带有类“ div-main”的元素时,该函数将运行。

collector.OnHTML(".div-main", func(e *colly.HTMLElement) {
    // Your code to handle the HTML element matching the selector
})

要知道要针对哪个CSS选择器,我们需要花一些时间检查我们要刮擦的网页。为此,请使用您的浏览器Web检查器并查看要提取的内容的元素。

虽然您可以在程序中多个ONHTML回调,但每个处理要刮擦的多个元素的CSS选择器的每个回调,但我觉得有一个触发页面元素的回调和触发的回调更加干净处理在同一功能中刮下级联的子元素。

让我们继续进行一些回顾:

  • 我们有我们的收藏家

  • 我们知道如何刮擦URL,我们知道如何通过URL

但是我们将如何处理刮擦的数据?

由于我们正在刮擦食谱,因此创建一个struct以存储我们有兴趣提取的食谱功能才有意义。

定义数据结构

我花了一些时间查看食谱页面,并确定要提取哪些元素:食谱名称,食谱规格(例如困难,准备时间,烹饪时间,服务尺寸和价格层)以及成分。

首先,我为食谱定义了一个struct,用urlname作为字符串。然后,我不得不想到用于配方规格的数据结构成分。不同的配方将具有不同的成分组合,但始终具有相同类型的规格,仅具有不同的值,因此为两者使用不同的数据结构是有意义的。

  • 由于配方规格是固定的跨不同配方(例如名称和URL字段)的字段,我决定创建一个独立的结构以保持RecipeSpecs

  • 对于成分,我需要一个更灵活数据结构,我可以将键值配对附加到其中,因为我不知道我会遇到多少键或哪些键在成分列表中。为此,我创建了一个类型map的字典对象,该对象定义了从字符串到字符串类型的键值对的映射。

type Dictionary map[string]string

type RecipeSpecs struct {
    difficulty, prepTime, cookingTime, servingSize, priceTier string
}

type Recipe struct {
    url, name      string
    ingredients    []Dictionary
    specifications RecipeSpecs
}

ph,随之而来的是,我准备将刮擦数据保存到我的结构。

请查看下面的代码,并在线评论,以解释如何将其放在一起。请注意,食谱是意大利语,但是由于拉丁语对英语的影响,许多单词与英语非常相似:

  • 艰难:难度

  • 准备:准备

  • cottura:烹饪(又称烹饪时间)

  • dosi per:剂量(又称服务尺寸)

  • 成本:成本

// initialise a slice of type Recipe (like a list of recipes)
// this way we'll be able to append each recipe to it, 
// and access the recipes outside the scope of the callback function.
var recipes []Recipe 


c.OnHTML("main", func(main *colly.HTMLElement) {
        // initialise a new recipe struct every time we visit a page
        recipe := Recipe{}
        // initialise a new Dictionary object to stoer the ingredients mappings
        ingredients_dictionary := Dictionary{}

        // assign the value of URL (the url we are visiting) to the recipe field
        recipe.url = url

        // find the recipe title, assign it to the struct, and print it in the command line
        recipe.name = main.ChildText(".gz-title-recipe")
        println("Scraping recipe for:", recipe.name)

        // iterate over each instance of a recipe spec
        // and assign its value to the recipe spec struct and the recipe
        main.ForEach(".gz-name-featured-data", func(i int, specListElement *colly.HTMLElement) {
            if strings.Contains(specListElement.Text, "Difficoltà: ") {
                recipe.specifications.difficulty = specListElement.ChildText("strong")
            }
            if strings.Contains(specListElement.Text, "Preparazione: ") {
                recipe.specifications.prepTime = specListElement.ChildText("strong")
            }
            if strings.Contains(specListElement.Text, "Cottura: ") {
                recipe.specifications.cookingTime = specListElement.ChildText("strong")
            }
            if strings.Contains(specListElement.Text, "Dosi per: ") {
                recipe.specifications.servingSize = specListElement.ChildText("strong")
            }
            if strings.Contains(specListElement.Text, "Costo: ") {
                recipe.specifications.priceTier = specListElement.ChildText("strong")
            }
        })

        // find the recipe introduction and ingredients and assign to the struct
        main.ForEach(".gz-ingredient", func(i int, ingredient *colly.HTMLElement) {
            ingredients_dictionary[ingredient.ChildText("a")] = ingredient.ChildText("span")
        })
        recipe.ingredients = append(recipe.ingredients, ingredients_dictionary)
        recipes = append(recipes, recipe)
    })

// finally, run the scraper 
collector.Visit(url)

结论ð

就是这样!恭喜!您已经成功地使用Colly库在Go中成功构建了一个网络刮板,并学到了一些意大利语ð®ð〜


现在,您可以将此知识应用于其他网站刮擦数据或增强刮板功能。

请记住,应按照网站的使用条款进行负责任地进行网络刮擦。始终尊重网站的资源,并考虑实施限制速率和错误处理机制,以确保如果您构建更复杂的应用程序,请确保刮擦过程。

我将来会重新访问该项目,并希望实施更多功能,例如:

  • 添加一个模块以在本地保存刮擦数据(例如CSV文件)。

  • 缓存页面,因此随后的运行不必再次下载同一页面。

我还应该实施什么?您喜欢这篇博客文章吗?在您的想法中发表评论,并下次见到ð

ð
您可以在Web Craper存储库中的GitHub上找到完整的代码库。