Come troncare il file al numero massimo di caratteri (non byte)


13

Come posso troncare un file di testo (codificato UTF-8) su un determinato numero di caratteri? Non mi interessa la lunghezza delle linee e il taglio può essere nel mezzo della parola.

  • cut sembra funzionare su linee, ma voglio un intero file.
  • head -c usa byte, non caratteri.

Si noti che l'implementazione GNU di cutstill non supporta i caratteri multi-byte. Se così fosse, potresti farlo cut -zc-1234 | tr -d '\0'.
Stéphane Chazelas,

Come vuoi gestire gli emoji? Alcuni sono più che un personaggio ... stackoverflow.com/questions/51502486/...
phuzi

2
Che cos'è un personaggio? alcuni simboli usano diversi punti di codice,
Jasen

Risposte:


14

Alcuni sistemi hanno un truncatecomando che tronca i file su un numero di byte (non caratteri).

Non conosco nessuno che si tronchi in un numero di caratteri, anche se potresti ricorrere a perlquale è installato di default sulla maggior parte dei sistemi:

perl

perl -Mopen=locale -ne '
  BEGIN{$/ = \1234} truncate STDIN, tell STDIN; last' <> "$file"
  • Con -Mopen=locale, usiamo la nozione di locale di cosa sono i caratteri (quindi nelle versioni locali che usano il set di caratteri UTF-8, ovvero caratteri codificati UTF-8). Sostituire con -CSse si desidera che gli I / O vengano decodificati / codificati in UTF-8 indipendentemente dal set di caratteri della locale.

  • $/ = \1234: impostiamo il separatore di record su un riferimento a un numero intero che è un modo per specificare record di lunghezza fissa (in numero di caratteri ).

  • quindi, dopo aver letto il primo record, tronciamo stdin in posizione (quindi alla fine del primo record) e usciamo.

GNU sed

Con GNU sed, potresti farlo (supponendo che il file non contenga caratteri NUL o sequenze di byte che non formano caratteri validi - entrambi dovrebbero essere veri per i file di testo):

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

Ma è molto meno efficiente, in quanto legge il file per intero, lo memorizza per intero in memoria e ne scrive una nuova copia.

GNU awk

Lo stesso con GNU awk:

awk -i inplace -v RS='^$' -e '{printf "%s", substr($0, 1, 1234)}' -E /dev/null "$file"
  • -e code -E /dev/null "$file" essendo un modo per passare nomi di file arbitrari a gawk
  • RS='^$': modalità slurp .

Shell incorporati

Con ksh93, basho zsh(con shell diverse da zsh, supponendo che il contenuto non contenga byte NUL):

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

Con zsh:

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

O:

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

Con ksh93o bash(attenzione è falso per i caratteri multi-byte in diverse versioni dibash ):

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

ksh93può anche troncare il file in atto invece di riscriverlo con il suo <>;operatore di reindirizzamento:

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

iconv + head

Per stampare i primi 1234 caratteri, un'altra opzione potrebbe essere quella di convertire in una codifica con un numero fisso di byte per carattere come UTF32BE/ UCS-4:

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

head -cnon è standard, ma abbastanza comune. Un equivalente standard sarebbe dd bs=1 count="$((1234 * 4))"ma sarebbe meno efficiente, in quanto leggerebbe l'ingresso e scriverà l'uscita un byte alla volta¹. iconvè un comando standard ma i nomi di codifica non sono standardizzati, pertanto è possibile trovare sistemi senzaUCS-4

Appunti

In ogni caso, sebbene l'output abbia un massimo di 1234 caratteri, potrebbe non essere un testo valido, in quanto potrebbe finire in una riga non delimitata.

Nota anche che mentre quelle soluzioni non tagliano il testo nel mezzo di un personaggio, potrebbero romperlo nel mezzo di un grafema , come un éespresso come U + 0065 U + 0301 ( eseguito da un accento acuto combinato), o grafemi di sillabe Hangul nelle loro forme decomposte.


¹ e sull'input della pipe non è possibile utilizzare bsvalori diversi da 1 in modo affidabile a meno che non si utilizzi l' iflag=fullblockestensione GNU, poiché si ddpotrebbero fare letture brevi se legge la pipe più velocemente di quanto la iconvriempia


potrebbe faredd bs=1234 count=4
Jasen il

2
@Jasen, non sarebbe affidabile. Vedi modifica.
Stéphane Chazelas,

Wow! saresti utile avere vicino! Pensavo di conoscere molti utili comandi Unix, ma questa è una lista incredibile di grandi opzioni.
Mark Stewart,

5

Se sai che il file di testo contiene Unicode codificato come UTF-8, devi prima decodificare UTF-8 per ottenere una sequenza di entità di caratteri Unicode e dividerle.

Sceglierei Python 3.x per il lavoro.

Con Python 3.x la funzione open () ha un argomento chiave in più encoding=per la lettura di file di testo . La descrizione del metodo io.TextIOBase.read () sembra promettente.

Quindi usando Python 3 sarebbe simile a questo:

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

Ovviamente uno strumento reale aggiungerebbe argomenti da riga di comando, gestione degli errori, ecc.

Con Python 2.x è possibile implementare il proprio oggetto simile a un file e decodificare il file di input riga per riga.


Sì, potrei farlo. Ma è per le macchine di compilazione CI, quindi mi piacerebbe di più usare alcuni comandi Linux standard.
Pitel,

5
Qualunque cosa "Linux standard" significhi sul tuo sapore Linux ...
Michael Ströder,

1
In effetti, Python, in qualche modo comunque, è abbastanza standard in questi giorni.
muru,

Ho già modificato la mia risposta con lo snippet per Python 3 che può elaborare esplicitamente file di testo.
Michael Ströder,

0

Vorrei aggiungere un altro approccio. Probabilmente non è la migliore prestazione saggia, e molto più lunga, ma facile da capire:

#!/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"

Invocalo con $ ./scriptname <desired chars> <input file>.

Questo rimuove l'ultimo carattere uno per uno fino a quando l'obiettivo non viene raggiunto, il che sembra davvero una cattiva prestazione saggia soprattutto per i file più grandi. Volevo solo presentarlo come un'idea per mostrare più possibilità.


Sì, questo è decisamente orribile per le prestazioni. Per un file di lunghezza n, wcconta sull'ordine di O (n ^ 2) byte totali per un punto target a metà del file. Dovrebbe essere possibile eseguire la ricerca binaria anziché la ricerca lineare utilizzando una variabile che si aumenta o diminuisce, come echo -n "${result::-$chop}" | wc -mo qualcosa del genere. (E mentre ci sei, rendilo sicuro anche se il contenuto del file inizia -eo qualcosa del genere, magari usando printf). Ma continuerai a non battere metodi che guardano ogni carattere di input una sola volta, quindi probabilmente non ne vale la pena.
Peter Cordes,

Hai decisamente ragione, più una risposta tecnica piuttosto che una risposta pratica. Puoi anche invertirlo per aggiungere carattere per carattere $resultfino a quando non corrisponde alla lunghezza desiderata, ma se la lunghezza desiderata è un numero elevato è altrettanto inefficiente.
coriandoli

1
Potresti iniziare vicino al posto giusto iniziando con $desired_charsbyte nella parte bassa o forse 4*$desired_charsnella parte alta. Ma penso comunque che sia meglio usare qualcos'altro interamente.
Peter Cordes,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.