ディープパイパーシング:Pythonでのユニットの解析

前回の記事では、Pyparsingの便利な解析ライブラリに精通し、式'import matplotlib.pyplot as plt'



パーサーを作成しました。



この記事では、ユニット解析問題の例を使用してPyparsingに飛び込みます。 段階的に、ロシア語の文字を検索し、ユニット名の有効性を確認し、ユーザーが角かっこで囲んだものをグループ化できる再帰的なパーサーを作成します。



注:この記事のコードはテストされ、 Sagemathclodに投稿されています 。 何かが突然機能しない場合(テキストのエンコードが原因である可能性が高い)、個人のメール、コメント、またはメールまたはVKで私に知らせてください。



始めましょう。 初期データとタスク。



例として、式を解析します。



 s = "*^2/(*^2)"
      
      





この測定単位は、解析でパーサーのすべての機能を使用する行を取得するために、ヘッドから取得されました。 取得する必要があります:



 res = [('',1.0), ('',2.0), ('',-1.0), ('',-2.0)]
      
      





s



除算を乗算で置き換え、括弧を開き、測定単位のパワーを明示的に配置すると、次のようになります。N * m ^ 2 /(kg * s ^ 2)= N ^ 1 * m ^ 2 * kg ^ -1 * s ^ -2 。



したがって、 res



変数の各タプルには、測定単位の名前と、それを上げる必要がある度合いが含まれています。 タプルの間に、精神的に増殖の兆候を置くことができます。



pyparsingを使用する前に、インポートする必要があります。



 from pyparsing import *
      
      





パーサーを作成するときに、*を使用したクラスに置き換えます。



構文解析パーサー手法



pyparsingを使用するときは、次のパーサー記述テクニックを遵守する必要があります。

  1. 最初に、最終行を構成するための「構成要素」であるキーワードまたは個々の重要な文字がテキスト行から選択されます。
  2. 「ブリック」用に個別のパーサーを作成します。
  3. 最終文字列のパーサーを「収集」します。


私たちの場合、主要な「ブリック」は、個々の測定単位の名前とその程度です。



測定単位のパーサーを作成します。 ロシア文字の解析。



測定単位は、文字で始まり、文字とドットで構成される単語です(mm.rt.st.など)。 pyparsingでは次のように書くことができます:



 ph_unit = Word(alphas, alphas+'.')
      
      





Word



クラスには2つの引数があります。 最初の引数は、単語の最初の文字を指定します。2番目の引数は、単語の他の文字を指定します。 測定単位は必ず文字で始まるため、最初の引数alphas



を設定します。 文字に加えて、単位にはピリオド(mm.rt.stなど)が含まれている場合があるため、 Word



の2番目の引数はalphas + '.'







残念ながら、測定単位を解析しようとすると、パーサーは英語の測定単位に対してのみ機能することがわかります。 これは、 alphas



は単なる文字ではなく、英語のアルファベットの文字を意味するためです。



この問題は非常に簡単です。 まず、ロシア語のすべての文字をリストする行を作成します。



 rus_alphas = ''
      
      





また、個々の測定単位のパーサーコードを次のように変更する必要があります。



 ph_unit = Word(alphas+rus_alphas, alphas+rus_alphas+'.')
      
      





これで、パーサーはユニットをロシア語と英語で理解できるようになりました。 他の言語の場合、パーサーコードは同様に記述されます。



パーサーの結果のエンコードの修正。



パーサーで測定単位をテストすると、ロシア文字がコード指定に置き換えられた結果を取得できます。 たとえば、Sageの場合:



 ph_unit.parseString("").asList() # : ['\xd0\xbc\xd0\xbc']
      
      





同じ結果が得られる場合は、すべて正常に機能しますが、エンコードを修正する必要があります。 私の場合(セージ)、「自家製」のbprint



(より良い印刷)機能の使用が機能しbprint







 def bprint(obj): print(obj.__repr__().decode('string_escape'))
      
      





この関数を使用して、正しいエンコードでSageの出力を取得します。



 bprint(ph_unit.parseString("").asList()) # : ['']
      
      





学位のパーサーを書く。 任意の数値の解析。



学位を解析することを学びます。 通常、次数は整数です。 ただし、まれに、度に小数部分が含まれたり、指数表記で記述されたりする場合があります。 したがって、たとえば次のような通常の番号のパーサーを作成します。



 test_num = "-123.456e-3"
      
      





任意の数字の「レンガ」は自然数であり、数字で構成されます。



 int_num = Word(nums)
      
      





