乐趣区

关于go:goquery的认识使用源码分析及实现原理

转载于:https://scriptrunz.com/zh-cn/…

goquery 是什么

goquery 是用 Go 实现的一个相似于 jQuery 的库,它封装了 Go 规范库 net/html 和 CSS 库 cascadia,提供了与 jQuery 相近的接口。

Go 驰名的爬虫框架 colly 就是基于 goquery 实现的。

goquery 能用来干什么

goquery 提供了与 jQuery 相近的接口,能够对爬取到的 HTML 进行过滤以取得本人想要的数据。

goquery quick start

Document 是 goquery 包的外围类之一,创立一个 Document 是应用 goquery 的第一步:

type Document struct {
    *Selection
    Url      *url.URL
    rootNode *html.Node
}

func NewDocumentFromNode(root *html.Node) *Document 
func NewDocument(url string) (*Document, error)
func NewDocumentFromReader(r io.Reader) (*Document, error)
func NewDocumentFromResponse(res *http.Response) (*Document, error)

通过源码能够晓得 Document 继承了 Selection(先不论 Selection 是什么),除此之外最重要的是 rootNode,它是 HTML 的根节点,Url 这个字段作用不大,在应用 NewDocumentNewDocumentFromResponse时会对该字段赋值。

领有 Document 类 后,咱们就能够利用从 Selection 类 继承的 Find 函数 来取得本人想要的数据,比方咱们想拿到

func TestFind(t *testing.T) {
    html := `<body>
                <div>DIV1</div>
                <div>DIV2</div>
                <span>SPAN</span>
            </body>
            `
    
    dom, err := goquery.NewDocumentFromReader(strings.NewReader(html))
    if err != nil {log.Fatalln(err)
    }

    dom.Find("div").Each(func(i int, selection *goquery.Selection) {fmt.Println(selection.Text())
    })
}
------------ 运行后果 --------------
=== RUN   TestFind
DIV1
DIV2

玩转 goquery.Find()

goquery 提供了大量的函数,集体认为最重要的是 Find 函数,把它用好了能力疾速从大量文本中筛选出咱们想要的数据,上面这一章次要展现应用Find 函数 的各种姿态:

查找多个标签

应用 , 逗号 找出多个标签:

func TestMultiFind(t *testing.T) {
    html := `<body>
                <div>DIV1</div>
                <div>DIV2</div>
                <span>SPAN</span>
            </body>
            `

    dom, err := goquery.NewDocumentFromReader(strings.NewReader(html))
    if err != nil {log.Fatalln(err)
    }

    dom.Find("div,span").Each(func(i int, selection *goquery.Selection) {fmt.Println(selection.Text())
    })
}
------------ 运行后果 --------------
=== RUN   TestMultiFind
DIV1
DIV2
SPAN

Id 选择器

应用 # 代表 Id 选择器。

func TestFind_IdSelector(t *testing.T) {
    html := `<body>
                <div id="div1">DIV1</div>
                <div>DIV2</div>
                <span>SPAN</span>
            </body>
            `

    dom, err := goquery.NewDocumentFromReader(strings.NewReader(html))
    if err != nil {log.Fatalln(err)
    }

    dom.Find("#div1").Each(func(i int, selection *goquery.Selection) {fmt.Println(selection.Text())
    })
}
------------ 运行后果 --------------
=== RUN   TestFind_IdSelector
DIV1

Class 选择器

应用 . 代表 Class 选择器。

func TestFind_ClassSelector(t *testing.T) {
    html := `<body>
                <div>DIV1</div>
                <div class="name">DIV2</div>
                <span>SPAN</span>
            </body>
            `

    dom, err := goquery.NewDocumentFromReader(strings.NewReader(html))
    if err != nil {log.Fatalln(err)
    }

    dom.Find(".name").Each(func(i int, selection *goquery.Selection) {fmt.Println(selection.Text())
    })
}
------------ 运行后果 --------------
=== RUN   TestFind_ClassSelector
DIV2

属性选择器

应用 [] 代表属性选择器。

func TestFind_AttributeSelector(t *testing.T) {
    html := `<body>
                <div>DIV1</div>
                <div lang="zh">DIV2</div>
                <span>SPAN</span>
            </body>
            `

    dom, err := goquery.NewDocumentFromReader(strings.NewReader(html))
    if err != nil {log.Fatalln(err)
    }

    dom.Find("div[lang]").Each(func(i int, selection *goquery.Selection) {fmt.Println(selection.Text())
    })
}
------------ 运行后果 --------------
=== RUN   TestFind_AttributeSelector
DIV2

属性选择器也反对表达式过滤,比方:

func TestFind_AttributeSelector_2(t *testing.T) {
    html := `<body>
                <div>DIV1</div>
                <div lang="zh">DIV2</div>
                <div lang="en">DIV3</div>
                <span>SPAN</span>
            </body>
            `

    dom, err := goquery.NewDocumentFromReader(strings.NewReader(html))
    if err != nil {log.Fatalln(err)
    }

    dom.Find("div[lang=zh]").Each(func(i int, selection *goquery.Selection) {fmt.Println(selection.Text())
    })
}
------------ 运行后果 --------------
=== RUN   TestFind_AttributeSelector_2
DIV2
选择器 阐明
Find(“div[lang]”) 筛选含有 lang 属性的 div 元素
Find(“div[lang=zh]”) 筛选 lang 属性为 zh 的 div 元素
Find(“div[lang!=zh]”) 筛选 lang 属性不等于 zh 的 div 元素
Find(“div[lang¦=zh]”) 筛选 lang 属性为 zh 或者 zh- 结尾的 div 元素
Find(“div[lang*=zh]”) 筛选 lang 属性蕴含 zh 这个字符串的 div 元素
Find(“div[lang~=zh]”) 筛选 lang 属性蕴含 zh 这个单词的 div 元素,单词以空格离开的
Find(“div[lang$=zh]”) 筛选 lang 属性以 zh 结尾的 div 元素,辨别大小写
Find(“div[lang^=zh]”) 筛选 lang 属性以 zh 结尾的 div 元素,辨别大小写

