Pergunta

Dada uma string como esta:

a,"string, with",vários,"valores e alguns",citado

Qual é um bom algoritmo para dividir isso com base em vírgulas, ignorando as vírgulas dentro das seções citadas?

A saída deve ser uma matriz:

["a", "string, with", "vários", "valores e alguns", "citado"]

Foi útil?

Solução

Se meu idioma preferido não oferecesse uma maneira de fazer isso sem pensar, eu consideraria inicialmente duas opções como a saída mais fácil:

  1. Pré-analise e substitua as vírgulas dentro da string por outro caractere de controle e depois divida-as, seguida por uma pós-análise na matriz para substituir o caractere de controle usado anteriormente pelas vírgulas.

  2. Alternativamente, divida-os nas vírgulas e depois analise a matriz resultante em outra matriz, verificando as aspas iniciais em cada entrada da matriz e concatenando as entradas até chegar a uma cotação final.

No entanto, estes são hacks, e se este for um exercício puramente “mental”, então suspeito que serão inúteis.Se este for um problema do mundo real, seria útil conhecer o idioma para que possamos oferecer alguns conselhos específicos.

Outras dicas

Parece que você tem algumas boas respostas aqui.

Para aqueles que desejam lidar com a análise de seus próprios arquivos CSV, siga os conselhos dos especialistas e Não crie seu próprio analisador CSV.

Seu primeiro pensamento é, "Preciso lidar com vírgulas entre aspas."

Seu próximo pensamento será, "Ah, merda, preciso lidar com aspas dentro de aspas.Citações escapadas.Aspas duplas.Aspas simples..."

É um caminho para a loucura.Não escreva o seu próprio.Encontre uma biblioteca com uma ampla cobertura de testes de unidade que atinja todas as partes difíceis e tenha passado por um inferno para você.Para .NET, use o gratuito Ajudantes de arquivos biblioteca.

Pitão:

import csv
reader = csv.reader(open("some.csv"))
for row in reader:
    print row

É claro que usar um analisador CSV é melhor, mas apenas por diversão você poderia:

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 

O autor aqui inseriu um blob de código C# que lida com o cenário com o qual você está tendo problemas:

Importações de arquivos CSV em .Net

Não deve ser muito difícil de traduzir.

E se um número ímpar de citações aparecer na string original?

Isso se parece estranhamente com a análise de CSV, que tem algumas peculiaridades no tratamento de campos citados.O campo só tem escape se for delimitado por aspas duplas, então:

campo1, "campo2, campo3", campo4, "campo5, campo6" campo7

torna-se

campo1

campo2, campo3

campo4

"campo5

campo6" campo7

Observe que se não começar e terminar com uma aspa, então não é um campo entre aspas e as aspas duplas são simplesmente tratadas como aspas duplas.

Insedentamente, meu código ao qual alguém vinculou não lida com isso corretamente, se bem me lembro.

Aqui está uma implementação simples de python baseada no 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

Eu uso isso para analisar strings, não tenho certeza se isso ajuda aqui;mas com algumas pequenas modificações, talvez?

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

Esta é uma análise padrão no estilo CSV.Muitas pessoas tentam fazer isso com expressões regulares.Você pode chegar a cerca de 90% com expressões regulares, mas realmente precisa de um analisador CSV real para fazer isso corretamente.Achei um analisador C# CSV rápido e excelente no CodeProject há alguns meses que eu recomendo fortemente!

Aqui está um em pseudocódigo (também conhecido comoPython) em uma passagem :-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'])

Aqui está um algoritmo simples:

  1. Determine se a string começa com um '"' personagem
  2. Divida a string em uma matriz delimitada pelo '"' personagem.
  3. Marque as vírgulas entre aspas com um espaço reservado #COMMA#
    • Se a entrada começar com um '"', marque os itens na matriz onde o índice% 2 == 0
    • Caso contrário, marque os itens na matriz onde o índice% 2 == 1
  4. Concatene os itens na matriz para formar uma string de entrada modificada.
  5. Divida a string em uma matriz delimitada pelo ',' personagem.
  6. Substitua todas as instâncias na matriz de #COMMA# espaços reservados com o ',' personagem.
  7. A matriz é a sua saída.

Aqui está a implementação do python:
(corrigido para lidar com '"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']

Eu simplesmente não pude resistir para ver se conseguiria fazê-lo funcionar em uma linha única do Python:

arr = [i.replace("|", ",") for i in re.sub('"([^"]*)\,([^"]*)"',"\g<1>|\g<2>", str_to_test).split(",")]

Retorna ['a', 'string, with', 'vários', 'valores e alguns', 'citado']

Funciona substituindo primeiro as citações 'internas para outro separador (|), dividindo a string em' 'e substituindo o | separador novamente.

Como você disse agnóstico em termos de linguagem, escrevi meu algoritmo na linguagem mais próxima possível do 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
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top