检查网页中的死链接(Golang 命令行教程)
网络上没有哪种内容能够幸免于腐烂和失效。博客文章会因为失效链接而变得毫无意义,图片也会因为服务器宕机而变得模糊不清。解决办法是定期检查失效内容,但这完全是徒劳无功的。幸运的是,我们可以利用计算机自动完成这些繁琐的工作。
在本教程中,我们将使用功能强大的 Go语言 Colly库编写一个网页爬虫工具。起初,我打算用经典的 Python 来写,但后来有机会学习一门新的编程语言。Go 语言语法清晰、标准库强大、支持并发,而且是编译型语言,这使得它成为实现我们都喜爱的命令行工具的有力竞争者。
你可以在这里获取完整的源代码,但请跟随我们的步骤,一起构建这个链接健康检查。由于这是我的第一个 Go 项目,以下内容并非专家意见。作为经验丰富的 Go 程序员,你肯定能找到改进我工作的方法。请在评论区留言。
要求
经过深思熟虑,我制定了我们MVP项目的准入标准:
- 我们需要一个程序,它接受一个 URL 作为参数,访问该 URL 及其中的所有链接。
- 链接遍历应在第一页或第二层遍历后结束,以避免长时间运行的任务。
- 该程序以一种便于识别死链接的方式显示链接列表。这意味着我们应该对终端输出进行颜色标记。
- 所有结果都应以换行符打印,以便我们使用
grep其他工具筛选数据。 - 为了优化性能,程序应该异步处理链接。
结构链接
首先,我们需要一种方法来表示链接,Go 语言提供了一个完美的解决方案——结构体(struct),它有点像其他语言中的对象。让我们定义一个Link结构体,用于存储链接的地址和 HTTP 响应中的状态码。
type Link struct {
url *url.URL
status int
}
请注意,我们使用*url.URL`URL` 作为链接成员的类型。由于 URL 本质上是一种结构化数据,我们希望以结构化的方式传递它们。这也能降低因传递和返回纯字符串而可能产生的安全风险。我们始终可以使用url.String()之前的方法获取 URL 的底层字符串表示形式,例如将其打印出来。
接下来,我们需要一些便捷的方法来对结构体中的数据进行建模。例如,我们想知道链接是否被认为是健康的。我选择当且仅当 HTTP 响应的状态码在指定2xx范围内时,链接才被认为是健康的,这是 HTTP 请求成功的传统标志。结构体方法isHealthy()负责告诉我们这一点。
func (link *Link) isHealthy() bool {
return link.status >= HTTP_MIN_STATUS && link.status <= HTTP_MAX_STATUS
}
为了使程序对我们有用,我们需要输出数据。让我们定义一个显示失效链接的方法。
func (link *Link) printFailure() {
fmt.Printf(
"Link to %s is %s with status %d\n",
link.url,
aurora.Red("down"),
aurora.Bold(link.status),
)
}
以下是另一种显示健康链接的方法。
func (link *Link) printSuccess() {
fmt.Printf(
"Link to %s is %s\n",
link.url,
aurora.Green("healthy"),
)
}
在上述方法中,我使用了第三方库Aurora来为字符串输出着色。目前简单的绿色和红色就足够了,但您可以随意尝试各种你能想象到的彩虹颜色组合。
初始化收集器
我们的教程正式开始。我们需要定义一个收集器,它是一个 Colly 结构体,其中包含几个用于抓取网页的实用方法。我们使用用户代理配置它,启用异步处理,并限制它只访问以 `http://` 或 `http://` 开头的 URL,以避免陷入 `http://`http或`http://` 等链接陷阱。此外,请求之间还会设置随机延迟,以降低触及某些服务器速率限制的概率。httpsmailto:
在我们的用例中,抓取操作的最大值depth至关重要,因为我们只想访问第一个链接及其下一级递归的链接。因此,该值为2。否则,我们将陷入无休止的循环,最终到达死胡同,这可能会耗费大量时间。
由于 Go 语言非常注重检查和处理可能出现的错误,我定义了一个handleError()函数,该函数接收一个错误值并在必要时输出它。这样一来,我就不必err != nil总是编写错误检查,代码也更易于阅读了。
func getCollector() *colly.Collector {
userAgent := flag.String("user-agent", DEFAULT_USER_AGENT, "User-Agent for scraping")
depth := flag.Int("depth", 2, "Recursion depth for scraping")
threads := flag.Int("threads", 4, "Number of threads to use for scraping")
flag.Parse()
collector := colly.NewCollector(
colly.Async(true),
colly.UserAgent(*userAgent),
colly.MaxDepth(*depth),
colly.URLFilters(
regexp.MustCompile("https?://.+$"),
),
)
limitError := collector.Limit(&colly.LimitRule{
DomainGlob: "*",
Parallelism: *threads,
RandomDelay: 1 * time.Second,
})
handleError(limitError)
return setHandlers(collector)
}
请记住flag.Parse()在定义命令行标志后调用该函数。您还需要使用星号表示法来解引用它们,以提取实际值。
设置处理程序
初始化收集器后,我们需要为其添加几个处理程序,Colly 的核心逻辑就位于这些处理程序中。我们定义了用于捕获连接错误、检查 HTML 元素和响应 HTTP 响应的处理程序。所有处理程序大多是回调函数,因此使用起来非常方便。
捕获错误
我们的错误处理程序负责处理错误,你猜对了!我们会捕获 HTTP 响应和错误值,并尝试将其显示出来。有时 Colly 不会报告任何错误,在这种情况下,我们会用我们那条不太有用的消息来代替。我们还能做什么呢?
最后,我们将重新格式化的错误信息传递给我们的自定义函数,以便将其正确打印出来。
collector.OnError(func(response *colly.Response, err error) {
url := response.Request.URL
reason := err.Error()
if reason == "" {
reason = "Unknown"
}
handleError(fmt.Errorf("Request to %s failed. Reason: %s", url, reason))
})
将 HTML 锚点加入白名单
Colly 每次遇到 HTML 元素时,都会检查是否存在OnHTML与该元素匹配的处理程序。我们希望访问网页上的每个链接,因此我们捕获所有带有href属性的 HTML 锚标签。如果您熟悉 CSS 选择器,可以修改此处处理程序的第一个参数img,例如,用于捕获无效标签。我选择保持现状,并使用之前所示的正则表达式来限制访问的 URL。
这里我们要丢弃调用返回的错误信息,element.Request.Visit()因为它们除了警告已达到最大递归深度之外,没有任何其他价值。在 Go 语言和类似语言中,下划线是向编译器或其他相关人员表明我们不在乎该值的绝佳方式。
collector.OnHTML("a[href]", func(element *colly.HTMLElement) {
link := element.Attr("href")
_ = element.Request.Visit(link)
})
窥探回应
以下是我们最重要的处理程序:捕获从 Colly 的访问中获得的所有 HTTP 响应。
我们从响应中捕获请求的 URL 及其状态到我们的链接结构中,并使用该结构的方法来告诉我们链接是否健康。
我们也可以将这些统计数据记录到文件中以便后续分析,但为了简便起见,我们直接打印出来。如有必要,定义其他结构体方法来进一步处理这些数据也很容易。
collector.OnResponse(func(response *colly.Response) {
link := Link{
url: response.Request.URL,
status: response.StatusCode,
}
if !link.isHealthy() {
link.printFailure()
return
}
link.printSuccess()
})
将它们连接起来
我们是不是漏掉了什么?啊,对了,没有这个main()函数,程序就运行不了。咱们把这些部件粘起来吧。
func main() {
target, urlError := getURL(os.Args)
handleFatal(urlError)
collector := getCollector()
handleError(collector.Visit(target.String()))
collector.Wait()
}
这里getURL()提供一个用于验证输入参数并获取第一个参数的函数。如果提供的参数数量错误,我们会使用该handleFatal()函数处理此错误,并输出错误信息,同时以非零状态码退出程序,以告知程序存在问题。
最后,我们初始化收集器,指示它访问我们指定的 URL,并等待所有异步进程完成。这一Wait()调用至关重要,否则我们的程序将无所事事地终止。
现在我们可以使用它来构建程序go build,或者直接使用它来测试程序go run。无论哪种方式,将 URL 传递给程序后,程序很快就会开始访问找到的链接并打印它们的健康状况。在代码仓库中,我还将此工具发布为 Docker 镜像,如果您想跳过编码直接使用它,可以使用镜像。运行方式如下:
docker run -it --rm nikoheikkila/go-link-health <URL>
由于采用了 Docker 多阶段构建,整个镜像只有 20 MB 多一点。
概括
不过,我们目前的实现方式并不完美。通过对 URL 运行命令,有时会遇到特定请求超时错误,需要手动检查。幸运的是,这些错误会输出到终端,只要终端应用程序配置正确,就可以轻松地逐个检查。
可能的改进方案包括将健康链接和失效链接分开分组,以便更轻松地使用切片进行检查;或者,如果提供了合适的标志,则以 JSON 格式输出数据。这些仍需进一步研究。
学习一门新语言并非易事,但对我而言,找到一个相关的应用场景并为其实现一个最小可行方案是最佳途径。其他人或许会坚持传统的课堂学习方式,阅读文章和参考资料,并记下问题以便进一步研究。
我属于那种必须把事情做完然后写下来的人。将编程和写作结合起来的关键在于克服内心的冒名顶替感。诚然,并非所有人都会觉得你的文章重要,他们或许还会对此直言不讳,但对于每一个自以为是的人,都有许多渴望学习的初学者正努力追随你的脚步。
鉴于此,我希望您喜欢阅读本文,我们下一篇教程再见。同时,如果您编写过 Golang 教程,请将链接发给我。
延伸阅读
最后,以下是我在学习 Go 语言时发现的一些有用的资源。
- 以实例为准;包含大量带注释的代码片段,用于执行基本和高级任务。
- GoDocs;用于核心 API 文档。
- 官方的 VS Code Go 扩展;非常感谢微软开发了这个扩展,让 Go 代码编写变得如此流畅。而且也不需要其他笨重的 IDE。
- 学习 Go 语言测试;如果你只能点击这篇文章中的一个链接,那就点击这个链接吧。它全面介绍了如何使用 TDD(测试驱动开发)方法编写 Go 代码。