因?yàn)橹鯖](méi)有開(kāi)發(fā) API,所以只能通過(guò)模擬瀏覽器操作的方式獲取數(shù)據(jù),這些數(shù)據(jù)有兩種格式:普通的 HTML 文檔和某些 Ajax 接口返回的 JSON(返回的數(shù)據(jù)實(shí)際上也是 HTML)。其實(shí)也就是爬蟲(chóng)了,抓取網(wǎng)頁(yè),然后提取數(shù)據(jù)。一般來(lái)說(shuō)從 HTML 文檔提取數(shù)據(jù)有這些做法:正則、XPath、CSS 選擇器等。對(duì)我來(lái)說(shuō),正則寫(xiě)起來(lái)比較復(fù)雜,代碼可讀性差而且維護(hù)起來(lái)麻煩;XPath 沒(méi)有詳細(xì)了解,不過(guò)用起來(lái)應(yīng)該不難,而且 Chrome 瀏覽器可以直接提取 XPath. zhihu-go 里用的是選擇器的方式,使用了 goquery.
goquery 是 “a little like that j-thing, only in Go”,也就是用 jQuery 的方式去操作 DOM. jQuery 大家都很熟,API 也很簡(jiǎn)單明了。本文不詳細(xì)介紹 goquery,下面選幾個(gè)場(chǎng)景(API)講講在 zhihu-go 里的應(yīng)用。
goquery 暴露了兩個(gè)結(jié)構(gòu)體: Document和 Selection. Document表示一個(gè) HTML 文檔, Selection用于像 jQuery 一樣操作,支持鏈?zhǔn)秸{(diào)用。goquery 需要指定一個(gè) HTML 文檔才能繼續(xù)后續(xù)的操作,有以下幾個(gè)構(gòu)造方式:
因?yàn)橹醯捻?yè)面需要登錄才能訪問(wèn)(還需要偽造請(qǐng)求頭),而且我們并不想手動(dòng)解析 HTML 來(lái)獲取 *html.Node,最后用到了另外兩個(gè)構(gòu)造方法。大致的使用場(chǎng)景是:
為了方便舉例說(shuō)明,下文采用這個(gè)定義: var doc *goquery.Document.
Selection有一系列類(lèi)似 jQuery 的方法, Document結(jié)構(gòu)體內(nèi)嵌了 *Selection,因此也能直接調(diào)用這些方法。主要的方法是 Selection.Find(selector string),傳入一個(gè)選擇器,返回一個(gè)新的,匹配到的 *Selection,所以能夠鏈?zhǔn)秸{(diào)用。
比如在用戶主頁(yè)(如 黃繼新),要獲取用戶的 BIO. 首先用 Chrome 定位到對(duì)應(yīng)的 HTML:
和知乎在一起
對(duì)應(yīng)的 go 代碼就是:
doc.Find("span.bio")
如果一個(gè)選擇器對(duì)應(yīng)多個(gè)結(jié)果,可以使用 First(), Last(), Eq(index int), Slice(start, end int)這些方法進(jìn)一步定位。
還是在用戶主頁(yè),在用戶資料欄的底下,從左往右展示了提問(wèn)數(shù)、回答數(shù)、文章數(shù)、收藏?cái)?shù)和公共編輯的次數(shù)。查看 HTML 源碼后發(fā)現(xiàn)這幾項(xiàng)的 class 是一樣的,所以只能通過(guò)下標(biāo)索引來(lái)區(qū)分。
先看 HTML 源碼:
提問(wèn)1336回答785文章91收藏44公共編輯51648
如果要定位找到回答數(shù),對(duì)應(yīng)的 go 代碼是:
doc.Find("div.profile-navbar").Find("span.num").Eq(1)
經(jīng)常需要獲取一個(gè)標(biāo)簽的內(nèi)容和某些屬性值,使用 goquery 可以很容易做到。
繼續(xù)上面獲取回答數(shù)的例子,用 Text() string方法可以獲取標(biāo)簽內(nèi)的文本內(nèi)容,其中包含所有子標(biāo)簽。
text := doc.Find("div.profile-navbar").Find("span.num").Eq(1).Text() // "785"
需要注意的是, Text()方法返回的字符串,可能前后有很多空白字符,可以視情況做清除。
獲取屬性值也很容易,有兩個(gè)方法:
常見(jiàn)的使用場(chǎng)景就是獲取一個(gè) a 標(biāo)簽的鏈接。繼續(xù)上面獲取回答的例子,如果想要得到用戶回答的主頁(yè),可以這么做:
href, _ := doc.Find("div.profile-navbar").Find("a.item").Eq(1).Attr("href")
還有其他設(shè)置屬性、操作 class 的方法,就不展開(kāi)討論了。
很多場(chǎng)景需要返回列表數(shù)據(jù),比如問(wèn)題的關(guān)注者列表、所有回答,某個(gè)答案的點(diǎn)贊的用戶列表等。這種情況下一般需要用到迭代,遍歷所有的同類(lèi)節(jié)點(diǎn),做某些操作。
goquery 提供了三個(gè)用于迭代的方法,都接受一個(gè)匿名函數(shù)作為參數(shù):
比如獲取一個(gè)收藏夾(如 黃繼新的收藏:關(guān)于知乎的思考)下所有的問(wèn)題,可以這么做(見(jiàn) 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 中也有用到,可以參見(jiàn) Answer.GetVotersN 方法: zhihu-go/answer.go.
有一個(gè)需求是把回答內(nèi)容輸出到 HTML,說(shuō)白了其實(shí)就是修復(fù)和清洗 HTML,具體的細(xì)節(jié)可以看 answer.go 里的 answerSelectionToHtml 函數(shù). 其中用到了一些需要修改文檔的操作。
比如,調(diào)用 Remove()方法把一個(gè)節(jié)點(diǎn)刪掉:
sel.Find("noscript").Each(func(_ int, tag *goquery.Selection) { tag.Remove() // 把無(wú)用的 noscript 去掉})
在節(jié)點(diǎn)后插入一段 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("
") // 在 img 標(biāo)簽后插入一個(gè)換行 }})
在標(biāo)簽尾部 append 一段內(nèi)容:
wrapper := ``doc, _ := goquery.NewDocumentFromReader(strings.NewReader(wrapper))doc.Find("body").AppendSelection(sel)
最終輸出為 html 文檔:
html, err := doc.Html()
上面的例子基本涵蓋了 zhihu-go 中關(guān)于 HTML 操作的場(chǎng)景,得益于 goquery 和 jQuery 的 API 風(fēng)格,實(shí)現(xiàn)起來(lái)還是非常簡(jiǎn)單的。
聲明:本網(wǎng)頁(yè)內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問(wèn)題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com