JSONパヌサヌの䜜成氎玉ずパヌルボタン

この蚘事は、ワシントン州シアトルのRuby開発者であるAaron Pattersonによっお曞かれたした。 圌は今から7幎間Ruby開発に情熱を傟けおおり、この玠晎らしい蚀語に察する愛情を喜んで共有したす。

みんなに敬瀌 気分が良いこずを願っおいたす。 今日、倪陜は雲の埌ろから少しの間芗いおいたので、私は確かにすべおを完璧に持っおいたす

この蚘事では、Rubyず組み合わせお䜿甚​​するための倚数のコンパむルツヌルに぀いお説明したす。 そしお、䞻題に飛び蟌むために、JSONパヌサヌを䜜成したす。 すでに私は次のような䞍機嫌な感嘆の声を聞いおいたす。「たあ、アヌロン、なぜ​​ すでに1,234,567の䜜品が曞かれおいたせんか」それだけです すでに1,234,567個のRuby JSONパヌサヌがありたす たた、JSON分析も行いたす。その文法は、䞀床にゞョブを完了するのに十分なほど単玔であり、Ruby甚に開発されたコンパむルツヌルを賢く䜿甚するのに十分に耇雑だからです。

読み続ける前に、これは決しおJSONの解析方法に関する蚘事ではなく、Rubyでの分析およびコンパむルツヌルの䜿甚方法に関する蚘事であるずいう事実に泚目したいず思いたす。

䜕が必芁ですか


Ruby 1.9.3でテストしたすが、遞択した実装で動䜜するはずです。 䞻にRaccやStringScannerなどのツヌルを䜿甚したす。

ラック

アナラむザヌを自動的に生成するには、Raccが必芁です。 これは、YACCに䌌た倚くの点でLALRアナラむザヌゞェネレヌタヌです。 最埌の略語は「Yet Another Compiler Compiler」別のコンパむラコンパむラを衚したすが、これはRubyのバヌゞョンであるため、Raccが刀明したした。 Raccは、䞀連の文法芏則拡匵子が.yファむルを、状態マシンの遷移芏則を蚘述するRubyファむルに倉換するこずを玄束しおいたす。 埌者は、Raccステヌトマシンランタむムによっお解釈されたす。 ランタむムにはRubyが付属しおいたすが、拡匵子が「.y」のファむルをマシンの状態テヌブルに倉換するツヌルはありたせん。 gem install racc実行しおgem install racc 。

以降、「。y」ファむルを䜜成したすが、゚ンドナヌザヌはそれらを実行できたせん。 これを行うには、たずRuby実行可胜コヌドに倉換しおから、このコヌドをgemにパックする必芁がありたす。 実際、これはgem Raccのみをむンストヌルするこずを意味し、゚ンドナヌザヌには必芁ありたせん。

これらすべおが頭に収たらない堎合でも心配しないでください。 理論から実践に移行し、コヌドの蚘述を開始するず、すべおが明らかになりたす。

StringScanner

StringScannerは、スキャナヌの原理に埓っお、文字列を順番に凊理できるクラスです名前が瀺すずおり。 行のどこにいるかに関する情報を保存し、正芏衚珟ず文字の盎接読み取りを䜿甚しお最初から最埌たで移動できるようにしたす。

さあ始めたしょう たず、 StringScannerオブゞェクトを䜜成し、 StringScanner䜿甚しおいく぀かの文字を凊理したす。
 irb(main):001:0> require 'strscan' => true irb(main):002:0> ss = StringScanner.new 'aabbbbb' => #<StringScanner 0/7 @ "aabbb..."> irb(main):003:0> ss.scan /a/ => "a" irb(main):004:0> ss.scan /a/ => "a" irb(main):005:0> ss.scan /a/ => nil irb(main):006:0> ss => #<StringScanner 2/7 "aa" @ "bbbbb"> irb(main):007:0> 

この䜍眮の正芏衚珟が適合しなくなったため、 StringScannerscanの 3回目の呌び出しでnil返されたこずに泚意しおください。 たた、 StringScannerむンスタンスに察しおinspectが呌び出されるず、文字列内のハンドラヌの珟圚䜍眮この堎合は2/7 を確認できたす。

StringScannergetchを䜿甚しお、ハンドラヌを文字ごずに移動するこずもできたす。
 irb(main):006:0> ss => #<StringScanner 2/7 "aa" @ "bbbbb"> irb(main):007:0> ss.getch => "b" irb(main):008:0> ss => #<StringScanner 3/7 "aab" @ "bbbb"> irb(main):009:0> 

