2009-11-29 25 views
6

¿Cuál es la forma más sencilla de convertir una cadena de palabras clave = valores de un diccionario, por ejemplo, la siguiente cadena:Una manera sencilla de convertir una cadena en un diccionario

name="John Smith", age=34, height=173.2, location="US", avatar=":,=)" 

al diccionario pitón siguiente:

{'name':'John Smith', 'age':34, 'height':173.2, 'location':'US', 'avatar':':,=)'} 

La clave 'avatar' es solo para mostrar que las cadenas pueden contener = y, por lo que una simple 'división' no funcionará. ¿Algunas ideas? ¡Gracias!

Respuesta

9

Esto funciona para mí:

# get all the items 
matches = re.findall(r'\w+=".+?"', s) + re.findall(r'\w+=[\d.]+',s) 

# partition each match at '=' 
matches = [m.group().split('=', 1) for m in matches] 

# use results to make a dict 
d = dict(matches) 
+0

Esto funciona: simplemente agregue rutinas para convertir los valores finales en cadenas/ints, etc., y quizás elimine las comillas dobles no deseadas incluidas en los valores. – twneale

+0

Muy bien, gracias! Sabía que las expresiones regulares serían la respuesta, ¡pero nunca logré aprender a usarlas de manera eficiente! – astrofrog

+5

Créanme amigo, valen la pena el esfuerzo. ¡Encuentre un buen probador de expresiones regulares interactivo (como redemo.py) y moje sus pies! – twneale

4

Editar: desde el módulo csv no trata como se desee con cotizaciones dentro campos, se tarda un poco más de trabajo para implementar esta funcionalidad:

import re 
quoted = re.compile(r'"[^"]*"') 

class QuoteSaver(object): 

    def __init__(self): 
    self.saver = dict() 
    self.reverser = dict() 

    def preserve(self, mo): 
    s = mo.group() 
    if s not in self.saver: 
     self.saver[s] = '"%d"' % len(self.saver) 
     self.reverser[self.saver[s]] = s 
    return self.saver[s] 

    def expand(self, mo): 
    return self.reverser[mo.group()] 

x = 'name="John Smith", age=34, height=173.2, location="US", avatar=":,=)"' 

qs = QuoteSaver() 
y = quoted.sub(qs.preserve, x) 
kvs_strings = y.split(',') 
kvs_pairs = [kv.split('=') for kv in kvs_strings] 
kvs_restored = [(k, quoted.sub(qs.expand, v)) for k, v in kvs_pairs] 

def converter(v): 
    if v.startswith('"'): return v.strip('"') 
    try: return int(v) 
    except ValueError: return float(v) 

thedict = dict((k.strip(), converter(v)) for k, v in kvs_restored) 
for k in thedict: 
    print "%-8s %s" % (k, thedict[k]) 
print thedict 

estoy emitiendo dos veces para thedict mostrar exactamente cómo y por qué difiere del resultado requerido; la salida es:

age  34 
location US 
name  John Smith 
avatar :,=) 
height 173.2 
{'age': 34, 'location': 'US', 'name': 'John Smith', 'avatar': ':,=)', 
'height': 173.19999999999999} 

Como se ve, la salida para el valor de punto flotante está conforme a lo solicitado cuando son emitidos directamente con print, pero no lo es y no puede ser (ya que noningún punto flotante valor que se mostrará 173.2 en tal caso! -) cuando se aplica el print a todo el dict (debido a que utiliza inevitablemente repr sobre las claves y valores - y la repr de 173.2 tiene esa forma, teniendo en cuenta las cuestiones habituales sobre cómo flotante los valores de puntos se almacenan en binario, no en decimal, etc., etc.). Puede definir una subclase dict que anule __str__ en valores especiales de punto flotante, supongo, si eso es realmente un requisito.

Pero, espero que esta distracción no interfiera con la idea central: siempre que las doblescomillas estén correctamente equilibradas (y no haya dobles comillas dentro de las dobles), este código realiza la tarea requerida de preservar " caracteres especiales "(comas e iguales signos, en este caso) de tomarse en su sentido normal cuando están dentro de comillas dobles, incluso si las comillas dobles comienzan dentro de un" campo "en lugar de al principio del campo (csv solo trata de la última condición). Inserte algunas impresiones intermedias si la forma en que funciona el código no es obvia: primero cambia todos los "campos de comillas dobles" en un formulario especialmente simple ("0", "1" y así sucesivamente), mientras graba por separado los contenidos reales correspondientes a esos simples las formas son; al final, las formas simples se cambian nuevamente a los contenidos originales. La función simple converter maneja finalmente la eliminación de comillas dobles (para cadenas) y la transformación de las cadenas sin comillas en enteros o flotantes.