数字の前にプラス記号またはマイナス記号を付けることができます。 この場合、結果にプラス記号を出力する必要はありません( Suppress()



使用)。



 pm_sign = Optional(Suppress("+") | Literal("-"))
      
      





縦線は「または」(プラスまたはマイナス)を意味します。 Literal()



は、テキスト文字列と完全に一致することを意味します。 したがって、 pm_sign



の式は、解析結果に出力する必要がないテキスト内のオプションの+文字、またはオプションのマイナス文字を見つける必要があることを意味します。



これで、整数のパーサーを作成できます。 数字はオプションのプラス記号またはマイナス記号で始まり、その後に数字が続き、その後にオプションのドット-小数部の区切り、数字、eが続き、その後に数字が続きます:オプションのプラスまたはマイナスと数字。 eの後の数値には小数部分がなくなりました。 pyparsingについて:



 float_num = pm_sign + int_num + Optional('.' + int_num) + Optional('e' + pm_sign + int_num)
      
      





これで、数値のパーサーができました。 パーサーの仕組みを見てみましょう。



 float_num.parseString('-123.456e-3').asList() #  ['-', '123', '.', '456', 'e', '-', '3']
      
      





ご覧のとおり、番号は個別のコンポーネントに分割されています。 必要ありません。番号を「収集」して戻したいと思います。 これはCombine()



行われます:



 float_num = Combine(pm_sign + int_num + Optional('.' + int_num) + Optional('e' + pm_sign + int_num))
      
      





チェック:



 float_num.parseString('-123.456e-3').asList() #  ['-123.456e-3']
      
      





いいね! しかし...出力はまだ文字列であり、数字が必要です。 ParseAction()



を使用して文字列を数値変換に追加します。



 float_num = Combine(pm_sign + int_num + Optional('.' + int_num) + Optional('e' + pm_sign + int_num)).setParseAction(lambda t: float(t.asList()[0]))
      
      





引数がt



である匿名関数lambda



を使用します。 最初に、結果をリスト(t.asList())



として取得します。 なぜなら 結果のリストには要素が1つしかないため、すぐに抽出できます: t.asList()[0]



float()



関数は、テキストを浮動小数点数に変換します。 Sageで作業している場合、 float



RR



実数Sageのクラスのコンストラクター)に置き換えることができます。



学位を持つ単位の解析。



別の測定単位は、測定単位の名前の後に、次数^と数字-上げる必要がある次数の記号が続きます。 pyparsingについて:



 single_unit = ph_unit + Optional('^' + float_num)
      
      





テストします:



 bprint(single_unit.parseString("^2").asList()) # : ['', '^', 2.0]
      
      





すぐに結論を改善します。 解析の結果として^を表示する必要はなく、結果をタプルとして表示する必要があります(この記事の冒頭のres変数を参照してください)。 出力を抑制するには、 Suppress()



を使用してリストをタプルに変換しますParseAction()







 single_unit = (ph_unit + Optional(Suppress('^') + float_num)).setParseAction(lambda t: tuple(t.asList()))
      
      





チェック:

 bprint(single_unit.parseString("^2").asList()) # : [('', 2.0)]
      
      







括弧で囲まれた解析単位。 再帰の実装。



興味深い場所に来ました-再帰の実装の説明。 測定単位を記述するとき、ユーザーは乗算と除算の符号の間にある1つ以上の測定単位を括弧で囲むことができます。 括弧で囲まれた式には、括弧で囲まれた別のネストされた式が含まれる場合があります(たとえば、 "(^2/ (^2 * ))"



)。 括弧で囲まれた一部の式を他の式に埋め込む機能は、再帰の原因です。 Pyparsingに移りましょう。



最初に、再帰があるという事実に注意を払わずに式を書きます。



 unit_expr = Suppress('(') + single_unit + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr))) + Suppress(")")
      
      





Optional



は、存在する場合と存在しない場合がある文字列の一部が含まれます。 OneOrMore



(「1つ以上」と翻訳)には、テキスト内で少なくとも1回出現する必要がある行の部分が含まれています。 OneOrMore



は2つの「用語」が含まれています。まず乗算と除算の符号を探し、次に測定単位またはネストされた式を探します。



unit_expr



ままにしておくことは不可能です。等号の左右にはunit_expr



、これは再帰を明確に示しています。 この問題は非常に簡単に解決されます。割り当て記号を<<に変更し、 unit_expr



前の行に特別なForward()



クラスの割り当てを追加する必要があります。



 unit_expr = Forward() unit_expr << Suppress('(') + single_unit + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr))) + Suppress(")")
      
      





