zoukankan      html  css  js  c++  java
  • goquery

    使用goquery

    会用jquery的,goquery基本可以1分钟上手,下面是goquery文档

    1. http://godoc.org/github.com/PuerkitoBio/goquery

    1、创建文档

    1. d,:= goquery.NewDocumentFromReader(reader io.Reader)
    2. d,:= goquery.NewDocument(url string)

    2、查找内容

    1. ele.Find("#title") //根据id查找
    2. ele.Find(".title") //根据class查找
    3. ele.Find("h2").Find("a") //链式调用

    3、获取内容

    1. ele.Html()
    2. ele.Text()

    4、获取属性

    1. ele.Attr("href")
    2. ele.AttrOr("href", "")

    5、遍历

    1. ele.Find(".item").Each(func(index int, ele *goquery.Selection){
    2.    
    3. })

    更多api请参考官方文档

    http://liyangliang.me/posts/2016/03/zhihu-go-insight-parsing-html-with-goquery/

    zhihu-go 源码解析:用 goquery 解析 HTML

    上一篇博客 简单介绍了 zhihu-go 项目的缘起,本篇简单介绍一下关于处理 HTML 的细节。

    因为知乎没有开发 API,所以只能通过模拟浏览器操作的方式获取数据,这些数据有两种格式:普通的 HTML 文档和某些 Ajax 接口返回的 JSON(返回的数据实际上也是 HTML)。其实也就是爬虫了,抓取网页,然后提取数据。一般来说从 HTML 文档提取数据有这些做法:正则、XPath、CSS 选择器等。对我来说,正则写起来比较复杂,代码可读性差而且维护起来麻烦;XPath 没有详细了解,不过用起来应该不难,而且 Chrome 浏览器可以直接提取 XPath. zhihu-go 里用的是选择器的方式,使用了 goquery.

    goquery 是 “a little like that j-thing, only in Go”,也就是用 jQuery 的方式去操作 DOM. jQuery 大家都很熟,API 也很简单明了。本文不详细介绍 goquery,下面选几个场景(API)讲讲在 zhihu-go 里的应用。

    创建 Document 对象

    goquery 暴露了两个结构体:Document 和 SelectionDocument 表示一个 HTML 文档,Selection 用于像 jQuery 一样操作,支持链式调用。goquery 需要指定一个 HTML 文档才能继续后续的操作,有以下几个构造方式:

    • NewDocumentFromNode(root *html.Node) *Document: 传入 *html.Node 对象,也就是根节点。
    • NewDocument(url string) (*Document, error): 传入 URL,内部用 http.Get 获取网页。
    • NewDocumentFromReader(r io.Reader) (*Document, error): 传入 io.Reader,内部从 reader 中读取内容并解析。
    • NewDocumentFromResponse(res *http.Response) (*Document, error): 传入 HTTP 响应,内部拿到res.Body(实现了 io.Reader) 后的处理方式类似 NewDocumentFromReader.

    因为知乎的页面需要登录才能访问(还需要伪造请求头),而且我们并不想手动解析 HTML 来获取*html.Node,最后用到了另外两个构造方法。大致的使用场景是:

    • 请求 HTML 页面(如问题页面),调用 NewDocumentFromResponse
    • 请求 Ajax 接口,返回的 JSON 数据里是一些 HTML 片段,用 NewDocumentFromReader,其中 r = strings.NewReader(html)

    为了方便举例说明,下文采用这个定义: var doc *goquery.Document.

    查找到指定节点

    Selection 有一系列类似 jQuery 的方法,Document 结构体内嵌了 *Selection,因此也能直接调用这些方法。主要的方法是 Selection.Find(selector string),传入一个选择器,返回一个新的,匹配到的*Selection,所以能够链式调用。

    比如在用户主页(如 黄继新),要获取用户的 BIO. 首先用 Chrome 定位到对应的 HTML:

    <span class="bio" title="和知乎在一起">和知乎在一起</span>
    

    对应的 go 代码就是:

    doc.Find("span.bio")
    

    如果一个选择器对应多个结果,可以使用 First()Last()Eq(index int)Slice(start, end int)这些方法进一步定位。

    还是在用户主页,在用户资料栏的底下,从左往右展示了提问数、回答数、文章数、收藏数和公共编辑的次数。查看 HTML 源码后发现这几项的 class 是一样的,所以只能通过下标索引来区分。

    先看 HTML 源码:

    <div class="profile-navbar clearfix">
    <a class="item " href="/people/jixin/asks">提问<span class="num">1336</span></a>
    <a class="item " href="/people/jixin/answers">回答<span class="num">785</span></a>
    <a class="item " href="/people/jixin/posts">文章<span class="num">91</span></a>
    <a class="item " href="/people/jixin/collections">收藏<span class="num">44</span></a>
    <a class="item " href="/people/jixin/logs">公共编辑<span class="num">51648</span></a>
    </div>
    

    如果要定位找到回答数,对应的 go 代码是:

    doc.Find("div.profile-navbar").Find("span.num").Eq(1)
    

    属性操作

    经常需要获取一个标签的内容和某些属性值,使用 goquery 可以很容易做到。

    继续上面获取回答数的例子,用 Text() string 方法可以获取标签内的文本内容,其中包含所有子标签。

    text := doc.Find("div.profile-navbar").Find("span.num").Eq(1).Text()    // "785"
    

    需要注意的是,Text() 方法返回的字符串,可能前后有很多空白字符,可以视情况做清除。

    获取属性值也很容易,有两个方法:

    • Attr(attrName string) (val string, exists bool): 返回属性值和该属性是否存在,类似从 map中取值
    • AttrOr(attrName, defaultValue string) string: 和上一个方法类似,区别在于如果属性不存在,则返回给定的默认值

    常见的使用场景就是获取一个 a 标签的链接。继续上面获取回答的例子,如果想要得到用户回答的主页,可以这么做:

    href, _ := doc.Find("div.profile-navbar").Find("a.item").Eq(1).Attr("href")
    

    还有其他设置属性、操作 class 的方法,就不展开讨论了。

    迭代

    很多场景需要返回列表数据,比如问题的关注者列表、所有回答,某个答案的点赞的用户列表等。这种情况下一般需要用到迭代,遍历所有的同类节点,做某些操作。

    goquery 提供了三个用于迭代的方法,都接受一个匿名函数作为参数:

    • Each(f func(int, *Selection)) *Selection: 其中函数 f 的第一个参数是当前的下标,第二个参数是当前的节点
    • EachWithBreak(f func(int, *Selection) bool) *Selection: 和 Each 类似,增加了中途跳出循环的能力,当 f 返回 false 时结束迭代
    • Map(f func(int, *Selection) string) (result []string)f 的参数与上面一样,返回一个 string 类型,最终返回 []string.

    比如获取一个收藏夹(如 黄继新的收藏:关于知乎的思考)下所有的问题,可以这么做(见 zhihu-go/collections.go):

    func getQuestionsFromDoc(doc *goquery.Document) []*Question {
    	questions := make([]*Question, 0, pageSize)
    	items := doc.Find("div#zh-list-answer-wrap").Find("h2.zm-item-title")
    	items.Each(func(index int, sel *goquery.Selection) {
    		a := sel.Find("a")
    		qTitle := strip(a.Text())
    		qHref, _ := a.Attr("href")
    		thisQuestion := NewQuestion(makeZhihuLink(qHref), qTitle)
    		questions = append(questions, thisQuestion)
    	})
    	return questions
    }
    

    EachWithBreak 在 zhihu-go 中也有用到,可以参见 Answer.GetVotersN 方法zhihu-go/answer.go.

    删除节点、插入 HTML、导出 HTML

    有一个需求是把回答内容输出到 HTML,说白了其实就是修复和清洗 HTML,具体的细节可以看 answer.go 里的 answerSelectionToHtml 函数. 其中用到了一些需要修改文档的操作。

    比如,调用 Remove() 方法把一个节点删掉:

    sel.Find("noscript").Each(func(_ int, tag *goquery.Selection) {
        tag.Remove() // 把无用的 noscript 去掉
    })
    

    在节点后插入一段 HTML:

    sel.Find("img").Each(func(_ int, tag *goquery.Selection) {
        var src string
        if tag.HasClass("origin_image") {
            src, _ = tag.Attr("data-original")
        } else {
            src, _ = tag.Attr("data-actualsrc")
        }
        tag.SetAttr("src", src)
        if tag.Next().Size() == 0 {
            tag.AfterHtml("<br>")   // 在 img 标签后插入一个换行
        }
    })
    

    在标签尾部 append 一段内容:

    wrapper := `<html><head><meta charset="utf-8"></head><body></body></html>`
    doc, _ := goquery.NewDocumentFromReader(strings.NewReader(wrapper))
    doc.Find("body").AppendSelection(sel)
    

    最终输出为 html 文档:

    html, err := doc.Html()
    

    总结

    上面的例子基本涵盖了 zhihu-go 中关于 HTML 操作的场景,得益于 goquery 和 jQuery 的 API 风格,实现起来还是非常简单的。

    goQuery中的输入字符串是CSS selector,其语法风格是 http://www.w3school.com.cn/cssref/css_selectors.asp

    CSS3 选择器

    在 CSS 中,选择器是一种模式,用于选择需要添加样式的元素。

    "CSS" 列指示该属性是在哪个 CSS 版本中定义的。(CSS1、CSS2 还是 CSS3。)

    选择器例子例子描述CSS
    .class .intro 选择 class="intro" 的所有元素。 1
    #id #firstname 选择 id="firstname" 的所有元素。 1
    * * 选择所有元素。 2
    element p 选择所有 <p> 元素。 1
    element,element div,p 选择所有 <div> 元素和所有 <p> 元素。 1
    element element div p 选择 <div> 元素内部的所有 <p> 元素。 1
    element>element div>p 选择父元素为 <div> 元素的所有 <p> 元素。 2
    element+element div+p 选择紧接在 <div> 元素之后的所有 <p> 元素。 2
    [attribute] [target] 选择带有 target 属性所有元素。 2
    [attribute=value] [target=_blank] 选择 target="_blank" 的所有元素。 2
    [attribute~=value] [title~=flower] 选择 title 属性包含单词 "flower" 的所有元素。 2
    [attribute|=value] [lang|=en] 选择 lang 属性值以 "en" 开头的所有元素。 2
    :link a:link 选择所有未被访问的链接。 1
    :visited a:visited 选择所有已被访问的链接。 1
    :active a:active 选择活动链接。 1
    :hover a:hover 选择鼠标指针位于其上的链接。 1
    :focus input:focus 选择获得焦点的 input 元素。 2
    :first-letter p:first-letter 选择每个 <p> 元素的首字母。 1
    :first-line p:first-line 选择每个 <p> 元素的首行。 1
    :first-child p:first-child 选择属于父元素的第一个子元素的每个 <p> 元素。 2
    :before p:before 在每个 <p> 元素的内容之前插入内容。 2
    :after p:after 在每个 <p> 元素的内容之后插入内容。 2
    :lang(language) p:lang(it) 选择带有以 "it" 开头的 lang 属性值的每个 <p> 元素。 2
    element1~element2 p~ul 选择前面有 <p> 元素的每个 <ul> 元素。 3
    [attribute^=value] a[src^="https"] 选择其 src 属性值以 "https" 开头的每个 <a> 元素。 3
    [attribute$=value] a[src$=".pdf"] 选择其 src 属性以 ".pdf" 结尾的所有 <a> 元素。 3
    [attribute*=value] a[src*="abc"] 选择其 src 属性中包含 "abc" 子串的每个 <a> 元素。 3
    :first-of-type p:first-of-type 选择属于其父元素的首个 <p> 元素的每个 <p> 元素。 3
    :last-of-type p:last-of-type 选择属于其父元素的最后 <p> 元素的每个 <p> 元素。 3
    :only-of-type p:only-of-type 选择属于其父元素唯一的 <p> 元素的每个 <p> 元素。 3
    :only-child p:only-child 选择属于其父元素的唯一子元素的每个 <p> 元素。 3
    :nth-child(n) p:nth-child(2) 选择属于其父元素的第二个子元素的每个 <p> 元素。 3
    :nth-last-child(n) p:nth-last-child(2) 同上,从最后一个子元素开始计数。 3
    :nth-of-type(n) p:nth-of-type(2) 选择属于其父元素第二个 <p> 元素的每个 <p> 元素。 3
    :nth-last-of-type(n) p:nth-last-of-type(2) 同上,但是从最后一个子元素开始计数。 3
    :last-child p:last-child 选择属于其父元素最后一个子元素每个 <p> 元素。 3
    :root :root 选择文档的根元素。 3
    :empty p:empty 选择没有子元素的每个 <p> 元素(包括文本节点)。 3
    :target #news:target 选择当前活动的 #news 元素。 3
    :enabled input:enabled 选择每个启用的 <input> 元素。 3
    :disabled input:disabled 选择每个禁用的 <input> 元素 3
    :checked input:checked 选择每个被选中的 <input> 元素。 3
    :not(selector) :not(p) 选择非 <p> 元素的每个元素。 3
    ::selection ::selection 选择被用户选取的元素部分。 3

    http://www.w3school.com.cn/cssref/css_selectors.asp

    package main
    
    import (
        "fmt"
        "log"
    
        "github.com/PuerkitoBio/goquery"
    )
    
    func ExampleScrape() {
        doc, err := goquery.NewDocument("http://studygolang.com/topics")
        if err != nil {
            log.Fatal(err)
        }
        /*
            dhead := doc.Find("head")
            dTitle := dhead.Find("title")
            fmt.Printf("title text:%s
    ", dTitle.Text())
            html, _ := dTitle.Html()
            fmt.Printf("title html:%s
    ", html)
            metaArr := dhead.Find("meta")
            for i := 0; i < metaArr.Length(); i++ {
                d, _ := metaArr.Eq(i).Attr("name")
                fmt.Println(d)
            }
        */
        doc.Find("div.wrapper .container .col-lg-9").Each(func(i int, cs *goquery.Selection) {
            d, _ := cs.Attr("class")
            fmt.Println(d)
        })
    }
    
    func main() {
        ExampleScrape()
        return
        doc, err := goquery.NewDocument("http://studygolang.com/topics")
        if err != nil {
            log.Fatal(err)
        }
        fmt.Println(doc.Html())            //.Html()得到html内容
        pTitle := doc.Find("title").Text() //直接提取title的内容
        class := doc.Find("h2").Text()
        fmt.Printf("class:%v
    ", class)
        fmt.Printf("title:%v
    ", pTitle)
        doc.Find(".topics .topic").Each(func(i int, contentSelection *goquery.Selection) {
            title := contentSelection.Find(".title a").Text()
            t := contentSelection.Find(".title a")
            log.Printf("the length;%d", t.Length())
            log.Println("", i+1, "个帖子的标题:", title)
        })
        /*
            t := doc.Find(".topics .topic")
            log.Printf("%+v", t)
            t = doc.Find(".topics")
            log.Printf("%+v", t)
            t = doc.Find(".topic")
            log.Printf("%+v", t)
            t = doc.Find("div.topic")
            log.Printf("div.topic:%+v", t)
        */
        t := doc.Find("div.topic").Find(".title a")
        log.Printf("div.topic.title a:%+v", t)
        for i := 0; i < t.Length(); i++ {
            d, _ := t.Eq(i).Attr("href")
            title, _ := t.Eq(i).Attr("title")
            fmt.Println(d)
            fmt.Println(title)
        }

    输出:

    col-lg-9 col-md-8 col-sm-7

    参考链接 

     http://liyangliang.me/posts/2016/03/zhihu-go-insight-parsing-html-with-goquery/ 

    http://www.tiege.me/?p=501

  • 相关阅读:
    梦断代码阅读笔记03
    用户场景分析
    学习进度8
    学习进度7
    梦断代码阅读笔记02
    学习进度6
    随堂小测app(nabcd)
    梦断代码阅读笔记01
    《构建之法》-6
    《构建之法》-5
  • 原文地址:https://www.cnblogs.com/diegodu/p/5761961.html
Copyright © 2011-2022 走看看