goquery
使用goquery
会用jquery的,goquery基本可以1分钟上手,下面是goquery文档
- http://godoc.org/github.com/PuerkitoBio/goquery
1、创建文档
- d,e := goquery.NewDocumentFromReader(reader io.Reader)
- d,e := goquery.NewDocument(url string)
2、查找内容
- ele.Find("#title") //根据id查找
- ele.Find(".title") //根据class查找
- ele.Find("h2").Find("a") //链式调用
3、获取内容
- ele.Html()
- ele.Text()
4、获取属性
- ele.Attr("href")
- ele.AttrOr("href", "")
5、遍历
- ele.Find(".item").Each(func(index int, ele *goquery.Selection){
- })
更多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 和 Selection. Document 表示一个 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\n", dTitle.Text())
html, _ := dTitle.Html()
fmt.Printf("title html:%s\n", 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\n", class)
fmt.Printf("title:%v\n", 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+, "个帖子的标题:", 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 := ; i < t.Length(); i++ {
d, _ := t.Eq(i).Attr("href")
title, _ := t.Eq(i).Attr("title")
fmt.Println(d)
fmt.Println(title)
}
输出:
col-lg- col-md- col-sm-
参考链接
http://liyangliang.me/posts/2016/03/zhihu-go-insight-parsing-html-with-goquery/
goquery的更多相关文章
- goquery 添加header 发起请求
goquery 添加header 发起请求 我们知道使用net/http 很容易发起GET or POST 请求:并且在发起http请求时候,可以很容易的对header进行干预 例如: client ...
- go语言爬虫goquery和grequests的使用
/*下载工具*/ package main import ( "fmt" //go语言版本的jquery "github.com/PuerkitoBio/goquery& ...
- go语言解析网页利器goquery使用教程(爬虫必备)
某些时候需要爬取网页中指定信息时,通常需要一些框架解析网页行成dom模型,然后来操作节点来获取相应的信息.在java中很显然就是Jsoup,而在Golang里,应该就是这个goquery了吧. goq ...
- go语言,第三方包相对路径导入包引起的问题及解决方案(goquery)
对go语言而言,跟踪init很显然包有且仅有一次被导入的可能. 但是重复引用了goquery包,后编译出现问题 项目涉及相关目录 ├── main.go└── parse └── parse.g ...
- 关于goquery的“non-standard import”错误
goquery运行缺包就用get github.com\andybalholm\cascadia下到gopath,然后出现“non-standard import”错误,说明github.com\an ...
- go语言 goquery爬虫
goquery 类似ruby的gem nokogiri goquery的选择器功能很强大,很好用.地址:https://github.com/PuerkitoBio/goquery 这是一个糗百首 ...
- goquery 解析不了noscript
今天在用goquery的时候 解析noscript标签的时候.发现一直获取不到里面的元素. google得到.需要去除noscript标签. s.Find("noscript"). ...
- Golang+chromedp+goquery 简单爬取动态数据
目录 Golang+chromedp+goquery 简单爬取动态数据 Golang的安装 下载golang软件 解压golang 配置golang 重新导入配置 chromedp框架的使用 实际的代 ...
- goquery 文档
https://www.itlipeng.cn/2017/04/25/goquery-%E6%96%87%E6%A1%A3/ http://blog.studygolang.com/2015/04/g ...
随机推荐
- Linux 发送信号
使用kill命令 --在命令行执行kill命令.向指定进程发送信号. 使用kill函数 int kill(pid_t pid,int sig); --参数pid指定一个要杀死的进程,而sig是要发送的 ...
- Log4net Dll用法
在导入Log4net的过程中,遇到一两个小bug. 开发平台必须是NET4 而不能是net4 client profile Log4Helper 里面的Namespace要和我们建立项目的名称一致. ...
- nginx缓存模块配置总结proxy_cache(未完)
简介:此缓存设置用到了第三方模块purge,使用的时候就在源链接和访问的具体内容之间加入关键字"/purge/"即可. 如:访问http://192.168.0.1/a.png 会 ...
- 使用Uploadify实现上传图片生成缩略图例子,实时显示进度条
不了解Uploadify的,先看看前一篇详细说明 http://www.cnblogs.com/XuebinDing/archive/2012/04/26/2470995.html Uploadify ...
- list to csv
import csv # ============================== # list to csv # ============================== a = [1,2, ...
- 信息安全系统设计基础实验一 20135211&20135216
北京电子科技学院(BESTI) 实 验 报 告 封面 课程:信息安全系统设计基础 班级:1352 姓名:(按贡献大小排名)李行之 刘蔚然 ...
- Cordova开发总结(插件篇)
最近刚刚做完一个用Cordova开发了一款电子商务的应用.在选用Cordova前,我有考察过,国内的Appcan, Apicloud等等的解决方案.其实Appcan,ApiCloud的混合方案挺完整的 ...
- 关于git托管的一些心得
GIT托管的一些心得 熟练运用软件进行GIT托管的好处 在上一周的学习中,我提出来了一个疑惑,就是为什么一定要用软件托管而不选择web托管,在这周的学习中,我通过实践体会到了一些运用软件托管的好处: ...
- 关于JavaScript打印去掉页眉页脚
因为这个问题,Google和百度都查了个遍,网上主要解决方案都是这一个代码: <script language="JavaScript"> var hkey_root, ...
- 每天一个linux命令(39):iostat命令
Linux系统中的 iostat 是I/O statistics(输入/输出统计)的缩写,iostat工具将对系统的磁盘操作活动进行监视.它的特点是汇报磁盘活动统计情况,同时也会 汇报出CPU使用情况 ...