+0

En cuanto a la solución similar de Managu, esto no funciona si la cadena de la derecha contiene comas (que lo hacen en el caso con el que estoy trabajando). – astrofrog

+0

Tiene razón == csv no entiende las comillas "en el medio" de los campos. Déjame descubrir algo más y arreglar mi respuesta. –

-2

¿Siempre separados por coma? Utilice el módulo CSV para dividir la línea en partes (no verificado):

import csv 
import cStringIO 

parts=csv.reader(cStringIO.StringIO(<string to parse>)).next() 
+0

Esto no funciona en el caso en que una cadena en el lado derecho contenga una coma, p. en el caso 'avatar' de arriba. Sin embargo, una coma solo estará presente en el lado derecho si está entre comillas internas, por lo que tal vez se pueda tener en cuenta. – astrofrog

+0

Oh, muy bien. Pensé que CSV era más inteligente que eso. – Managu

+0

CSV * debe * tenerlo en cuenta si usa el dialecto correcto. –

1

El siguiente código produce el comportamiento correcto, pero es sólo un poco largo! He agregado un espacio en el avatar para mostrar que funciona bien con comas y espacios y signos de igualdad dentro de la cadena. Alguna sugerencia para acortarlo?

import hashlib 

string = 'name="John Smith", age=34, height=173.2, location="US", avatar=":, =)"' 

strings = {} 

def simplify(value): 
    try: 
     return int(value) 
    except: 
     return float(value) 

while True: 
    try: 
     p1 = string.index('"') 
     p2 = string.index('"',p1+1) 
     substring = string[p1+1:p2] 
     key = hashlib.md5(substring).hexdigest() 
     strings[key] = substring 
     string = string[:p1] + key + string[p2+1:] 
    except: 
     break 

d = {}  
for pair in string.split(', '): 
    key, value = pair.split('=') 
    if value in strings: 
     d[key] = strings[value] 
    else: 
     d[key] = simplify(value) 

print d  
0

Creo que sólo necesita fijar maxsplit = 1, por ejemplo, el siguiente debe funcionar.

string = 'name="John Smith", age=34, height=173.2, location="US", avatar=":, =)"' 
newDict = dict(map(lambda(z): z.split("=",1), string.split(", "))) 

Editar (ver comentario):

no me di cuenta de que " 'era un valor bajo avatar, el mejor enfoque sería para escapar'," allí donde se está generando datos. Incluso mejor sería algo así como JSON;). Sin embargo, como alternativa a la expresión regular, puedes intentar usar shlex, que creo que produce un código de aspecto más limpio.

import shlex 

string = 'name="John Smith", age=34, height=173.2, location="US", avatar=":, =)"' 
lex = shlex.shlex (string) 
lex.whitespace += "," # Default whitespace doesn't include commas 
lex.wordchars += "." # Word char should include . to catch decimal 
words = [ x for x in iter(lex.get_token, '') ] 
newDict = dict (zip(words[0::3], words[2::3])) 
+0

me da este '{'': ')"', 'name': '"John Smith"', 'age': '34', 'height': '173.2', 'ubicación': '"US"', 'avatar': '":'}' – YOU

2

Aquí es un enfoque más detallado del problema utilizando pyparsing. Tenga en cuenta las acciones de análisis que hacen la conversión automática de tipos de cadenas a ints o flotantes. Además, la clase QuotedString quita implícitamente las comillas del valor cotizado. Finalmente, la clase Dict toma cada grupo 'key = val' en la lista delimitada por comas, y asigna nombres de resultados usando los tokens de clave y valor.

from pyparsing import * 

key = Word(alphas) 
EQ = Suppress('=') 
real = Regex(r'[+-]?\d+\.\d+').setParseAction(lambda t:float(t[0])) 
integer = Regex(r'[+-]?\d+').setParseAction(lambda t:int(t[0])) 
qs = QuotedString('"') 
value = real | integer | qs 

dictstring = Dict(delimitedList(Group(key + EQ + value))) 

