🐙

pyparsingを使えば正規表現がわからなくてもパース処理が書ける

2023/09/04に公開

はじめに

詳しい説明は他の記事に譲りますが、pyparsingはPEG(Parsing Expression Grammar)という手法を用いた構文解析ライブラリです。
pyparsingを利用することで、比較的簡単にパース処理を記述することができます。

パース処理が書けるようになると、コマンドやプログラムが吐き出したログから欲しいデータだけを取り出せるようになるほか、データの変換処理やユーザが入力した文字列の処理などが書けるようになります。

本記事は「正規表現がイマイチ理解できなくて困っている」「パース処理を書きたいけど、正規表現が書けない」という方々に向けた記事になります。

本記事で使用したPythonおよびpyparsingのバージョンは以下の通りです。
OSは Windows 11 Home を使用しています。

Python 3.10.0
pyparsing 3.0.9


Hello, world!

以下のプログラムはpyparsingの使用例です。
非常に簡単な例ですが、pyparsingの雰囲気がわかると思います。

# pyparsingのインポート
from pyparsing import (
    alphas,
    Word,
)

# パース処理の対象となる文字列
target = 'Hello, world!'
# 「単語, 単語!」という形の文字列にマッチするパターンを定義
pattern = Word(alphas) + ',' + Word(alphas) + '!'

# 定義したパターンで文字列をパース処理する
result = pattern.parse_string(target)

print(result)
# ['Hello', ',', 'world', '!']
# 文字列は単語, カンマ, 感嘆符の4つに分解される

ライブラリの解説

pyparsingで利用できるクラスやメソッド, 定数の一部を紹介します。
全ては紹介しないので、他に利用できるクラスやメソッドを知りたい場合は公式ドキュメントを参照してください。

「まずは使用例からざっくりと使い方を理解したい!」という方はこのセクションを飛ばしてください。

parse_string メソッド

parse_string(source_string, parse_all = False)

source_stringで指定した文字列をパース処理するメソッドです。
基本的にpyparsingでパース処理を実行する際にはこのメソッドを使用します。

parse_allTrueに設定すると、過不足なくパース処理が成功しない限り例外を発生させます。

from pyparsing import (
    alphas,
    Word,
)

# 「単語!」という形の文字列にマッチするパターンを定義
pattern = Word(alphas) + '!'

result = pattern.parse_string('Hello!')
print(result)
# ['Hello', '!']

result = pattern.parse_string('Hello!', parse_all = True)
print(result)
# ['Hello', '!']
# 過不足なくパース処理が成功しているので例外は発生しない

result = pattern.parse_string('Hello!!!')
print(result)
# ['Hello', '!']
# パターンにマッチするところまでを返す
# 余分な ! は無視される

result = pattern.parse_string('Hello!!!', parse_all = True)
# pyparsing.exceptions.ParseException: Expected '!', found end of text
# ! が2つ多い(過不足がある)ので例外が発生する

result = pattern.parse_string('Hello')
# pyparsing.exceptions.ParseException: Expected '!', found end of text
# そもそもパターンにマッチしない場合はparse_allの有無に関係なく例外が発生する

set_results_name メソッド

set_results_name(string, list_all_matches = False)

特定の要素にstringで指定した名前(辞書型のキー)を付けるメソッドです。
基本的に後述のLiteralクラスやWordクラスで利用します。
パース結果を辞書型として扱うときに役立ちます。

同名の名前が付いた要素が複数存在する場合、一番最後にマッチした要素を返します。
list_all_matchesTrueに設定すると、全ての要素を返すようになります。

from pyparsing import (
    alphas,
    Word,
)

# パース処理の対象となる文字列
target = 'Ant & Dec'
# &を基準にして文字列を前後2つに分けるパターンを定義
pattern = Word(alphas).set_results_name('name') + '&' + Word(alphas).set_results_name('name')

result = pattern.parse_string(target)
print(result['name'])
# Dec
# 最後の要素であるDecのみを返す

pattern = Word(alphas).set_results_name('name', list_all_matches = True) + '&' + Word(alphas).set_results_name('name')
result = pattern.parse_string(target)
print(result['name'])
# ['Ant', 'Dec']
# 同名の要素が存在する場合、いずれかのメソッドでlist_all_matchesがTrueになっていれば全ての要素を返す

