Divida un archivo en varios archivos según el delimitador


86

Tengo un archivo con -|como delimitador después de cada sección ... necesito crear archivos separados para cada sección usando unix.

ejemplo de archivo de entrada

wertretr
ewretrtret
1212132323
000232
-|
ereteertetet
232434234
erewesdfsfsfs
0234342343
-|
jdhg3875jdfsgfd
sjdhfdbfjds
347674657435
-|

Resultado esperado en el archivo 1

wertretr
ewretrtret
1212132323
000232
-|

Resultado esperado en el archivo 2

ereteertetet
232434234
erewesdfsfsfs
0234342343
-|

Resultado esperado en el archivo 3

jdhg3875jdfsgfd
sjdhfdbfjds
347674657435
-|

1
¿Está escribiendo un programa o desea hacerlo utilizando utilidades de línea de comandos?
rkyser

1
será preferible usar utilidades de línea de comando ..
user1499178

Podría usar awk, sería fácil escribir un programa de 3 o 4 líneas para hacerlo. Lamentablemente no tengo práctica.
ctrl-alt-delor

Respuestas:


97

Un trazador de líneas, sin programación. (excepto la expresión regular, etc.)

csplit --digits=2  --quiet --prefix=outfile infile "/-|/+1" "{*}"

probado en: csplit (GNU coreutils) 8.30

Notas sobre el uso en Apple Mac

"Para los usuarios de OS X, tenga en cuenta que la versión csplitque viene con el sistema operativo no funciona. Querrá la versión en coreutils (instalable a través de Homebrew), que se llama gcsplit". - @Danial

"Solo para agregar, puedes hacer que la versión para OS X funcione (al menos con High Sierra). Solo necesitas modificar un poco los argumentos csplit -k -f=outfile infile "/-\|/+1" "{3}". Las características que no parecen funcionar son "{*}", tuve que ser específico en el número de separadores, y es necesario agregarlo -kpara evitar que se eliminen todos los archivos de salida si no puede encontrar un separador final. Además, si lo desea --digits, debe usar -nen su lugar ". - @Pebbl


31
@ zb226 Lo hice en mucho tiempo, por lo que no se necesitaba explicación.
ctrl-alt-delor

5
Sugiero agregar --elide-empty-files, de lo contrario, habrá un archivo vacío al final.
luator

8
Para los usuarios de OS X, tenga en cuenta que la versión de csplit que viene con el sistema operativo no funciona. Querrá la versión en coreutils (instalable a través de Homebrew), que se llama gcsplit .
Daniel

10
Solo para aquellos que se preguntan qué significan los parámetros: --digits=2controla la cantidad de dígitos utilizados para numerar los archivos de salida (2 es el valor predeterminado para mí, por lo que no es necesario). --quietsuprime la salida (tampoco es realmente necesario o solicitado aquí). --prefixespecifica el prefijo de los archivos de salida (el predeterminado es xx). Por lo tanto, puede omitir todos los parámetros y obtendrá archivos de salida como xx12.
Christopher K.

3
Solo para agregar, puede obtener la versión para OS X para que funcione (al menos con High Sierra). Solo necesitas modificar un poco los argumentos csplit -k -f=outfile infile "/-\|/+1" "{3}". Las características que no parecen funcionar son "{*}", tuve que ser específico en el número de separadores y necesitaba agregar -kpara evitar que se borren todos los archivos de salida si no puede encontrar un separador final. Además, si lo desea --digits, debe usar -nen su lugar.
Pebbl

38
awk '{f="file" NR; print $0 " -|"> f}' RS='-\\|'  input-file

Explicación (editada):

RSes el separador de registros, y esta solución usa una extensión gnu awk que le permite tener más de un carácter. NRes el número de registro.

La declaración de impresión imprime un registro seguido de " -|"en un archivo que contiene el número de registro en su nombre.