getchメ゜ッドは次の文字を返し、ポむンタヌを1぀進めたす。

順次文字列凊理の基本を理解したので、Raccの䜿甚方法を芋おみたしょう。

Raccの基本


先ほど蚀ったように、RaccはLALRアナラむザヌゞェネレヌタヌです。 これは、制限された正芏衚珟セットを䜜成できるメカニズムであり、比范の実行過皋でさたざたな䜍眮で任意のコヌドを実行できるメカニズムであるず想定できたす。

䟋を芋おみたしょう。 次の圢匏の正芏衚珟の眮換を確認したいずしたす (a|c)*abb 。 ぀たり、任意の数の文字「a」たたは「c」の埌に「abb」が続く堎合を登録したす。 これをRacc文法に倉換するために、この正芏衚珟を構成郚分に分割しおから、再床組み立おようずしたす。 個々の文法芁玠は、生成芏則たたは生成物ず呌ばれたす。 したがっお、この衚珟を分解しお、補品の倖芳ずRaccの文法の圢匏を芋おみたしょう。

たず、文法ファむルを䜜成したす。 ファむルの先頭には、取埗したいRubyクラスの宣蚀があり、その埌に補品を宣蚀するこずを意味するruleキヌワヌドが続き、その埌にendを瀺すendキヌワヌドが続きたす。
 class Parser rule end 

「a | c」の補品を远加したす。 圌女をa_or_cず呌びたしょう
 class Parser rule a_or_c : 'a' | 'c' ; end 

結果ずしお、文字「a」たたは「c」ずのマッチングを実行するルヌルa_or_cがありたす。 比范を1回以䞊実行するために、 a_or_cs呌ばれる再垰的な補品を䜜成したす。
 class Parser rule a_or_cs : a_or_cs a_or_c | a_or_c ; a_or_c : 'a' | 'c' ; end 

前述したように、 a_or_csは再垰的であり、正芏衚珟(a|c)+ず同等です。 次に、「abb」の補品を远加したす。
 class Parser rule a_or_cs : a_or_cs a_or_c | a_or_c ; a_or_c : 'a' | 'c' ; abb : 'a' 'b' 'b'; end 

そしお、すべおの匊補䜜を完了したす。
 class Parser rule string : a_or_cs abb | abb ; a_or_cs : a_or_cs a_or_c | a_or_c ; a_or_c : 'a' | 'c' ; abb : 'a' 'b' 'b'; end 

この最終出力は、1぀以䞊の文字「a」たたは「c」の埌に「abb」たたは独立した文字列「abb」が存圚するパタヌンず䞀臎したす。 これはすべお、 (a|c)*abbずいう圢匏の元の正芏衚珟ず同等です。

アヌロン、でも぀たらない

これは、正芏衚珟よりもはるかに長いこずを知っおいたす。 しかし、1぀プラスがありたす。マッピングプロセスの任意の堎所で任意のRubyコヌドを远加しお実行できたす。 たずえば、独立した文字列「abb」に出䌚うたびに、次のようなものを印刷できたす。
 class Parser rule string : a_or_cs abb | abb { puts " abb, !" } ; a_or_cs : a_or_cs a_or_c | a_or_c ; a_or_c : 'a' | 'c' ; abb : 'a' 'b' 'b'; end 

実行するコヌドは䞭括匧で囲み、その実行を担圓するルヌルの盎埌に配眮する必芁がありたす。 これで、独自のJSONアナラむザヌを䜜成する準備が敎いたした。この堎合、このアナラむザヌは、取埗した知識を備えたむベントベヌスのむベントです。

アナラむザヌを䜜成する


アナラむザヌは、パヌサヌ、字句アナラむザヌ、ドキュメントプロセッサの3぀のコンポヌネントオブゞェクトで構成されたす。 Racc文法に基づいお構築されたパヌサヌは、入力ストリヌムからのデヌタに぀いお字句解析プログラムにアクセスしたす。 パヌサヌは、共通のデヌタストリヌムからJSON芁玠を分離するたびに、察応するむベントをドキュメントハンドラヌに送信したす。 ドキュメントハンドラヌは、JSONからデヌタを収集し、それをRubyのデヌタ構造に倉換したす。 JSON圢匏の゜ヌスデヌタを分析するプロセスでは、以䞋のグラフに瀺すように、呌び出しが行われたす。

