¿Cómo extraer parcialmente un gran archivo comprimido de texto sin formato?

19

Tengo un archivo zip con un tamaño de 1,5 GB.

Su contenido es un gran archivo de texto plano ridículo (60 GB) y actualmente no tengo suficiente espacio en mi disco para extraerlo ni quiero extraerlo todo, incluso si lo tuviera.

En cuanto a mi caso de uso, sería suficiente si puedo inspeccionar partes del contenido.

Por lo tanto, quiero descomprimir el archivo como una secuencia y acceder a un rango del archivo (como uno puede a través de la cabeza y la cola en un archivo de texto normal).

Ya sea por memoria (por ejemplo, extraer un máximo de 100 kb a partir de la marca de 32 GB) o por líneas (deme las líneas de texto sin formato 3700-3900).

¿Hay alguna manera de lograr eso?

k0pernikus
fuente
1
Lamentablemente, no es posible buscar en un archivo individual dentro de un archivo zip. Por lo que cualquier soloution implicará la lectura a través del archivo hasta el punto que le interesa.
plugwash
55
@plugwash Como entiendo la pregunta, el objetivo no es evitar leer el archivo zip (o incluso el archivo descomprimido), sino simplemente evitar almacenar todo el archivo descomprimido en la memoria o en el disco. Básicamente, trate el archivo descomprimido como una secuencia .
ShreevatsaR

Respuestas:

28

Tenga en cuenta que gzippuede extraer ziparchivos (al menos la primera entrada en el ziparchivo). Entonces, si solo hay un archivo enorme en ese archivo, puede hacer:

gunzip < file.zip | tail -n +3000 | head -n 20

Para extraer las 20 líneas comenzando con la 3000 por ejemplo.

O:

gunzip < file.zip | tail -c +3000 | head -c 20

Por lo mismo con bytes (suponiendo una headimplementación que soporte -c).

Para cualquier miembro arbitrario en el archivo, de manera Unixy:

bsdtar xOf file.zip file-to-extract | tail... | head...

Con la headconstrucción de ksh93(como cuando /opt/ast/binestá adelante $PATH), también puede hacer:

.... | head     -s 2999      -c 20
.... | head --skip=2999 --bytes=20

Tenga en cuenta que, en cualquier caso, gzip/ bsdtar/ unzipsiempre tendrá que descomprimir (y descartar aquí) toda la sección del archivo que conduce a la parte que desea extraer. Eso se debe a cómo funciona el algoritmo de compresión.

Stéphane Chazelas
fuente
Si gzippuede manejarlo, lo hará el resto "Z" conscientes utilidades ( zcat, zless, etc.) También trabajo?
ivanivan
@ivanivan, en sistemas en los que se basan gzip(generalmente cierto zless, no necesariamente de zcatqué en algunos sistemas solo se deben leer .Zarchivos), sí.
Stéphane Chazelas
14

Una solución que utiliza descomprimir -p y dd, por ejemplo, para extraer 10 kb con 1000 bloques de desplazamiento:

$ unzip -p my.zip | dd ibs=1024 count=10 skip=1000 > /tmp/out

Nota: No probé esto con datos realmente enormes ...

tonioc
fuente
En el caso general de más de una vez un archivo dentro de un solo archivo, se puede usar unzip -l ARCHIVEpara enumerar el contenido del archivo y unzip -p ARCHIVE PATHextraer el contenido de un solo objeto PATHpara stdout.
David Foerster
3
En general, el uso dden tuberías con conteo u omisión no es confiable, ya que hará muchos read()s de hasta 1024 bytes. Por lo tanto, solo se garantiza que funcione correctamente si unzipescribe en la tubería en trozos cuyo tamaño es un múltiplo de 1024.
Stéphane Chazelas
4

Si tiene control sobre la creación de ese gran archivo zip, ¿por qué no considerar usar una combinación de gzipy zless?

Esto le permitiría usarlo zlesscomo un buscapersonas y ver el contenido del archivo sin tener que molestarse con la extracción.

Si no puede cambiar el formato de compresión, obviamente esto no funcionaría. Si es así, siento que zlesses bastante conveniente.

111 ---
fuente
1
Yo no. Estoy descargando el archivo comprimido proporcionado por una empresa externa.
k0pernikus
3

