3
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【Python2/3】書式文字列をパースする

3
Last updated at Posted at 2017-09-18

Cのsprintfに相当する機能が、Pythonには今のところ3種類もあります。(f-stringは書式文字列を使わないので除外)

名称? 使い方
%-formatting fmt % values
str.format() fmt.format(*values) / fmt.format(**values)
Template strings string.Template(fmt).substitute(**values)

この記事では、上の表の values部分に要求されるタプルの要素数、または辞書のキーを調べる方法をまとめます。
正直、かなり使用頻度の低い知識だと思いますが、logging.Formatterを継承して自作のFormatterを作るときなどではもしかしたら役に立つかもしれません。

概要

  • %-formatting → formatを失敗させて例外を見る。
  • str.formatstring.Formatter().parser(fmt)
  • Template string → string.Template.pattern.finditer(fmt)

%-formatting (printf style formatting)

他の2つとは違ってパースするための専用機能が標準パッケージに見当たらないので自分で頑張ります。
今回は投げ返される例外をもとに解読する関数を作ってみました。fmt % valuesvalues の部分に要求されている型が、タプルならその長さを、辞書ならそのキーを返します。

def parse_printf_style_format(fmt):
    if not isinstance(fmt, (bytes, str)):
        raise TypeError('got ' + type(fmt).__name__)
        
    try:
        fmt % ()
    except TypeError as e:
        if e.args[0] == 'not enough arguments for format string':
            values = ()
        elif e.args[0] == 'format requires a mapping':
            values = {}
        else:
            raise
    else:
        return None
    
    if isinstance(values, tuple):
        while True:
            try:
                fmt % values
            except TypeError as e:
                if e.args[0] == 'not enough arguments for format string':
                    values += (0,)
                else:
                    raise ValueError('invalid format: ' + repr(fmt))
            else:
                return len(values)
    elif isinstance(values, dict):
        while True:
            try:
                fmt % values
            except TypeError as e:
                if e.args[0] == 'not enough arguments for format string':
                    raise ValueError('invalid format: ' + repr(fmt))
                else:
                    raise
            except KeyError as e:
                values[e.args[0]] = 0
            else:
                return tuple(values.keys())
    else:
        assert False

使用例

>>> parse_printf_style_format('%d %s %x')
3
>>> parse_printf_style_format('%(foo)s, %(bar)d')
('foo', 'bar')

str.format()

string.Formatter().parse(fmt) で一撃です。詳しくは公式ドキュメントを参照してください。

>>> import string
>>> fmt = '{foo:s}, {{bar:d}}, and {:f}'
>>> list(string.Formatter().parse(fmt))
[('', 'foo', 's', None),
 (', {', None, None, None),
 ('bar:d}', None, None, None),
 (', and ', '', 'f', None)]

Template strings

string.Template.pattern.finditer(fmt) で一撃です。
プレースホルダにマッチする正規表現がpattern属性に格納されています。

>>> import string
>>> fmt = '${this_is_braced} $$this_is_escaped $@this_is_invalid $this_is_named'
>>> print(string.Template.pattern.pattern)
    \$(?:
      (?P<escaped>\$) |   # Escape sequence of two delimiters
      (?P<named>[_a-z][_a-z0-9]*)      |   # delimiter and a Python identifier
      {(?P<braced>[_a-z][_a-z0-9]*)}   |   # delimiter and a braced identifier
      (?P<invalid>)              # Other ill-formed delimiter exprs
    )
>>> [match.groupdict() for match in string.Template.pattern.finditer(fmt)]
[{'braced': 'this_is_braced', 'escaped': None, 'invalid': None, 'named': None},
 {'braced': None, 'escaped': '$', 'invalid': None, 'named': None},
 {'braced': None, 'escaped': None, 'invalid': '', 'named': None},
 {'braced': None, 'escaped': None, 'invalid': None, 'named': 'this_is_named'}]

所感

%-fomattingの冷遇され方がひどい。

3
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?