Photo by Ilya Pavlov

 ちょいとGolangでhtmlオブジェクトのパースをすることがあり、それに関して忘備録がてら書いていきます。とはいえ、godocをスラスラ読める方には特に必要ないかもしれませんが…。

広告

はじめに

 swimmyというGo言語のパッケージを作成しています。URLからHTMLコンテンツを取得し、OGP等を取得していろいろやるためのパッケージで、どうせならHTMLパースもするかと思い、処理方法を探していました。
 結果として、GoQueryというパッケージは見つかったのですが、なんというか、求める機能には十分すぎるというか、もう少し単純な処理でいいというか。ちょっとname属性とかタグの中身を軽く見たいだけだったのです。あとgo getしなくていい単純な方法があるならそっちしたい。
 というわけで探してみると、ちょうど取り入れてあったgolang.org/x/パッケージの中のx/net/htmlを使えばちょうどよさそうなことが判明。
 さてやるか、と思ってみると意外に日本語の解説などが少なかった上に、godoc.orgでドキュメントを見ても使うのにちょっと時間がかかったので、メモがてら色々書いておきます。

GolangでHTMLを扱えるパッケージ

 承前で述べた通り、代表的なものはどうやらGoQueryです。所謂スクレイピングに便利らしい感じ。いらない要素を省くためにHTMLサニタイザーであるbluemondayを通して、いる要素をGoQueryで抜き出すという様子の模様。
 ただ、GoQueryもbluemondayも、結局のところx/net/htmlを利用しています。サニタイジング処理はややこしそうなのでswimmyではbluemondayも使っていますが、jQuery的な処理を要求しないような、低レイヤな処理だとx/net/htmlでよさそうです。

net/htmlパッケージを導入する

go get -u golang.org/x/net

 あたりでできるはずです。
 というか、x系パッケージは基本的にいろいろ便利というか、準標準っぽいので、go wikiなどで探してgo get しておくのがおすすめ。

net/htmlパッケージ

 net/htmlパッケージを使ったHTMLの処理/操作方法には3通りあります。

  • 低レイヤなTokenizerを使う(Tokenizerはドキュメントにはlow-level APIと位置付けられています)
  • Tokenizerより少し高度なTokenを使う(Tokenはドキュメントではhigh-level APIと位置付けられています)
  • html.Parse関数を使って一気に解析木(の一番上のNode構造体)を得る(上記2つは順番に解析して行っていくイメージですが、この場合は最初に一気に解析するようです)

 無駄のない処理をしようとすれば一番上のTokenizerで処理かと思われますが、その分低レイヤなので、そこまで便利ではありません。といいつつswimmyでは使ってしまいましたが…。
 一括でパースしておく必要がない場合は、Tokenを使うと便利だと思います。

 つまりどっちかというと、
+ Tokenizerを使って順番に処理
+ Tokenizer
+ Token
+ 一括してパースしたのち処理
+ html.Parseを使う

 という感じです。

Tokenizer・Tokenを使ったHTMLの処理

 Tokenを使うにしろTokenizerを使うにしろ、以下のようにしてTokenizer(処理を進めるに従い、HTMLをパースしてタグや間に挟まれるテキストといった小さな単位に分割していくもの)を生成する必要があります。

t := html.NewTokenizer(reader)

 readerはio.Readerインタフェースを実装したものを与えます。Tokenizerはバッファリングもしているようですし、前から順番に解析という都合上、ファイルから読みだす場合は、os.FileOpen等で読み出したFile構造体そのままのでいいのではないかしら。

 次に、解析を進めるにあたって、基本的な流れですが、TokenizerもTokenも実はほぼ一緒で、処理を終えたらTokenizerに設定されているNext()関数で次のトークンに移ります。ただ、Tokenを生成するとちょっと便利になる(その代わりTokenという構造体を生成する分、メモリコピーその他が発生する)という感じ。
 Next()関数はTokenizerが解析するトークンを一つ次へ移すと同時に、そのトークンの種類がどんなものかをTokenType型で返します。
 TokenTypeといっても中身はuint32型ですので、switchやif構文などで定数として設定されている値と比較してトークンの種類を判別することができます。
 これを使って、トークンの種別に似合った処理をしていくのです。
 ファイルの読出しから、おおざっぱに書くと以下のような感じ。

