在本文中,我将编写一个小型Web搜寻器。我不确定我的网站是否在整个站点范围内都有漂亮的页面标题,并且标题是否重复,因此我编写了这个小实用程序来查找。
我将从编写一个从命令行接受起始页的命令开始,然后跟随任何以原始网址为基础的链接。
稍后,我将添加一个可选标志以检测该网站是否具有重复标题,可能对SEO有用。
简介golang.org/x/net/html
这golang.org/x
软件包是Go团队维护的软件包,但是由于各种原因,它们不是标准库的一部分。
也许它们过于具体,不会被大多数Go开发人员所使用。也许它们仍在开发或试验中,所以它们不能包含在stdlib中,它必须符合Go 1.0的承诺,即没有向后不兼容的更改-当某些东西进入stdlib时,它就是“最终的”。
这些软件包之一是golang.org/x/net/html
。
要安装它,执行
go get golang.org/x/net...
在本文中,我将特别使用html.Parse()
功能,以及html.Node
结构:
package html
type Node struct {
Type NodeType
Data string
Attr []Attribute
FirstChild, NextSibling *node
}
type NodeType int32
const (
ErrorNode NodeType = iota
TextNode
DocumentNode
ElementNode
CommentNode
DoctypeNode
)
type Attribute struct {
Key, Val string
}
func Parse(r io.Reader) (*Node, error)
列出网站链接和页面标题
下面的第一个程序接受URL并计算找到的唯一链接,给出如下输出:
http://localhost:1313/go-filesystem-structure/ -> Filesystem Structure of a Go project
http://localhost:1313/golang-measure-time/ -> Measuring execution time in a Go program
http://localhost:1313/go-tutorial-fortune/ -> Go CLI tutorial: fortune clone
http://localhost:1313/go-tutorial-lolcat/ -> Build a Command Line app with Go: lolcat
让我们开始main()
,因为它显示了程序功能的高级概述。
- 得到
url
从CLI参数使用`os.Args [1] - 实例化
visited
,这是包含键字符串和值字符串的地图,我们将在其中存储网站页面的URL和标题 - 来电
analyze()
。url
被传递两次,因为该函数是递归的,第二个参数用作递归调用的基本URL 遍历
visited
地图,通过引用传递给analyze()
现在已经填满了所有值,所以我们可以打印它们package main
import ( “fmt” “net/http” “os” “strings”
<span style="color:#e6db74">"golang.org/x/net/html"</span>
)
func main() { url := os.Args[1] if url == “” { fmt.Println(“Usage:
webcrawler <url>
“) os.Exit(1) } visited := map[string]string{} analyze(url, url, &visited) for k, v := range visited { fmt.Printf(”%s -> %s\n”, k, v) } }
很简单?进去吧analyze()
。首先,它要求parse()
,给出了指向URL的字符串,它将获取并解析它,并返回html.Node指针和一个错误。
func parse(URL字符串)(* html.Node,错误)
检查成功后,analyze()
使用获取页面标题pageTitle()
,它给出了对html.Node的引用,对其进行扫描,直到找到title标记,然后返回其值。
func pageTitle(n * html.Node)字符串
有了页面标题后,我们可以将其添加到visited
地图。
接下来,我们通过调用获得所有页面链接pageLinks()
,它具有给定的起始页面节点,它将递归扫描所有页面节点,并返回找到的唯一链接的列表(无重复)。
func pageLinks(链接[] string,n * html.Node)[] string
一旦获得链接切片,我们将对其进行迭代,然后进行一些检查:visited
尚未包含该页面,这意味着我们尚未访问该页面,并且链接必须包含baseurl
作为前缀。如果这两个断言都得到确认,我们可以致电analyze()
与链接网址。
// analyze given a url and a basurl, recoursively scans the page
// following all the links and fills the `visited` map
func analyze(url, baseurl string, visited *map[string]string) {
page, err := parse(url)
if err != nil {
fmt.Printf("Error getting page %s %s\n", url, err)
return
}
title := pageTitle(page)
(*visited)[url] = title
<span style="color:#75715e">//recursively find links
links := pageLinks(nil, page)
for _, link := range links {
if (*visited)[link] == “” && strings.HasPrefix(link, baseurl) {
analyze(link, baseurl, visited)
}
}
}
pageTitle()
使用golang.org/x/net/html
我们在上面介绍的API。在第一次迭代中n
是个<html>
节点。我们正在寻找标题标签。第一次迭代永远不会满足此要求,因此我们遍历了<html>
首先,然后是它的兄弟姐妹,我们称pageTitle()
递归地传递新节点。
最终,我们将到达<title>
标签:html.Node
实例与Type
等于html.ElementNode
(请参见上文)和Data
等于title
,然后我们通过访问其内容返回其内容FirstChild.Data
财产
// pageTitle given a reference to a html.Node, scans it until it
// finds the title tag, and returns its value
func pageTitle(n *html.Node) string {
var title string
if n.Type == html.ElementNode && n.Data == "title" {
return n.FirstChild.Data
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
title = pageTitle(c)
if title != "" {
break
}
}
return title
}
pageLinks()
没有什么不同pageTitle()
,但它在找到第一个项目时不会停止,而是查找每个链接,因此我们必须传递links
slice作为此递归函数的参数。通过检查链接来发现链接html.Node
已html.ElementNode
Type
,Data
必须是a
而且他们必须有一个Attr
和Key
href
,否则可能会成为锚点。
// pageLinks will recursively scan a `html.Node` and will return
// a list of links found, with no duplicates
func pageLinks(links []string, n *html.Node) []string {
if n.Type == html.ElementNode && n.Data == "a" {
for _, a := range n.Attr {
if a.Key == "href" {
if !sliceContains(links, a.Val) {
links = append(links, a.Val)
}
}
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
links = pageLinks(links, c)
}
return links
}
sliceContains()
是一个由调用的效用函数pageLinks()
检查切片中的唯一性。
// sliceContains returns true if `slice` contains `value`
func sliceContains(slice []string, value string) bool {
for _, v := range slice {
if v == value {
return true
}
}
return false
}
最后一个功能是parse()
。它使用http
stdlib功能以获取URL的内容(http.Get()
),然后使用golang.org/x/net/html
html.Parse()
用于解析HTTP请求中的响应主体的API,返回一个html.Node
参考。
// parse given a string pointing to a URL will fetch and parse it
// returning an html.Node pointer
func parse(url string) (*html.Node, error) {
r, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("Cannot get page")
}
b, err := html.Parse(r.Body)
if err != nil {
return nil, fmt.Errorf("Cannot parse page")
}
return b, err
}
检测重复的标题
因为我想使用命令行标志要检查重复项,我将略微更改将URL传递给程序的方式:而不是使用os.Args
,我也将使用标记传递URL。
这是修改后的main()
功能,并在进行通常的准备工作之前先进行标志解析analyze()
执行和打印值。此外,最后还有一个检查dup
布尔标志,如果为true,则运行checkDuplicates()
。
import (
"flag"
//...
)
func main() {
var url string
var dup bool
flag.StringVar(&url, “url”, “”, “the url to parse”)
flag.BoolVar(&dup, “dup”, false, “if set, check for duplicates”)
flag.Parse()
<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">url</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">""</span> {
<span style="color:#a6e22e">flag</span>.<span style="color:#a6e22e">PrintDefaults</span>()
<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Exit</span>(<span style="color:#ae81ff">1</span>)
}
<span style="color:#a6e22e">visited</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">string</span>{}
<span style="color:#a6e22e">analyze</span>(<span style="color:#a6e22e">url</span>, <span style="color:#a6e22e">url</span>, <span style="color:#f92672">&</span><span style="color:#a6e22e">visited</span>)
<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">link</span>, <span style="color:#a6e22e">title</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">visited</span> {
<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">"%s -> %s\n"</span>, <span style="color:#a6e22e">link</span>, <span style="color:#a6e22e">title</span>)
}
<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">dup</span> {
<span style="color:#a6e22e">checkDuplicates</span>(<span style="color:#f92672">&</span><span style="color:#a6e22e">visited</span>)
}
}
checkDuplicates
使用url-> title的地图并对其进行迭代以构建自己的地图uniques
地图,这次以页面标题为键,因此我们可以检查uniques[title] == ""
确定标题是否已经存在,我们可以通过打印访问使用该标题输入的第一页uniques[title]
。
// checkDuplicates scans the visited map for pages with duplicate titles
// and writes a report
func checkDuplicates(visited *map[string]string) {
found := false
uniques := map[string]string{}
fmt.Printf("\nChecking duplicates..\n")
for link, title := range *visited {
if uniques[title] == "" {
uniques[title] = link
} else {
found = true
fmt.Printf("Duplicate title \"%s\" in %s but already found in %s\n", title, link, uniques[title])
}
}
<span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">found</span> {
<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">"No duplicates were found 😇"</span>)
}
}
学分
Go编程语言Donovan和Kernighan撰写的这本书在整本书中均以网络爬虫为例,并在不同的章节中对其进行了更改,以引入新的概念。本文提供的代码从本书中汲取了灵感。
更多教程:
- 使用NGINX反向代理服务Go服务
- 在Go中复制结构
- Go Web服务器的基础
- 在Go中对地图类型进行排序
- 简而言之去指针
- 转到标签说明
- 开始日期和时间格式
- 使用Go进行JSON处理
- 可变参数函数
- 去弦备忘单
- 转到空界面说明
- 使用VS Code和Delve调试Go
- 命名为Go返回参数
- 在Go中生成随机数和字符串
- Go项目的文件系统结构
- Go中的二进制搜索算法
- 在Go中使用命令行标志
- GOPATH解释
- 使用Go构建一个命令行应用程序:lolcat
- 使用Go构建CLI命令:Cowsay
- 在Go中使用壳管
- Go CLI教程:财富克隆
- 使用Go列出文件夹中的文件
- 使用Go从GitHub获取存储库列表
- 去,将一小段字符串附加到文件中
- 去,将字符串转换为字节片
- 使用Go可视化您本地的Git贡献
- Go CPU和内存分析入门
- 解决Go程序中的“不支持索引”错误
- 测量Go程序中的执行时间
- 使用Go构建Web爬网程序以检测重复的标题
- 最佳实践:指针还是价值接收者?
- 最佳实践:您应该使用方法还是函数?
- Go数据结构:集
- 前往地图备忘单
- 在Go中生成泛型类型的实现
- Go数据结构:字典
- Go数据结构:哈希表
- 在“通过通道”中实现事件侦听器
- Go数据结构:堆栈
- Go数据结构:队列
- Go数据结构:二进制搜索树
- Go数据结构:图形
- Go数据结构:链表
- Go数据结构的完整指南
- 比较Go值
- Go是面向对象的吗?
- 在Go中使用SQL数据库
- 在Go中使用环境变量
- 上篇教程:PostgreSQL支持的REST API
- 在Go Web服务器上启用CORS
- 在Docker容器中部署Go应用程序
- 为什么Go是作为PHP开发人员学习的功能强大的语言
- 去,删除io.Reader.ReadString换行符
- 开始,如何观看更改并重建程序
- 去算一下自约会以来的月份
- 在Go中访问HTTP POST参数