しかし、ビゞネスに取り掛かりたしょう。 たず、字句解析に焊点を圓お、次にパヌサヌの文法を扱い、最埌にドキュメントハンドラヌを䜜成しおプロセスを完了したす。

字句解析噚


字句アナラむザはIO機胜に基づいお構築されおいたす。 それから゜ヌスデヌタを読み取りたす。 next_token呌び出されるたびnext_token字句解析next_token入力ストリヌムから1぀のトヌクンを読み取り、それを返したす。 JSON仕様から借甚した次のトヌクンのリストで動䜜したす 。


配列やオブゞェクトのような耇雑なタむプの堎合、パヌサヌが責任を負いたす。

next_tokenによっお返される倀はnext_token 。

パヌサヌは、字句解析next_token呌び出すずきに、結果ずしお2぀の芁玠の配列たたはnilを受け取るこずを想定しおいたす。 配列の最初の芁玠にはトヌクンの名前を含める必芁があり、2番目の芁玠には䜕でもかたいたせん通垞、これは単なるテキストの䞀臎です。 nil返すこずによりnil字句解析nilトヌクンがもうないこずを報告したす。

TokenizerレキシカルアナラむザヌTokenizer 

クラスコヌドを芋お、それが䜕をするのか芋おみたしょう
 module RJSON class Tokenizer STRING = /"(?:[^"\\]|\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4}))*"/ NUMBER = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/ TRUE = /true/ FALSE = /false/ NULL = /null/ def initialize io @ss = StringScanner.new io.read end def next_token return if @ss.eos? case when text = @ss.scan(STRING) then [:STRING, text] when text = @ss.scan(NUMBER) then [:NUMBER, text] when text = @ss.scan(TRUE) then [:TRUE, text] when text = @ss.scan(FALSE) then [:FALSE, text] when text = @ss.scan(NULL) then [:NULL, text] else x = @ss.getch [x, x] end end end end 

最初に、StringScanner文字列ハンドラヌず組み合わせお䜿甚​​するいく぀かの正芏衚珟の宣蚀がありたす。 これらはjson.orgから取埗した定矩に基づいお構築されたす。 StringScannerむンスタンスがコンストラクタヌで䜜成されたす。 䜜成時に文字列が必芁なため、IOオブゞェクトの読み取りを呌び出したす。 ただし、これは、字句解析プログラムがオブゞェクトのIOからデヌタを読み取らない代替実装を陀倖するものではありたせんが、必芁に応じお行いたす。

䞻な䜜業はnext_tokenメ゜ッドで行われたす。 文字列ハンドラヌにデヌタがない堎合はnil返したす。そうでない堎合は、正しい正芏衚珟が芋぀かるたで各正芏衚珟をチェックしたす。 䞀臎が芋぀かった堎合、パタヌンに䞀臎するテキストずずもにトヌクン名䟋:STRING を返したす。 どの正芏衚珟も䞀臎しない堎合、ハンドラヌから1文字が読み取られ、読み取られた倀がトヌクンの名前ずその倀ずしお同時に返されたす。

字句解析噚にJSON圢匏の文字列の䟋を瀺し、出力で取埗するトヌクンを確認したしょう。
 irb(main):003:0> tok = RJSON::Tokenizer.new StringIO.new '{"foo":null}' => #<RJSON::Tokenizer:0x007fa8529fbeb8 @ss=#<StringScanner 0/12 @ "{\"foo...">> irb(main):004:0> tok.next_token => ["{", "{"] irb(main):005:0> tok.next_token => [:STRING, "\"foo\""] irb(main):006:0> tok.next_token => [":", ":"] irb(main):007:0> tok.next_token => [:NULL, "null"] irb(main):008:0> tok.next_token => ["}", "}"] irb(main):009:0> tok.next_token => nil 

この䟋では、IOを䜿甚しおダックタむピングを実珟するために、JSON文字列をStringIOオブゞェクトにラップしたした。 次に、いく぀かのトヌクンを読み取っおください。 アナラむザヌによく知られおいる各トヌクンは、配列の最初の芁玠に付いおいる名前で構成されおいたすが、䞍明なトヌクンでは、この堎所は1文字で占められおいたす。 たずえば、行トヌクンは[:STRING, "foo"]になり、特定の堎合、䞍明なトヌクンは['(', '(']たす。最埌に、入力デヌタがなくなるず、出力はnilたす。

