前回のブログでは zhihu-go プロジェクトの起源を簡単に紹介しましたが、この記事では HTML の処理の詳細を簡単に紹介します。
Zhihu は API を開発していないため、ブラウザーの操作をシミュレートすることによってのみデータを取得できます。データには、通常の HTML ドキュメントと、一部の Ajax インターフェイスによって返される JSON の 2 つの形式があります (返されるデータは実際には HTML です)。 )。実際には、これは Web ページを巡回してデータを抽出するクローラーです。一般に、HTML ドキュメントからデータを抽出するには、正規表現、XPath、CSS セレクターなどの方法があります。私にとって、正規表現は書くのがより複雑で、コードは読みにくく、保守も面倒です。XPath については詳しく知りませんが、使用するのは難しくないはずです。Chrome ブラウザは XPath を直接抽出できます。 selector は zhihu-go で使用されます。このメソッドは goquery を使用します。
goquery は「Go でのみ使用される j のものに少し似ています」、つまり、jQuery を使用して DOM を操作することを意味します。 API も非常にシンプルかつ明確です。この記事では goquery について詳しくは紹介しません。いくつかのシナリオ (API) を選択して、zhihu-go での goquery の応用について説明します。
goquery は、Document と Selection の 2 つの構造を公開します。Document は HTML ドキュメントを表し、Selection は jQuery のように動作するために使用され、チェーン呼び出しをサポートします。 goquery は、後続の操作を続行するために HTML ドキュメントを指定する必要があります。いくつかの構築メソッドがあります。
指定されたノードを検索します
たとえば、ユーザーのホームページ (Huang Jixin など) では、まず Chrome を使用して、対応する HTML を見つけます。
<span class="bio" title="和知乎在一起">和知乎在一起</span>
doc.Find("span.bio")
ユーザーのホームページでは、ユーザー情報欄の下に、質問、回答、記事、コレクション、公開編集の数が左から右に表示されます。 HTML ソース コードを確認したところ、これらの項目のクラスは同じであるため、添字インデックスによってのみ区別できることがわかりました。
最初に 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>
doc.Find("div.profile-navbar").Find("span.num").Eq(1)
回答数を取得する上記の例を続けると、Text() 文字列メソッドを使用して、すべてのサブタグを含むタグ内のテキスト コンテンツを取得できます。
text := doc.Find("div.profile-navbar").Find("span.num").Eq(1).Text() // "785"
属性値を取得するのも簡単です。次の 2 つのメソッドがあります。
href, _ := doc.Find("div.profile-navbar").Find("a.item").Eq(1).Attr("href")
反復
goquery には反復のための 3 つのメソッドが用意されており、いずれもパラメータとして匿名関数を受け入れます。
比如获取一个收藏夹(如 黄继新的收藏:关于知乎的思考)下所有的问题,可以这么做(见 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,具体的细节可以看 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 风格,实现起来还是非常简单的。