Metadados de Campo Zero - Adicionando metadados em seus arquivos CSV

Esse artigo fala sobre adicionar metadados em seus arquivos CSV enquanto mantém compatibilidade com a RFC4180.

0002

2024/10/01 01:10

EN-US | PT-BR

1 TL;DR

Use o primeiro campo para propósitos de metadados, com um csv embutido começando com csv-metadata-key,csv-metadata-value como o cabeçalho, defina csv-type-name se você quiser definir o tipo do seu csv e csv-field-zero se quiser definir o valor do primeiro campo (o campo índice zero) e use chaves vazias como comentários ou para pular uma linha e tome cuidado com aspas duplas, já que uma aspas duplas agora são duas e uma aspas duplas escapada agora são quatro.

  1. "csv-metadata-key,csv-metadata-value
  2. csv-type-name,vectors
  3. csv-field-zero,x
  4. ,
  5. ,""
  6. Isso é um comentário
  7. ""
  8. ,
  9. chave,valor
  10. chave,""valor""
  11. outraChave,""valor com """"aspas duplas""""!""
  12. ",y,z
  13. 1,2,3
  14. 4,5,6
  15. 7,8,9

2 Introdução

Esse artigo assume que a RFC4180 é o jeito certo de escrever e ler arquivos CSV.

Na internet você pode encontrar vários jeitos de adicionar metadados para um arquivo csv, alguns deles transformam seu csv em formatos proprietários que apenas o seu programa pode entender enquanto outros precisam de arquivos adicionais, arquivos zip ou outros formatos de dados como JSON ou XML, nesse artigo eu proponho um método para adicionar metadados para arquivos csv.

3 Alternativas

3.1 # como comentários ou metadados

Esse parece ser o jeito mais comum de adicionar metadados ou comentários para um csv, ele requer um parser customizado ou pré-processamento, não possui padrão e pode ser meio ambíguo já que um campo começando com # também pode ser um campo válido.

  1. #Isso é meu arquivo csv!
  2. #tipo=vetores
  3. x,y,z
  4. 1,2,3
  5. 4,5,6
  6. 7,8,9

3.2 Arquivo de metadados/Arquivo Zip

Um arquivo para propósitos de metadados é carregado junto com o arquivo csv normalmente com o mesmo nome do arquivo csv mais um sufixo (-meta.tipo) e usa qualquer formato de dados, ambos os arquivos podem ser juntados em um arquivo zip.

  1. meu-csv.csv:
  2. x,y,z
  3. 1,2,3
  4. 4,5,6
  5. 7,8,9
  6. meu-csv-meta.xml:
  7. <meta>
  8. <descricao>Isso é meu arquivo csv!</descricao>
  9. <tipo>vetores</tipo>
  10. </meta>
  11. meu-csv.zip:
  12. meu-csv.csv
  13. meu-csv-meta.xml

3.3 W3C

Eles tentaram (em inglês), eu nunca ouvi falar desse formato da W3C até que comecei a pesquisar sobre metadados em arquivos csv.

  1. #publisher,W3C
  2. #updated,2015-10-17T00:00:00Z
  3. #name,sensor,temperature
  4. #datatype,string,float
  5. sensor,temperature
  6. s-1,25.5

3.4 Referências (em inglês)

4 Minha alternativa

Então, se queremos adicionar metadados em nossos arquivos csv, ele deve continuar sendo um único arquivo csv, ele não deve usar outros formatos de dados, ele deve ser compatível com um parser que consiga ler RFC4180, ele tem que ser identificável e simples o bastante para que as pessoas considerem o seu uso.

4.1 Campo Zero

Depois de pensar sobre, não há melhor lugar para adicionar metadados do que se não no primeiro campo do arquivo csv.

  1. "metadados",y,z
  2. 1,2,3
  3. 4,5,6
  4. 7,8,9

4.2 Sem outros formatos de dados

Ao invés de usar outros formatos de dados como json ou xml, nós usamos um csv embutido para isso, isso elimina o requerimento para outros formatos de dados.

  1. "a,b,c,d",y,z
  2. 1,2,3
  3. 4,5,6
  4. 7,8,9