Para ver líneas específicas del archivo, canalice la salida al editor de flujo de Unix, sed . Esto puede procesar flujos de datos arbitrariamente grandes, por lo que incluso puede usarlos para cambiar los datos. Para ver las líneas 3700-3900 como lo solicitó, ejecute lo siguiente.

unzip -p file.zip | sed -n 3700,3900p
Diomidis Spinellis
fuente
77
sed -n 3700,3900pseguirá leyendo hasta el final del archivo. Es mejor usar sed '3700,$!d;3900q'para evitar eso, o incluso en general más eficiente:tail -n +3700 | head -n 201
Stéphane Chazelas
3

Me preguntaba si era posible hacer algo más eficiente que descomprimir desde el inicio del archivo hasta el punto. Parece que la respuesta es no. Sin embargo, en algunas CPU (Skylake) zcat | tailno sube la CPU a la velocidad de reloj completa. Vea abajo. Un decodificador personalizado podría evitar ese problema y guardar las llamadas del sistema de escritura de tubería, y tal vez ser ~ 10% más rápido. (O ~ 60% más rápido en Skylake si no modifica la configuración de administración de energía).


Lo mejor que podría hacer con un zlib personalizado con una skipbytesfunción sería analizar los símbolos en un bloque de compresión para llegar al final sin hacer el trabajo de reconstruir realmente el bloque descomprimido. Esto podría ser significativamente más rápido (probablemente al menos 2 veces) que llamar a la función de decodificación regular de zlib para sobrescribir el mismo búfer y avanzar en el archivo. Pero no sé si alguien ha escrito tal función. (Y creo que esto realmente no funciona a menos que el archivo se haya escrito especialmente para permitir que el decodificador se reinicie en un determinado bloque).

Tenía la esperanza de que hubiera una manera de saltar los bloques Deflate sin decodificarlos, porque eso sería mucho más rápido. El árbol Huffman se envía al comienzo de cada bloque, por lo que puede decodificar desde el comienzo de cualquier bloque (creo). Oh, creo que el estado del decodificador es más que el árbol Huffman, también es el 32kB anterior de datos decodificados, y esto no se restablece / olvida a través de los límites de bloque de forma predeterminada. Los mismos bytes pueden seguir siendo referenciados repetidamente, por lo que solo pueden aparecer literalmente una vez en un archivo comprimido gigante. (por ejemplo, en un archivo de registro, el nombre de host probablemente permanece "activo" en el diccionario de compresión todo el tiempo, y cada instancia hace referencia al anterior, no al primero).

El zlibmanual dice que debe usar Z_FULL_FLUSHal llamar deflatesi desea que la secuencia comprimida pueda buscarse en ese punto. "Restablece el estado de compresión", por lo que creo que sin eso, las referencias hacia atrás pueden ir a los bloques anteriores. Entonces, a menos que su archivo zip se haya escrito con bloques de descarga completa ocasionales (como cada 1G o algo tendría un impacto insignificante en la compresión), creo que tendría que hacer más trabajo de decodificación hasta el punto que deseaba de lo que inicialmente estaba pensando. Supongo que probablemente no puedas comenzar al comienzo de cualquier bloque.


El resto de esto se escribió mientras pensaba que sería posible encontrar el comienzo del bloque que contiene el primer byte que desea y decodificar desde allí.

Pero desafortunadamente, el inicio de un bloque Deflate no indica cuánto tiempo es para bloques comprimidos. Los datos incompatibles se pueden codificar con un tipo de bloque sin comprimir que tiene un tamaño de 16 bits en bytes en la parte frontal, pero los bloques comprimidos no: RFC 1951 describe el formato de forma bastante legible . Los bloques con codificación dinámica de Huffman tienen el árbol al frente del bloque (para que el descompresor no tenga que buscar en la secuencia), por lo que el compresor debe haber mantenido todo el bloque (comprimido) en la memoria antes de escribirlo.

La distancia máxima de referencia hacia atrás es de solo 32 kB, por lo que el compresor no necesita mantener muchos datos sin comprimir en la memoria, pero eso no limita el tamaño del bloque. Los bloques pueden tener varios megabytes de longitud. (Esto es lo suficientemente grande como para que el disco valga la pena incluso en una unidad magnética, frente a la lectura secuencial en la memoria y simplemente omitir datos en la RAM, si fuera posible encontrar el final del bloque actual sin analizarlo).

zlib crea bloques el mayor tiempo posible: según Marc Adler , zlib solo comienza un nuevo bloque cuando el búfer de símbolos se llena, lo que con la configuración predeterminada es 16.383 símbolos (literales o coincidencias)


Comprimí la salida de seq(que es extremadamente redundante y, por lo tanto, probablemente no sea una gran prueba), pero pv < /tmp/seq1G.gz | gzip -d | tail -c $((1024*1024*1000)) | wc -cfunciona con solo ~ 62 MiB / s de datos comprimidos en un Skylake i7-6700k a 3.9GHz, con DDR4-2666 RAM. Eso es 246MiB / s de datos descomprimidos, que es un cambio considerable en comparación con la memcpyvelocidad de ~ 12 GiB / s para tamaños de bloque demasiado grandes para caber en la memoria caché.

(Con el energy_performance_preferenceajuste predeterminado en balance_powerlugar de balance_performance, el gobernador interno de la CPU de Skylake decide ejecutar solo a 2.7GHz, ~ 43 MiB / s de datos comprimidos. Lo uso sudo sh -c 'for i in /sys/devices/system/cpu/cpufreq/policy[0-9]*/energy_performance_preference;do echo balance_performance > "$i";done'para ajustarlo. Probablemente estas llamadas frecuentes del sistema no parecen estar realmente vinculadas a la CPU trabajar para la unidad de administración de energía.)

TL: DR: zcat | tail -cestá vinculado a la CPU incluso en una CPU rápida, a menos que tenga discos muy lentos. gzip usó el 100% de la CPU en la que se ejecutaba (y ejecutó 1.81 instrucciones por reloj, según perf), y tailutilizó 0.162 de la CPU en la que se ejecutó (0.58 IPC). El sistema estaba en su mayor parte inactivo.

Estoy usando Linux 4.14.11-1-ARCH, que tiene KPTI habilitado de manera predeterminada para evitar Meltdown, por lo que todas esas writellamadas al sistema gzipson más caras de lo que solían ser: /


Tener la búsqueda incorporada en unzipo zcat(pero aún utilizando la zlibfunción de decodificación regular ) ahorraría todas esas escrituras de tubería, y conseguiría que las CPU Skylake funcionen a la velocidad de reloj completa. (Este downclocking para algunos tipos de carga es exclusivo de Intel Skylake y posterior, que han descargado la toma de decisiones de frecuencia de la CPU del sistema operativo, porque tienen más datos sobre lo que está haciendo la CPU y pueden aumentar / disminuir más rápido. Esto es normalmente bueno, pero aquí lleva a Skylake a no acelerar a toda velocidad con una configuración de gobernador más conservadora).

No hay llamadas del sistema, solo reescribir un búfer que se ajuste en la caché L2 hasta que llegue a la posición de byte inicial que desea, probablemente supondría al menos un% de diferencia. Tal vez incluso el 10%, pero solo estoy inventando números aquí. No he perfilado zlibningún detalle para ver qué tan grande es la huella de caché, y cuánto duele el vaciado TLB (y, por lo tanto, el vaciado uop-cache) en cada llamada al sistema con KPTI habilitado.


Hay algunos proyectos de software que agregan un índice de búsqueda al formato de archivo gzip . Esto no le ayuda si no puede lograr que nadie genere archivos comprimidos buscables para usted, pero otros futuros lectores pueden beneficiarse.

Es de suponer que ninguno de estos proyectos tienen una función de decodificación que sabe cómo saltar a través de una corriente desinflarse sin un índice, ya que sólo están diseñados para trabajar cuando un índice está disponible.

Peter Cordes
fuente
1

Puede abrir el archivo zip en una sesión de Python, utilizando zf = zipfile.ZipFile(filename, 'r', allowZip64=True)y una vez abierto puede abrir, para leer, cualquier archivo dentro del archivo zip y leer líneas, etc., como si fuera un archivo normal.

Steve Barnes
fuente