Cómo truncar el archivo al número máximo de caracteres (no bytes)


13

¿Cómo puedo truncar un archivo de texto (codificado en UTF-8) a un número dado de caracteres? No me importan las longitudes de línea y el corte puede estar en el medio de la palabra.

  • cut Parece funcionar en líneas, pero quiero un archivo completo.
  • head -c usa bytes, no caracteres.

Tenga en cuenta que la implementación de GNU de cuttodavía no admite caracteres de varios bytes. Si lo hiciera, podrías hacerlo cut -zc-1234 | tr -d '\0'.
Stéphane Chazelas

¿Cómo quieres manejar los emojis? Algunos son más de un personaje ... stackoverflow.com/questions/51502486/…
phuzi

2
¿Qué es un personaje? algunos símbolos usan varios puntos de código,
Jasen

Respuestas:


14

Algunos sistemas tienen un truncatecomando que trunca los archivos a una cantidad de bytes (no caracteres).

No conozco ninguno que se trunca a varios caracteres, aunque podría recurrir al perlque está instalado de forma predeterminada en la mayoría de los sistemas:

perl

perl -Mopen=locale -ne '
  BEGIN{$/ = \1234} truncate STDIN, tell STDIN; last' <> "$file"
  • Con -Mopen=locale, usamos la noción de la configuración regional de qué caracteres son (por lo tanto, en las configuraciones regionales que usan el juego de caracteres UTF-8, eso es caracteres codificados UTF-8). Reemplace con -CSsi desea que la E / S se decodifique / codifique en UTF-8, independientemente del conjunto de caracteres del entorno local.

  • $/ = \1234: configuramos el separador de registros como una referencia a un entero, que es una forma de especificar registros de longitud fija (en número de caracteres ).

  • luego, al leer el primer registro, truncamos stdin en su lugar (al final del primer registro) y salimos.

GNU sed

Con GNU sed, podría hacerlo (suponiendo que el archivo no contenga caracteres NUL o secuencias de bytes que no formen caracteres válidos, los cuales deberían ser ciertos para los archivos de texto):

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

Pero eso es mucho menos eficiente, ya que lee el archivo completo y lo almacena completo en la memoria, y escribe una nueva copia.

GNU awk

Lo mismo 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" siendo una forma de pasar nombres de archivos arbitrarios a gawk
  • RS='^$': modo sorber .

Conchas incorporadas

Con ksh93, basho zsh(con conchas distintos zsh, suponiendo que el contenido no contiene NUL bytes):

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(cuidado , es falso para los caracteres de varios bytes en varias versiones debash ):

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

ksh93También puede truncar el archivo en su lugar en lugar de reescribirlo con su <>;operador de redirección:

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

iconv + cabeza

Para imprimir los primeros 1234 caracteres, otra opción podría ser convertir a una codificación con un número fijo de bytes por carácter como UTF32BE/ UCS-4:

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

head -cNo es estándar, sino bastante común. Un equivalente estándar sería dd bs=1 count="$((1234 * 4))"pero sería menos eficiente, ya que leería la entrada y escribiría la salida un byte a la vez¹. iconves un comando estándar pero los nombres de codificación no están estandarizados, por lo que puede encontrar sistemas sinUCS-4

Notas

En cualquier caso, aunque la salida tendría como máximo 1234 caracteres, puede terminar siendo texto no válido, ya que posiblemente terminaría en una línea no delimitada.

También tenga en cuenta que si bien esas soluciones no cortarían el texto en el medio de un carácter, podrían romperlo en el medio de un grafema , como un éexpresado como U + 0065 U + 0301 ( eseguido de un acento agudo combinado), o grafemas de sílabas Hangul en sus formas descompuestas.


¹ y en la entrada de tubería no puede usar bsvalores distintos de 1 de manera confiable a menos que use la iflag=fullblockextensión GNU, ya que ddpodría hacer lecturas cortas si lee la tubería más rápido de lo que la iconvllena


podría hacerdd bs=1234 count=4
Jasen

2
@Jasen, eso no sería confiable. Ver editar.
Stéphane Chazelas

¡Guauu! ¡sería útil tenerlo cerca! Pensé que conocía muchos comandos útiles de Unix, pero esta es una lista increíble de excelentes opciones.
Mark Stewart

5

Si sabe que el archivo de texto contiene Unicode codificado como UTF-8, primero debe decodificar el UTF-8 para obtener una secuencia de entidades de caracteres Unicode y dividirlas.

Elegiría Python 3.x para el trabajo.

Con Python 3.x, la función open () tiene un argumento adicional de palabra clave encoding=para leer archivos de texto . La descripción del método io.TextIOBase.read () parece prometedora.

Entonces, usando Python 3 se vería así:

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

Obviamente, una herramienta real agregaría argumentos de línea de comandos, manejo de errores, etc.

Con Python 2.x, podría implementar su propio objeto tipo archivo y decodificar el archivo de entrada línea por línea.


Sí, podría hacer eso. Pero es para las máquinas de compilación de CI, por lo que me gustaría utilizar algún comando estándar de Linux.
Pitel

55
Lo que sea que significa "Linux estándar" en su sabor Linux ...
Michael Ströder

1
De hecho, Python, alguna versión de todos modos, es bastante estándar en estos días.
muru

Ya he editado mi respuesta con un fragmento para Python 3 que puede procesar explícitamente archivos de texto.
Michael Ströder

0

Me gustaría agregar otro enfoque. Probablemente no sea el mejor rendimiento sabio, y mucho más largo, pero 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"

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

Esto elimina el último carácter uno por uno hasta que se cumpla el objetivo, lo que parece realmente un mal rendimiento, especialmente para archivos más grandes. Solo quería presentar esto como una idea para mostrar más posibilidades.


Sí, esto es definitivamente horrible para el rendimiento. Para un archivo de longitud n, wccuenta con el orden de O (n ^ 2) bytes totales para un punto objetivo a la mitad del archivo. Debería ser posible la búsqueda binaria en lugar de la búsqueda lineal utilizando una variable que aumente o disminuya, como echo -n "${result::-$chop}" | wc -mo algo así. (Y mientras lo hace, hágalo seguro incluso si el contenido del archivo comienza con -ealgo o algo así, tal vez usando printf). Pero aún así no superarás los métodos que solo miran cada carácter de entrada una vez, por lo que probablemente no valga la pena.
Peter Cordes

Definitivamente tienes razón, más una respuesta técnica que una respuesta práctica. También puede revertirlo para agregar char by char $resulthasta que coincida con la longitud deseada, pero si la longitud deseada es un número alto, es igual de ineficiente.
confeti

1
Puede comenzar cerca del lugar correcto comenzando con $desired_charsbytes en el extremo inferior o tal vez 4*$desired_charsen el extremo superior. Pero aún así creo que es mejor usar algo completamente diferente.
Peter Cordes
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.