したがって、パーサーを作成するときに、事前に再帰を予測する必要はありません。 最初に式に再帰がなかったかのように記述し、それが表示されたら、=記号を<<に置き換えて、上記の行にForward()



クラス割り当てを追加します。



チェック:



 bprint(unit_expr.parseString("(*/^2)").asList()) # : [('',), '*', ('',), '/', ('', 2.0)]
      
      







測定単位の共通式の解析。



最後のステップが残ります。測定単位の一般式です。 pyparsingについて:



 parse_unit = (unit_expr | single_unit) + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr)))
      
      





式の形式は(a | b) + (c | d)



ことに注意してください。 ここにはブラケットが必要であり、数学と同じ役割を果たします。 括弧を使用して、最初の用語がunit_expr



またはsingle_unit



であり、2番目の用語がオプションの式であることを最初に確認する必要があることを示したいと思います。 角かっこを削除すると、 parse_unit



unit_expr



またはsingle_unit



+オプションの式であることがsingle_unit



ますが、これは意図したものではありません。 同じ推論がOptional()



内の式にも適用されます。



ドラフトパーサー。 結果のエンコードの修正。



そこで、パーサーのドラフトバージョンを作成しました。



 from pyparsing import * rus_alphas = '' ph_unit = Word(rus_alphas+alphas, rus_alphas+alphas+'.') int_num = Word(nums) pm_sign = Optional(Suppress("+") | Literal("-")) float_num = Combine(pm_sign + int_num + Optional('.' + int_num) + Optional('e' + pm_sign + int_num)).setParseAction(lambda t: float(t.asList()[0])) single_unit = (ph_unit + Optional(Suppress('^') + float_num)).setParseAction(lambda t: tuple(t.asList())) unit_expr = Forward() unit_expr << Suppress('(') + single_unit + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr))) + Suppress(")") parse_unit = (unit_expr | single_unit) + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr)))
      
      





チェック:



 print(s) # s = "*^2/(*^2)" — .  . bprint(parse_unit.parseString(s).asList()) # : [('',), '*', ('', 2.0), '/', ('',), '*', ('', 2.0)]
      
      





括弧で囲まれたユニットのグループ化。



取得したい結果にすでに近づいています。 最初に実装する必要があるのは、ユーザーが括弧で囲んだ測定単位をグループ化することです。 このため、PyparsingはGroup()



使用し、これをunit_expr



に適用しunit_expr







 unit_expr = Forward() unit_expr << Group(Suppress('(') + single_unit + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr))) + Suppress(")"))
      
      





何が変わったのか見てみましょう:



 bprint(parse_unit.parseString(s).asList()) # : [('',), '*', ('', 2.0), '/', [('',), '*', ('', 2.0)]]
      
      







次数のないタプルに次数1を入れます。



小数点の後のいくつかのタプルは無料です。 タプルは測定単位に対応し、形式(測定単位、次数)を持っていることを思い出させてください。 パーサーの結果の特定の部分に名前を付けることができることを思い出してください( 前の記事で説明しました)。 特に、見つかった測定単位の名前を'unit_name'



とし、その次数を'unit_degree'



ます。 setParseAction()



で、匿名関数lambda()



setParseAction()



を作成します。これは、ユーザーが単位の次数を指定しなかった場所に1を置きます)。 pyparsingについて:



 single_unit = (ph_unit('unit_name') + Optional(Suppress('^') + float_num('unit_degree'))).setParseAction(lambda t: (t.unit_name, float(1) if t.unit_degree == "" else t.unit_degree))
      
      





これで、パーサー全体が次の結果を生成します。



 bprint(parse_unit.parseString(s).asList()) # : [('', 1.0), '*', ('', 2.0), '/', [('', 1.0), '*', ('', 2.0)]]
      
      





上記のコードでは、 float(1)



代わりに、単に1.0



書くことができますが、Sageでは、この場合、 float



型ではなく、実数用の独自のSage型を取得します。



パーサーの結果から*および/記号を削除し、括弧を開きます。



私たちがやるべきことは、パーサーの結果として、*と/の記号と囲まれた角括弧を削除することだけです。 ネストされたリストの前(つまり、[)の前に除算がある場合、ネストされたリストの測定単位の度数記号を逆にする必要があります。 これを行うには、 setParseAction()



で使用する別のtransform_unit()



関数をparse_unit



ます。



 def transform_unit(unit_list, k=1): res = [] for v in unit_list: if isinstance(v, tuple): res.append(tuple((v[0], v[1]*k))) elif v == "/": k = -k elif isinstance(v, list): res += transform_unit(v, k=k) return(res) parse_unit = ((unit_expr | single_unit) + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr)))).setParseAction(lambda t: transform_unit(t.asList()))
      
      





