Как обрезать файл до максимального количества символов (не байт)


13

Как я могу обрезать текстовый файл (в кодировке UTF-8) до заданного количества символов? Меня не волнует длина строки, и разрез может быть в середине слова.

  • cut Кажется, работает на линии, но я хочу целый файл.
  • head -c использует байты, а не символы.

Обратите внимание, что реализация GNU по- cutпрежнему не поддерживает многобайтовые символы. Если бы это было так, вы могли бы сделать cut -zc-1234 | tr -d '\0'.
Стефан Шазелас

Как вы хотите справиться с эмодзи? Некоторые из них более одного персонажа ... stackoverflow.com/questions/51502486/…
phuzi

2
Какой персонаж? некоторые символы используют несколько кодовых точек,
Jasen

Ответы:


14

В некоторых системах есть truncateкоманда, которая усекает файлы до количества байтов (не символов).

Я не знаю ни одного, который усекает число символов, хотя вы могли бы прибегнуть к тому, perlкоторый установлен по умолчанию на большинстве систем:

Perl

perl -Mopen=locale -ne '
  BEGIN{$/ = \1234} truncate STDIN, tell STDIN; last' <> "$file"
  • С помощью -Mopen=localeмы используем понятие локали о том, что такое символы (поэтому в локалях, использующих кодировку UTF-8, это символы в кодировке UTF-8). Замените на, -CSесли вы хотите, чтобы ввод / вывод декодировался / кодировался в UTF-8 независимо от кодировки локали.

  • $/ = \1234: мы устанавливаем разделитель записей на ссылку на целое число, которое является способом указания записей фиксированной длины (в количестве символов ).

  • затем, прочитав первую запись, мы обрезаем stdin на месте (то есть в конце первой записи) и завершаем работу.

GNU sed

С GNU sedвы могли бы это сделать (предполагая, что файл не содержит символов NUL или последовательностей байтов, которые не образуют допустимых символов - оба из которых должны быть истинными для текстовых файлов):

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

Но это гораздо менее эффективно, поскольку он читает файл полностью, сохраняет его в памяти и записывает новую копию.

GNU awk

То же самое с GNU awk:

awk -i inplace -v RS='^$' -e '{printf "%s", substr($0, 1, 1234)}' -E /dev/null "$file"
  • -e code -E /dev/null "$file" являясь одним из способов передачи произвольных имен файлов gawk
  • RS='^$': режим slurp .

Встроенные оболочки

С ksh93, bashили zsh(с оболочками, отличными от zshпредположения, что содержимое не содержит байтов NUL):

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

С zsh:

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

Или:

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

С помощью ksh93или bash(остерегайтесь фальшивых многобайтовых символов в нескольких версияхbash ):

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

ksh93также можно обрезать файл на месте вместо того, чтобы переписать его с помощью <>;оператора перенаправления:

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

iconv + голова

Чтобы напечатать первые 1234 символа, другим вариантом может быть преобразование в кодировку с фиксированным числом байтов на символ, например UTF32BE/ UCS-4:

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

head -cне является стандартным, но довольно распространенным. Стандартный эквивалент будет, dd bs=1 count="$((1234 * 4))"но будет менее эффективным, поскольку он будет читать входные данные и записывать выходные данные по одному байту за раз ». iconvявляется стандартной командой, но имена кодировок не стандартизированы, поэтому вы можете найти системы безUCS-4

Примечания

В любом случае, хотя вывод будет содержать не более 1234 символов, он может оказаться недействительным текстом, так как он может закончиться строкой без разделителей.

Также обратите внимание, что хотя эти решения не будут обрезать текст в середине символа, они могут разбить его в середине графемы , как éвыражение, выраженное как U + 0065 U + 0301 (с eпоследующим комбинированным острым акцентом), или графемы слогов хангыль в их разложенных формах.


¹ и при вводе канала нельзя использовать bsзначения, отличные от 1, до тех пор, пока вы не используете iflag=fullblockрасширение GNU, как это ddможет сделать короткое чтение, если оно читает канал быстрее, чем iconvзаполняет его.


мог сделатьdd bs=1234 count=4
Jasen

2
@ Джейсен, это не будет надежным. Смотрите редактировать.
Стефан Шазелас

Вот это да! вам было бы удобно иметь рядом! Я думал, что знаю много удобных команд Unix, но это невероятный список отличных опций.
Марк Стюарт,

5

Если вы знаете, что текстовый файл содержит Unicode, закодированный как UTF-8, вы должны сначала декодировать UTF-8, чтобы получить последовательность символов Unicode и разделить их.

Я бы выбрал Python 3.x для этой работы.

В Python 3.x функция open () имеет дополнительный аргумент ключевого слова encoding=для чтения текстовых файлов . Описание метода io.TextIOBase.read () выглядит многообещающим.

Таким образом, с помощью Python 3 это будет выглядеть так:

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

Очевидно, что настоящий инструмент добавляет аргументы командной строки, обработку ошибок и т. Д.

В Python 2.x вы можете реализовать свой собственный файловый объект и построчно декодировать входной файл.


Да, я мог бы сделать это. Но это для машин сборки CI, поэтому я бы хотел использовать стандартную команду Linux.
Pitel

5
Что бы ни означало «стандартный Linux» для вашего вкуса Linux ...
Майкл Стрёдер,

1
Действительно, Python, в любом случае, его версия в наши дни довольно стандартна.
Муру

Я уже отредактировал свой ответ с помощью фрагмента для Python 3, который может явно обрабатывать текстовые файлы.
Майкл Стрёдер

0

Я хотел бы добавить другой подход. Вероятно, не самая лучшая производительность, и гораздо дольше, но легко понять:

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

Вызвать это с $ ./scriptname <desired chars> <input file>.

Это удаляет последний символ один за другим, пока цель не будет достигнута, что кажется очень плохой производительностью, особенно для больших файлов. Я просто хотел представить это как идею, чтобы показать больше возможностей.


Да, это определенно ужасно для производительности. Для файла длиной n wcсчитается порядка O (n ^ 2) байтов для целевой точки на полпути в файл. Должна быть возможность бинарного поиска вместо линейного поиска с использованием переменной, которую вы увеличиваете или уменьшаете, например echo -n "${result::-$chop}" | wc -mили что-то в этом роде. (И пока вы на нем, сделайте его безопасным, даже если содержимое файла начинается с -eчего-то, возможно, с использованием printf). Но вы все равно не побьете методы, которые смотрят каждый входной символ только один раз, так что, вероятно, оно того не стоит.
Питер Кордес

Вы определенно правы, скорее технический, а не практический ответ. Вы также можете изменить его, чтобы добавить символ за символом в поле, $resultпока оно не достигнет желаемой длины, но если желаемая длина является большим числом, это столь же неэффективно.
конфетти

1
Вы можете начать близко к нужному месту, начав с $desired_charsбайтов в нижнем конце или, возможно, 4*$desired_charsв верхнем. Но все же я думаю, что лучше использовать что-то еще полностью.
Питер Кордес
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.