pattern = Word(alphas).set_results_name('ant') + '&' + Word(alphas).set_results_name('dec')
result = pattern.parse_string(target)
print(result['ant'])
# Ant
print(result['dec'])
# Dec
# もちろん異なる辞書型のキーを指定して良い

Literal クラス

特定の文字列(定数のようなもの)を定義するクラスです。
Literal('!')'!'は同じものとして扱われます。

似たようなクラスとして、大文字・小文字を気にしないCaselessLiteralクラスがあります。

from pyparsing import (
    alphas,
    Literal,
    Word,
)

# 「Hello 単語!」という形の文字列にマッチするパターンを定義
pattern = Literal('Hello') + Word(alphas) + Literal('!')

result = pattern.parse_string('Hello world!')
print(result)
# ['Hello', 'world', '!']

result = pattern.parse_string('Hello there!')
print(result)
# ['Hello', 'there', '!']

result = pattern.parse_string('Goodmorning there!')
# pyparsing.exceptions.ParseException: Expected 'Hello', found 'Goodmorning'
# Hello が無いのでパース処理は失敗する

Keyword クラス

特定の単語を定義するクラスです。
Literalクラスと似ていますが、単語として扱うかという点において違います。

似たようなクラスとして、大文字・小文字を気にしないCaselessKeywordクラスがあります。

from pyparsing import (
    alphas,
    Keyword,
    Word,
)

# 「単語 !」という形の文字列にマッチするパターンを定義(!は独立した単語)
pattern = Word(alphas) + Keyword('!')

result = pattern.parse_string('Hello !')
print(result)
# ['Hello', '!']

result = pattern.parse_string('Hello!')
# pyparsing.exceptions.ParseException: Expected Keyword '!', keyword was immediately preceded by keyword character, found 'o'
# 単語として認識してもらうには半角スペースが必要になる
# なお、Literalであれば上記の文字列でもマッチする

Word クラス

文字列を定義するクラスです。
後述するaplhasnumsと組み合わせて使います。

from pyparsing import (
    alphas,
    Word,
)

# 「単語 単語」という形の文字列にマッチするパターンを定義
pattern = Word(alphas) + Word(alphas)

result = pattern.parse_string('Hello world')
print(result)
# ['Hello', 'world']

result = pattern.parse_string('Hello there')
print(result)
# ['Hello', 'there']

result = pattern.parse_string('Hellothere')
# pyparsing.exceptions.ParseException: Expected W:(A-Za-z), found end of text
# 単語が1つしかないのでパース処理は失敗する

ZeroOrMore クラス

指定したパターンが0回以上繰り返すことを定義するクラスです。
ある一定のパターンが繰り返し登場する場合に使います。

# pyparsingのインポート
from pyparsing import (
    alphas,
    Word,
    ZeroOrMore,
)

# 「単語.」という形の文字列にマッチするパターンを定義
# ZeroOrMoreを使っているので、ドットはあってもなくても良い
pattern = Word(alphas) + ZeroOrMore('.')

result = pattern.parse_string('Hello')
print(result)
# ['Hello']

result = pattern.parse_string('Hello.')
print(result)
# ['Hello', '.']

result = pattern.parse_string('Hello...')
print(result)
# ['Hello', '.', '.', '.']

OneOrMore クラス

指定したパターンが1回以上繰り返すことを定義するクラスです。
ある一定のパターンが繰り返し登場する場合に使います。

# pyparsingのインポート
from pyparsing import (
    alphas,
    Word,
    OneOrMore,
)

# 「単語.」という形の文字列にマッチするパターンを定義
# OneOrMoreを使っているので、ドットは1つ以上なければいけない
pattern = Word(alphas) + OneOrMore('.')

result = pattern.parse_string('Hello')
# pyparsing.exceptions.ParseException: Expected '.', found end of text
# ドットがないのでパース処理は失敗する

result = pattern.parse_string('Hello.')
print(result)
# ['Hello', '.']

result = pattern.parse_string('Hello...')
print(result)
# ['Hello', '.', '.', '.']

Suppress クラス

指定したパターンをパース結果から除外することを定義するクラスです。
文字列の中にある特定の単語や区切り文字を無視したいときに使います。

# pyparsingのインポート
from pyparsing import (
    alphas,
    Word,
    Suppress,
)

# 「単語, 単語」という形の文字列にマッチするパターンを定義
pattern = Word(alphas) + ',' + Word(alphas)

result = pattern.parse_string('Hello, world')
print(result)
# ['Hello', ',', 'world']
# カンマもパース結果に出力される

