JavaScriptでパヌサヌを曞く方法

...すなわち、単玔な構造から耇雑な構造を構築するこずにより、それほど耇雑ではない構造甚のLL構文解析を曞く方法。 時折、XMLに䌌た構造や䜕らかのデヌタURLなど、耇雑でないものを解析する必芁がありたす。通垞は、読みづらい読みにくいコヌドのシヌトか、さらに耇雑でトリッキヌな解析ラむブラリぞの䟝存関係のいずれかです。 ここでは、いく぀かの有名なアむデアそのうちのいく぀かはHabréで出䌚ったものを組み合わせお、非垞に耇雑なパヌサヌを、ごく少数のコヌド行を維持しながら、できるだけシンプルか぀簡朔に蚘述する方法を瀺したす。 たずえば、XMLに䌌た構造パヌサヌを䜜成したす。 はい、泚意を匕くためにここに写真を挿入したせん。 蚘事には写真がたったくないので、読みにくくなりたす。



䞻なアむデア



入力テキストの各郚分が個別の関数「パタヌン」ず呌びたしょうによっお解析され、これらの関数を組み合わせるこずで、より耇雑なテキストを解析できるより耇雑な関数を取埗できるずいう事実にありたす。 したがっお、パタヌンずは、解析を実行するexecメ゜ッドを持぀オブゞェクトです。 この関数は、䜕からどこで解析するかを瀺し、解析された堎所ず解析が終了した堎所を返したす。

var digit = { exec: function (str, pos) { var chr = str.charAt(pos); if (chr >= "0" && chr <= "9") return { res: +chr, end: pos + 1}; } }; 


珟圚、数字は解析数字パタヌンであり、次のように䜿甚できたす。

 assert.deepEqual(digit.exec("5", 0), { res: 5, end: 1 }); assert.deepEqual(digit.exec("Q", 0), void 0); 


なぜそのようなむンタヌフェヌスなのか JSには、非垞によく䌌たむンタヌフェヌスを持぀組み蟌みのRegExpクラスがあるためです。 䟿宜䞊、これらのパタヌンをむンスタンスずするPatternクラスRegExpの類䌌物ずしお芋おくださいを導入したす。

 function Pattern(exec) { this.exec = exec; } 


次に、倚少耇雑なパヌサヌで圹立぀いく぀かの単玔なパタヌンを玹介したす。

シンプルなパタヌン



最も単玔なパタヌンはtxtです-固定された、あらかじめ決められたテキスト行を解析したす

 function txt(text) { return new Pattern(function (str, pos) { if (str.substr(pos, text.length) == text) return { res: text, end: pos + text.length }; }); } 


次のように適甚されたす。

 assert.deepEqual(txt("abc").exec("abc", 0), { res: "abc", end: 3 }); assert.deepEqual(txt("abc").exec("def", 0), void 0); 


JSにデフォルトのコンストラクタヌがある堎合C ++など、より耇雑なパタヌンを構築するコンテキストで、txt「abc」゚ントリを「abc」に短瞮できたす。

次に、正芏衚珟の類䌌物を玹介し、rgxず呌びたす。

 function rgx(regexp) { return new Pattern(function (str, pos) { var m = regexp.exec(str.slice(pos)); if (m && m.index === 0) return { res: m[0], end: pos + m[0].length }; }); } 


次のように適甚したす。

 assert.deepEqual(rgx(/\d+/).exec("123", 0), { res: "123", end: 3 }); assert.deepEqual(rgx(/\d+/).exec("abc", 0), void 0); 


繰り返したすが、JSにデフォルトのコンストラクタヌがある堎合、rgx/ abc /゚ントリは/ abc /に短瞮できたす。

組み合わせパタヌン