f,_ := os.Open(“test.html”)
t := html.NewTokenizer(f)
loop := true
for loop {
tt := t.Next()
loop = tt != html.ErrorToken
if loop {
switch tt {
case html.TextToken:
//テキストトークンに対する処理
}
}
}

 上記のコードでは、最後までTokenizerが処理し終わるとNext()からはhtml.ErrorTokenが返るので、ttがhtml.ErrorTokenになった場合はループを打ち切っています。

 ちなみに、トークンの種類は以下のような感じ。

  • ErrorToken: エラートークン。だいたい最後の合図か、パース時にエラーが起こった時にNext()から返る。
  • TextToken: テキストトークン。タグではなく、その間に挟まれたりあるいは挟まれていないテキスト部分。
  • StartTagToken: 開始タグ。例: <p>
  • EndTagToken: 終了タグ。例: </p>
  • SelfClosingTagToken: 自己閉じタグ…と訳せばいいのかは不明だが、開始タグがそのまま「/」を使って閉じているタグ。例: <meta />
  • CommentToken: コメントトークン。名前の通り、htmlにおけるコメント部分。

 さて肝心の処理部分ですが、ここにTokenizerとTokenどちらを使うかの違いが発生してきます。

 まずTokenizerから情報を読み出す場合は、様々な関数を用いなければなりません。ついでにタグ名や属性名、テキストトークンの場合はテキストがbyte配列で返ってきます。さらに読み出しはじめではタグに設定されている属性がいくつあるかもわかりません(関数を使って連続処理をしていけば最終的には判明します)。
 例えば<h1 class=”test”>test</h1>という感じのタグをパースする場合、まずNext()で開始タグの種類が解析されるので、その種類を判定します。この場合はStartTagTokenでしょうか。
 そして判定後、タグ名(h1)を得たい場合はTagName()関数を使います。すると、タグ名が第一の戻り値、属性を持っているかどうかが第二の戻り値として帰ってきます。属性を持っているときその属性を解析したいなら、さらにTagAttr()関数で属性を順次取得していきます。
 というわけで一連の流れをコードにしてみると、以下のようになります。

str := “<h1 class=”test”>test</h1>”
sr := strings.Reader(str)

t := html.NewTokenizer(sr)
loop := true

for loop{
tt := t.Next()
loop = tt != html.ErrorToken
if tt == html.StartTagToken{
tn, hasAttr := tt.TagName() //ここでtnにh1、hasAttrはtrue
if hasAttr {
key,val, moreAttr := t.TagAttr() //属性名とその値、さらに(他に)属性があるかのフラグ。

        //属性に対する処理…

        hasAttr = moreAttr
    }   
}

}


 ちなみにテキストトークン(上記の例ではh1タグに挟まれたtestという文字列の部分)は、タグ名などがないのですが、その場合はText()関数を使うことでその内容を得ることができます。
 また、Next()の呼び出してTagNameで返ってきたSliceの中身は変わる可能性が非常に大きいので、後々記録しておきたい値などはstringにしたり、コピーして取っておくことになります。

 で、Tokenizerだけだとこんな感じにややこしいのですが、Token構造体をTokenizerからToken()関数で得ておくと、(流石にErrorTokenは処理してくれないと思うので、TokenTypeでの判別は行ったほうがいいですが)以下のコードのように、属性やタグ名をstringに変更し、属性もメンバとして数が分かった状態で扱えるようになります。また、Tokenを生成した後Nextをかましても、先に生成されたToken構造体の中身は変更されません。