4.3 Identificável

Nós usamos um cabeçalho de csv para isso que só pode ter uma chave e um valor, qualquer outro cabeçalho não é válido e deve ser processado como um campo normal, mesmo que o cabeçalho seja meio único, ainda é uma boa ideia para os parsers adicionarem uma opção para ler como texto ao invés de metadados, redefinição de campos é permitida e é feita na ordem de aparição, como isso é um arquivo csv embutido, aspas duplas devem ser escritas como duas ao invés de uma e quatro ao invés de duas dentro de campos com aspas duplas.

  1. "csv-metadata-key,csv-metadata-value
  2. a,10
  3. b,20
  4. x,""campo, com, vírgulas""
  5. y,""campo com """"aspas duplas""""!""
  6. c,30
  7. c,40
  8. ",y,z
  9. 1,2,3
  10. 4,5,6
  11. 7,8,9

4.4 Identificação dos dados CSV

Nós adicionamos um opcional "csv-type-name" para identificação de tipos, similar aos namespaces de XML, isso permite que um arquivo csv seja facilmente distinguível de outro, assim como é pra XML eu recomendo usar uma URL para isso mas qualquer string é válida, até uma vazia ou nada.

  1. "csv-metadata-key,csv-metadata-value
  2. csv-type-name,https://example.com/vectors.txt
  3. ",y,z
  4. 1,2,3
  5. 4,5,6
  6. 7,8,9

4.5 Valor do Campo Zero

Nós adicionamos um opcional "csv-field-zero" para definir o valor do campo zero (o primeiro campo), se não presente uma string vazia é assumida.

  1. "csv-metadata-key,csv-metadata-value
  2. csv-type-name,https://example.com/vectors.txt
  3. csv-field-zero,x
  4. ",y,z
  5. 1,2,3
  6. 4,5,6
  7. 7,8,9

4.6 Compatibilidade

Se é necessário manter compatível com um sistema, podemos mover todos os dados para a direita e remover o "csv-field-zero", isso não afeta nada, altera apenas a estrutura do csv em si e faz o cabeçalho reaparecer corretamente novamente.

  1. "csv-metadata-key,csv-metadata-value
  2. csv-type-name,https://example.com/vectors.txt
  3. ",x,y,z
  4. ,1,2,3
  5. ,4,5,6
  6. ,7,8,9

4.7 Comentários de uma ou mais linhas e pulo de linhas

O Fato de que permitimos a redefinição de campos significa que agora podemos utilizar chaves vazias como comentários de uma ou mais linhas ou para pular linhas, cuidado deve ser tomado com as aspas duplas, já que isso ainda é um campo normal.

  1. "csv-metadata-key,csv-metadata-value
  2. csv-type-name,https://example.com/vectors.txt
  3. ,
  4. ,Comentário de uma linha!
  5. ,
  6. ,""
  7. Isso é
  8. um comentário
  9. de várias linhas!
  10. ""
  11. ",x,y,z
  12. ,1,2,3
  13. ,4,5,6
  14. ,7,8,9

5 Em código

Escrever e ler se torna bem simples, o parser de CSV que estou usando foi feito por mim mesmo com menos de 100 linhas, que é uma das principais vantagens de CSV, a classe aceita valores null como strings vazias.

5.1 Lendo

  1. CSV csv = CSV.read(...);
  2. String keyHeader = "csv-metadata-key";
  3. String valueHeader = "csv-metadata-value";
  4. String header = keyHeader + "," + valueHeader;
  5. String quotedHeader = "\"" + keyHeader + "\",\"" + valueHeader + "\"";
  6. Map<String, String> metadata = new LinkedHashMap<>();
  7. String fieldZero = csv.get(0, 0);
  8. //cheque se o campo começa com o cabeçalho
  9. if (fieldZero.startsWith(header) || fieldZero.startsWith(quotedHeader)) {
  10. //leia os metadados como csv e cheque se o cabeçalho é válido
  11. CSV csvMetadata = CSV.read(fieldZero);
  12. if (csvMetadata.getNumberOfFields() == 2
  13. && csvMetadata.get(0, 0).equals(keyHeader)
  14. && csvMetadata.get(1, 0).equals(valueHeader)
  15. ) {
  16. //leia os metadados, ignorando o cabeçalho
  17. for (int metaRecord = 1; metaRecord < csvMetadata.getNumberOfRecords(); metaRecord++) {
  18. String k = csvMetadata.get(0, metaRecord);
  19. String v = csvMetadata.get(1, metaRecord);
  20. //remove comentários
  21. if (k.isEmpty()) {
  22. continue;
  23. }
  24. metadata.put(k, v);
  25. }
  26. //define o campo zero
  27. csv.set(0, 0, metadata.get("csv-field-zero"));
  28. }
  29. }
  30. System.out.println("Type: "+metadata.get("csv-type-name"));
  31. System.out.println("Field zero: "+csv.get(0, 0));

