Fractionner un gros fichier en morceaux sans diviser l'entrée

8

J'ai un fichier .msg assez volumineux formaté au format UIEE.

$ wc -l big_db.msg
8726593 big_db.msg

Essentiellement, le fichier est composé d'entrées de différentes longueurs qui ressemblent à ceci:

UR|1
AA|Condon, Richard
TI|Prizzi's Family
CN|Collectable- Good/Good
MT|FICTION
PU|G.P. Putnam & Sons
DP|1986
ED|First Printing.
BD|Hard Cover
NT|0399132104
KE|MAFIA
KE|FICTION
PR|44.9
XA|4
XB|1
XC|BO
XD|S

UR|10
AA|Gariepy, Henry
TI|Portraits of Perseverance
CN|Good/No Jacket
MT|SOLD
PU|Victor Books
DP|1989
BD|Mass Market Paperback
NT|1989 tpb g 100 meditations from the Book of Job "This book...help you
NT| persevere through the struggles of your life..."
KE|Bible
KE|religion
KE|Job
KE|meditations
PR|28.4
XA|4
XB|5
XC|BO
XD|S

Ceci est un exemple de deux entrées, séparées par une ligne vierge. Je souhaite diviser ce gros fichier en fichiers plus petits sans casser une entrée en deux fichiers.

Chaque entrée individuelle est séparée par une nouvelle ligne (une ligne complètement vierge) dans le fichier. Je souhaite diviser ce fichier de 8,7 millions de lignes en 15 fichiers. Je comprends que des outils comme splitexistent, mais je ne sais pas trop comment diviser le fichier, mais le diviser uniquement sur une nouvelle ligne afin qu'une seule entrée ne soit pas divisée en plusieurs fichiers.

user2036066
la source
csplitexiste aussi.
mikeserv
Pouvez-vous créer des fichiers temporaires?
Braiam
@Braiam, je ne sais pas ce que tu veux dire, mais je pense que oui. J'ai un accès complet sur le système de fichiers.
user2036066
il signifie créer des fichiers qui sont utilisés temporairement pour le processus
polym
1
Pourquoi exactement 15 fichiers, si je peux demander? Les préfixes avant le tuyau |(comme UR, AA, TI) pertinents pour le nombre de fichiers, même le même pour être exact?
polym

Réponses:

2

Voici une solution qui pourrait fonctionner:

seq 1 $(((lines=$(wc -l </tmp/file))/16+1)) $lines |
sed 'N;s|\(.*\)\(\n\)\(.*\)|\1d;\1,\3w /tmp/uptoline\3\2\3|;P;$d;D' |
sed -ne :nl -ne '/\n$/!{N;bnl}' -nf - /tmp/file

Cela fonctionne en permettant au premier sedd'écrire le sedscript du second . La seconde sedpremière rassemble toutes les lignes d'entrée jusqu'à ce qu'elle rencontre une ligne vierge. Il écrit ensuite toutes les lignes de sortie dans un fichier. Le premier sedécrit un script pour le second lui indiquant où écrire sa sortie. Dans mon cas de test, ce script ressemblait à ceci:

1d;1,377w /tmp/uptoline377
377d;377,753w /tmp/uptoline753
753d;753,1129w /tmp/uptoline1129
1129d;1129,1505w /tmp/uptoline1505
1505d;1505,1881w /tmp/uptoline1881
1881d;1881,2257w /tmp/uptoline2257
2257d;2257,2633w /tmp/uptoline2633
2633d;2633,3009w /tmp/uptoline3009
3009d;3009,3385w /tmp/uptoline3385
3385d;3385,3761w /tmp/uptoline3761
3761d;3761,4137w /tmp/uptoline4137
4137d;4137,4513w /tmp/uptoline4513
4513d;4513,4889w /tmp/uptoline4889
4889d;4889,5265w /tmp/uptoline5265
5265d;5265,5641w /tmp/uptoline5641

Je l'ai testé comme ceci:

printf '%s\nand\nmore\nlines\nhere\n\n' $(seq 1000) >/tmp/file

Cela m'a fourni un fichier de 6000 lignes, qui ressemblait à ceci:

<iteration#>
and
more
lines
here
#blank

... répété 1000 fois.

Après avoir exécuté le script ci-dessus:

set -- /tmp/uptoline*
echo $# total splitfiles
for splitfile do
    echo $splitfile
    wc -l <$splitfile
    tail -n6 $splitfile
done    

PRODUCTION

15 total splitfiles
/tmp/uptoline1129
378
188
and
more
lines
here

/tmp/uptoline1505
372
250
and
more
lines
here

/tmp/uptoline1881
378
313
and
more
lines
here

/tmp/uptoline2257
378
376
and
more
lines
here

/tmp/uptoline2633
372
438
and
more
lines
here

/tmp/uptoline3009
378
501
and
more
lines
here

/tmp/uptoline3385
378
564
and
more
lines
here

/tmp/uptoline3761
372
626
and
more
lines
here

/tmp/uptoline377
372
62
and
more
lines
here

/tmp/uptoline4137
378
689
and
more
lines
here

/tmp/uptoline4513
378
752
and
more
lines
here

/tmp/uptoline4889
372
814
and
more
lines
here

/tmp/uptoline5265
378
877
and
more
lines
here

/tmp/uptoline5641
378
940
and
more
lines
here

/tmp/uptoline753
378
125
and
more
lines
here
mikeserv
la source
3

En utilisant la suggestion de csplit:

Fractionnement basé sur les numéros de ligne

$ csplit file.txt <num lines> "{repetitions}"

Exemple

Disons que j'ai un fichier contenant 1 000 lignes.

$ seq 1000 > file.txt

$ csplit file.txt 100 "{8}"
288
400
400
400
400
400
400
400
400
405

donne des fichiers comme ceci:

$ wc -l xx*
  99 xx00
 100 xx01
 100 xx02
 100 xx03
 100 xx04
 100 xx05
 100 xx06
 100 xx07
 100 xx08
 101 xx09
   1 xx10
1001 total

Vous pouvez contourner la limitation statique d'avoir à spécifier le nombre de répétitions en pré-calculant les nombres en fonction du nombre de lignes dans votre fichier particulier à l'avance.

$ lines=100
$ echo $lines 
100

$ rep=$(( ($(wc -l file.txt | cut -d" " -f1) / $lines) -2 ))
$ echo $rep
8

$ csplit file.txt 100 "{$rep}"
288
400
400
400
400
400
400
400
400
405

Fractionnement basé sur des lignes vides

Si, par contre, vous souhaitez simplement fractionner un fichier sur des lignes vides contenues dans le fichier, vous pouvez utiliser cette version de split:

$ csplit file2.txt '/^$/' "{*}"

Exemple

Supposons que j'ai ajouté 4 lignes vides à ce qui file.txtprécède et créez le fichier file2.txt. Vous pouvez voir qu'ils ont été ajoutés manuellement comme suit:

$ grep -A1 -B1 "^$" file2.txt
20

21
--
72

73
--
112

113
--
178

179

Ce qui précède montre que je les ai ajoutés entre les numéros correspondants dans mon exemple de fichier. Maintenant, lorsque j'exécute la csplitcommande:

$ csplit file2.txt '/^$/' "{*}"
51
157
134
265
3290

Vous pouvez voir que j'ai maintenant 4 fichiers qui ont été divisés en fonction de la ligne vierge:

$ grep -A1 -B1 '^$' xx0*
xx01:
xx01-21
--
xx02:
xx02-73
--
xx03:
xx03-113
--
xx04:
xx04-179

Références

slm
la source
J'ai édité l'OP avec ma tentative d'utilisation et je n'ai pas pu le faire fonctionner.
user2036066
Le fichier n'a pas été divisé sur une nouvelle ligne vierge, ce que j'ai essayé d'accomplir.
user2036066
@ user2036066 - vous voulez diviser le fichier en 15 morceaux de fichier en vous assurant qu'il n'y a pas de fractionnement sur une ligne partielle ou autre chose?
slm
@ user2036066 - attendez que le fichier comporte 14 à 15 lignes complètement vides sur lesquelles vous souhaitez fractionner?
slm
Modifié à nouveau l'op avec plus de contexte @slm
user2036066
3

Si vous ne vous souciez pas de l'ordre des enregistrements, vous pouvez faire:

gawk -vRS= '{printf "%s", $0 RT > "file.out." (NR-1)%15}' file.in

Sinon, vous devez d'abord obtenir le nombre d'enregistrements, pour savoir combien mettre dans chaque fichier de sortie:

gawk -vRS= -v "n=$(gawk -vRS= 'END {print NR}' file.in)" '
  {printf "%s", $0 RT > "file.out." int((NR-1)*15/n)}' file.in
Stéphane Chazelas
la source
Utiliser awk pour diviser sur des lignes vides a également été ma première pensée - +1
godlygeek
Quels sont file.inet file.out?
mikeserv
1

Si vous cherchez à diviser uniquement à la fin d'une ligne, vous devriez pouvoir le faire avec l' -loption pour split.

Si vous cherchez à diviser sur une ligne vide ( \n\n), voici comment je le ferais dans ksh. Je ne l'ai pas testé, et ce n'est probablement pas idéal, mais quelque chose dans ce sens fonctionnerait:

filenum=0
counter=0
limit=580000

while read LINE
do
  counter=counter+1

  if (( counter >= limit ))
  then
    if [[ $LINE == "" ]]
    then
      filenum=filenum+1
      counter=0
    fi
  fi

  echo $LINE >>big_db$filenum.msg
done <big_db.msg
hornj
la source
1
Il est possible que j'ai mal lu, mais op demande comment se séparer \n\n, je pense.
mikeserv
Cela ne m'aide pas vraiment car cela divisera toujours le fichier à mi-entrée. J'en ai besoin pour que le fichier ne soit divisé que sur une ligne vierge.
user2036066
Oui, j'ai mal lu, désolé. Ce n'est peut-être pas le meilleur moyen, je voudrais simplement lire le fichier d'origine dans une boucle avec un compteur du nombre de lignes que vous avez passées, et une fois que vous avez atteint le nombre que vous souhaitez diviser, commencez la sortie vers un nouveau fichier à la prochaine ligne blanche.
hornj
Tentative de tester ce script en ce moment.
user2036066
1
Je pense que OP ne demande pas comment se séparer \n\n, mais plutôt ne pas se séparer au milieu d'une ligne. Il appelle une nouvelle ligne une ligne vierge.
polym
0

Essayer awk

awk 'BEGIN{RS="\n\n"}{print $0 > FILENAME"."FNR}' big_db.msg
dchirikov
la source
Tentative de cette solution en ce moment
user2036066
2
Cette solution crée un nouveau fichier pour chaque entrée, ce qui n'est pas du tout ce que je veux.
user2036066
0

Si vous ne vous souciez pas de l'ordre des enregistrements mais que vous êtes particulièrement soucieux d'obtenir un certain nombre de fichiers de sortie, la réponse de Stéphane est la voie que j'irais. Mais j'ai le sentiment que vous pourriez vous soucier davantage de spécifier une taille que chaque fichier de sortie ne doit pas dépasser. Cela le rend plus facile car vous pouvez lire votre fichier d'entrée et collecter des enregistrements jusqu'à ce que vous atteigniez cette taille, puis démarrer un nouveau fichier de sortie. Si cela fonctionne pour vous, la plupart des langages de programmation peuvent gérer votre tâche avec un court script. Voici une implémentation awk:

BEGIN {
    RS = "\n\n"
    ORS = "\n\n"
    maxlen = (maxlen == 0 ? 500000 : maxlen)
    oi = 1
}

{
    reclen = length($0) + 2
    if (n + reclen > maxlen) {
        oi++
        n = 0
    }
    n += reclen
    print $0 > FILENAME"."oi
}

Placez-le dans un fichier, par exemple program.awk, et exécutez-le avec awk -v maxlen=10000 -f program.awk big_db.msgoù la valeur de maxlenest le plus d'octets que vous souhaitez dans un fichier. Il utilisera 500k par défaut.

Si vous souhaitez obtenir un nombre défini de fichiers, le moyen le plus simple consiste probablement à diviser simplement la taille de votre fichier d'entrée par le nombre de fichiers que vous souhaitez, puis à ajouter un peu à ce nombre pour obtenir maxlen. Par exemple, pour obtenir 15 fichiers de vos 8726593 octets, divisez par 15 pour obtenir 581773 et ajoutez-en, alors donnez peut-être maxlen=590000ou maxlen=600000. Si vous voulez le faire de façon répétée, il serait possible de configurer le programme pour le faire.

David Z
la source