字句解析と構文解析
文字列をトークンへ分け、演算のまとまりと括弧の対応を学びます。
説明
プログラムの文字列は、まず数値や演算子などのトークンに分けられます。これが字句解析です。構文解析はトークンの並びを文法に従って組み立て、どの演算がどの部分を対象にするかという木構造を作ります。
12 + 3 * (4 - 1)をトークンへ分け、Pythonの構文木で加算・乗算・減算の関係を表示します。次に括弧だけをスタックで検査し、閉じ括弧が早すぎる場合や開き括弧が残る場合を調べます。文字の分割、括弧の対応、式全体の文法は別の検査です。
1数字のまとまりを一つにする
空白を除き、連続するASCII数字を一つの数値トークンとして取り出します。この式で使う演算子と括弧も個別に取り出します。
import re文字列のパターン処理; expression = '12 + 3 * (4 - 1)'対象の式; tokens = re.findall(r'[0-9]+|[+*()-]', expression)整数・演算子・括弧を抽出; tokensトークン列12は1と2の二つではなく一つのトークンです。この簡単な抽出式は対象外の文字を無視するため、一般の入力検証にはそのまま使えません。今回は指定した式の構成を調べます。
2同じ文字でも優先順位がある
抽象構文木を作り、階層付きで表示します。ast.parseは解析だけを行い、式の値は計算しません。
import ast構文木を扱う; tree = ast.parse(expression, mode='eval')単一式として解析; print(ast.dump(tree, indent=2))木を階層表示最上位の演算はAddです。左が12、右はMultで、乗算の右側にSub(4,1)があります。括弧そのものは木のノードとして残らず、減算が乗算の子になる構造へ反映されています。
3木の各部分を取り出す
根の演算、右の演算、その右の演算の種類を取り出します。
type(tree.body.op).__name__根の演算, type(tree.body.right.op).__name__右部分の演算, type(tree.body.right.right.op).__name__さらに右の演算, 12 + 3 * (4 - 1)同じ式の値演算はAdd・Mult・Sub、計算値は21です。減算4-1を乗算の右引数にすることが、木の親子関係として表されています。
4括弧を変えると木が変わる
(12 + 3) * 4 - 1では、根が減算になります。文字として同じ数を使っていても、まとまりが変わります。
other = ast.parse('(12 + 3) * 4 - 1', mode='eval')括弧を変更; print(ast.dump(other, indent=2))別の木; (12 + 3) * 4 - 1変更後の値計算値は59です。根Subの左がMult、その左がAddになっています。式を文字列の左から順に機械的に計算するだけでは、この優先順位を扱えません。
5括弧だけをスタックで調べる
開き括弧の位置を積み、閉じ括弧で直近の位置を取り出します。開きがないのに閉じが来たら、その位置で失敗します。
def brackets(text):括弧の検査
stack, trace = [], []開き位置と対応記録
for i, ch in enumerate(text):文字位置を走査
if ch == '(': stack.append(i)開きを積む
elif ch == ')':閉じを処理
if not stack: return False, i, trace対応する開きがない
trace.append((stack.pop(), i))直近の開きと対応
return not stack, stack, trace未閉鎖の有無と対応表対応した開き・閉じの位置をtraceに残します。最後にstackが空なら括弧は対応済み、残っていれば未閉鎖の開き括弧があります。数値や演算子の文法までは調べません。
6入れ子と二種類の不一致
((1)+2)、1)、(1+2を順に検査します。
[(text, brackets(text)) for text in ['((1)+2)', '1)', '(1+2']]正常・余分な閉じ・未閉鎖正常な式は内側(1,3)、外側(0,6)の順に対応します。1)は位置1で開きが足りず、(1+2は位置0の開きが最後まで残ります。最後に開いた括弧から閉じるため、スタックが適しています。
7括弧が合っても式とは限らない
1 + * 2には括弧の不一致がありませんが、演算子の並びが文法違反です。解析エラーの種類だけを返す関数で調べます。
def parse_status(text):式の解析結果
try:解析を試す
ast.parse(text, mode='eval')単一式の文法検査
return 'accepted'解析成功
except SyntaxError as error:構文エラーを捕捉
return type(error).__name__例外の種類を返す構文エラーは入力を解析する段階で分かります。括弧だけの検査では、演算子の引数が足りないことを検出できません。
8検査の対象を照合
正常な式、演算子の不正な並び、未閉鎖括弧を調べます。
[(text, brackets(text)[0], parse_status(text)) for text in [expression, '1 + * 2', '(1+2']]括弧検査と構文解析を比較正常式だけが解析に成功します。1 + * 2は括弧検査TrueでもSyntaxErrorです。字句・括弧・構文はそれぞれ異なる条件を調べており、一つの検査が通っても全ての妥当性は保証されません。
まとめ
字句解析は数値や演算子へ分け、構文解析はそれらの関係を木にします。
演算の優先順位や括弧は木の親子関係へ反映されます。
括弧の対応はスタックで調べられますが、式全体の文法には別の解析が必要です。
操作履歴と解説を振り返れます。
Python 3
import re; expression = '12 + 3 * (4 - 1)'; tokens = re.findall(r'[0-9]+|[+*()-]', expression); tokens['12', '+', '3', '*', '(', '4', '-', '1', ')']import ast; tree = ast.parse(expression, mode='eval'); print(ast.dump(tree, indent=2))Expression(
body=BinOp(
left=Constant(value=12),
op=Add(),
right=BinOp(
left=Constant(value=3),
op=Mult(),
right=BinOp(
left=Constant(value=4),
op=Sub(),
right=Constant(value=1)))))type(tree.body.op).__name__, type(tree.body.right.op).__name__, type(tree.body.right.right.op).__name__, 12 + 3 * (4 - 1)('Add', 'Mult', 'Sub', 21)other = ast.parse('(12 + 3) * 4 - 1', mode='eval'); print(ast.dump(other, indent=2)); (12 + 3) * 4 - 1Expression(
body=BinOp(
left=BinOp(
left=BinOp(
left=Constant(value=12),
op=Add(),
right=Constant(value=3)),
op=Mult(),
right=Constant(value=4)),
op=Sub(),
right=Constant(value=1)))
59def brackets(text):
stack, trace = [], []
for i, ch in enumerate(text):
if ch == '(': stack.append(i)
elif ch == ')':
if not stack: return False, i, trace
trace.append((stack.pop(), i))
return not stack, stack, trace[(text, brackets(text)) for text in ['((1)+2)', '1)', '(1+2']][('((1)+2)', (True, [], [(1, 3), (0, 6)])), ('1)', (False, 1, [])), ('(1+2', (False, [0], []))]def parse_status(text):
try:
ast.parse(text, mode='eval')
return 'accepted'
except SyntaxError as error:
return type(error).__name__[(text, brackets(text)[0], parse_status(text)) for text in [expression, '1 + * 2', '(1+2']][('12 + 3 * (4 - 1)', True, 'accepted'), ('1 + * 2', True, 'SyntaxError'), ('(1+2', False, 'SyntaxError')]演習完了