Analizar los valores de un bloque de texto basado en teclas específicas

Estoy analizando un texto de una fuente fuera de mi control, que no está en un formato muy conveniente. Tengo líneas como esta:

Categoría del problema: Esfuerzos humanos Subcategoría del problema: Exploración espacial Tipo de problema: Error al iniciar Versión del software: 9.8.77.omni.3 Detalles del problema: Problema con la cámara de la barrera de señal.

Quiero dividir la línea por teclas como esta:

Problem_Category = "Human Endeavors" Problem_Subcategory = "Space Exploration" Problem_Type = "Failure to Launch" Software_Version = "9.8.77.omni.3" Problem_Details = "Issue with signal barrier chamber." 

Las claves siempre estarán en el mismo orden, y siempre van seguidas de un punto y coma, pero no hay necesariamente espacio o líneas nuevas entre un valor y la siguiente clave. No estoy seguro de qué se puede usar como delimitador para analizar esto, ya que los dos puntos y los espacios también pueden aparecer en los valores. ¿Cómo puedo analizar este texto?

Si tu bloque de texto es esta cadena:

 text = 'Problem Category: Human Endeavors Problem Subcategory: Space ExplorationProblem Type: Failure to LaunchSoftware Version: 9.8.77.omni.3Problem Details: Issue with signal barrier chamber.' 

Entonces

 import re names = ['Problem Category', 'Problem Subcategory', 'Problem Type', 'Software Version', 'Problem Details'] text = 'Problem Category: Human Endeavors Problem Subcategory: Space ExplorationProblem Type: Failure to LaunchSoftware Version: 9.8.77.omni.3Problem Details: Issue with signal barrier chamber.' pat = r'({}):'.format('|'.join(names)) data = dict(zip(*[iter(re.split(pat, text, re.MULTILINE)[1:])]*2)) print(data) 

cede el dict

 {'Problem Category': ' Human Endeavors ', 'Problem Details': ' Issue with signal barrier chamber.', 'Problem Subcategory': ' Space Exploration', 'Problem Type': ' Failure to Launch', 'Software Version': ' 9.8.77.omni.3'} 

Para que pudieras asignar

 text = df_dict['NOTE_DETAILS'][0] ... df_dict['NOTE_DETAILS'][0] = data 

y luego podría acceder a las subcategorías con indexación dict:

 df_dict['NOTE_DETAILS'][0]['Problem_Category'] 

Precaución, sin embargo. Dicts / DataFrames profundamente nesteds de listas de dicts es generalmente un mal diseño. Como dice el zen de Python , Flat es mejor que nested.

Dado que conoce las palabras clave antes de tiempo, particione el texto en “palabra clave actual”, “texto restante”, luego continúe particionando el texto restante con la siguiente palabra clave.

 # get input from somewhere raw = 'Problem Category: Human Endeavors Problem Subcategory: Space ExplorationProblem Type: Failure to LaunchSoftware Version: 9.8.77.omni.3Problem Details: Issue with signal barrier chamber.' # these are the keys, in order, without the colon, that will be captured keys = ['Problem Category', 'Problem Subcategory', 'Problem Type', 'Software Version', 'Problem Details'] prev_key = None remaining = raw out = {} for key in keys: # get the value from before the key and after the key prev_value, _, remaining = remaining.partition(key + ':') # start storing values after the first iteration, since we need to partition the second key to get the first value if prev_key is not None: out[prev_key] = prev_value.strip() # what key to store next iteration prev_key = key # capture the final value (since it lags behind the parse loop) out[prev_key] = remaining.strip() # out now contains the parsed values, print it out nicely for key in keys: print('{}: {}'.format(key, out[key])) 

Esto imprime:

 Problem Category: Human Endeavors Problem Subcategory: Space Exploration Problem Type: Failure to Launch Software Version: 9.8.77.omni.3 Problem Details: Issue with signal barrier chamber. 

Odio y temo las expresiones regulares, así que aquí hay una solución que utiliza solo métodos integrados.

 #splits a string using multiple delimiters. def multi_split(s, delims): strings = [s] for delim in delims: strings = [x for s in strings for x in s.split(delim) if x] return strings s = "Problem Category: Human Endeavors Problem Subcategory: Space ExplorationProblem Type: Failure to LaunchSoftware Version: 9.8.77.omni.3Problem Details: Issue with signal barrier chamber." categories = ["Problem Category", "Problem Subcategory", "Problem Type", "Software Version", "Problem Details"] headers = [category + ": " for category in categories] details = multi_split(s, headers) print details details_dict = dict(zip(categories, details)) print details_dict 

Resultado (nuevas líneas agregadas por mí para facilitar la lectura):

 [ 'Human Endeavors ', 'Space Exploration', 'Failure to Launch', '9.8.77.omni.3', 'Issue with signal barrier chamber.' ] { 'Problem Subcategory': 'Space Exploration', 'Problem Details': 'Issue with signal barrier chamber.', 'Problem Category': 'Human Endeavors ', 'Software Version': '9.8.77.omni.3', 'Problem Type': 'Failure to Launch' } 

Ese es el trabajo para el análisis general de BNF que maneja la ambigüedad muy bien. Utilicé perl y Marpa , un analizador general de BNF. Espero que esto ayude.

 use 5.010; use strict; use warnings; use Marpa::R2; my $g = Marpa::R2::Scanless::G->new( { source => \(<<'END_OF_SOURCE'), :default ::= action => [ name, values ] pairs ::= pair+ pair ::= name (' ') value name ::= 'Problem Category:' name ::= 'Problem Subcategory:' name ::= 'Problem Type:' name ::= 'Software Version:' name ::= 'Problem Details:' value ::= [\s\S]+ :discard ~ whitespace whitespace ~ [\s]+ END_OF_SOURCE } ); my $input = <parse( \$input ) }; my @pairs; ast_traverse($ast); for my $pair (@pairs){ my ($name, $value) = @$pair; say "$name = $value"; } sub ast_traverse{ my $ast = shift; if (ref $ast){ my ($id, @children) = @$ast; if ($id eq 'pair'){ my ($name, $value) = @children; chop $name->[1]; shift @$value; $value = join('', @$value); chomp $value; push @pairs, [ $name->[1], '"' . $value . '"' ]; } else { ast_traverse($_) for @children; } } } 

Esto imprime:

 Problem Category = "Human Endeavors " Problem Subcategory = "Space Exploration" Problem Type = "Failure to Launch" Software Version = "9.8.77.omni.3" Problem Details = "Issue with signal barrier chamber."