Como truncar o arquivo para o número máximo de caracteres (não bytes)


13

Como posso truncar um arquivo de texto (codificado em UTF-8) para um determinado número de caracteres? Eu não me importo com comprimentos de linha e o corte pode estar no meio da palavra.

  • cut parece operar em linhas, mas eu quero um arquivo inteiro.
  • head -c usa bytes, não caracteres.

Observe que a implementação do GNU cutainda não suporta caracteres de vários bytes. Se tivesse, você poderia fazer cut -zc-1234 | tr -d '\0'.
Stéphane Chazelas

Como você deseja lidar com emojis? Alguns são mais que um personagem ... stackoverflow.com/questions/51502486/...
phuzi

2
O que é um personagem? alguns símbolos usar vários pontos de código,
Jasen

Respostas:


14

Alguns sistemas possuem um truncatecomando que trunca arquivos para um número de bytes (não caracteres).

Não conheço nenhum que trunque para vários caracteres, embora você possa recorrer ao perlque está instalado por padrão na maioria dos sistemas:

perl

perl -Mopen=locale -ne '
  BEGIN{$/ = \1234} truncate STDIN, tell STDIN; last' <> "$file"
  • Com -Mopen=locale, usamos a noção de localidade do que são caracteres (portanto, em localidades que usam o conjunto de caracteres UTF-8, são caracteres codificados em UTF-8). Substitua por -CSse desejar que a E / S seja decodificada / codificada em UTF-8, independentemente do conjunto de caracteres da localidade.

  • $/ = \1234: definimos o separador de registros como uma referência a um número inteiro, que é uma maneira de especificar registros de comprimento fixo (em número de caracteres ).

  • depois de ler o primeiro registro, truncamos o stdin no lugar (portanto, no final do primeiro registro) e saímos.

GNU sed

Com o GNU sed, você pode fazer (supondo que o arquivo não contenha caracteres NUL ou seqüências de bytes que não formem caracteres válidos - os quais devem ser verdadeiros para arquivos de texto):

sed -Ez -i -- 's/^(.{1234}).*/\1/' "$file"

Mas isso é muito menos eficiente, pois ele lê o arquivo na íntegra, armazena-o inteiro na memória e grava uma nova cópia.

GNU awk

O mesmo com o GNU awk:

awk -i inplace -v RS='^$' -e '{printf "%s", substr($0, 1, 1234)}' -E /dev/null "$file"
  • -e code -E /dev/null "$file" sendo uma maneira de passar nomes de arquivos arbitrários para gawk
  • RS='^$': modo slurp .

Shell builtins

Com ksh93, bashou zsh(com shells diferentes de zsh, assumindo que o conteúdo não contenha NUL bytes):

content=$(cat < "$file" && echo .) &&
  content=${content%.} &&
  printf %s "${content:0:1234}" > "$file"

Com zsh:

read -k1234 -u0 s < $file &&
  printf %s $s > $file

Ou:

zmodload zsh/mapfile
mapfile[$file]=${mapfile[$file][1,1234]}

Com ksh93ou bash(cuidado , é falso para caracteres de vários bytes em várias versões dobash ):

IFS= read -rN1234 s < "$file" &&
  printf %s "$s" > "$file"

ksh93também pode truncar o arquivo no lugar em vez de reescrevê-lo com seu <>;operador de redirecionamento:

IFS= read -rN1234 0<>; "$file"

iconv + cabeça

Para imprimir os primeiros 1234 caracteres, outra opção pode ser converter em uma codificação com um número fixo de bytes por caractere como UTF32BE/ UCS-4:

iconv -t UCS-4 < "$file" | head -c "$((1234 * 4))" | iconv -f UCS-4

head -cnão é padrão, mas bastante comum. Um equivalente padrão seria, dd bs=1 count="$((1234 * 4))"mas seria menos eficiente, pois leria a entrada e gravava a saída um byte de cada vez¹. iconvé um comando padrão, mas os nomes de codificação não são padronizados; portanto, você pode encontrar sistemas semUCS-4

Notas

De qualquer forma, embora a saída tenha no máximo 1234 caracteres, pode acabar não sendo um texto válido, pois possivelmente terminaria em uma linha não delimitada.

Observe também que, embora essas soluções não cortem texto no meio de um caractere, elas podem quebrá-lo no meio de um grafema , como um éexpresso como U + 0065 U + 0301 (a eseguido de um sotaque agudo combinado), ou sílaba de Hangul nas formas decompostas.


¹ e na entrada do tubo, você não pode usar bsvalores diferentes de 1 com confiabilidade, a menos que use a iflag=fullblockextensão GNU, como ddpoderia fazer leituras curtas se ler o tubo mais rapidamente do que o iconvpreenchê-lo


poderia fazerdd bs=1234 count=4
Jasen

2
@ Jason, isso não seria confiável. Veja editar.
Stéphane Chazelas

Uau! você seria útil para ter por perto! Eu pensei que conhecia muitos comandos Unix úteis, mas esta é uma lista incrível de ótimas opções.
Mark Stewart

5

Se você souber que o arquivo de texto contém Unicode codificado como UTF-8, primeiro decodifique o UTF-8 para obter uma sequência de entidades de caracteres Unicode e dividi-las.

Eu escolheria o Python 3.x para o trabalho.

Com o Python 3.x, a função open () possui um argumento extra de palavra-chave encoding=para a leitura de arquivos de texto . A descrição do método io.TextIOBase.read () parece promissora.

Então, usando o Python 3, ficaria assim:

truncated = open('/path/to/file.txt', 'rt', encoding='utf-8').read(1000)

Obviamente, uma ferramenta real adicionaria argumentos de linha de comando, tratamento de erros etc.

Com o Python 2.x, você pode implementar seu próprio objeto semelhante a um arquivo e decodificar o arquivo de entrada linha por linha.


Sim, eu poderia fazer isso. Mas é para máquinas de criação de CI, então eu gostaria de usar mais algum comando padrão do Linux.
Pitel

5
O que quer que "Linux padrão" significa em seu sabor Linux ...
Michael Ströder

1
De fato, o Python, alguma versão dele de qualquer maneira, é bastante padrão atualmente.
Muru

Já editei minha resposta com o trecho de código para Python 3, que pode processar explicitamente arquivos de texto.
Michael Ströder

0

Eu gostaria de adicionar outra abordagem. Provavelmente não é o melhor desempenho, é muito mais longo, mas fácil de entender:

#!/bin/bash

chars="$1"
ifile="$2"
result=$(cat "$ifile")
rcount=$(echo -n "$result" | wc -m)

while [ $rcount -ne $chars ]; do
        result=${result::-1}
        rcount=$(echo -n "$result" | wc -m)
done

echo "$result"

Invoque-o com $ ./scriptname <desired chars> <input file> .

Isso remove o último caracter um por um até que o objetivo seja atingido, o que parece realmente ruim em termos de desempenho, especialmente para arquivos maiores. Eu só queria apresentar isso como uma idéia para mostrar mais possibilidades.


Sim, isso é definitivamente horrível para o desempenho. Para um arquivo de comprimento n, wcconta com a ordem de O (n ^ 2) total de bytes para um ponto de destino na metade do arquivo. Deve ser possível pesquisar binário em vez de pesquisa linear, usando uma variável que você aumenta ou diminui, como echo -n "${result::-$chop}" | wc -malgo assim. (E enquanto você estiver nisso, proteja-o mesmo que o conteúdo do arquivo comece com -ealgo assim, talvez usando printf). Mas você ainda não vence métodos que apenas olham para cada caractere de entrada uma vez, portanto provavelmente não vale a pena.
Peter Peter Cordes

Você está definitivamente certo, mais uma resposta técnica do que uma resposta prática. Você também pode revertê-lo para adicionar char a char $resultaté que corresponda ao comprimento desejado, mas se o comprimento desejado for um número alto, é igualmente ineficiente.
Confetti

1
Você pode começar perto do lugar certo, começando com $desired_charsbytes na extremidade inferior ou talvez 4*$desired_charsna extremidade superior. Mas ainda acho que é melhor usar algo completamente diferente.
Peter Peter Cordes
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.