web-dev-qa-db-fra.com

Convertir ou déformater une chaîne en variables (comme format (), mais en sens inverse) en Python

J'ai des chaînes de caractères de la forme Version 1.4.0\n et Version 1.15.6\n, et j'aimerais un moyen simple d'en extraire les trois nombres. Je sais que je peux mettre des variables dans une chaîne avec la méthode de formatage; Je veux fondamentalement faire cela à l'envers, comme ceci:

# 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

Quelqu'un d’autre que j’ai trouvé a posé la même question, mais la réponse était spécifique à leur cas particulier: Utilisez la chaîne de format Python à l’inverse pour analyser

Une réponse générale (comment faire format() en sens inverse) serait génial! Une réponse à mon cas spécifique serait également très utile.

26
evsmith

En fait, la bibliothèque d’expressions régulières Python fournit déjà les fonctionnalités générales que vous demandez. Il vous suffit de modifier légèrement la syntaxe du motif

>>> 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']

Comme vous pouvez le constater, vous devez changer les chaînes de formatage (un) de {0} à (? P <_0>. +). Vous pourriez même exiger une décimale avec (? P <_0>\d +). De plus, vous devez échapper certains caractères pour les empêcher d’être interprétés comme des caractères spéciaux regex. Mais cela peut être automatisé à nouveau, par exemple. avec

>>> re.sub(r'\\{(\d+)\\}', r'(?P<_\1>.+)', re.escape('Version {0}.{1}.{2}'))
'Version\\ (?P<_0>.+)\\.(?P<_1>.+)\\.(?P<_2>.+)'
0
Uche
>>> import re
>>> re.findall('(\d+)\.(\d+)\.(\d+)', 'Version 1.15.6\n')
[('1', '15', '6')]
8
Willian

Juste pour construire la réponse de Uche , je cherchais un moyen d'inverser une chaîne via un modèle avec kwargs. J'ai donc mis en place la fonction suivante:

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

Qui fonctionne selon:

>>> 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'}
5
DanH

EDIT: Voir aussi cette réponse pour un peu plus d’informations sur parse et parmatter.

Le paquet pypi parse sert bien cet objectif:

pip install parse

Peut être utilisé comme ceci:

>>> 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']

Notez que les documents disent _ le package parse n'émule pas EXACTEMENT le mini-langage de spécification de format par défaut; il utilise également des indicateurs de type spécifiés par re. Il convient de noter que s signifie "espace" par défaut, plutôt que str. Cela peut être facilement modifié pour être cohérent avec la spécification de format en remplaçant le type par défaut pour s par str (en utilisant extra_types):

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

Voici une idée conceptuelle pour une modification de la classe intégrée string.Formatter à l'aide du package parse afin d'ajouter la fonctionnalité unformat que j'ai moi-même utilisée:

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__

IMPORTANT: le nom de la méthode parse est déjà utilisé par la classe Formatter. J'ai donc choisi unformat pour éviter les conflits.

UPDATE: Vous pourriez l'utiliser comme ceci - très similaire à la classe string.Formatter .

Formatage (identique à '{:d} {:d}'.format(1, 2)):

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

Unformatting:

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

Ceci est bien sûr d'une utilisation très limitée, comme indiqué ci-dessus. Cependant, j'ai mis en place un package pypi ( parmatter - un projet initialement destiné à mon usage personnel, mais peut-être que d'autres le trouveront utile), qui explore quelques idées sur la manière de mettre cette idée à profit pour un travail plus utile. Le paquet repose largement sur le paquet parse susmentionné.

2
Rick Teachey

Ce

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

vous donnera les valeurs int pour a, b et c

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

En fonction de la régularité ou de l'irrégularité de vos formats de numéro/version, vous pouvez envisager l'utilisation de expressions régulières , bien que s'ils restent dans ce format, je serais favorable à la solution plus simple si cela fonctionne pour vous.

2
Levon

Il y a quelque temps, j'ai créé le code ci-dessous qui inverse le format mais se limite aux cas dont j'avais besoin.

Et , je n'ai jamais essayé, mais je pense que c'est aussi le but du parse library

Mon code:

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

pour votre cas, voici un exemple d'utilisation:

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)]

Notez la filter(None,v) pour supprimer les versions non analysables (qui ne renvoient aucune). Ici ce n'est pas nécessaire.

2
Juh_