その後、パーサーは目的の形式でユニットを返します。



 bprint(transform_unit(parse_unit.parseString(s).asList())) # : [('', 1.0), ('', 2.0), ('', -1.0), ('', -2.0)]
      
      





transform_unit()



関数はネストを削除することに注意してください。 変換中に、すべての括弧が展開されます。 括弧の前に分割記号がある場合、括弧内の単位の度記号は逆になります。



解析プロセスでの測定単位の直接検証の実装。



実行が約束された最後のことは、測定単位の早期検証を導入することでした。 言い換えると、パーサーが測定単位を見つけるとすぐに、データベースと照合します。



Python辞書をデータベースとして使用します。



 unit_db = {'':{'':1, '':1/10, '':1/100, '':1/1000, '':1000, '':1/1000000}, '':{'':1}, '':{'':1, '':1000}, '':{'':1}, '':{'':1, '':0.001}}
      
      





測定単位をすばやく確認するには、測定単位を入れてPythonのセットを作成するとよいでしょう。



 unit_set = set([t for vals in unit_db.values() for t in vals])
      
      





check_unit



関数を作成します。この関数は測定単位を確認し、それをsetParseAction



に挿入しph_unit







 def check_unit(unit_name): if not unit_name in unit_set: raise ValueError("        : " + unit_name) return(unit_name) ph_unit = Word(rus_alphas+alphas, rus_alphas+alphas+'.').setParseAction(lambda t: check_unit(t.asList()[0]))
      
      





パーサーの出力は変更されませんが、データベースまたは科学にない測定単位が見つかった場合、ユーザーはエラーメッセージを受け取ります。 例:



 ph_unit.parseString("") #    : Error in lines 1-1 Traceback (most recent call last): … File "", line 1, in <lambda> File "", line 3, in check_unit ValueError:         : 
      
      





最後の行は、エラーに関するユーザーへのメッセージです。



完全なパーサーコード。 おわりに



結論として、完全なパーサーコードを提供します。 *をimport行"from pyparsing import *"



使用されるクラスに置き換えることを忘れないでください。



 from pyparsing import nums, alphas, Word, Literal, Optional, Combine, Forward, Group, Suppress, OneOrMore def bprint(obj): print(obj.__repr__().decode('string_escape')) #     unit_db = {'':{'':1, '':1/10, '':1/100, '':1/1000, '':1000, '':1/1000000}, '':{'':1}, '':{'':1, '':1000}, '':{'':1}, '':{'':1, '':0.001}} unit_set = set([t for vals in unit_db.values() for t in vals]) #           rus_alphas = '' def check_unit(unit_name): """      . """ if not unit_name in unit_set: raise ValueError("        : " + unit_name) return(unit_name) ph_unit = Word(rus_alphas+alphas, rus_alphas+alphas+'.').setParseAction(lambda t: check_unit(t.asList()[0])) #    int_num = Word(nums) pm_sign = Optional(Suppress("+") | Literal("-")) float_num = Combine(pm_sign + int_num + Optional('.' + int_num) + Optional('e' + pm_sign + int_num)).setParseAction(lambda t: float(t.asList()[0])) #       single_unit = (ph_unit('unit_name') + Optional(Suppress('^') + float_num('unit_degree'))).setParseAction(lambda t: (t.unit_name, float(1) if t.unit_degree == "" else t.unit_degree)) #      unit_expr = Forward() unit_expr << Group(Suppress('(') + single_unit + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr))) + Suppress(")")) #       def transform_unit(unit_list, k=1): """     ,  ,       *  / """ res = [] for v in unit_list: if isinstance(v, tuple): res.append(tuple((v[0], v[1]*k))) elif v == "/": k = -k elif isinstance(v, list): res += transform_unit(v, k=k) return(res) parse_unit = ((unit_expr | single_unit) + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr)))).setParseAction(lambda t: transform_unit(t.asList())) # s = "*^2/(*^2)" bprint(parse_unit.parseString(s).asList())
      
      





あなたが私の記事を読んでくれた忍耐に感謝します。 この記事で紹介したコードはSagemathcloudに投稿されていることを思い出してください 。 Habréに登録していない場合は、 メールで質問を送信するか、 VKに メールを送信してください。 次の記事では、 Sagemathcloudを紹介して、Pythonでの作業をどれだけ簡素化できるかを示します。 その後、まったく新しいレベルでPyparsingの構文解析のトピックに戻ります。



Daria FrolovaとNikita Konovalovが、公開前に記事をチェックしてくれたことに感謝します。



All Articles