簡體   English   中英

在 Python 中將字符串轉換或取消格式化為變量(如 format(),但相反)

[英]Convert, or unformat, a string to variables (like format(), but in reverse) in Python

我有Version 1.4.0\\nVersion 1.15.6\\n形式的字符串,我想要一種從它們中提取三個數字的簡單方法。 我知道我可以使用 format 方法將變量放入字符串中; 我基本上想倒退,像這樣:

# So I know I can do this:
x, y, z = 1, 4, 0
print 'Version {0}.{1}.{2}\n'.format(x,y,z)
# Output is 'Version 1.4.0\n'

# But I'd like to be able to reverse it:

mystr='Version 1.15.6\n'
a, b, c = mystr.unformat('Version {0}.{1}.{2}\n')

# And have the result that a, b, c = 1, 15, 6

我發現的其他人問了同樣的問題,但答復是針對他們的特定情況的: 反向使用 Python 格式字符串進行解析

一般答案(如何反向執行format() )會很棒! 不過,我的具體案例的答案也會非常有幫助。

只是為了建立在Uche 的回答基礎上,我正在尋找一種通過 kwargs 模式反轉字符串的方法。 所以我整理了以下功能:

def string_to_dict(string, pattern):
    regex = re.sub(r'{(.+?)}', r'(?P<_\1>.+)', pattern)
    values = list(re.search(regex, string).groups())
    keys = re.findall(r'{(.+?)}', pattern)
    _dict = dict(zip(keys, values))
    return _dict

其工作原理如下:

>>> p = 'hello, my name is {name} and I am a {age} year old {what}'

>>> s = p.format(name='dan', age=33, what='developer')
>>> s
'hello, my name is dan and I am a 33 year old developer'
>>> string_to_dict(s, p)
{'age': '33', 'name': 'dan', 'what': 'developer'}

>>> s = p.format(name='cody', age=18, what='quarterback')
>>> s
'hello, my name is cody and I am a 18 year old quarterback'
>>> string_to_dict(s, p)
{'age': '18', 'name': 'cody', 'what': 'quarterback'}
>>> import re
>>> re.findall('(\d+)\.(\d+)\.(\d+)', 'Version 1.15.6\n')
[('1', '15', '6')]

編輯:另請參閱此答案,了解有關parseparmatter更多信息。

pypi 包parse很好地服務於這個目的:

pip install parse

可以這樣使用:

>>> import parse
>>> result=parse.parse('Version {0}.{1}.{2}\n', 'Version 1.15.6\n')
<Result ('1', '15', '6') {}>
>>> values=list(result)
>>> print(values)
['1', '15', '6']

請注意,文檔說parse包默認情況下不會完全模擬格式規范迷你語言 它還使用一些由re指定的類型指示符。 特別要注意的是, s默認表示“空白”,而不是str 通過將s的默認類型更改為str (使用extra_types ),可以輕松地將其修改為與格式規范一致:

result = parse.parse(format_str, string, extra_types=dict(s=str))

這是使用parse包修改string.Formatter內置類以添加我自己使用的unformat功能的概念性想法:

import parse
from string import Formatter
class Unformatter(Formatter):
    '''A parsable formatter.'''
    def unformat(self, format, string, extra_types=dict(s=str), evaluate_result=True):
        return parse.parse(format, string, extra_types, evaluate_result)
    unformat.__doc__ = parse.Parser.parse.__doc__

重要提示:方法名稱parse已經被Formatter類使用,所以我選擇了unformat來避免沖突。

更新:您可以像這樣使用它 - 與string.Formatter類非常相似。

格式(與'{:d} {:d}'.format(1, 2) ):

>>> formatter = Unformatter() 
>>> s = formatter.format('{:d} {:d}', 1, 2)
>>> s
'1 2' 

取消格式化:

>>> result = formatter.unformat('{:d} {:d}', s)
>>> result
<Result (1, 2) {}>
>>> tuple(result)
(1, 2)

這當然是非常有限的用途,如上所示。 但是,我已經提供了一個 pypi 包( parmatter - 一個最初供我自己使用的項目,但也許其他人會發現它有用),它探索了如何將這個想法用於更有用的工作的一些想法。 該包在很大程度上依賴於上述parse包。 編輯:在我的帶下幾年的經驗之后,我意識到parmatter (我的第一個包裹!)是一個可怕的、令人尷尬的想法,並已將其刪除。

實際上,Python 正則表達式庫已經提供了您所要求的一般功能。 你只需要稍微改變模式的語法

>>> import re
>>> from operator import itemgetter
>>> mystr='Version 1.15.6\n'
>>> m = re.match('Version (?P<_0>.+)\.(?P<_1>.+)\.(?P<_2>.+)', mystr)
>>> map(itemgetter(1), sorted(m.groupdict().items()))
['1', '15', '6']

如您所見,您必須將(取消)格式字符串從 {0} 更改為 (?P<_0>.+)。 你甚至可以要求一個帶有 (?P<_0>\\d+) 的小數。 此外,您必須對某些字符進行轉義以防止它們被解釋為正則表達式特殊字符。 但這反過來可以再次自動化,例如

>>> re.sub(r'\\{(\d+)\\}', r'(?P<_\1>.+)', re.escape('Version {0}.{1}.{2}'))
'Version\\ (?P<_0>.+)\\.(?P<_1>.+)\\.(?P<_2>.+)'

前段時間我制作了下面的代碼,它與格式相反,但僅限於我需要的情況。

而且,我從來沒有嘗試過,但我認為這也是parse library的目的

我的代碼:

import string
import re

