So kürzen Sie eine Datei auf die maximale Anzahl von Zeichen (nicht Bytes)

13

Wie kann ich eine (UTF-8-codierte) Textdatei auf eine bestimmte Anzahl von Zeichen kürzen? Ich interessiere mich nicht für Linienlängen und der Schnitt kann in der Mitte des Wortes sein.

  • cut scheint auf Zeilen zu funktionieren, aber ich möchte eine ganze Datei.
  • head -c Verwendet Bytes, keine Zeichen.
Pitel
quelle
Beachten Sie, dass die GNU-Implementierung von cutimmer noch keine Mehrbyte-Zeichen unterstützt. Wenn es so wäre, könntest du es tun cut -zc-1234 | tr -d '\0'.
Stéphane Chazelas
Wie wollen Sie mit Emojis umgehen? Einige sind mehr als ein Charakter ... stackoverflow.com/questions/51502486/…
phuzi
2
Was ist ein Charakter? Einige Symbole verwenden mehrere Codepunkte.
Jasen

Antworten:

14

Einige Systeme verfügen über einen truncateBefehl, mit dem Dateien auf eine bestimmte Anzahl von Bytes (keine Zeichen) gekürzt werden .

Ich kenne keine, die auf eine Reihe von Zeichen gekürzt sind, obwohl Sie auf die zurückgreifen könnten, perldie auf den meisten Systemen standardmäßig installiert ist:

perl

perl -Mopen=locale -ne '
  BEGIN{$/ = \1234} truncate STDIN, tell STDIN; last' <> "$file"
  • Mit verwenden -Mopen=localewir die Vorstellung des Gebietsschemas, was Zeichen sind (in Gebietsschemas, die den UTF-8-Zeichensatz verwenden, sind das UTF-8-codierte Zeichen). Ersetzen Sie durch, -CSwenn I / O unabhängig vom Zeichensatz des Gebietsschemas in UTF-8 decodiert / codiert werden soll.

  • $/ = \1234: Wir setzen das Datensatztrennzeichen auf einen Verweis auf eine Ganzzahl, mit der Datensätze mit fester Länge (in Zeichenanzahl ) angegeben werden können.

  • Wenn wir dann den ersten Datensatz lesen, kürzen wir stdin an der richtigen Stelle (also am Ende des ersten Datensatzes) und beenden ihn.

GNU sed

Mit GNU sedkönnen Sie Folgendes tun (vorausgesetzt, die Datei enthält keine NUL-Zeichen oder Folgen von Bytes, die keine gültigen Zeichen bilden - beides sollte für Textdateien gelten):

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

Dies ist jedoch weitaus weniger effizient, da die Datei vollständig gelesen, vollständig im Speicher abgelegt und eine neue Kopie geschrieben wird.

GNU awk

Gleiche mit GNU awk:

awk -i inplace -v RS='^$' -e '{printf "%s", substr($0, 1, 1234)}' -E /dev/null "$file"
  • -e code -E /dev/null "$file" Dies ist eine Möglichkeit, beliebige Dateinamen an zu übergeben gawk
  • RS='^$': Schlürfen Modus .

Shell eingebaut

Mit ksh93, bashoder zsh(mit Muscheln ausgenommen zsh, den Inhalt unter der Annahme nicht NUL enthalten Bytes):

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

Mit zsh:

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

Oder:

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

Mit ksh93oder bash(Vorsicht, es ist falsch für Multi-Byte-Zeichen in mehreren Versionen vonbash ):

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

ksh93Sie können die Datei auch direkt abschneiden, anstatt sie mit dem <>;Umleitungsoperator neu zu schreiben :

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

iconv + head

Um die ersten 1234 Zeichen zu drucken , besteht eine andere Möglichkeit darin, in eine Kodierung mit einer festen Anzahl von Bytes pro Zeichen wie UTF32BE/ zu konvertieren UCS-4:

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

head -cist nicht Standard, aber ziemlich häufig. Ein Standardäquivalent wäre dd bs=1 count="$((1234 * 4))"jedoch weniger effizient, da es die Eingabe lesen und die Ausgabe byteweise schreiben würde¹. iconvist ein Standardbefehl, aber die Codierungsnamen sind nicht standardisiert, sodass Sie möglicherweise Systeme ohne findenUCS-4

Anmerkungen

Obwohl die Ausgabe höchstens 1234 Zeichen hat, ist sie möglicherweise kein gültiger Text, da sie möglicherweise in einer nicht begrenzten Zeile endet.

Beachten Sie auch, dass diese Lösungen zwar keinen Text in der Mitte eines Zeichens ausschneiden, ihn jedoch in der Mitte eines Diagrammsé unterbrechen könnten , wie beispielsweise U + 0065 U + 0301 ( egefolgt von einem kombinierenden Akzent). oder Hangul Silbengrapheme in ihren zerlegten Formen.


¹ und bei der Pipe-Eingabe können Sie nur dann bszuverlässig Werte außer 1 verwenden, wenn Sie die iflag=fullblockGNU-Erweiterung verwenden, da ddkurze Lesevorgänge möglich sind, wenn die Pipe schneller gelesen wird, als sie iconvgefüllt wird

Stéphane Chazelas
quelle
könnte tundd bs=1234 count=4
Jasen
2
@Jasen, das wäre nicht zuverlässig. Siehe Bearbeiten.
Stéphane Chazelas
Beeindruckend! Sie wären praktisch, um in der Nähe zu haben! Ich dachte, ich kenne viele nützliche Unix-Befehle, aber dies ist eine unglaubliche Liste großartiger Optionen.
Mark Stewart
5

Wenn Sie wissen, dass die Textdatei als UTF-8 codiertes Unicode enthält, müssen Sie zuerst das UTF-8 decodieren, um eine Sequenz von Unicode-Zeicheneinheiten zu erhalten und diese zu teilen.

Ich würde Python 3.x für den Job wählen.

Mit Python 3.x verfügt die Funktion open () über ein zusätzliches Schlüsselwortargument encoding=zum Lesen von Textdateien . Die Beschreibung der Methode io.TextIOBase.read () sieht vielversprechend aus.

Unter Verwendung von Python 3 würde dies folgendermaßen aussehen:

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

Offensichtlich würde ein echtes Tool Befehlszeilenargumente, Fehlerbehandlung usw. hinzufügen.

Mit Python 2.x können Sie ein eigenes dateiähnliches Objekt implementieren und die Eingabedatei zeilenweise dekodieren.

Michael Ströder
quelle
Ja, das könnte ich tun. Da es sich jedoch um CI-Build-Maschinen handelt, würde ich gerne einen Standard-Linux-Befehl verwenden.
Pitel
5
Was auch immer "Standard-Linux" für Ihr Linux-Flair bedeutet ...
Michael Ströder
1
Tatsächlich ist Python, eine Version davon, heutzutage ziemlich Standard.
muru
Ich habe meine Antwort bereits mit Snippet für Python 3 bearbeitet, das explizit Textdateien verarbeiten kann.
Michael Ströder
0

Ich möchte einen anderen Ansatz hinzufügen. Wahrscheinlich nicht die beste Leistung und viel länger, aber einfach zu verstehen:

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

Rufe es mit auf $ ./scriptname <desired chars> <input file>.

Dadurch wird das letzte Zeichen nacheinander entfernt, bis das Ziel erreicht ist. Dies scheint besonders bei größeren Dateien eine sehr schlechte Leistung zu sein. Ich wollte dies nur als Idee präsentieren, um mehr Möglichkeiten aufzuzeigen.

Konfetti
quelle
Ja, das ist definitiv schrecklich für die Leistung. wcZählt für eine Datei mit der Länge n in der Größenordnung von 0 (n ^ 2) Gesamtbytes für einen Zielpunkt auf halbem Weg in der Datei. Es sollte möglich sein, eine Binärsuche anstelle einer linearen Suche durchzuführen, indem Sie eine Variable verwenden, die Sie vergrößern oder verkleinern echo -n "${result::-$chop}" | wc -m. (Und wenn Sie schon dabei sind, machen Sie es sicher, auch wenn der Inhalt der Datei mit -eoder etwas beginnt , vielleicht mit printf). Aber Sie werden Methoden, die jedes Eingabezeichen nur einmal betrachten, immer noch nicht schlagen, es lohnt sich also wahrscheinlich nicht.
Peter Cordes
Sie haben definitiv recht, eher eine technische als eine praktische Antwort. Sie können den Wert auch umkehren, um Zeichen für Zeichen hinzuzufügen, $resultbis die gewünschte Länge erreicht ist. Wenn die gewünschte Länge jedoch eine hohe Zahl ist, ist sie genauso ineffizient.
Konfetti
1
Sie könnten nahe an der richtigen Stelle beginnen, indem Sie mit $desired_charsBytes am unteren Ende oder vielleicht 4*$desired_charsam oberen Ende beginnen. Trotzdem denke ich, dass es am besten ist, etwas ganz anderes zu verwenden.
Peter Cordes