次に、既存のパタヌンを組み合わせた耇数のパタヌンを入力する必芁がありたす。 これらの「コンビネヌタヌ」の䞭で最も単玔なものはoptです。これにより、任意のパタヌンがオプションになりたす。 元のパタヌンpがテキストを解析できない堎合、同じテキストのoptpはすべおが解析され、解析結果のみが空であるず蚀いたす

 function opt(pattern) { return new Pattern(function (str, pos) { return pattern.exec(str, pos) || { res: void 0, end: pos }; }); } 


䜿甚䟋

 assert.deepEqual(opt(txt("abc")).exec("abc"), { res: "abc", end: 3 }); assert.deepEqual(opt(txt("abc")).exec("123"), { res: void 0, end: 0 }); 


JSで挔算子のオヌバヌロヌドずデフォルトのコンストラクタヌが可胜な堎合、レコヌドoptpをp ||に枛らすこずができたす。 void 0これは、optの実装方法から明らかにわかりたす。

次に耇雑なコンビネヌタはexcです。最初のパタヌンを解析でき、2番目のパタヌンを解析できないもののみを解析したす。

 function exc(pattern, except) { return new Pattern(function (str, pos) { return !except.exec(str, pos) && pattern.exec(str, pos); }); } 


Wpがpパタヌン解析するテキストのセットである堎合、Wexcp、q= Wp\ Wq。 これは、たずえば、文字Hを陀くすべおの倧文字を解析する必芁がある堎合に䟿利です。

 var p = exc(rgx(/[AZ]/), txt("H")); assert.deepEqual(p.exec("R", 0), { res: "R", end: 1 }); assert.deepEqual(p.exec("H", 0), void 0); 


JSに挔算子のオヌバヌロヌドがある堎合、excp1、p2をp1-p2たたはtoP2 && p1に枛らすこずができたすただし、このため、組み合わせパタヌンall /を導入する必芁があり、これは&&挔算子ずしお機胜したす 。

次に、コンビネヌタパタヌンがありたす。いく぀かのパタヌンを取り、これらのパタヌンの最初のパタヌンを解析する新しいパタヌンを䜜成したす。 Wanyp1、p2、p3、...= Wp1v Wp2v Wp3v ...

 function any(...patterns) { return new Pattern(function (str, pos) { for (var r, i = 0; i < patterns.length; i++) if (r = patterns[i].exec(str, pos)) return r; }); } 


[] .slice.callarguments、0のような䞍噚甚なコヌドを避けるために、...パタヌン調和rest_parameters構成を䜿甚したした。 anyの䜿甚䟋

 var p = any(txt("abc"), txt("def")); assert.deepEqual(p.exec("abc", 0), { res: "abc", end: 3 }); assert.deepEqual(p.exec("def", 0), { res: "def", end: 3 }); assert.deepEqual(p.exec("ABC", 0), void 0); 


JSに挔算子のオヌバヌロヌドがある堎合、任意のp1、p2をp1 ||に枛らすこずができたす。 p2。

次のコンビネヌタヌパタヌンはseqです-パタヌンのシヌケンスによっお䞎えられたテキストを順次解析し、結果の配列を生成したす。

 function seq(...patterns) { return new Pattern(function (str, pos) { var i, r, end = pos, res = []; for (i = 0; i < patterns.length; i++) { r = patterns[i].exec(str, end); if (!r) return; res.push(r.res); end = r.end; } return { res: res, end: end }; }); } 


次のように適甚されたす。

 var p = seq(txt("abc"), txt("def")); assert.deepEqual(p.exec("abcdef"), { res: ["abc", "def"], end: 6 }); assert.deepEqual(p.exec("abcde7"), void 0); 


JSに挔算子のオヌバヌロヌドがある堎合、seqp1、p2をp1、p2に枛らすこずができたすコンマ挔算子がオヌバヌロヌドされたす。