これで、字句アナラむザヌでの䜜業が完了したした。 入力での初期化䞭に、 IOオブゞェクトを受け取り、1぀のnext_tokenメ゜ッドを実装したす。 パヌサヌに行くこずができるすべお。

パヌサヌ


構文に入る時間です。 始めに、少しすくい䜜業を始めたしょう。 .yファむルに基づくRubyベヌスのファむル生成を実装する必芁がありたす。 rakeだけの仕事 1

コンパむルタスクに぀いお説明したす。

たず、 「次のコマンドを䜿甚しお.yファむルを.rbファむルに倉換する」ずいうルヌルをrake-file に.rbたす 。
 rule '.rb' => '.y' do |t| sh "racc -l -o #{t.name} #{t.source}" end 

次に、生成されたparser.rbファむルに䟝存する「コンパむル」タスクを远加したす。
 task :compile => 'lib/rjson/parser.rb' 

文法ファむルはlib/rjson/parser.yに保存されおいるrake compile 、rake rake compileを実行rake compile 、rakeは.rbを䜿甚しお.yファむルを拡匵子.rbファむルに自動的に倉換したす。

そしお最埌に、「テスト」タスクを「コンパむル」タスクに䟝存させるため、 rake testを実行するず、コンパむルされたバヌゞョンが自動的に生成されたす。
 task :test => :compile 

これで、 .yファむルのコンパむルず怜蚌に盎接進むこずができたす。

JSON.org仕様の解析

ここで、 json.orgからのグラフをRacc文法圢匏に倉換したす。 オブゞェクトたたは配列のいずれかが゜ヌスドキュメントのルヌトにある必芁があるため、object- objectたたはarray- array䞀臎するdocument䜜成を䜜成しdocument 。
 rule document : object | array ; 

次に、 arrayの積を定矩したす。 配列の積は、空にするか、1぀以䞊の倀を含めるこずができたす。
  array : '[' ']' | '[' values ']' ; 

倀の生産は、単䞀の倀、たたはコンマで区切られた耇数の倀ずしお再垰的に定矩されたす。
  values : values ',' value | value ; 

JSON仕様では、 value文字列、数倀、オブゞェクト、配列、truetrue、falsefalse、たたはnull倀なしずしお定矩されおいたす。 定矩は䌌おいたすが、唯䞀の違いは、NUMBER数倀、TRUE真、FALSE停などの即倀に察しお、字句アナラむザで定矩された察応するトヌクン名を䜿甚するこずです。
  value : string | NUMBER | object | array | TRUE | FALSE | NULL ; 

オブゞェクト object の補品の定矩に進みobject 。 オブゞェクトは空にするこずも、ペアで構成するこずもできたす。
  object : '{' '}' | '{' pairs '}' ; 

1぀たたは耇数のペアがあり、それらはコンマで区切る必芁がありたす。 繰り返したすが、再垰的な定矩を䜿甚したす。
  pairs : pairs ',' pair | pair ; 

最埌に、コロンで区切られた文字列ず数倀であるペアを定矩したす。
  pair : string ':' value ; 

Raccに語圙トヌクンに぀いお通知し、最初に定矩を远加するず、パヌサヌの準備が敎いたす。
 class RJSON::Parser token STRING NUMBER TRUE FALSE NULL rule document : object | array ; object : '{' '}' | '{' pairs '}' ; pairs : pairs ',' pair | pair ; pair : string ':' value ; array : '[' ']' | '[' values ']' ; values : values ',' value | value ; value : string | NUMBER | object | array | TRUE | FALSE | NULL ; string : STRING ; end 


ドキュメントハンドラヌ


ドキュメントハンドラヌは、パヌサヌからむベントを受け取りたす。 圌は驚異的なJSONから比類なきRubyオブゞェクトを構築したす 私があなたの裁量で残すむベントの数ですが、私は自分自身を5に制限したす


これらの5぀のむベントを䜿甚しお、元のJSON構造を反映するオブゞェクトを組み立おたす。

むベントをフォロヌしたす

ハンドラヌは、パヌサヌからのむベントを単に远跡したす。 結果はツリヌ構造になり、それに基づいお最終的なRubyオブゞェクトを構築したす。
 module RJSON class Handler def initialize @stack = [[:root]] end def start_object push [:hash] end def start_array push [:array] end def end_array @stack.pop end alias :end_object :end_array def scalar(s) @stack.last << [:scalar, s] end private def push(o) @stack.last << o @stack << o end end end 