1
RSes el separador de registros, y esta solución usa una extensión gnu awk que le permite tener más de un carácter. NR es el número de registro. La declaración de impresión imprime un registro seguido de "- |" en un archivo que contiene el número de registro en su nombre.
William Pursell

1
@rzetterbeg Esto debería funcionar bien con archivos grandes. awk procesa el archivo un registro a la vez, por lo que solo lee lo que necesita. Si la primera aparición del separador de registros aparece muy tarde en el archivo, puede ser un problema de memoria, ya que un registro completo debe caber en la memoria. Además, tenga en cuenta que usar más de un carácter en RS no es estándar en awk, pero esto funcionará en gnu awk.
William Pursell

4
Para mí, dividió 3.3 GB en 31.728 s
Cleankod

3
@ccf El nombre del archivo es solo la cadena en el lado derecho del >, por lo que puede construirlo como desee. por ejemplo,print $0 "-|" > "file" NR ".txt"
William Pursell

1
@AGrush Eso depende de la versión. Puedes hacerloawk '{f="file" NR; print $0 " -|" > f}'
William Pursell

7

Debian lo ha hecho csplit, pero no sé si eso es común a todas / la mayoría / las demás distribuciones. Sin embargo, si no es así, no debería ser demasiado difícil rastrear la fuente y compilarla ...


1
Estoy de acuerdo. Mi caja de Debian dice que csplit es parte de gnu coreutils. Entonces, cualquier sistema operativo Gnu, como todas las distribuciones Gnu / Linux, lo tendrá. Wikipedia también menciona 'The Single UNIX® Specification, Issue 7' en la página csplit, así que sospecho que la tienes.
ctrl-alt-delor

3
Dado que csplitestá en POSIX, esperaría que esté disponible esencialmente en todos los sistemas similares a Unix.
Jonathan Leffler

1
Aunque csplit es POISX, el problema (parece que estoy haciendo una prueba en el sistema Ubuntu frente a mí) es que no hay una forma obvia de hacer que use una sintaxis de expresiones regulares más moderna. Compare: csplit --prefix gold-data - "/^==*$/vs csplit --prefix gold-data - "/^=+$/. Al menos GNU grep tiene -e.
new123456

5

Resolví un problema ligeramente diferente, donde el archivo contiene una línea con el nombre donde debe ir el texto que sigue. Este código de Perl hace el truco para mí:

#!/path/to/perl -w

#comment the line below for UNIX systems
use Win32::Clipboard;

# Get command line flags

#print ($#ARGV, "\n");
if($#ARGV == 0) {
    print STDERR "usage: ncsplit.pl --mff -- filename.txt [...] \n\nNote that no space is allowed between the '--' and the related parameter.\n\nThe mff is found on a line followed by a filename.  All of the contents of filename.txt are written to that file until another mff is found.\n";
    exit;
}

# this package sets the ARGV count variable to -1;

use Getopt::Long;
my $mff = "";
GetOptions('mff' => \$mff);

# set a default $mff variable
if ($mff eq "") {$mff = "-#-"};
print ("using file switch=", $mff, "\n\n");

while($_ = shift @ARGV) {
    if(-f "$_") {
    push @filelist, $_;
    } 
}

# Could be more than one file name on the command line, 
# but this version throws away the subsequent ones.

$readfile = $filelist[0];

open SOURCEFILE, "<$readfile" or die "File not found...\n\n";
#print SOURCEFILE;

while (<SOURCEFILE>) {
  /^$mff (.*$)/o;
    $outname = $1;
#   print $outname;
#   print "right is: $1 \n";

if (/^$mff /) {

    open OUTFILE, ">$outname" ;
    print "opened $outname\n";
    }
    else {print OUTFILE "$_"};
  }

¿Puede explicar por qué funciona este código? Tengo una situación similar a la que ha descrito aquí: los nombres de archivo de salida requeridos están incrustados dentro del archivo. Pero no soy un usuario habitual de Perl, así que no puedo entender este código.
shiri