str := “<h1 class=”test”>test</h1>”
sr := strings.Reader(str)

tr := html.NewTokenizer(sr)
loop := true

for loop{
tt := tr.Next()
loop = tt != html.ErrorToken
if loop{
t := tr.Token() //tにToken構造体が入る
name := t.Data //tのメンバであるDataに、タグトークンの場合はタグ名、テキストトークンの場合はテキストの内容が入っている
attrs := t.Attr //tのメンバであるAttrに、Attribute型のスライスの形で属性の情報が入っている。

    //Token等を用いた処理…
}

}


 ただし、Token()関数内ではTokenizerを使った例で回したようなhasAttr, moreAttrを使った処理が回る上にAttribute構造体のスライスを作ったうえでbyte型配列で返ってきていた値はstringとし、Token構造体を生成するので、おそらくメモリのロケーション・アロケーションは少しだけ大きくなりますし、属性を全部解析する気がなく途中で打ち切りたいといった処理だとはTokenizerを使うことになります。

 ちなみに、Tokenのメンバは以下のようになります。これらにアクセスすることで、Tokenizerによる(どちらかといえば)低レベルな処理を記述することなく、タグ名や属性を得ることができます。ちなみにタグ等のHTML文書内で使用される形で取得したい場合はString()を使えばいい模様。

  • Type TokenType: 前述したものと同じトークン型。
  • DataAtom atom.Atom: タグ名をAtom(uint32)型で表したもの。よく使用されるタグ名に当てはめられており、タグ名を高速にマッチングさせることができる。
  • Data string: トークンの中身。タグの場合はタグ名、テキストやコメントの場合はその中身。
  • Attr []Attribute: 属性の中身。Attribute構造体はNamespace, Key, Val三つのメンバ(全てstring型)を持つ。

 こんな感じで順次処理をすることで、HTMLを処理していくことが可能です。

html.Parse(r io.Reader)を使った処理

 html.Parseを使う場合は、

root, err := html.Parse(r)
if err != nil{
log.Fatal(err)
}

 といった形でhtml.Node構造体(上記コードではrootに代入される)を取得することで行います。正直私が求めていたものはここまでする必要はないので少し短めになりますが、とりあえず書いておきます。

 godoc.orgにあるドキュメントによると、Node構造体は以下のような形で、様々なメンバを持ちます。

type Node struct{
Parent, FirstChild, LastChild, PrevSibling, NextSibling *Node
Type NodeType
DataAtom atom.Atom
Data string
Namespace string
Attr []Attribute
}

 Node、Child、Parentなどというメンバ名にあるように、これらは構文解析器のノード(葉)として働きます。html.Parseで返ってきた*html.Nodeは、つまり解析木の根(root)といえます。
 このrootのChildを辿ることで、解析木を辿り、処理を行うことが可能です。
 なお、Node構造体からはFirstChildとLastChildにしか直接たどり着けませんが、Siblingには男女の区別をつけない「きょうだい」という意味があります。ですので、これを使ってFirstChildのNextSlibingを参照することでSecondChildを辿ることができます。木構造とその探索について知っていれば、それなりに簡単に処理の流れ自体は書くことができると思います。幅優先探索か深さ優先探索かは処理の目的・内容によると思いますが、どちらでもコーディングすることができるでしょう。
 ちなみに、godoc.orgのドキュメント等にあるExampleのコード(深さ優先探索)では、再帰処理の必要性からか、main関数内で関数を変数に代入していますが、Nodeは上に述べたような仕組みですから、普通にfuncを使ってこれを処理する関数も定義可能です。

その他コメントなど

 以上、x/net/htmlパッケージでのパース方法でした。更に詳しい方法は、godoc.orgでドキュメントを参照する形になるかと思います。
 godoc.orgでドキュメントを参照してコードを書いて、やっとここまで整理できた…。
 英語は読めないのではないのですが、やっぱりちょっと苦手です…。

広告

関連コンテンツと広告

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA