Por que o sed não fez nenhuma alteração em um arquivo?

1

Pergunta curta:

Por que sed não pode fazer qualquer alteração em um arquivo e há alguma maneira de verificar?

Longa pergunta:

Eu tentei executar um comando sed que sempre trabalhou com meus arquivos antes. Aprendi este aqui em setembro. Cada trimestre eu recebo 4 arquivos enormes com um monte de espaço em branco e uma coluna que deveria ser uma, mas é dividida em duas. Eu corro o seguinte comando para percorrer o espaço em branco e mesclar as colunas 41 e 42:

sudo sed -i -e 's/ \{1,\}"/"/g' -e 's/" \{1,\}/"/g' -e 's/","//41' original_file.txt

Pela primeira vez ontem, nada aconteceu. Ele espera cerca de 3 segundos e, em seguida, nada acontece, enquanto normalmente deve levar de 20 a 30 minutos. Eu verifico o arquivo e os espaços ainda estão lá. Eu tenho 3 vezes o tamanho do arquivo ainda livre no sistema e duas vezes o tamanho do arquivo disponível na RAM (512GB de RAM), não que o RAM importe, só queria jogar isso dentro.

Eu tentei escrevê-lo em outro arquivo usando

sudo sed -e 's/ \{1,\}"/"/g' -e 's/" \{1,\}/"/g' -e 's/","//41' original_file.txt > formatted_file.txt

Isso cria formatted_file.txt , mas fica completamente em branco.

Alguém pode dizer o que estou fazendo errado ou como verificar o problema?

EDITAR:

A entrada de amostra pode ser vista em stackoverflow exceto que existem mais de 300 colunas.

    
por Aunt Jemima 11.01.2018 / 03:30

1 resposta

2

Nos comentários, foi descoberto que o arquivo de entrada está em big-endian UTF-16 , em vez de ASCII simples de 7 bits ou ASCII estendido de 8 bits. UTF-16 é um formato de 2 bytes por caractere e, se usado para codificar ASCII simples, o caractere "ASCII" tem 0x00 (um byte NUL, exibido como ^@ por cat -A , less , e outros programas) como o primeiro byte do par de 2 bytes (big-endian. invertido para little-endian).

A correção é converter o arquivo em ASCII simples. por exemplo. em vez de usar o utilitário padrão fromdos ou similar para converter CR-LF (finais de linha dos / windows) para LF (finais de linha unix), você precisa fazer algo como o seguinte para converter o texto em um formato utilizável por o restante do script sed :

sed -e '1 s/^\xff\xfe|^\xfe\xff//; s/\x00//g; s/\x0d$//'

Este script sed :

  • retira os marcadores de ordem de bytes 0xfffe ou 0xfeff do início da primeira linha.
  • remove todos os caracteres NUL de todas as linhas de entrada, onde quer que ocorram.
  • remove o caractere de retorno de carro ( 0x0d ) do final de qualquer linha

Observação: isso é adequado apenas para texto codificado em UTF-16 que contenha apenas caracteres que, de outra forma, seriam ASCII. Ele manipulará completamente qualquer arquivo de texto UTF-16 que contenha outros tipos de caracteres (por exemplo, texto que não seja em inglês).

Por fim, perl tem excelente suporte a texto em vários formatos comuns, incluindo ascii simples, UTF-8, UTF-16 e muito mais. Possui módulos de biblioteca para trabalhar e converter entre todos os formatos. É bastante fácil converter scripts sed simples em perl , portanto, uma versão perl do script pode ser tão simples quanto (não testada, mas pode até funcionar):

#!/usr/bin/perl

use strict;
use feature 'unicode_strings';

while(<>) {

  s/^\xff\xfe|^\xfe\xff// if ($. == 1);  # strip Byte Order marker from 1st line

  s/\x0d$//;  # strip CR from each end-of-line
  s/ *"/"/g;  # get rid of all spaces immediately before " characters
  s/" */"/g;  # get rid of all spaces immediately after " characters

  # A very primitive split(). Should use a real CSV parser here, like the
  # Text::CSV module which properly copes with embedded quotes and commas etc
  # in string fields.   This would also allow proper processing of each field to
  # remove any extra whitespace characters rather than the quick-and-dirty hack of
  # global regexp substitutions above.
  my @fields = split /,/;

  # perl arrays start from zero.  This appends the "fake" field 42 onto field 41,
  # and then deletes field 42.
  $fields[40] .= $fields[41];
  delete $fields[41];

  print join(',',@fields), "\n";
}

Resposta antiga que ainda contém informações úteis (IMO):

awk é uma ferramenta melhor para esse trabalho que sed .

Por exemplo, com o GNU awk (ou qualquer outro awk que entenda o PCRE como \s e \S ):

awk '{$0=gensub(/\s*(\S+)/,"\1",42)}1' original > fixed

Isso mescla a coluna 41 & 42 removendo quaisquer espaços imediatamente anteriores à coluna 42.

Para não-PCRE awk , use [[:space:]] em vez de \s e [^[:space:]] em vez de \S :

awk '{$0=gensub(/[[:space:]]*(\[^[:space:]]+)/,"\1",42)}1' original > fixed

Além disso, dependendo da natureza exata do arquivo de entrada, perl pode ser uma ferramenta ainda melhor para esse trabalho do que awk . Por exemplo, ele possui módulos para analisar arquivos CSV e trabalhar com os campos individuais em um registro CSV.

BTW, IMO que sed script é horrível, até porque você está usando vários -e args em vez de um único script sed com ; como separador de comando. Se você quiser usar sed , use-a pelo menos de forma eficaz e eficiente. Seu script sed é melhor escrito como:

sed -e 's/ \{1,\}"/"/g; s/" \{1,\}/"/g; s/","//41' original > fixed

ou até mesmo:

sed -e 's/ \{1,\}"/"/g
        s/" \{1,\}/"/g
        s/","//41' original > fixed

Você ainda precisa corrigir o bug, mas pelo menos você terá algo mais legível para depurar - o que torna muito mais fácil ver onde o problema pode estar.

Além disso, BTW, -i ou --in-place não é uma edição "no local" como você imagina. Ele funciona criando um arquivo temporário e depois colocando-o no lugar. Isso quebra tudo o que requer que o inode permaneça o mesmo, incluindo hard links.

É muito melhor gravar a saída alterada em um arquivo temporário (por exemplo, temp.txt) e, em seguida, em cat temp.txt > original.txt; rm temp.txt - que sobrescreve o arquivo original com a versão alterada, mantendo o mesmo inode.

    
por 11.01.2018 / 06:56

Tags