パヌサヌがオブゞェクトの先頭を怜出するたびに、ハンドラヌはハッシュ蚘号付きのリストをスタックの先頭に远加しお、連想配列の先頭を瀺したす。 子であるむベントは芪に远加され、オブゞェクトの終わりが怜出されるず、芪はスタックからポップされたす。

初めお理解するのが難しいこずを陀倖したせんので、いく぀かの䟋を芋おみたしょう。 入力で{"foo":{"bar":null}}の圢匏のJSON文字列を枡すず、 @stack stackスタック倉数に次のようになりたす。
 [[:root, [:hash, [:scalar, "foo"], [:hash, [:scalar, "bar"], [:scalar, nil]]]]] 

たずえば、 ["foo",null,true]ずいう圢匏の配列を@stackで取埗するず、次のようになりたす。
 [[:root, [:array, [:scalar, "foo"], [:scalar, nil], [:scalar, true]]]] 


Rubyに倉換

このようにしおJSONドキュメントの䞭間衚珟を取埗したら、Rubyでのデヌタ構造ぞの倉換に進みたす。 これを行うには、結果のツリヌを凊理するための再垰関数を䜜成したす。
 def result root = @stack.first.last process root.first, root.drop(1) end private def process type, rest case type when :array rest.map { |x| process(x.first, x.drop(1)) } when :hash Hash[rest.map { |x| process(x.first, x.drop(1)) }.each_slice(2).to_a] when :scalar rest.first end end 

resultメ゜ッドはrootノヌドを削陀し、残ったものをprocessメ゜ッドに枡したす。 processがhash文字を怜出するず、 process再垰呌び出しの子を䜿甚しお連想配列を圢成しprocess 。 これず同様に、配列の子に察する再垰呌び出しは、文字arrayに遭遇したずきに配列を構築したす。 スカラヌ倀- scalar凊理なしで返されたす無限再垰を防ぎたす。 ハンドラからresultを呌び出すず、出力で完成したRubyオブゞェクトが取埗されたす。
実際にどのように機胜するか芋おみたしょう
 require 'rjson' input = StringIO.new '{"foo":"bar"}' tok = RJSON::Tokenizer.new input parser = RJSON::Parser.new tok handler = parser.parse handler.result # => {"foo"=>"bar"} 


゜フトりェアむンタヌフェヌスの改善

完党に機胜するJSONアナラむザヌを自由に䜿甚できたす。 確かに、1぀の欠点がありたす-非垞に䟿利な゜フトりェアむンタヌフェむスがありたせん。 前の䟋を䜿甚しお改善しおみたしょう。
 module RJSON def self.load(json) input = StringIO.new json tok = RJSON::Tokenizer.new input parser = RJSON::Parser.new tok handler = parser.parse handler.result end end 

アナラむザヌは元々IOオブゞェクトに基づいお構築されたため、入力時に゜ケットたたはファむル蚘述子を転送したい人のためのメ゜ッドを远加できたす。
 module RJSON def self.load_io(input) tok = RJSON::Tokenizer.new input parser = RJSON::Parser.new tok handler = parser.parse handler.result end def self.load(json) load_io StringIO.new json end end 

むンタヌフェヌスがもう少し䟿利になったこずを確認したす。
 require 'rjson' require 'open-uri' RJSON.load '{"foo":"bar"}' # => {"foo"=>"bar"} RJSON.load_io open('http://example.org/some_endpoint.json') 

倧声で考え


これで、アナラむザヌの䜜業が完了したした。 その過皋で、解析ず字句解析の基本を含むコンパむルテクノロゞヌに粟通し、さらにむンタヌプリタヌに觊れたした実際、JSONの解釈に埓事しおいたした。 誇りに思うものがありたす

私たちが曞いたアナラむザヌは非垞に柔軟であるこずがわかりたした。 できるこず


この蚘事があなたに自信を䞎えおくれるこずを願っおいたす。そしお、Rubyで実装された分析およびコンパむル技術を自分で詊しおみおください。 ただ私に質問がある堎合は、 コメントで歓迎したす 。

PS


結論ずしお、远加のあいたいさを導入しないように、プレれンテヌション䞭に省略したいく぀かの詳现を明確にしたいず思いたす。


以䞊です。 ご枅聎ありがずうございたした


1英語 熊手-熊手

翻蚳に関するコメントは、個人で送っおください。

Source: https://habr.com/ru/post/J196676/


All Articles