Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyでパーサーを自作する方法!StringScannerを使った実装手順を徹底解説

パース(構文解析)とは、文字列の集まりから意味を読み取り、プログラムが扱える形のデータへと変換する技術です。正規表現でも文字列の解析は可能ですが、すべての場面に適しているわけではありません。

たとえば、正規表現でHTMLを解析するのはあまり良い方法ではないというのは、プログラミング界隈ではよく知られた話です。

Rubyにはnokogiriという強力なライブラリがあり、HTMLの解析はこれに任せられます。しかし、自分でパーサーを一から作ってみると、文字列処理や構文解析の仕組みについて多くのことを学べます。それでは早速始めていきましょう!

Rubyでのパースの基本:StringScannerクラス

今回作成するパーサーの中心となるのが、StringScannerクラスです。

このクラスは対象となる文字列のコピーと位置ポインタを保持しており、ポインタを移動させながら特定のトークン(記号やキーワード)を探していくことができます。

今回使用する主なメソッドは以下の3つです。

  • .peek — ポインタを進めずに先頭の文字を覗き見る
  • .scan_until — 指定したパターンが見つかるまでスキャンする
  • .getch — 1文字読み取って返す

また、.scan(untilなし)も便利なメソッドなので覚えておくと良いでしょう。

注意:

もしStringScannerが使えない場合は、require 'strscan' を追加してみてください。

このクラスの挙動を理解しやすいよう、ドキュメント代わりにテストを2つ書いてみました。

describe StringScanner do
  let (:buff) { StringScanner.new "testing" }

  it "can peek one step ahead" do
    expect(buff.peek 1).to eq "t"
  end

  it "can read one char and return it" do
    expect(buff.getch).to eq "t"
    expect(buff.getch).to eq "e"
  end
end

ここで重要なポイントが1つあります。getchやscanなど一部のメソッドは位置ポインタを進めますが、peekは進めません。任意の時点で .inspectp を使ってスキャナの状態を出力すれば、現在どの位置を指しているかを確認できます。

Parserクラスの設計

実際の処理の大部分を担うのがParserクラスです。解析したいテキストを受け取って初期化すると、内部でStringScannerを生成し、parseメソッドを呼び出す構造になっています。

def initialize(str)
  @buffer = StringScanner.new(str)
  @tags   = []
  parse
end

テストコードでは次のように定義します。

let(:parser) { Parser.new "<body>testing</body> <title>parsing with ruby</title>" }

このクラスがどのように動作するのかは後ほど詳しく見ていくとして、まずはプログラムのもう1つの構成要素を確認しましょう。

Tagクラス:解析結果を格納するシンプルなコンテナ

このクラスは非常にシンプルで、主にパース結果を保持するためのデータ格納用クラスとして機能します。

class Tag
  attr_reader :name
  attr_accessor :content

  def initialize(name)
    @name = name
  end
end

パースを実行してみよう

何かをパースするには、入力テキストの中からパターンを見つけ出す必要があります。たとえば、HTMLのコードは次のような形式になっていますね。

<tag>contents</tag>

この中には明確に2つの要素が含まれています。それはタグ名タグ内のテキスト(コンテンツ)です。BNF記法を使って正式な文法として定義すると、次のようになります。

tag = <opening_tag> <contents> <closing_tag>
opening_tag = "<" <tag_name> ">"
closing_tag = "</" <tag_name> ">"

実装では、StringScannerのpeekメソッドを使って、入力バッファの次の記号が開始タグかどうかを判定します。開始タグであれば、Parserクラスのfind_tagメソッドとfind_contentメソッドを呼び出します。

def parse_element
  if @buffer.peek(1) == '<'
    @tags << find_tag
    last_tag.content = find_content
  end
end

find_tagメソッドの処理内容は以下の通りです。

  • 開始タグの「<」という文字を消費(読み飛ばし)する
  • 閉じ記号「>」が見つかるまでスキャンする
  • タグ名を持つ新しいTagオブジェクトを生成して返す

コードは次のようになります。chopで最後の1文字を削除している点に注目してください。これはscan_untilの戻り値に「>」が含まれてしまうためで、それを取り除くために必要な処理です。

def find_tag
  @buffer.getch
  tag = @buffer.scan_until />/
  Tag.new(tag.chop)
end

次のステップはタグ内のコンテンツを取得することです。直前のscan_untilが位置ポインタを適切な場所まで進めてくれているので、難しくありません。再びscan_untilを使って終了タグを検索し、その手前までの文字列をコンテンツとして返します。

def find_content
  tag = last_tag.name
  content = @buffer.scan_until /<\/#{tag}>/
  content.sub("</#{tag}>", "")
end

あとは、入力バッファにタグがなくなるまでparse_elementをループで呼び出し続けるだけです。

def parse
  until @buffer.eos?
    skip_spaces
    parse_element
  end
end

完全なコードはGitHub(https://github.com/matugm/simple-parser)で公開しています。また、「nested_tags」ブランチには、タグの中に別のタグが入れ子になった構造にも対応できる拡張版があるので、ぜひ参考にしてみてください。

まとめ

パーサーを自作するのはとても興味深いテーマであり、同時にかなり複雑になることもあります。

ゼロからパーサーを書きたくない場合は、いわゆる「パーサージェネレーター」と呼ばれるツールを活用するのも良い選択肢です。Rubyの世界ではtreetopparsletといったライブラリが利用できます。


  1. Rubyでファイルを読み書きする方法を実例つきで徹底解説

    Rubyでファイルの読み書きができるようになると、ファイルからデータを抽出したり、新しいファイルを作成したり、必要な情報を素早く見つけたりできるようになります。この記事では、Rubyにおけるファイル操作の基本を、実際のコード例とともにわかりやすく解説します。 この記事で学べること Rubyでファイルを読み込む方法 Rubyでファイルに書き込む方法 覚えておきたいFileクラスのメソッド ディレクトリ操作の基礎 FileUtilsモジュールの使い方 まとめ それでは始めましょう! Rubyでファイルを読み込む方法 Rubyでファイルを読み込む基本的な流れは、次の3ステップです。 op

  2. SwiftUIでデザインシステムを構築する方法:実践ガイドとベストプラクティス

    1つのプロダクトを支えるデザインシステムを構築することは決して簡単ではありません。スケーラビリティを確保しながら、堅牢性と柔軟性の両方を兼ね備える必要があるからです。難しい課題ではありますが、これまで多くの優れたリソースが、ビジュアル面とプログラム面の両方で良いシステムを構築するための有用な原則やアプローチを共有してきました。それらの知見の上に立ち、本記事ではまだあまり語られていない領域であるSwiftUIでのデザインシステム構築に焦点を当てて解説します。なぜこの記事を書いたのかニューヨークのITP(Interactive Telecommunications Program)での最初の夏、私