Ahora para analizar su cadena de texto original, almacenar los resultados en dd. Pyparsing devuelve un objeto de tipo ParseResults, pero esta clase tiene muchas características similares a dict (soporte para keys(), items(), in, etc.), o puede emitir un dict verdadero de Python llamando a asDict(). Calling dump() muestra todos los tokens en la lista analizada original, más todos los elementos nombrados. Los últimos dos ejemplos de muestran cómo acceder a los elementos con nombre dentro de ParseResults como si fueran atributos de un objeto de Python.

text = 'name="John Smith", age=34, height=173.2, location="US", avatar=":,=)"' 
dd = dictstring.parseString(text) 
print dd.keys() 
print dd.items() 
print dd.dump() 
print dd.asDict() 
print dd.name 
print dd.avatar 

Lienzo:

['age', 'location', 'name', 'avatar', 'height'] 
[('age', 34), ('location', 'US'), ('name', 'John Smith'), ('avatar', ':,=)'), ('height', 173.19999999999999)] 
[['name', 'John Smith'], ['age', 34], ['height', 173.19999999999999], ['location', 'US'], ['avatar', ':,=)']] 
- age: 34 
- avatar: :,=) 
- height: 173.2 
- location: US 
- name: John Smith 
{'age': 34, 'height': 173.19999999999999, 'location': 'US', 'avatar': ':,=)', 'name': 'John Smith'} 
John Smith 
:,=) 
0

hacerlo paso a paso

d={} 
mystring='name="John Smith", age=34, height=173.2, location="US", avatar=":,=)"'; 
s = mystring.split(", ") 
for item in s: 
    i=item.split("=",1) 
    d[i[0]]=i[-1] 
print d 
1

Aquí es un enfoque con eval, he considerado que es tan poco fiable sin embargo, pero sus obras para su ejemplo.

>>> import re 
>>> 
>>> s='name="John Smith", age=34, height=173.2, location="US", avatar=":,=)"' 
>>> 
>>> eval("{"+re.sub('(\w+)=("[^"]+"|[\d.]+)','"\\1":\\2',s)+"}") 
{'age': 34, 'location': 'US', 'name': 'John Smith', 'avatar': ':,=)', 'height': 173.19999999999999} 
>>> 

Actualizar:

mejor utilizar el señalado por Chris Lutz en el comentario, creo que es más fiable, ya que incluso hay (simple/doble) cita en los valores de diccionario, puede ser que las obras .

+1

Si vas a usar 'eval' porque no solo haces' eval ("dict (" + s + ")") ' ? No necesitamos hacer ninguna sustitución de expresiones regulares aquí cuando Python ya admite esta sintaxis. –

+0

¡Uy! mi mal entonces – YOU

1

Aquí hay una versión algo más robusta de la solución de expresión regular:

import re 

keyval_re = re.compile(r''' 
    \s*         # Leading whitespace is ok. 
    (?P<key>\w+)\s*=\s*(    # Search for a key followed by.. 
     (?P<str>"[^"]*"|\'[^\']*\')|  # a quoted string; or 
     (?P<float>\d+\.\d+)|    # a float; or 
     (?P<int>\d+)      # an int. 
    )\s*,?\s*       # Handle comma & trailing whitespace. 
    |(?P<garbage>.+)      # Complain if we get anything else! 
    ''', re.VERBOSE) 

def handle_keyval(match): 
    if match.group('garbage'): 
     raise ValueError("Parse error: unable to parse: %r" % 
         match.group('garbage')) 
    key = match.group('key') 
    if match.group('str') is not None: 
     return (key, match.group('str')[1:-1]) # strip quotes 
    elif match.group('float') is not None: 
     return (key, float(match.group('float'))) 
    elif match.group('int') is not None: 
     return (key, int(match.group('int'))) 

Convierte automáticamente flotadores & Ints al tipo correcto; maneja comillas simples y dobles; maneja espacios en blanco extraños en varios lugares; y se queja si se suministra una cadena mal formateada

>>> s='name="John Smith", age=34, height=173.2, location="US", avatar=":,=)"' 
>>> print dict(handle_keyval(m) for m in keyval_re.finditer(s)) 
{'age': 34, 'location': 'US', 'name': 'John Smith', 'avatar': ':,=)', 'height': 173.19999999999999} 
1

Yo sugeriría una manera perezosa de hacer esto.

test_string = 'name="John Smith", age=34, height=173.2, location="US", avatar=":,=)"' 
eval("dict({})".format(test_string)) 

{ 'edad': 34 'ubicación': 'Estados Unidos', 'Avatar': ':, =)', 'nombre': 'John Smith', 'height': 173,2}

Espero que esto ayude a alguien!

Cuestiones relacionadas