そしお最埌に、repコンビネヌタヌパタヌン-既知のパタヌンをテキストに䜕床も適甚し、結果の配列を生成したす。 原則ずしお、これは、たずえばカンマで区切られた同じタむプの特定の構造の解析に䜿甚されるため、repは2぀の匕数を取りたす。結果が興味深いメむンパタヌンず、結果が砎棄される分離パタヌンです。

 function rep(pattern, separator) { var separated = !separator ? pattern : seq(separator, pattern).then(r => r[1]); return new Pattern(function (str, pos) { var res = [], end = pos, r = pattern.exec(str, end); while (r && r.end > end) { res.push(r.res); end = r.end; r = separated.exec(str, end); } return { res: res, end: end }; }); } 


蚱容される繰り返し数を制埡するパラメヌタヌminおよびmaxをさらに远加できたす。 ここでは、関数z{return z [1]}を蚘述しないために、矢印関数r => r [1]調和arrow_functionsを䜿甚したした。 パタヌンthenを䜿甚しおrepがseqに枛少するこずに泚意しおくださいPromisethenから取埗したアむデア

 function Pattern(exec) { ... this.then = function (transform) { return new Pattern(function (str, pos) { var r = exec(str, pos); return r && { res: transform(r.res), end: r.end }; }); }; } 


この方法では、最初の結果に任意の倉換を適甚するこずにより、別のパタヌンから掚枬できたす。 ずころで、Haskellの専門家は、このパタヌンがパタヌンからモナドを䜜るず蚀うこずは可胜ですか

さお、担圓者は次のように適甚されたす。

 var p = rep(rgx(/\d+/), txt(",")); assert.deepEqual(p.exec("1,23,456", 0), { res: ["1", "23", "456"], end: 8 }); assert.deepEqual(p.exec("123ABC", 0), { res: ["123"], end: 3 }); assert.deepEqual(p.exec("ABC", 0), void 0); 


担圓者のオペレヌタヌのオヌバヌロヌドずの明確な類掚は私には発生したせん。

結果は、これらのrep / seq / anyのすべおに぀いお玄70行です。 これでコンビネヌタヌパタヌンのリストが完成し、XMLに䌌たテキストを認識するパタヌンの実際の構築に進むこずができたす。

XMLのようなテキストのパヌサヌ



このようなXMLのようなテキストに制限したす。

 <?xml version="1.0" encoding="utf-8"?> <book title="Book 1"> <chapter title="Chapter 1"> <paragraph>123</paragraph> <paragraph>456</paragraph> </chapter> <chapter title="Chapter 2"> <paragraph>123</paragraph> <paragraph>456</paragraph> <paragraph>789</paragraph> </chapter> <chapter title="Chapter 3"> ... </chapter> </book> 


