巨大なファイルのcat行Xから行Y


132

私は(> 2GB)巨大なテキストファイルを持っていると私はしたいとcatラインXY(例えば57890010から57890000)。

私が理解から私は、配管によってこれを行うことができますheadtail、すなわち、またはその逆

head -A /path/to/file | tail -B

または代わりに

tail -C /path/to/file | head -D

ここでABCおよびDファイル内の行の数から計算することができる、XY

しかし、このアプローチには2つの問題があります。

  1. あなたは計算する必要がありABCD
  2. コマンドは、読みたいと思うよりもはるかに多くのpipeを相互に送信できます(たとえば、巨大なファイルの途中で数行だけを読んでいる場合)

シェルを動作させて、必要な行を出力する方法はありますか?(Xとのみを提供しながらY)?


1
参考までに、6つの方法の実際の速度テストの比較を追加しました。
ケビン

回答:


119

私はsed解決策を提案しますが、完全を期すために、

awk 'NR >= 57890000 && NR <= 57890010' /path/to/file

最後の行の後に切り取るには:

awk 'NR < 57890000 { next } { print } NR == 57890010 { exit }' /path/to/file

速度テスト:

  • によって生成された100,000,000行のファイル seq 100000000 > test.in
  • 読み取り行50,000,000-50,000,010
  • 順不同のテスト
  • realbashの組み込みで報告される時間time
 4.373  4.418  4.395    tail -n+50000000 test.in | head -n10
 5.210  5.179  6.181    sed -n '50000000,50000010p;57890010q' test.in
 5.525  5.475  5.488    head -n50000010 test.in | tail -n10
 8.497  8.352  8.438    sed -n '50000000,50000010p' test.in
22.826 23.154 23.195    tail -n50000001 test.in | head -n10
25.694 25.908 27.638    ed -s test.in <<<"50000000,50000010p"
31.348 28.140 30.574    awk 'NR<57890000{next}1;NR==57890010{exit}' test.in
51.359 50.919 51.127    awk 'NR >= 57890000 && NR <= 57890010' test.in

これらは決して正確なベンチマークではありませんが、違いは明確で、これらの各コマンドの相対速度を十分に理解できるほど十分に再現可能です*。

*:最初の2、の間を除きsed -n p;qhead|tail本質的に同じように見えます、。


11
好奇心から:テスト間でどのようにディスクキャッシュをフラッシュしましたか?
パウェルミアン

2
どの程度tail -n +50000000 test.in | head -n10と違った、tail -n-50000000 test.in | head -n10正しい結果を与えるだろうか?
ジル

4
わかりました、私は行っていくつかのベンチマークを行いました。tail | headはsedよりもはるかに高速で、その差は予想よりもはるかに大きいです。
ジル

3
@Gillesあなたが正しい、私の悪い。tail+|headsedよりも10〜15%高速です。そのベンチマークを追加しました。
ケビン

1
質問は行を要求することを理解していますが、を使用し-cて文字をスキップする場合、tail+|head瞬時です。もちろん、「50000000」とは言えず、探しているセクションの先頭を手動で検索する必要があります。
ダニーキルヒマイヤー14

51

XからYまでの行(1から番号付けを開始)が必要な場合は、

tail -n +$X /path/to/file | head -n $((Y-X+1))

tail最初のX-1行を読み取って破棄し(それを回避する方法はありません)、次の行を読み取って印刷します。head要求された行数を読み取って出力し、終了します。ときにhead終了するには、tail受信SIGPIPEの信号をと死ぬので、入力ファイルからの行のバッファサイズ分(典型的には数キロバイト)以上のものを読んだことがありません。

または、gorkyplが示唆したように、sedを使用します。

sed -n -e "$X,$Y p" -e "$Y q" /path/to/file

ただし、sedソリューションは大幅に遅くなります(少なくともGNUユーティリティとBusyboxユーティリティの場合、パイピングが遅く、sedが速いOSでファイルの大部分を抽出する場合、sedはより競争力があるかもしれません)。Linuxでの簡単なベンチマークは次のとおりです。データはによって生成されseq 100000000 >/tmp/a、環境はLinux / amd64、/tmptmpfsであり、それ以外の場合はマシンはアイドル状態であり、スワッピングではありません。

real  user  sys    command
 0.47  0.32  0.12  </tmp/a tail -n +50000001 | head -n 10 #GNU
 0.86  0.64  0.21  </tmp/a tail -n +50000001 | head -n 10 #BusyBox
 3.57  3.41  0.14  sed -n -e '50000000,50000010 p' -e '50000010q' /tmp/a #GNU
11.91 11.68  0.14  sed -n -e '50000000,50000010 p' -e '50000010q' /tmp/a #BusyBox
 1.04  0.60  0.46  </tmp/a tail -n +50000001 | head -n 40000001 >/dev/null #GNU
 7.12  6.58  0.55  </tmp/a tail -n +50000001 | head -n 40000001 >/dev/null #BusyBox
 9.95  9.54  0.28  sed -n -e '50000000,90000000 p' -e '90000000q' /tmp/a >/dev/null #GNU
23.76 23.13  0.31  sed -n -e '50000000,90000000 p' -e '90000000q' /tmp/a >/dev/null #BusyBox

使用するバイト範囲がわかっている場合は、開始位置に直接スキップすることで、より速く抽出できます。しかし、行については、最初から読み、改行を数える必要があります。ブロックサイズがbで、0から始まるxからyの排他的なブロックを抽出するには:

dd bs=$b seek=$x count=$((y-x)) </path/to/file

1
間にキャッシングはありませんか?tail | headとsedの違いは私には大きすぎるようです。
パウェウルミアン

@gorkypl私はいくつかの手段を講じ、時間は同等でした。私が書いたように、これはすべてRAMで発生しています(すべてがキャッシュにあります)。
ジル

1
@Gilles tail will read and discard the first X-1 lineは、行数が最後から指定されると回避されるようです。このような場合、実行時間に従ってtailは最後から逆方向に読み取るようです。読んでください:http://unix.stackexchange.com/a/216614/79743

1
@BinaryZebraはい、入力が通常のファイルである場合、tail(GNU tailを含む)の一部の実装には、最後から読み取るためのヒューリスティックがあります。これにより、tail | head他の方法と比較してソリューションが改善されます。
ジル

22

このhead | tailアプローチは、これを行うための最良かつ最も「慣用的な」方法の1つです。

X=57890000
Y=57890010
< infile.txt head -n "$Y" | tail -n +"$X"

コメントでジルが指摘したように、より速い方法は

< infile.txt tail -n +"$X" | head -n "$((Y - X))"

これが速い理由は、アプローチと比較して、最初のX-1行がパイプを通過する必要がないためhead | tailです。

フレーズとしてのあなたの質問は少し誤解を招くものであり、おそらくこのアプローチに対するあなたの根拠のない不安のいくつかを説明しています。

  • あなたは計算する必要が言うABCDシェルはとにかく、あなたのために行うことができますが、あなたが見ることができるように、ファイルの行数が必要とされていないと、せいぜい1回の計算が必要です。

  • パイピングが必要以上の行を読み込むのではないかと心配しています。実際、これは真実ではありませんtail | head。ファイルI / Oの面で得られるほど効率的です。まず、必要な最小限の作業を検討します。ファイルのX番目の行を見つけるための一般的な唯一の方法は、すべてのバイトを読み取り、X個の改行シンボルをカウントするときに停止することです。X番目の行のオフセット。* X *番目の行に到達したら、すべての行を読み取って印刷し、Y番目の行で停止する必要があります。したがって、Y行未満の読み取りで対処する方法はありません。今、Yhead -n $Y以下しか読み取らない行(最も近いバッファユニットに丸められますが、バッファを正しく使用するとパフォーマンスが向上するため、そのオーバーヘッドを心配する必要はありません)。さらに、tailを読み取ることはないheadので、head | tail可能な限り少ない行数を読み取ることを示しました(これに加えて、無視している無視できるバッファリングもいくつか)。パイプを使用しない単一ツールアプローチの唯一の効率性の利点は、プロセス数が少ない(したがってオーバーヘッドが少ない)ことです。


1
リダイレクトが前に最初に行かれるのを見たことがない。クール、それはパイプの流れを明確にします。
クラック

14

最もオーソドックスな方法(上記のGillesが指摘したように、最速ではありません)はを使用することsedです。

あなたの場合:

X=57890000
Y=57890010
sed -n -e "$X,$Y p" -e "$Y q" filename

この-nオプションは、関連する行のみが標準出力に出力されることを意味します。

終了行番号の最後のpは、指定された範囲の行を印刷することを意味します。スクリプトの2番目の部分のqは、ファイルの残りをスキップすることで時間を節約します。


1
私はほぼ同程度になると予想sedtail | headていたが、それtail | headはかなり速いことが判明した(私の答えを参照)。
ジル

1
私が読んだから、私は、知らないtail/ headファイルのどちらかの端をトリミングするので、より多くの「正統派」とみなされ、彼らがために作られているものを正確です。これらの資料では、sed代替が必要な場合にのみ画像に入り、さらに複雑なタスクの構文がAWKよりもはるかに悪いため、より複雑なものが発生し始めるとすぐに画像から押し出されるようです。 。
underscore_d

7

選択する範囲がわかっていれば、最初の行から:lStart最後の行まで:lEnd計算できます:

lCount="$((lEnd-lStart+1))"

行の合計量がわかっている場合lAll:ファイルの最後までの距離も計算できます。

toEnd="$((lAll-lStart+1))"

次に、両方を知ることができます。

"how far from the start"            ($lStart) and
"how far from the end of the file"  ($toEnd).

それらのうちの最小のものを選択する:tailnumberこのように:

tailnumber="$toEnd"; (( toEnd > lStart )) && tailnumber="+$linestart"

一貫して最も高速に実行されるコマンドを使用できます。

tail -n"${tailnumber}" ${thefile} | head -n${lCount}

$linestart選択されている場合は、追加のプラス( "+")記号に注意してください。

唯一の注意点は、行の合計数が必要であり、検索にさらに時間がかかる可能性があることです。
通常どおり:

linesall="$(wc -l < "$thefile" )"

測定される時間は次のとおりです。

lStart |500| lEnd |500| lCount |11|
real   user   sys    frac
0.002  0.000  0.000  0.00  | command == tail -n"+500" test.in | head -n1
0.002  0.000  0.000  0.00  | command == tail -n+500 test.in | head -n1
3.230  2.520  0.700  99.68 | command == tail -n99999501 test.in | head -n1
0.001  0.000  0.000  0.00  | command == head -n500 test.in | tail -n1
0.001  0.000  0.000  0.00  | command == sed -n -e "500,500p;500q" test.in
0.002  0.000  0.000  0.00  | command == awk 'NR<'500'{next}1;NR=='500'{exit}' test.in


lStart |50000000| lEnd |50000010| lCount |11|
real   user   sys    frac
0.977  0.644  0.328  99.50 | command == tail -n"+50000000" test.in | head -n11
1.069  0.756  0.308  99.58 | command == tail -n+50000000 test.in | head -n11
1.823  1.512  0.308  99.85 | command == tail -n50000001 test.in | head -n11
1.950  2.396  1.284  188.77| command == head -n50000010 test.in | tail -n11
5.477  5.116  0.348  99.76 | command == sed -n -e "50000000,50000010p;50000010q" test.in
10.124  9.669  0.448  99.92| command == awk 'NR<'50000000'{next}1;NR=='50000010'{exit}' test.in


lStart |99999000| lEnd |99999010| lCount |11|
real   user   sys    frac
0.001  0.000  0.000  0.00  | command == tail -n"1001" test.in | head -n11
1.960  1.292  0.660  99.61 | command == tail -n+99999000 test.in | head -n11
0.001  0.000  0.000  0.00  | command == tail -n1001 test.in | head -n11
4.043  4.704  2.704  183.25| command == head -n99999010 test.in | tail -n11
10.346  9.641  0.692  99.88| command == sed -n -e "99999000,99999010p;99999010q" test.in
21.653  20.873  0.744  99.83 | command == awk 'NR<'99999000'{next}1;NR=='99999010'{exit}' test.in

選択した行が開始近くまたは終了近くにある場合、時間が大幅に変化することに注意してください。ファイルの一方の側でうまく機能しているように見えるコマンドは、ファイルのもう一方の側で非常に遅くなる場合があります。


コメントは詳細なディスカッション用ではありません。この会話はチャットに移動さました
テルドン

@BinaryZebra- より良い方法
mikeserv

0

これは十分に頻繁に行うので、このスクリプトを書きました。行番号を見つける必要はありません。スクリプトがすべて行います。

#!/bin/bash

# $1: start time
# $2: end time
# $3: log file to read
# $4: output file

# i.e. log_slice.sh 18:33 19:40 /var/log/my.log /var/log/myslice.log

if [[ $# != 4 ]] ; then 
echo 'usage: log_slice.sh <start time> <end time> <log file> <output file>'
echo
exit;
fi

if [ ! -f $3 ] ; then
echo "'$3' doesn't seem to exit."
echo 'exiting.'
exit;
fi

sline=$(grep -n " ${1}" $3|head -1|cut -d: -f1)  #what line number is first occurrance of start time
eline=$(grep -n " ${2}" $3|head -1|cut -d: -f1)  #what line number is first occurrance of end time

linediff="$((eline-sline))"

tail -n+${sline} $3|head -n$linediff > $4

2
聞かれていない質問に答えています。あなたの答えは10%でtail|head、これは質問と他の回答で広範囲に議論されており、90%が質問の一部ではない指定された文字列/パターンが現れる行番号を決定しています。PSシェルのパラメーターと変数は常に引用する必要があります。たとえば、「$ 3」と「$ 4」。
G-マン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.