当然也能够将多个属性筛选器组合,比方:Find("div[id][lang=zh]")

子节点选择器

应用 > 代表子节点选择器。

func TestFind_ChildrenSelector(t *testing.T) {
    html := `<body>
                <div>DIV1</div>
                <div>DIV2</div>
                <span>SPAN</span>
            </body>
            `

    dom, err := goquery.NewDocumentFromReader(strings.NewReader(html))
    if err != nil {log.Fatalln(err)
    }

    dom.Find("body>span").Each(func(i int, selection *goquery.Selection) {fmt.Println(selection.Text())
    })
}
------------ 运行后果 --------------
=== RUN   TestFind_ChildrenSelector
SPAN

此外 + 示意相邻,~示意共有(父节点雷同即为 true)

内容过滤器

过滤文本

应用 :contains($text) 来过滤字符串。

func TestFind_ContentFilter_Contains(t *testing.T) {
    html := `<body>
                <div>DIV1</div>
                <div>DIV2</div>
                <span>SPAN</span>
            </body>
            `

    dom, err := goquery.NewDocumentFromReader(strings.NewReader(html))
    if err != nil {log.Fatalln(err)
    }

    dom.Find("div:contains(V2)").Each(func(i int, selection *goquery.Selection) {fmt.Println(selection.Text())
    })
}
------------ 运行后果 --------------
=== RUN   TestFind_ContentFilter_Contains
DIV2

过滤节点

func TestFind_ContentFilter_Has(t *testing.T) {
    html := `<body>
                <span>SPAN1</span>
                <span>
                    SPAN2
                    <div>DIV</div>
                </span>
            </body>
            `

    dom, err := goquery.NewDocumentFromReader(strings.NewReader(html))
    if err != nil {log.Fatalln(err)
    }

    dom.Find("span:has(div)").Each(func(i int, selection *goquery.Selection) {fmt.Println(selection.Text())
    })
}
------------ 运行后果 --------------
=== RUN   TestFind_ContentFilter_Has
SPAN2
DIV

此外,还有 :first-child:first-of-type 过滤器别离能够筛选出第一个子节点、第一个同类型的子节点。

相应的 :last-child:last-of-type:nth-child(n):nth-of-type(n) 用法相似,不做过多解释。

goquery 源码剖析

Find 函数 是 goquery 最外围的函数:

func (s *Selection) Find(selector string) *Selection {return pushStack(s, findWithMatcher(s.Nodes, compileMatcher(selector)))
}

Find 函数 的性能由pushStack 函数实现

func pushStack(fromSel *Selection, nodes []*html.Node) *Selection {result := &Selection{nodes, fromSel.document, fromSel}
    return result
}

该函数就是拿着 nodes 参数 去创立一个新的 Selection 类,构建一个 Selection 链表。

无论是函数命名pushStack,还是 Selection 类的字段都能够证实下面的判断:

type Selection struct {Nodes    []*html.Node
    document *Document
    prevSel  *Selection // 上一个节点的地址
}

当初焦点来到了 pushStack 函数的 nodes 参数nodes 参数 是什么间接决定了咱们构建了一个怎么的链表、决定了 Find 函数 的最终返回值,这就须要咱们钻研下 findWithMatcher 函数 的实现:

func findWithMatcher(nodes []*html.Node, m Matcher) []*html.Node {return mapNodes(nodes, func(i int, n *html.Node) (result []*html.Node) {
        for c := n.FirstChild; c != nil; c = c.NextSibling {
            if c.Type == html.ElementNode {result = append(result, m.MatchAll(c)...)
            }
        }
        return
    })
}

findWithMatcher 函数 的性能由mapNodes 函数 实现:

func mapNodes(nodes []*html.Node, f func(int, *html.Node) []*html.Node) (result []*html.Node) {set := make(map[*html.Node]bool)
    for i, n := range nodes {if vals := f(i, n); len(vals) > 0 {result = appendWithoutDuplicates(result, vals, set)
        }
    }
    return result
}

mapNodes 函数 参数 f 的返回值 []*html.Node 做去重解决,所以重点在于这个参数 f func(int, *html.Node) []*html.Node 的实现:

func(i int, n *html.Node) (result []*html.Node) {
    for c := n.FirstChild; c != nil; c = c.NextSibling {
        if c.Type == html.ElementNode {result = append(result, m.MatchAll(c)...)
        }
    }
    return
}![img.png](img.png)

函数遍历 html.Node 节点,并利用MatchAll 函数 筛选出想要的数据

type Matcher interface {Match(*html.Node) bool
    MatchAll(*html.Node) []*html.Node
    Filter([]*html.Node) []*html.Node}

func compileMatcher(s string) Matcher {cs, err := cascadia.Compile(s)
    if err != nil {return invalidMatcher{}
    }
    return cs
}

MatchAll 函数 Matcher 接口 定义,而 compileMatcher(s string) 恰好通过利用 cascadia 库 返回一个 Matcher 实现类,其 参数 s 就是咱们上文提到的匹配规定,比方dom.Find("div")

图解源码

应用 Find 函数 时,goquery 做了什么:

总结

本文次要介绍了 goquery 最外围的 Find 函数 的用法及其源码实现,其实除了Find 函数,goquery 还提供了大量的函数帮忙咱们过滤数据,因为函数泛滥且没那么重要,自己就没有持续钻研,当前有机会再深入研究下。

退出移动版