Diviser une chaîne en ignorant les sections entre guillemets
-
08-06-2019 - |
Question
Étant donné une chaîne comme celle-ci :
a,"chaîne, avec",diverses,"valeurs et certaines", entre guillemets
Quel est un bon algorithme pour diviser cela en fonction des virgules tout en ignorant les virgules à l'intérieur des sections citées ?
Le résultat doit être un tableau :
[ "a", "string, with", "various", "values, and some", "quoted" ]
La solution
Si la langue de mon choix n'offrait pas un moyen de le faire sans réfléchir, j'envisagerais dans un premier temps deux options comme solution de facilité :
Pré-analysez et remplacez les virgules dans la chaîne par un autre caractère de contrôle, puis divisez-les, suivi d'une post-analyse sur le tableau pour remplacer le caractère de contrôle utilisé précédemment par les virgules.
Vous pouvez également les diviser sur les virgules, puis post-analyser le tableau résultant dans un autre tableau en vérifiant les guillemets de début sur chaque entrée du tableau et en concaténant les entrées jusqu'à ce que j'atteigne un guillemet de fin.
Ce sont cependant des hacks, et s’il s’agit d’un pur exercice « mental », je soupçonne qu’ils s’avéreront inutiles.S'il s'agit d'un problème réel, il serait utile de connaître la langue afin que nous puissions offrir des conseils spécifiques.
Autres conseils
On dirait que vous avez de bonnes réponses ici.
Pour ceux d'entre vous qui cherchent à gérer leur propre analyse de fichiers CSV, tenez compte des conseils des experts et Ne lancez pas votre propre analyseur CSV.
Votre première pensée est, "Je dois gérer les virgules entre guillemets."
Votre prochaine pensée sera, "Oh, merde, je dois gérer les guillemets à l'intérieur des guillemets.Citations échappées.Double citation.Guillemets simples..."
C'est un chemin vers la folie.N'écrivez pas le vôtre.Trouvez une bibliothèque avec une couverture étendue de tests unitaires qui aborde toutes les parties difficiles et a traversé l'enfer pour vous.Pour .NET, utilisez le logiciel gratuit Aides-fichiers bibliothèque.
Python:
import csv
reader = csv.reader(open("some.csv"))
for row in reader:
print row
Bien sûr, utiliser un analyseur CSV est préférable, mais juste pour le plaisir, vous pouvez :
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
L'auteur a déposé ici un blob de code C# qui gère le scénario avec lequel vous rencontrez un problème :
Importations de fichiers CSV dans .Net
Cela ne devrait pas être trop difficile à traduire.
Et si un nombre impair de citations apparaissent dans la chaîne d'origine?
Cela ressemble étrangement à l'analyse CSV, qui présente certaines particularités dans la gestion des champs cités.Le champ n'est échappé que s'il est délimité par des guillemets doubles, donc :
champ1, "champ2, champ3", champ4, "champ5, champ6" champ7
devient
champ1
champ2, champ3
champ4
"champ5
champ6" champ7
Remarquez que s'il ne commence pas et ne se termine pas par une citation, alors ce n'est pas un champ entre guillemets et les guillemets doubles sont simplement traités comme des guillemets doubles.
Par ailleurs, mon code auquel quelqu'un est lié ne gère pas cela correctement, si je me souviens bien.
Voici une implémentation python simple basée sur le pseudocode 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
J'utilise ceci pour analyser des chaînes, je ne sais pas si cela aide ici ;mais avec quelques petites modifications peut-être ?
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
Il s'agit d'une analyse standard de style CSV.Beaucoup de gens essaient de le faire avec des expressions régulières.Vous pouvez atteindre environ 90 % avec les expressions régulières, mais vous avez vraiment besoin d'un véritable analyseur CSV pour le faire correctement.j'ai trouvé un rapide et excellent analyseur C# CSV sur CodeProject il y a quelques mois que je recommande vivement !
En voici un en pseudocode (alias.Python) en un seul passage :-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'])
Voici un algorithme simple :
- Déterminez si la chaîne commence par un
'"'
personnage - Divisez la chaîne en un tableau délimité par le
'"'
personnage. - Marquez les virgules citées avec un espace réservé
#COMMA#
- Si la saisie commence par un
'"'
, marquez les éléments du tableau où l'index % 2 == 0 - Sinon, marquez les éléments du tableau où l'index % 2 == 1
- Si la saisie commence par un
- Concaténez les éléments du tableau pour former une chaîne d'entrée modifiée.
- Divisez la chaîne en un tableau délimité par le
','
personnage. - Remplacer toutes les instances du tableau de
#COMMA#
espaces réservés avec le','
personnage. - Le tableau est votre sortie.
Voici l'implémentation python :
(corrigé pour gérer '"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']
Je n'ai tout simplement pas pu résister à l'idée de voir si je pouvais le faire fonctionner dans un one-liner Python :
arr = [i.replace("|", ",") for i in re.sub('"([^"]*)\,([^"]*)"',"\g<1>|\g<2>", str_to_test).split(",")]
Renvoie ['a', 'string, with', 'various', 'values, and some', 'quoted']
Il fonctionne en remplaçant d'abord le ',' Inside Quotes à un autre séparateur (|), en divisant la chaîne sur ',' et en remplaçant le | séparateur à nouveau.
Puisque vous avez dit indépendant du langage, j'ai écrit mon algorithme dans le langage le plus proche possible du pseudocode :
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