_def_re   = '.+'
_int_re   = '[0-9]+'
_float_re = '[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?'

_spec_char = '[\^$.|?*+()'

def format_parse(text, pattern):
    """
    Scan `text` using the string.format-type `pattern`

    If `text` is not a string but iterable return a list of parsed elements

    All format-like pattern cannot be process:
      - variable name cannot repeat (even unspecified ones s.t. '{}_{0}')
      - alignment is not taken into account
      - only the following variable types are recognized:
           'd' look for and returns an integer
           'f' look for and returns a  float

    Examples::

        res = format_parse('the depth is -42.13', 'the {name} is {value:f}')
        print res
        print type(res['value'])
        # {'name': 'depth', 'value': -42.13}
        # <type 'float'>

        print 'the {name} is {value:f}'.format(**res)
        # 'the depth is -42.130000'

        # Ex2: without given variable name and and invalid item (2nd)
        versions = ['Version 1.4.0', 'Version 3,1,6', 'Version 0.1.0']
        v = format_parse(versions, 'Version {:d}.{:d}.{:d}')
        # v=[{0: 1, 1: 4, 2: 0}, None, {0: 0, 1: 1, 2: 0}]

    """
    # convert pattern to suitable regular expression & variable name
    v_int = 0   # available integer variable name for unnamed variable 
    cur_g = 0   # indices of current regexp group name 
    n_map = {}  # map variable name (keys) to regexp group name (values)
    v_cvt = {}  # (optional) type conversion function attached to variable name
    rpattern = '^'    # stores to regexp pattern related to format pattern        

    for txt,vname, spec, conv in string.Formatter().parse(pattern):
        # process variable name
        if len(vname)==0:
            vname = v_int
            v_int += 1
        if vname not in n_map:
            gname = '_'+str(cur_g)
            n_map[vname] = gname
            cur_g += 1                   
        else:    
            gname = n_map[vname]

        # process type of required variables 
        if   'd' in spec: vtype = _int_re;   v_cvt[vname] = int
        elif 'f' in spec: vtype = _float_re; v_cvt[vname] = float
        else:             vtype = _def_re;

        # check for regexp special characters in txt (add '\' before)
        txt = ''.join(map(lambda c: '\\'+c if c in _spec_char else c, txt))

        rpattern += txt + '(?P<'+gname+'>' + vtype +')'

    rpattern += '$'

    # replace dictionary key from regexp group-name to the variable-name 
    def map_result(match):
        if match is None: return None
        match = match.groupdict()
        match = dict((vname, match[gname]) for vname,gname in n_map.iteritems())
        for vname, value in match.iteritems():
            if vname in v_cvt:
                match[vname] = v_cvt[vname](value)
        return match

    # parse pattern
    if isinstance(text,basestring):
        match = re.search(rpattern, text)
        match = map_result(match)
    else:
        comp  = re.compile(rpattern)
        match = map(comp.search, text)
        match = map(map_result, match)

    return match

對於您的情況,這是一個使用示例:

versions = ['Version 1.4.0', 'Version 3.1.6', 'Version 0.1.0']
v = format_parse(versions, 'Version {:d}.{:d}.{:d}')
# v=[{0: 1, 1: 4, 2: 0}, {0: 3, 1: 1, 2: 6}, {0: 0, 1: 1, 2: 0}]

# to get the versions as a list of integer list, you can use:
v = [[vi[i] for i in range(3)] for vi in filter(None,v)]

注意filter(None,v)刪除不可解析的版本(返回 None)。 這里沒有必要。

a, b, c = (int(i) for i in mystr.split()[1].split('.'))

將為您提供abc int

>>> a
1
>>> b
15
>>> c
6

根據您的數字/版本格式的規則或不規則(即一致性)的程度,您可能需要考慮使用正則表達式,但如果它們保持這種格式,如果對您有用,我更喜歡更簡單的解決方案.

如果您不想使用 parse 模塊,這里有一個解決方案。 它將格式字符串轉換為具有命名組的正則表達式。 它做出了一些假設(在文檔字符串中描述),在我的情況下是可以的,但在你的情況下可能不行。

def match_format_string(format_str, s):
    """Match s against the given format string, return dict of matches.

    We assume all of the arguments in format string are named keyword arguments (i.e. no {} or
    {:0.2f}). We also assume that all chars are allowed in each keyword argument, so separators
    need to be present which aren't present in the keyword arguments (i.e. '{one}{two}' won't work
    reliably as a format string but '{one}-{two}' will if the hyphen isn't used in {one} or {two}).

    We raise if the format string does not match s.

    Example:
    fs = '{test}-{flight}-{go}'
    s = fs.format('first', 'second', 'third')
    match_format_string(fs, s) -> {'test': 'first', 'flight': 'second', 'go': 'third'}
    """

    # First split on any keyword arguments, note that the names of keyword arguments will be in the
    # 1st, 3rd, ... positions in this list
    tokens = re.split(r'\{(.*?)\}', format_str)
    keywords = tokens[1::2]

    # Now replace keyword arguments with named groups matching them. We also escape between keyword
    # arguments so we support meta-characters there. Re-join tokens to form our regexp pattern
    tokens[1::2] = map(u'(?P<{}>.*)'.format, keywords)
    tokens[0::2] = map(re.escape, tokens[0::2])
    pattern = ''.join(tokens)

    # Use our pattern to match the given string, raise if it doesn't match
    matches = re.match(pattern, s)
    if not matches:
        raise Exception("Format string did not match")

    # Return a dict with all of our keywords and their values
    return {x: matches.group(x) for x in keywords}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM