Nokogiri の使い方 🪚

# 前提
- [Nokogiri](https://github.com/sparklemotion/nokogiri) の使い方について
    - Ruby で XML とか HTML をいい感じに扱えるライブラリ
    - バージョン 1.6.1
        - [参考本](https://www.amazon.co.jp/Ruby%E3%81%AB%E3%82%88%E3%82%8B%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%A9%E3%83%BC%E9%96%8B%E7%99%BA%E6%8A%80%E6%B3%95-%E5%B7%A1%E5%9B%9E%E3%83%BB%E8%A7%A3%E6%9E%90%E6%A9%9F%E8%83%BD%E3%81%AE%E5%AE%9F%E8%A3%85%E3%81%A821%E3%81%AE%E9%81%8B%E7%94%A8%E4%BE%8B-%E4%BD%90%E3%80%85%E6%9C%A8-%E6%8B%93%E9%83%8E-ebook/dp/B00TO6KMEK/ref=sr_1_1?crid=2W5UWSR3XFF6S&keywords=ruby%E3%81%AB%E3%82%88%E3%82%8B%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%A9%E3%83%BC%E9%96%8B%E7%99%BA%E6%8A%80%E6%B3%95+%E5%B7%A1%E5%9B%9E+%E8%A7%A3%E6%9E%90%E6%A9%9F%E8%83%BD%E3%81%AE%E5%AE%9F%E8%A3%85%E3%81%A821%E3%81%AE%E9%81%8B%E7%94%A8%E4%BE%8B&qid=1665625595&qu=eyJxc2MiOiIwLjAxIiwicXNhIjoiMC4wMCIsInFzcCI6IjAuMDAifQ%3D%3D&sprefix=ruby+%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%A9%2Caps%2C148&sr=8-1)と同じなら多分大丈夫



# サンプル

こんなHTMLドキュメントがあったとして、

```html
<html>
  <head>
    <title>Lightweight language</title>
  </head>
  <body id="test_id">
    <h1>Lightweight language</h1>
    <div>
      <ul>
        <li>
          <a href="link_ruby" title="ruby">Ruby</a>
        </li>
        <li>
          <a href="link_python" title="python">Python</a>
        </li>
        <li>
          <a href="link_php" title="php">PHP</a>
        </li>
        <li>
          <a href="link_perl" title="perl">Perl</a>
        </li>
      </ul>
    </div> 
  </body>
</html>
```

ここから「各言語とそのリンク」を抽出したい

```ruby
require 'open-uri'
require 'nokogiri'

html = Nokogiri::HTML.parse(URI.open('/tmp/test.html'))

# 親タグとして `body(id="test_id") 直下に div　直下に ul　直下に li` を持つ `aタグ` を取得
# ちなみに、`直下` 限定せずに `配下` にあれば良い場合は、`>` の代わりに ` ` を指定する
#   html.css('body[id="test_id"] a').each do |a|
html.css('body[id="test_id"] > div > ul > li > a').each do |a|
  p a.text.strip
  p a[:href]
end

=>

"Ruby"
"link_ruby"
"Python"
"link_python"
"PHP"
"link_php"
"Perl"
"link_perl"
```

他にも色々できて、例えば、対象ドキュメントのテキスト内容を全てくっ付けて返してくれる

```ruby
html = Nokogiri::HTML.parse(open('/tmp/test.html'))

p html.text

=>

"LightweightlanguageLightweightlanguageRubyPythonPHPPerl"
```

# 利用方法の大枠

ツリーを構築/検索して、ノードを参照する

- ツリーの構築
  - HTMLやXMLのドキュメントを解析して `Nokogiri::HTML::Document` に変換
- ツリーの検索
  - 検索系メソッドを利用して探索を行い、目当てのノードを特定する
- ノードの参照
  - 取得できた `Nokogiri::XML::NodeSet` または `Nokogiri::XML::Element` から目当てのデータを抽出する

# ツリーの構築
## 解析の種類
DOMの他にもSAXやReaderやPullがあるらしいけど、有名どころのDOMについて説明する

## DOM 解析
Nokogiri では HTML/XML ドキュメントのどちらも解析できる

```ruby
doc = Nokogiri::HTML.parse(html_document) # HTML ドキュメントの解析
doc = Nokogiri::XML.parse(xml_document) # XML ドキュメントの解析
```

* 第1引数には、IO オブジェクトまたは文字列オブジェクトを指定する
  * open_uri を直接 Nokogiri に渡すことも可能（文字列を使用するより若干効率は上がるらしい？）
* 第3引数には、対象ページの文字コードを指定する
  * 解析対象の文字コードが UTF8 以外の場合、大抵解析が失敗するので指定する
* 第2引数はURL、第4引数はオプション、どちらも大抵は指定せず事足りる

## オブジェクトの可視化

```ruby
html = Nokogiri::HTML(open('/tmp/test.html'))
```

メモリ上ではどのようなオブジェクトが作られているのか
簡略化するとこんなツリー状のオブジェクトが作られている

![noko1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2627625/73d0f8c9-a878-a1be-88d2-407d921c6aec.png)

## 各オブジェクトについて

* Nokogiri によって、複数のオブジェクトが作成されている
  * `Nokogiri::XML::Document` とか `Nokogiri::XML::Element` とか
* 全てのオブジェクトがノードである
  * `Nokogiri::XML::Node` を継承している
    * Nogogiri::XML::NodeSet < Enumerable
    * Nokogiri::HTML::Document < Nokogiri::XML::Document < Nokogiri::XML::Node
    * Nokogiri::XML::Element < Nokogiri::XML::Node
    * Nokogiri::XML::Text < Nokogiri::XML::CharacterData < Nokogiri::XML::Node
    * etc...
  * 検索系のメソッドは `Nokogiri::XML::Node` にまとまっているため、同じ検索メソッドがこれらオブジェクトに対して使える

他にも色々あるけど、作られているオブジェクトの説明を簡単に

* Nokogiri::XML::Node
  * ノードに対する操作や検索処理を規定する
  * 具体的には、 `Nokogiri::XML::Searchable` をincludeして各種メソッドを実装
    * SearchableはDOM検索のインターフェース
* Nokogiri::XML::NodeSet
  * Nokogiri::XML::Node オブジェクトのリストを持つ
  * Nokogiri::XML::Searchable＃css/＃xpathの実行結果
* Nokogiri::HTML::Document
  * Nokogiriによって解析されたHTMLドキュメント
    * Nokogiri::HTML.parseの戻り値
* Nokogiri::XML::DTD
  * 対象ドキュメントが DTD による文書構造に従っているかどうかを検証している？
* Nokogiri::XML::Element
  * Nokogiriで HTML 要素を扱うためのオブジェクト
    * C 拡張なのできっと早い
* Nokogiri::XML::Text
  * Nokogiri で HTML テキストを扱うためのオブジェクト

# ツリーの検索

## 検索方法

大まかに 3 種類

* XPath
  * XML形式の文書から特定の部分を指定して抽出するための簡潔な構文
* CSS
  * ウェブページのスタイルを指定するための言語
* その他
  * child とか parentとか相対的に位置を特定する

## よくある流れ

* `Nokogiri::HTML::Document` オブジェクトに対して、CSSセレクタやXPathで検索を行い、検索結果として `Nokogiri::XML::NodeSet` オブジェクトを取得
* NodeSet は Node のリストは配列のように扱えるため、each や [] で該当ノードである Element を特定する
  * CSS や Xpath は NodeSet を返すが、at や child など Element を返すものもあるので要確認

## 検索メソッド
よく使うであろう Nokogiri::XML::Node のメソッドについて
Nodeset と Element 両方に対して使える

### XPath
親タグとして `body(id="test_id"), div, ul` を持つ `li` 全てを検索

```ruby
html.xpath('//body[@id="test_id"]/div/ul/li').each do |li|
  p li.text
  p li.at('a')[:href]
end
```

* 引数について
  * 冒頭の `/` が階層構造のルートを表す
  * その他の `/` で各タグの階層情報を指定
  * `/` の間に何もない場合は、配下にあることを示す
  * `div` や `ul` はタグ名の指定
      * `*` は、任意のタグに一致するワイルドカードとして使える
  * 属性（例えばidの値とか）を指定するときは `タグ名[@属性名=xxx]` を使う
  * 引数は複数指定できる
* 戻り値について
  * NodeSet を返す
  * 結果が無ければ空の NodeSet
* その他
    * 現状 CSS では、実現できないことができる（2022/10/14）
        * [〇〇な子を持つ親を取ってくる](https://zenn.dev/koinori/articles/869abf304a07b2662494)
        * [内部的にcssセレクタをxpathに変換することで対応しているものもある](https://rooter.jp/web-crawling/nokogiri-css-selector-contains/)


### CSS
上記を CSS で置き換える

```ruby
html.css('body[id="test_id"] > div > ul > li').each do |li|
  p li.text
  p li.at('a')[:href]
end
```

* 引数について
  * `/` の代わりに `>` または空白を使う
  * `>` は親タグ直下のタグを指定したいときに使う
    * xpathで言う `//h3/a`
        * h3 直下に a が来る
  * 空白はタグ間に任意のタグを許容したいときに使う
    * xpathで言う `//h3//a`
        * h3 と a 間に任意のタグを許容する
  * 属性（例えばidの値とか）を指定するときは `タグ名[属性名=xxx]` を使う
  * class 属性の包含を指定するときは タグ名.class名 を使う
    * `タグ名[属性名=xxx]` は完全一致する必要がある
  * テキストから検索したいときは `タグ名:contains("テキスト")` を使う
    * `html.at('a:contains("Ruby")')`
  * 引数は複数指定できる
* 戻り値について
  * NodeSet を返す
  * 結果が無ければ空の NodeSet

### 補足

* 便利メソッド
  * `search("検索")`
    * 引数に XPath または CSS を指定できる
    * 戻り値として、NodeSetを返す、無ければ空の NodeSet
  * `at("検索")`
    * 引数に XPath または CSS を指定できる
    * 戻り値として、最初のノードの Element を返す、無ければ nil
* ChromeやFirefox（Firebug）で指定タグのパスが抽出できる
  * Chrome の場合
      * バージョン: 106.0.5249.103（Official Build） （x86_64）
      * 開発者コンソールを開いて、Elements タブで該当要素を選択する
      * 右クリックして、`Copy > Copy Selector / Copy XPath` でコピペ

## その他

ノードを指定して任意の相対ノードにアクセスできる

* child
  * 最初の子ノードが Element で返る
* children
  * 子ノード（Element）の配列が返る
* previous_sibling、previous
  * 兄ノードが Element で返る
    * 手前にあるノード
* next_sibling、next
  * 弟ノードが Element で返る
    * 後にあるノード
* parent
  * 親ノードが Element で返る
* ancestors
  * 祖先ノード（Element）の配列が返る

# ノード情報の参照

ノードに関する情報を取得するためのメソッド
基本的に Nodeset と Element 両方に対して使えるが一部例外あり

## ドキュメントの参照

Nokogiri::XML::Node と NodeSet で基本的には同じ API が使えるが、一部のメソッド（content、to_str）がない
NodeSet の場合、リスト内の全てのノードに対してメソッドを適用した結果を返してくれる

* to_s
  * ノード全体のテキストをつなぎ合わせた文字列が返る
* content、text、inner_text、to_str
  * 子孫ノードのテキスト内容をつなぎ合わせた文字列が返る
  * エイリアス多
* to_html、 to_s
  * ノード全体HTMLをつなぎ合わせた文字列が返る
    * 本人ノードも含めた inner_html
* to_xhtml、 to_xml
  * ノード全体を XHTML をつなぎ合わせた文字列が返る
* inner_html
  * 子孫ノードの HTML をつなぎ合わせた文字列が返る

## 属性情報の参照

通常の Ruby ハッシュのように扱える
Element に対して使う

* ["属性名"]、get_attribute("属性名")
  * 属性値を文字列で返す、無ければ nil
* key?("属性名")、has_attribute?("属性名")
  * 属性の有無をtrue/falseで返す
* keys
  * 属性名を文字列の配列で返す
* values
  * 属性値を文字列の配列で返す
* attributes
  * 属性名と属性オブジェクトのハッシュを返す
* attribute("属性名")
  * 属性オブジェクトを返す
* each { |k,v| }
  * 属性名と属性値を返しブロック呼び出し

# まとめ
* ツリーを探索しているイメージを持つとわかりやすい
    * 今自分が操作しているのが NodeSet なのか Element なのか知っていた方が混乱なさそう
        * CSS や Xpath は NodeSet を返すが、at や child など Element を返す
* 大抵のケースでは最初のサンプルで事足りそう
    * 取得部分の細かい指定が必要な場合には children 等を使う
        * CSS で検索して NodeSet を取ってきて、それを each で回して各要素の情報を取得する
    * テキストから検索するのは結構使いそう
        * CSSで書くと `html.at('a:contains("Ruby")')` など