5.2 Escrevendo

  1. CSV csv = CSV.read(...);
  2. String keyHeader = "csv-metadata-key";
  3. String valueHeader = "csv-metadata-value";
  4. ...
  5. Map<String, String> metadata = new LinkedHashMap<>();
  6. ...
  7. //define o campo zero
  8. metadata.put("csv-field-zero", csv.get(0, 0));
  9. //cria um novo csv com o tamanho de metadata mais 1
  10. CSV csvMetadata = new CSV(null, 2, metadata.size() + 1);
  11. //escreve o cabeçalho
  12. csvMetadata.set(0, 0, keyHeader);
  13. csvMetadata.set(1, 0, valueHeader);
  14. //escreve o metadata
  15. int index = 1;
  16. for (Map.Entry<String, String> e:metadata.entrySet()) {
  17. csvMetadata.set(0, index, e.getKey());
  18. csvMetadata.set(1, index, e.getValue());
  19. index++;
  20. }
  21. //escreve a saída no campo zero
  22. csv.set(0, 0, csvMetadata.toString());
  23. System.out.println(csv.toString());

6 Riscos de segurança

Embutir CSVs dentro de CSVs pode se tornar um potencial vetor de ataque DoS por causa de como as aspas duplas são escapadas, para cada nível de profundidade a quantidade de aspas duplas necessárias é dobrada, na profundidade 32, a quantidade necessária de aspas duplas para uma única aspas duplas precisaria de gigabytes de caracteres para escapar ela.

Se você realmente precisa embutir múltiplos CSVs dentro de um arquivo CSV sem arquivos adicionais, o melhor jeito seria criar um file system com um arquivo zip, escrever todos os seus CSVs nele e então embutir o zip como base64 em um csv mestre, cuidado deve ser tomado já que zip bombs ainda são uma possibilidade.

  1. "csv-metadata-key,csv-metadata-value
  2. csv-type-name,https://example.com/vectors.txt
  3. meusOutrosCsvsDados,c2Rmc2Rmc2Rmc2RmZXdyZXdyY2J2eHZuY2ZqbnRyeXVqdHk3aTZ5dWtqaGcsbWd2bnZjYnhjdmFzZFFFVw==
  4. meusOutrosCsvsRaiz,raiz.csv
  5. ",x,y,z
  6. ,1,2,3
  7. ,4,5,6
  8. ,7,8,9

7 Em editores externos

Se corretamente escapado, qualquer editor que suporte a RFC4180 deve conseguir ler e preservar os metadados.

7.1 Google Sheets

Google Sheets foi o melhor, parece seguir a RFC4180 em importar e exportar arquivos csv.

7.2 LibreOffice

LibreOffice conseguiu importar/exportar corretamente mas ele tem um estranho comportamento de trocar aspas duplas por aspas duplas curvadas no editor e a falta de um scroll suave torna chata a edição de grandes campos de metadados.

7.3 Excel

Excel nunca suportou a RFC4180 até onde eu sei, se as coisas não mudaram, excel ainda deve ser o diabo dos arquivos csv, gerando arquivos diferentes que dependem de qual país você está, garantindo que sua vida como programador será dolorida.

7.4 Outros Editores Online

Eu consegui encontrar vários editores online pequenos de arquivos csv, todos os que eu testei parecem seguir a RFC4180.