Convertir, o formatear, una cadena a variables (como format (), pero a la inversa) en Python

Tengo cadenas del formulario Version 1.4.0\n y Version 1.15.6\n , y me gustaría una forma sencilla de extraer los tres números de ellos. Sé que puedo poner variables en una cadena con el método de formato; Básicamente quiero hacer eso al revés, así:

 # 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 

Alguien más que encontré hizo la misma pregunta, pero la respuesta fue específica para su caso particular: Use la cadena de formato Python en sentido inverso para el análisis

¡Una respuesta general (cómo hacer el format() a la inversa) sería genial! Una respuesta para mi caso específico sería muy útil también.

En realidad, la biblioteca de expresiones regulares de Python ya proporciona la funcionalidad general que está solicitando. Solo tienes que cambiar la syntax del patrón ligeramente

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

Como puede ver, tiene que cambiar las cadenas de formato (un) de {0} a (? P <_0>. +). Incluso podrías requerir un decimal con (? P <_0> \ d +). Además, tiene que escapar de algunos de los caracteres para evitar que se interpreten como caracteres especiales de expresiones regulares. Pero esto en turm puede ser automatizado de nuevo por ejemplo con

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

Solo para aprovechar la respuesta de Uche , estaba buscando una manera de revertir una cadena a través de un patrón con kwargs. Así que armé la siguiente función:

 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 

Que funciona según:

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

Esta

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

te dará valores int para a , c

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

Dependiendo de la regularidad o irregularidad de los formatos de su número / versión, es posible que desee considerar el uso de expresiones regulares , aunque si se mantienen en este formato, prefiero la solución más simple si le funciona. .

Hace algún tiempo hice el siguiente código que hace lo contrario al formato, pero limitado a los casos que necesitaba.

Y , nunca lo probé, pero creo que este es también el propósito de la parse library

Mi código:

 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 st '{}_{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} #  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 

Para su caso, aquí hay un ejemplo de uso:

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

Observe el filter(None,v) para eliminar las versiones no analizables (que devuelven Ninguno). Aquí no es necesario.

EDITAR: También vea esta respuesta para obtener un poco más de información sobre parmatter y parmatter .

El paquete pypi parse sirve bien para este propósito:

 pip install parse 

Se puede utilizar de esta manera:

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

Tenga en cuenta que los documentos dicen que el paquete parse no emula EXACTAMENTE la mini-lengua de especificación de formato por defecto; también utiliza algunos indicadores de tipo especificados por re . De especial interés es que s significa “espacios en blanco” por defecto, en lugar de str . Esto se puede modificar fácilmente para que sea coherente con la especificación de formato cambiando el tipo predeterminado de s a str (usando extra_types ):

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

Aquí hay una idea conceptual para una modificación de la string.Formatter la string.Formatter incorporada usando el paquete parse para agregar la capacidad de unformat que yo mismo he usado:

 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__ 

IMPORTANTE: el parse nombre del método ya está en uso por la clase Formatter , por lo que he elegido unformat para evitar conflictos.

ACTUALIZACIÓN: Podría usarlo así, muy similar a la clase string.Formatter .

Formato (idéntico a '{:d} {:d}'.format(1, 2) ):

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

Sin formato

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

Esto es, por supuesto, de uso muy limitado como se muestra arriba. Sin embargo, he puesto un paquete pypi ( parmatter , un proyecto originalmente para mi propio uso, pero tal vez a otros les resulte útil) que explora algunas ideas de cómo poner esta idea en un trabajo más útil. El paquete se basa en gran medida en el paquete parse antes mencionado.

Aquí hay una solución en caso de que no quiera usar el módulo de análisis. Convierte cadenas de formato en expresiones regulares con grupos nombrados. Hace algunas suposiciones (descritas en la cadena de documentación) que estuvieron bien en mi caso, pero puede que no estén bien en la suya.

 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 (ie 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 (ie '{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}