# 結果に含めたくない要素(今回はカンマ)をSuppressで定義
pattern = Word(alphas) + Suppress(',') + Word(alphas)

result = pattern.parse_string('Hello, world')
print(result)
# ['Hello', 'world']
# カンマはパース結果に出力されなくなる

Group クラス

指定したパターンが1つのグループであることを定義するクラスです。
複雑なパターンの定義や意味ごとに区切りたいときに使います。

# pyparsingのインポート
from pyparsing import (
    alphas,
    nums,
    Word,
    Group,
    Suppress,
)

# 「単語:単語,」という形の文字列にマッチするパターンを定義
w = Word(alphas) + Suppress(':') + Word(nums) +  Suppress(',')
# 上記のパターンを3つ繰り返す文字列にマッチするパターンを定義
pattern = Group(w) + Group(w) + Group(w)

result = pattern.parse_string('one: 1, two: 2, three: 3,')
print(result)
# [['one', '1'], ['two', '2'], ['three', '3']]
# Groupで定義したものはリストとして1つにまとめられる
# カンマはSuppressで定義しているので結果に含まれない

定数

以下の定数は主にパターンの定義に使用します。

  • alphas
    abcdefghijklmnopqrstuvwxyzおよびABCDEFGHIJKLMNOPQRSTUVWXYZを表す定数です。
    Pythonのstring.ascii_lettersと同等です。
  • nums
    0123456789を表す定数です。
    Pythonのstring.digitsと同等です。
  • alphanums
    前述のalphasnumsを合わせた定数です。
  • printables
    Pythonのstring.printableからstring.whitespaceを除いた定数です。
    前述のalphanumsと記号(string.punctuation)を合わせたものになります。

Pythonのstringライブラリについては以下を参照してください。
(Pythonのバージョンに注意)


https://docs.python.org/ja/3.10/library/string.html

使用例

pyparsingの使用例としてpsコマンドの結果をパース処理するプログラムを紹介します。

目標は「データ部分をPID, TTY, TIME, CMDの4種類に分ける」です。

    PID TTY          TIME CMD
   2818 pts/0    00:00:00 bash
   2846 pts/0    00:00:00 ps
# pyparsingのインポート
from pyparsing import (
    alphas,
    nums,
    printables,
    Word,
    Keyword,
)

# psコマンドの結果(行単位で読み込んだと想定)
targets = [
    '    PID TTY          TIME CMD',
    '   2818 pts/0    00:00:00 bash',
    '   2846 pts/0    00:00:00 ps',
]

# パターンの定義
header = Keyword('PID') + Keyword('TTY') + Keyword('TIME') + Keyword('CMD')
data = Word(nums) + Word(alphas + nums + '/') + Word(nums + ':') + Word(printables)
patterns = [header, data]

# パース結果を保持するためのリスト
parse_results = []

# パース処理
for target in targets:
    for pattern in patterns:
        try:
            # パターンを1つずつ試していく
            result = pattern.parse_string(target)
        except ParseException:
            # パースに失敗した場合、次のパターンを試す
            continue
        else:
            # パースに成功した場合、その結果をリストに追加する
            parse_results.append(list(result))

# パース結果の表示
for result in parse_results:
    print(result)

上記のプログラムを動かすと、以下のような出力が得られます。

['PID', 'TTY', 'TIME', 'CMD']
['2818', 'pts/0', '00:00:00', 'bash']
['2846', 'pts/0', '00:00:00', 'ps']

意図した通り、PID, TTY, TIME, CMDの4種類に分けることが出来ていると思います。

おわりに

本記事では、正規表現が苦手な人向けに、Pythonの構文解析ライブラリの1つであるpyparsingの使用方法について解説しました。

私は正規表現の書き方をまったく覚えられない(覚えていない)人間なので、こういうライブラリがあるのは嬉しいなと思っています。

この記事が皆様のプログラミングライフの助けになれば幸いです。

参考文献

pyparsingの公式ドキュメント


https://pyparsing-docs.readthedocs.io/en/latest/

pyparsingのGitHub上にあるwiki


https://github.com/pyparsing/pyparsing/wiki

pyparsingのGitHub上にあるpyparsingを使ったサンプル
より詳しい使用例が見たい人はこちらから


https://github.com/pyparsing/pyparsing/tree/master/examples

PyPIのpyparsingのページ
pyparsingのインストールはこちらから


https://pypi.org/project/pyparsing/

Discussion