zhihu-go ソース コード分析: goquery を使用して HTML_html/css_WEB-ITnose を解析する
Jun 21, 2016 am 08:52 AM
前回のブログでは 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 の応用について説明します。
Document オブジェクトの作成
goquery は、Document と Selection の 2 つの構造を公開します。Document は HTML ドキュメントを表し、Selection は jQuery のように動作するために使用され、チェーン呼び出しをサポートします。 goquery は、後続の操作を続行するために HTML ドキュメントを指定する必要があります。いくつかの構築メソッドがあります。
- NewDocumentFromNode(root *html.Node) *Document: *html.Node オブジェクトを渡します。ルートノード。
- NewDocument(url string) (*Document, error): URL を渡し、内部で http.Get を使用して Web ページを取得します。
- NewDocumentFromReader(r io.Reader) (*Document, error): io.Reader を渡し、内部でリーダーからコンテンツを読み取り、解析します。
- NewDocumentFromResponse(res *http.Response) (*Document, error): HTTP 応答を渡し、内部で res.Body を取得します (io.Reader を実装)。処理方法は NewDocumentFromReader
- HTML ページ (質問ページなど) をリクエストし、NewDocumentFromResponse を呼び出します。
- Ajax インターフェイスをリクエストします。返された JSON データにはいくつかの HTML フラグメントが含まれており、次のように使用します。 NewDocumentFromReader、r = strings.NewReader(html)
指定されたノードを検索します
Selection には jQuery に似た一連のメソッドがあります。 *Selection は Document 構造に埋め込まれているため、これらのメソッドを直接呼び出すこともできます。メインのメソッドは、Selection.Find(selector string) で、セレクターを渡して、一致する新しい *Selection を返すため、チェーン内で呼び出すことができます。
たとえば、ユーザーのホームページ (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)
多くの場合、タグのコンテンツと特定の属性値を取得する必要がありますが、これは goquery を使用して簡単に行うことができます。
回答数を取得する上記の例を続けると、Text() 文字列メソッドを使用して、すべてのサブタグを含むタグ内のテキスト コンテンツを取得できます。
text := doc.Find("div.profile-navbar").Find("span.num").Eq(1).Text() // "785"
属性値を取得するのも簡単です。次の 2 つのメソッドがあります。
- Attr(attrName string) (val string, contains bool): 属性値と、属性が存在します。同様に、マップから値を取得します
- AttrOr(attrName,defaultValue string) string: 前のメソッドと同様ですが、違いは、属性が存在しない場合、指定されたデフォルト値が返されることです
href, _ := doc.Find("div.profile-navbar").Find("a.item").Eq(1).Attr("href")
反復
多くのシナリオでは、質問のフォロワーのリスト、すべての回答、回答に「いいね!」をしたユーザーのリストなど、リスト データを返す必要があります。この場合、一般に、同様のノードをすべて走査し、特定の操作を実行するには反復が必要です。
goquery には反復のための 3 つのメソッドが用意されており、いずれもパラメータとして匿名関数を受け入れます。
- 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 风格,实现起来还是非常简单的。

人気の記事

人気の記事

ホットな記事タグ

メモ帳++7.3.1
使いやすく無料のコードエディター

SublimeText3 中国語版
中国語版、とても使いやすい

ゼンドスタジオ 13.0.1
強力な PHP 統合開発環境

ドリームウィーバー CS6
ビジュアル Web 開発ツール

SublimeText3 Mac版
神レベルのコード編集ソフト(SublimeText3)

ホットトピック











公式アカウントのキャッシュの更新の難しさ:バージョンの更新後のユーザーエクスペリエンスに影響を与える古いキャッシュを回避する方法は?

HTML5フォーム検証属性を使用してユーザー入力を検証するにはどうすればよいですか?

&lt; iframe&gt;の目的は何ですか タグ?使用する際のセキュリティ上の考慮事項は何ですか?

ビューポートメタタグとは何ですか?レスポンシブデザインにとってなぜそれが重要なのですか?
