ホームページ  >  記事  >  ウェブフロントエンド  >  zhihu-go ソース コード分析: goquery を使用して HTML_html/css_WEB-ITnose を解析する

zhihu-go ソース コード分析: goquery を使用して HTML_html/css_WEB-ITnose を解析する

WBOY
WBOYオリジナル
2016-06-21 08:52:341812ブラウズ

前回のブログでは 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
Zhihu ページにアクセスするにはログインが必要であり (リクエスト ヘッダーも偽造する必要がある)、*html.Node を取得するために HTML を手動で解析したくないため、最終的に他の 2 つの構築方法を使用しました。 。一般的な使用シナリオは次のとおりです。

    HTML ページ (質問ページなど) をリクエストし、NewDocumentFromResponse を呼び出します。
  • Ajax インターフェイスをリクエストします。返された JSON データにはいくつかの HTML フラグメントが含まれており、次のように使用します。 NewDocumentFromReader、r = strings.NewReader(html)
説明の便宜上、この定義は以下で使用されます: var doc *goquery.Document.

指定されたノードを検索します

Selection には jQuery に似た一連のメソッドがあります。 *Selection は Document 構造に埋め込まれているため、これらのメソッドを直接呼び出すこともできます。メインのメソッドは、Selection.Find(selector string) で、セレクターを渡して、一致する新しい *Selection を返すため、チェーン内で呼び出すことができます。

たとえば、ユーザーのホームページ (Huang Jixin など) では、まず Chrome を使用して、対応する HTML を見つけます。

<span class="bio" title="和知乎在一起">和知乎在一起</span>
対応する go コード。

doc.Find("span.bio")
セレクターが複数の結果に対応する場合、First()、Last()、Eq(index int)、Slice(start, end int) などのメソッドを使用してさらに位置を指定できます。

ユーザーのホームページでは、ユーザー情報欄の下に、質問、回答、記事、コレクション、公開編集の数が左から右に表示されます。 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>
回答の数を見つけたい場合、対応する go コードは次のとおりです。

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"
Text() メソッドで返される文字列には前後に空白文字が多く含まれる場合がありますが、状況に応じて削除できます。

属性値を取得するのも簡単です。次の 2 つのメソッドがあります。

    Attr(attrName string) (val string, contains bool): 属性値と、属性が存在します。同様に、マップから値を取得します
  • AttrOr(attrName,defaultValue string) string: 前のメソッドと同様ですが、違いは、属性が存在しない場合、指定されたデフォルト値が返されることです
共通 使用シナリオは、a タグ付きのリンクを取得することです。上記の回答取得の例を続けて、ユーザー回答のホームページを取得したい場合は、次のようにすることができます:

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 风格,实现起来还是非常简单的。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。