La verdadera carne está en el whileciclo final . Si encuentra la mffexpresión regular al principio de la línea, usa el resto de la línea como el nombre del archivo para abrir y comenzar a escribir. Nunca cierra nada, por lo que se quedará sin identificadores de archivos después de unas pocas docenas.
tripleee

La secuencia de comandos se mejoraría eliminando la mayor parte del código antes del whileciclo final y cambiando awhile (<>)
tripleee

4

El siguiente comando funciona para mí. Espero eso ayude.

awk 'BEGIN{file = 0; filename = "output_" file ".txt"}
    /-|/ {getline; file ++; filename = "output_" file ".txt"}
    {print $0 > filename}' input

1
Esto se quedará sin identificadores de archivos después de unas pocas docenas de archivos. La solución es explícitamente closeel archivo anterior cuando inicia uno nuevo.
tripleee

@tripleee cómo se cierra (pregunta awk para principiantes). ¿Puede proporcionar un ejemplo actualizado?
Jesper Rønn-Jensen

1
@ JesperRønn-Jensen Este cuadro es probablemente demasiado pequeño para cualquier ejemplo útil, pero básicamente if (file) close(filename);antes de asignar un nuevo filenamevalor.
tripleee

aah encontró la manera de cerrarla: ; close(filename). Realmente simple, pero realmente corrige el ejemplo anterior
Jesper Rønn-Jensen

1
@ JesperRønn-Jensen Deshice su edición porque proporcionó un guión roto. Probablemente se deberían evitar las ediciones significativas en las respuestas de otras personas; siéntase libre de publicar una nueva respuesta propia (tal vez como un wiki de la comunidad ) si cree que se merece una respuesta separada.
tripleee

2

También puedes usar awk. No estoy muy familiarizado con awk, pero lo siguiente pareció funcionar para mí. Generaba part1.txt, part2.txt, part3.txt y part4.txt. Tenga en cuenta que el último archivo partn.txt que genera está vacío. No estoy seguro de cómo solucionarlo, pero estoy seguro de que podría hacerse con algunos ajustes. ¿Alguna sugerencia de alguien?

archivo awk_pattern:

BEGIN{ fn = "part1.txt"; n = 1 }
{
   print > fn
   if (substr($0,1,2) == "-|") {
       close (fn)
       n++
       fn = "part" n ".txt"
   }
}

comando bash:

awk -f awk_pattern input.file


2

Aquí hay una secuencia de comandos de Python 3 que divide un archivo en varios archivos según un nombre de archivo proporcionado por los delimitadores. Archivo de entrada de ejemplo:

# Ignored

######## FILTER BEGIN foo.conf
This goes in foo.conf.
######## FILTER END

# Ignored

######## FILTER BEGIN bar.conf
This goes in bar.conf.
######## FILTER END

Aquí está el guión:

#!/usr/bin/env python3

import os
import argparse

# global settings
start_delimiter = '######## FILTER BEGIN'
end_delimiter = '######## FILTER END'

# parse command line arguments
parser = argparse.ArgumentParser()
parser.add_argument("-i", "--input-file", required=True, help="input filename")
parser.add_argument("-o", "--output-dir", required=True, help="output directory")

args = parser.parse_args()

# read the input file
with open(args.input_file, 'r') as input_file:
    input_data = input_file.read()

# iterate through the input data by line
input_lines = input_data.splitlines()
while input_lines:
    # discard lines until the next start delimiter
    while input_lines and not input_lines[0].startswith(start_delimiter):
        input_lines.pop(0)

    # corner case: no delimiter found and no more lines left
    if not input_lines:
        break

    # extract the output filename from the start delimiter
    output_filename = input_lines.pop(0).replace(start_delimiter, "").strip()
    output_path = os.path.join(args.output_dir, output_filename)

    # open the output file
    print("extracting file: {0}".format(output_path))
    with open(output_path, 'w') as output_file:
        # while we have lines left and they don't match the end delimiter
        while input_lines and not input_lines[0].startswith(end_delimiter):
            output_file.write("{0}\n".format(input_lines.pop(0)))

        # remove end delimiter if present
        if not input_lines:
            input_lines.pop(0)

