巨大なテキストファイル(> 2GB)があり、cat
行X
からY
まで(たとえば、57890000から57890010)したいとします。
私が理解していることから、head
をtail
にパイプすることでこれを行うことができます。
head -A /path/to/file | tail -B
または代わりに
tail -C /path/to/file | head -D
ここで、A
、B
、C
およびD
は、ファイルの行数X
およびY
。
しかし、このアプローチには2つの問題があります。
A
、B
、C
とD
を計算する必要があります。pipe
できます多くの読み取りに興味のある行数よりも多い(たとえば、巨大なファイルの途中で数行だけを読み取っている場合)シェルで必要な行を操作して出力する方法はありますか? (X
とY
のみを提供しながら)?
sed
ソリューションをお勧めしますが、完全を期すために、
awk 'NR >= 57890000 && NR <= 57890010' /path/to/file
最終行の後で切り取るには:
awk 'NR < 57890000 { next } { print } NR == 57890010 { exit }' /path/to/file
速度テスト(ここではmacOS、他のシステムではYMMV):
seq 100000000 > test.in
によって生成された100,000,000行のファイルreal
bash
の組み込み関数によって報告された時間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;q
とhead|tail
を除いて、本質的に同じように見えます。
行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、/tmp
はtmpfsであり、それ以外の場合、マシンはアイドル状態であり、スワップしません。
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で、xからyまでのブロックを0から開始して抽出するには:
dd bs="$b" seek="$x" count="$((y-x))" </path/to/file
head | tail
アプローチは、これを行うための最良かつ最も「慣用的」な方法の1つです。
X=57890000
Y=57890010
< infile.txt head -n "$Y" | tail -n +"$X"
コメントでGillesが指摘したように、より速い方法は
< infile.txt tail -n +"$X" | head -n "$((Y - X))"
これが速い理由は、最初のX-1行がhead | tail
アプローチと比較してパイプを通過する必要がないためです。
フレーズとしての質問は少し誤解を招くものであり、おそらくこのアプローチに対する根拠のない不安のいくつかを説明しています。
A
、B
、C
、D
を計算する必要があると言いますが、ご覧のとおり、ファイルの行数は必要ありません。ほとんどの1計算が必要ですが、シェルはとにかくそれを行うことができます。
配管が必要以上の行を読み取ることを心配しています。実際、これは真実ではありません。tail | head
は、ファイルI/Oの点で、ほぼ同じくらい効率的です。最初に、必要な最小作業量を検討します。ファイルの[〜#〜] x [〜#〜]番目の行を見つけるには、これを行う唯一の一般的な方法は、すべてのバイトを読み取って、[〜#〜] x [〜#〜]改行記号を数えるときに停止することです[〜#〜] x [〜#〜]番目の行のファイルオフセットを占う方法はありません。 * X *番目の行に到達したら、それらを印刷するためにすべての行を読み取って[〜#〜] y [〜#〜]番目の行。したがって、[〜#〜] y [〜#〜]未満の行を読み取ることで、アプローチを回避することはできません。現在、head -n $Y
は、[〜#〜] y [〜#〜]行しか読み取っていません(最も近いバッファユニットに丸められますが、バッファを正しく使用するとパフォーマンスが向上するため、そのオーバーヘッドについて心配する必要はありません)。さらに、tail
はhead
以下の値を読み取らないため、head | tail
は可能な限り少ない行数を読み取ることを示しました(ここでも、バッファリングは無視できる程度です)無視)。パイプを使用しない単一ツールのアプローチの唯一の効率上の利点は、プロセスが少ない(したがってオーバーヘッドが少ない)ことです。
最もオーソドックスな方法(ただし、上記の Gilles で示されているように、最速ではない)は、sed
を使用することです。
あなたの場合:
X=57890000
Y=57890010
sed -n -e "$X,$Y p" -e "$Y q" filename
-n
オプションは、関連する行のみがstdoutに出力されることを意味します。
終了行番号の最後のpは、指定された範囲の行を印刷することを意味します。スクリプトの2番目の部分のqは、ファイルの残りの部分をスキップすることにより、時間を節約します。
選択する範囲がわかっている場合、最初の行: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
選択した線が始点または終点に近い場合、時間が大幅に変化することに注意してください。ファイルの片側でうまく機能しているように見えるコマンドは、ファイルの反対側では非常に遅くなる可能性があります。
私はこれを頻繁に行うので、このスクリプトを書きました。行番号を見つける必要はありません。スクリプトがすべてを行います。
#!/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