たず、name = "value"ずいう圢匏の名前付き属性を認識するパタヌンを蚘述したす。これは明らかにXMLでよく芋られたす。

 var name = rgx(/[az]+/i).then(s => s.toLowerCase()); var char = rgx(/[^"&]/i); var quoted = seq(txt('"'), rep(char), txt('"')).then(r => r[1].join('')); var attr = seq(name, txt('='), quoted).then(r => ({ name: r[0], value: r[2] })); 


ここで、attrは名前付き属性を文字列圢匏の倀で解析し、quoted-匕甚笊で文字列を解析したす。char-文字列内の1文字を解析したすこれを別のパタヌンずしお蚘述する必芁がありたすか 、ただしnameは属性名を解析したす倧文字ず小文字の䞡方を解析したすが、すべおの文字が小さい解析枈みの名前を返したす。 attrアプリケヌションは次のようになりたす。

 assert.deepEqual( attr.exec('title="Chapter 1"', 0), { res: { name: "title", value: "Chapter 1" }, end: 17 }); 


次に、<Xml ...>のようなヘッダヌを解析できるパタヌンを䜜成したす。

 var wsp = rgx(/\s+/); var attrs = rep(attr, wsp).then(r => { var m = {}; r.forEach(a => (m[a.name] = a.value)); return m; }); var header = seq(txt('<?xml'), wsp, attrs, txt('?>')).then(r => r[2]); 


ここで、wspは1぀以䞊のスペヌスを解析し、attrsは1぀以䞊の名前付き属性を解析し、解析したものを蟞曞の圢匏で返したすrepは名前ず倀のペアの配列を返したすが、蟞曞はより䟿利なので、配列は内郚の蟞曞に倉換されたす、ヘッダヌはヘッダヌを解析し、同じディクショナリの圢匏でタむトル属性のみを返したす。

 assert.deepEqual( header.exec('<?xml version="1.0" encoding="utf-8"?>', 0), { res: { version: "1.0", encoding: "utf-8" }, end: ... }); 


次に、<node ...> ...の圢匏の構文解析に移りたしょう。

 var text = rep(char).then(r => r.join('')); var subnode = new Pattern((str, pos) => node.exec(str, pos)); var node = seq( txt('<'), name, wsp, attrs, txt('>'), rep(any(text, subnode), opt(wsp)), txt('</'), name, txt('>')) .then(r => ({ name: r[1], attrs: r[3], nodes: r[5] })); 


ここでは、テキストはノヌド内のテキストを解析し、xml゚ンティティを認識するために孊習できる文字パタヌンを䜿甚し、サブノヌドは内郚ノヌド実際にはサブノヌド=ノヌドを解析し、ノヌドは属性ず内郚ノヌドでノヌドを解析したす。 なぜこのようなサブノヌドのトリッキヌな定矩ですか nodeの定矩でn​​odeを盎接参照するずnode = seq...、node、...など、nodeの定矩時にこの倉数はただ空であるこずがわかりたす。 サブノヌドのトリックは、この埪環䟝存を排陀​​したす。

ヘッダヌを持぀ファむル党䜓を認識するパタヌンを決定するこずは残っおいたす。

 var xml = seq(header, node).then(r => ({ root: r[1], attrs: r[0] })); 


したがっお、アプリケヌションは次のずおりです。

 assert.deepEqual( xml.exec(src), { attrs: { version: '1.0', encoding: 'utf-8' }, root: { name: 'book', attrs: { title: 'Book 1' }, nodes: [ { name: 'chapter', attrs: { title: 'Chapter 1' }, nodes: [...] }, ... ] } }); 


ここでは、1぀の匕数でパタヌンexecを呌び出したす。これの意味は、最初から文字列を解析し、最埌たで解析されるこずを確認するこずですが、最埌たで解析されるため、その堎所ぞのポむンタなしで解析されたものだけを返すだけで十分ですパヌサヌが停止した堎所これが行の終わりであるこずは既に知っおいたす

 function Pattern(name, exec) { ... this.exec = function (str, pos) { var r = exec(str, pos || 0); return pos >= 0 ? r : !r ? null : r.end != str.length ? null : r.res; }; } 


実際には、パヌサヌ党䜓が20行になっおいたすrep、seq、anyなどを実装する70行に぀いお忘れないでください。

 var name = rgx(/[az]+/i).then(s => s.toLowerCase()); var char = rgx(/[^"&]/i); var quoted = seq(txt('"'), rep(char), txt('"')).then(r => r[1].join('')); var attr = seq(name, txt('='), quoted).then(r => ({ name: r[0], value: r[2] })); var wsp = rgx(/\s+/); var attrs = rep(attr, wsp).then(r => { var m = {}; r.forEach(a => (m[a.name] = a.value)); return m; }); var header = seq(txt('<?xml'), wsp, attrs, txt('?>')).then(r => r[2]); var text = rep(char).then(r => r.join('')); var subnode = new Pattern((str, pos) => node.exec(str, pos)); var node = seq( txt('<'), name, wsp, attrs, txt('>'), rep(any(text, subnode), opt(wsp)), txt('</'), name, txt('>')) .then(r => ({ name: r[1], attrs: r[3], nodes: r[5] })); var xml = seq(header, node).then(r => ({ root: r[1], attrs: r[0] })); 


JSたたはC ++で挔算子をオヌバヌロヌドするず、次のようになりたす。

 var name = rgx(/[az]+/i).then(s => s.toLowerCase()); var char = rgx(/[^"&]/i); var quoted = ('"' + rep(char) + '"').then(r => r[1].join('')); var attr = (name + '=' + quoted).then(r => ({ name: r[0], value: r[2] })); var wsp = rgx(/\s+/); var attrs = rep(attr, wsp).then(r => { var m = {}; r.forEach(a => (m[a.name] = a.value)); return m; }); var header = ('<?xml' + wsp + attrs + '?>').then(r => r[2]); var text = rep(char).then(r => r.join('')); var subnode = new Pattern((str, pos) => node.exec(str, pos)); var node = ('<' + name + wsp + attrs + '>' + rep(text | subnode) + (wsp | null) + '</' + name + '>') .then(r => ({ name: r[1], attrs: r[3], nodes: r[5] })); var xml = (header + node).then(r => ({ root: r[1], attrs: r[0] })); 


ここでの各倉数は1぀のABNFルヌルに厳密に察応するため、RFCの蚘述およびそこでABNFが気に入っおいるに埓っお䜕かを解析する必芁がある堎合、それらのルヌルを転送するこずは機械的な問題です。 さらに、ABNFルヌル自䜓およびEBNFおよびPEGは厳密に圢匏的であるため、これらのルヌルのパヌサヌを蚘述し、rep、seqなどを呌び出す代わりに、次のように蚘述できたす。

 var dataurl = new ABNF('"data:" mime ";" attrs, "," data', { mime: /[az]+\/[az]+/i, attrs: ..., data: /.*/ }).then(r => ({ mime: r[1], attrs: r[3], data: r[5] })); 


そしおい぀ものように適甚したす

 assert.deepEqual( dataurl.exec('data:text/plain;charset="utf-8",how+are+you%3f'), { mime: "text/plain", attrs: { charset: "utf-8" }, data: "how are you?" }); 


さらにいく぀かのブナ



解析に倱敗した堎合、パタヌンexecがnull /未定矩を返すのはなぜですか 䟋倖をスロヌしないのはなぜですか このように䟋倖を䜿甚するず、パヌサヌは20秒ごずに遅くなりたす。 䟋倖は䟋倖的な堎合に適しおいたす。

説明した方法を䜿甚するず、すべおの目的に適さないLLパヌサヌを䜜成できたす。 たずえば、次の圢匏のXMLを解析する必芁がある堎合

 <book attr1="..." attr2="..."    ??? 


それが立っおいる堎所で??? /およびsimple>の䞡方になるこずがありたす。 LLパヌサヌがこれをすべお<book ...>ずしお解析しようずしたが、その堎所にある堎合??? >>であるこずが刀明した堎合、パヌサヌは、これが<book ... />であるずいう仮定の䞋で、解析ずやり盎しに非垞に時間がかかったすべおを砎棄したす。 LRパヌサヌにはこの欠点はありたせんが、䜜成するのは困難です。

たた、LLパヌサヌは、優先順䜍が異なる挔算子などがあるさたざたな構文/数匏の解析にはあたり適しおいたせん。 LLパヌサヌはもちろん䜜成できたすが、やや混乱し、動䜜が遅くなりたす。 LRパヌサヌはそれ自䜓で混乱したすが、高速です。 したがっお、このような匏は、いわゆる Crockford がよく説明した Prattのアルゎリズムこのリンクが玫色の堎合は、おそらく私よりもパヌサヌをよく理解しおおり、おそらくこれをすべお読むこずに退屈しおいるでしょう。

誰かが圹に立぀ずいいな。 か぀お、さたざたな䞍噚甚さのパヌサヌを䜜成したしたが、䞊蚘の方法は私にずっお発芋でした。

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


All Articles