Finalmente, así es como se ejecuta:

$ python3 script.py -i input-file.txt -o ./output-folder/

2

Úselo csplitsi lo tiene.

Si no lo tiene, pero tiene Python ... no use Perl.

Lectura perezosa del archivo

Es posible que su archivo sea demasiado grande para guardarlo en la memoria de una vez; es preferible leer línea por línea. Suponga que el archivo de entrada se llama "samplein":

$ python3 -c "from itertools import count
with open('samplein') as file:
    for i in count():
        firstline = next(file, None)
        if firstline is None:
            break
        with open(f'out{i}', 'w') as out:
            out.write(firstline)
            for line in file:
                out.write(line)
                if line == '-|\n':
                    break"

Esto leerá todo el archivo en la memoria, lo que significa que será ineficaz o incluso fallará para archivos grandes.
tripleee

1
@tripleee He actualizado la respuesta para manejar archivos muy grandes.
Aaron Hall

0
cat file| ( I=0; echo -n "">file0; while read line; do echo $line >> file$I; if [ "$line" == '-|' ]; then I=$[I+1]; echo -n "" > file$I; fi; done )

y la versión formateada:

#!/bin/bash
cat FILE | (
  I=0;
  echo -n"">file0;
  while read line; 
  do
    echo $line >> file$I;
    if [ "$line" == '-|' ];
    then I=$[I+1];
      echo -n "" > file$I;
    fi;
  done;
)

4
Como siempre, el cates inútil .
tripleee

1
@Reishin La página vinculada explica con mucho más detalle cómo puede evitar caten un solo archivo en cada situación. Hay una pregunta de Stack Overflow con más discusión (aunque la respuesta aceptada es en mi humilde opinión); stackoverflow.com/questions/11710552/useless-use-of-cat
tripleee

1
El caparazón es típicamente muy ineficiente en este tipo de cosas de todos modos; si no puede usar csplit, una solución Awk probablemente sea mucho más preferible a esta solución (incluso si tuviera que solucionar los problemas informados por shellcheck.net, etc., tenga en cuenta que actualmente no encuentra todos los errores en esto).
tripleee

@tripleee pero si la tarea es hacerlo sin awk, csplit y etc., ¿solo bash?
Reishin

1
Entonces el catsigue siendo inútil, y el resto del guión podría simplificarse y corregirse bastante; pero seguirá siendo lento. Ver, por ejemplo, stackoverflow.com/questions/13762625/…
tripleee

0

Este es el tipo de problema para el que escribí context-split: http://stromberg.dnsalias.org/~strombrg/context-split.html

$ ./context-split -h
usage:
./context-split [-s separator] [-n name] [-z length]
        -s specifies what regex should separate output files
        -n specifies how output files are named (default: numeric
        -z specifies how long numbered filenames (if any) should be
        -i include line containing separator in output files
        operations are always performed on stdin

Uh, esto parece esencialmente un duplicado de la csplitutilidad estándar . Vea la respuesta de @ richard .
tripleee

Esta es en realidad la mejor solución en mi opinión. Tuve que dividir un volcado de mysql 98G y csplit por alguna razón se consume toda mi RAM y me matan. Aunque solo debería tener que coincidir con una línea a la vez. No tiene sentido. Este script de Python funciona mucho mejor y no consume toda la memoria RAM.
Stefan Midjich

0

Aquí hay un código de Perl que hará la cosa

#!/usr/bin/perl
open(FI,"file.txt") or die "Input file not found";
$cur=0;
open(FO,">res.$cur.txt") or die "Cannot open output file $cur";
while(<FI>)
{
    print FO $_;
    if(/^-\|/)
    {
        close(FO);
        $cur++;
        open(FO,">res.$cur.txt") or die "Cannot open output file $cur"
    }
}
close(FO);
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.