Dividir una cadena ignorando las secciones entre comillas
-
08-06-2019 - |
Pregunta
Dada una cadena como esta:
a,"cadena, con",varios,"valores y algunos", citado
¿Cuál es un buen algoritmo para dividir esto en función de comas ignorando las comas dentro de las secciones citadas?
La salida debería ser una matriz:
[ "a", "cadena, con", "varios", "valores y algunos", "citado" ]
Solución
Si el idioma que elegí no ofreciera una forma de hacer esto sin pensar, inicialmente consideraría dos opciones como la salida más fácil:
Realice un análisis previo y reemplace las comas dentro de la cadena con otro carácter de control, luego divídalas, seguido de un análisis posterior en la matriz para reemplazar el carácter de control utilizado anteriormente con las comas.
Alternativamente, divídalos entre comas y luego analice posteriormente la matriz resultante en otra matriz verificando las comillas iniciales en cada entrada de la matriz y concatenando las entradas hasta llegar a una cita final.
Sin embargo, estos son trucos, y si se trata de un ejercicio puramente "mental", sospecho que resultarán inútiles.Si se trata de un problema del mundo real, sería útil conocer el idioma para poder ofrecer algunos consejos específicos.
Otros consejos
Parece que tienes algunas buenas respuestas aquí.
Para aquellos que quieran manejar su propio análisis de archivos CSV, presten atención a los consejos de los expertos y No utilice su propio analizador CSV.
Tu primer pensamiento es, "Necesito manejar comas dentro de comillas".
Tu próximo pensamiento será, "Oh, mierda, necesito manejar comillas dentro de comillas.Citas escapadas.Doble comillas.Comillas simples..."
Es un camino hacia la locura.No escribas el tuyo.Encuentre una biblioteca con una amplia cobertura de pruebas unitarias que abarque todas las partes difíciles y que haya pasado por un infierno para usted.Para .NET, utilice el programa gratuito Ayudantes de archivos biblioteca.
Pitón:
import csv
reader = csv.reader(open("some.csv"))
for row in reader:
print row
Por supuesto, usar un analizador CSV es mejor, pero solo por diversión puedes:
Loop on the string letter by letter.
If current_letter == quote :
toggle inside_quote variable.
Else if (current_letter ==comma and not inside_quote) :
push current_word into array and clear current_word.
Else
append the current_letter to current_word
When the loop is done push the current_word into array
El autor aquí incluyó un blob de código C# que maneja el escenario con el que tienes problemas:
Importaciones de archivos CSV en .Net
No debería ser demasiado difícil de traducir.
¿Qué pasa si aparece un número impar de citas en la cadena original?
Esto se parece asombrosamente al análisis de CSV, que tiene algunas peculiaridades en el manejo de campos entre comillas.El campo solo tiene carácter de escape si está delimitado por comillas dobles, por lo que:
campo1, "campo2, campo3", campo4, "campo5, campo6" campo7
se convierte
campo1
campo2, campo3
campo4
"campo5
campo6" campo7
Observe que si no comienza ni termina con una cita, entonces no es un campo entrecomillado y las comillas dobles simplemente se tratan como comillas dobles.
Incidentemente, mi código al que alguien se vinculó en realidad no maneja esto correctamente, si mal no recuerdo.
Aquí hay una implementación simple de Python basada en el pseudocódigo de Pat:
def splitIgnoringSingleQuote(string, split_char, remove_quotes=False):
string_split = []
current_word = ""
inside_quote = False
for letter in string:
if letter == "'":
if not remove_quotes:
current_word += letter
if inside_quote:
inside_quote = False
else:
inside_quote = True
elif letter == split_char and not inside_quote:
string_split.append(current_word)
current_word = ""
else:
current_word += letter
string_split.append(current_word)
return string_split
Utilizo esto para analizar cadenas, no estoy seguro si ayuda aquí;¿Pero quizás con algunas modificaciones menores?
function getstringbetween($string, $start, $end){
$string = " ".$string;
$ini = strpos($string,$start);
if ($ini == 0) return "";
$ini += strlen($start);
$len = strpos($string,$end,$ini) - $ini;
return substr($string,$ini,$len);
}
$fullstring = "this is my [tag]dog[/tag]";
$parsed = getstringbetween($fullstring, "[tag]", "[/tag]");
echo $parsed; // (result = dog)
/mp
Este es un análisis estándar de estilo CSV.Mucha gente intenta hacer esto con expresiones regulares.Puedes llegar a aproximadamente el 90% con expresiones regulares, pero realmente necesitas un analizador CSV real para hacerlo correctamente.Encontre un Analizador rápido y excelente de C# CSV en CodeProject Hace unos meses que lo recomiendo mucho!
Aquí hay uno en pseudocódigo (también conocido comoPython) en una sola pasada :-P
def parsecsv(instr):
i = 0
j = 0
outstrs = []
# i is fixed until a match occurs, then it advances
# up to j. j inches forward each time through:
while i < len(instr):
if j < len(instr) and instr[j] == '"':
# skip the opening quote...
j += 1
# then iterate until we find a closing quote.
while instr[j] != '"':
j += 1
if j == len(instr):
raise Exception("Unmatched double quote at end of input.")
if j == len(instr) or instr[j] == ',':
s = instr[i:j] # get the substring we've found
s = s.strip() # remove extra whitespace
# remove surrounding quotes if they're there
if len(s) > 2 and s[0] == '"' and s[-1] == '"':
s = s[1:-1]
# add it to the result
outstrs.append(s)
# skip over the comma, move i up (to where
# j will be at the end of the iteration)
i = j+1
j = j+1
return outstrs
def testcase(instr, expected):
outstr = parsecsv(instr)
print outstr
assert expected == outstr
# Doesn't handle things like '1, 2, "a, b, c" d, 2' or
# escaped quotes, but those can be added pretty easily.
testcase('a, b, "1, 2, 3", c', ['a', 'b', '1, 2, 3', 'c'])
testcase('a,b,"1, 2, 3" , c', ['a', 'b', '1, 2, 3', 'c'])
# odd number of quotes gives a "unmatched quote" exception
#testcase('a,b,"1, 2, 3" , "c', ['a', 'b', '1, 2, 3', 'c'])
Aquí hay un algoritmo simple:
- Determine si la cadena comienza con un
'"'
personaje - Divida la cadena en una matriz delimitada por el
'"'
personaje. - Marque las comas entre comillas con un marcador de posición.
#COMMA#
- Si la entrada comienza con un
'"'
, marque aquellos elementos en la matriz donde el índice % 2 == 0 - De lo contrario, marque aquellos elementos en la matriz donde el índice % 2 == 1
- Si la entrada comienza con un
- Concatene los elementos de la matriz para formar una cadena de entrada modificada.
- Divida la cadena en una matriz delimitada por el
','
personaje. - Reemplace todas las instancias en la matriz de
#COMMA#
marcadores de posición con el','
personaje. - La matriz es su salida.
Aquí está la implementación de Python:
(fijado para manejar '"a,b",c,"d,e,f,h","i,j,k"')
def parse_input(input):
quote_mod = int(not input.startswith('"'))
input = input.split('"')
for item in input:
if item == '':
input.remove(item)
for i in range(len(input)):
if i % 2 == quoted_mod:
input[i] = input[i].replace(",", "#COMMA#")
input = "".join(input).split(",")
for item in input:
if item == '':
input.remove(item)
for i in range(len(input)):
input[i] = input[i].replace("#COMMA#", ",")
return input
# parse_input('a,"string, with",various,"values, and some",quoted')
# -> ['a,string', ' with,various,values', ' and some,quoted']
# parse_input('"a,b",c,"d,e,f,h","i,j,k"')
# -> ['a,b', 'c', 'd,e,f,h', 'i,j,k']
Simplemente no pude resistirme a ver si podía hacerlo funcionar en una sola línea de Python:
arr = [i.replace("|", ",") for i in re.sub('"([^"]*)\,([^"]*)"',"\g<1>|\g<2>", str_to_test).split(",")]
Devuelve ['a', 'cadena, con', 'varios', 'valores y algunos', 'citado']
Funciona primero reemplazando las citas ',' interiores a otro separador (|), dividiendo la cadena en ',' y reemplazando el | separador de nuevo.
Como dijiste agnóstico del lenguaje, escribí mi algoritmo en el lenguaje más cercano posible al pseudocódigo:
def find_character_indices(s, ch):
return [i for i, ltr in enumerate(s) if ltr == ch]
def split_text_preserving_quotes(content, include_quotes=False):
quote_indices = find_character_indices(content, '"')
output = content[:quote_indices[0]].split()
for i in range(1, len(quote_indices)):
if i % 2 == 1: # end of quoted sequence
start = quote_indices[i - 1]
end = quote_indices[i] + 1
output.extend([content[start:end]])
else:
start = quote_indices[i - 1] + 1
end = quote_indices[i]
split_section = content[start:end].split()
output.extend(split_section)
output += content[quote_indices[-1] + 1:].split()
return output