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