如何从文件中select随机行

我有一个文本文件包含10数百行,不同的长度。 现在我想随机selectN行,将它们保存在另一个文件中,并从原始文件中删除它们。 我已经find了这个问题的一些答案,但他们大多数使用一个简单的想法:sorting文件,并select第一或最后N行。 不幸的是,这个想法对我不起作用,因为我想保留行的顺序。 我试过这段代码,但是速度很慢,需要几个小时。

FILEsrc=$1; FILEtrg=$2; MaxLines=$3; let LineIndex=1; while [ "$LineIndex" -le "$MaxLines" ] do # count number of lines NUM=$(wc -l $FILEsrc | sed 's/[ \r\t].*$//g'); let X=(${RANDOM} % ${NUM} + 1); echo $X; sed -n ${X}p ${FILEsrc}>>$FILEtrg; #write selected line into target file sed -i -e ${X}d ${FILEsrc}; #remove selected line from source file LineIndex=`expr $LineIndex + 1`; done 

我发现这行代码中最耗时的一行:

 sed -i -e ${X}d ${FILEsrc}; 

有什么办法来克服这个问题,并使代码更快? 由于我很着急,请问您可以给我发送完整的c / c ++代码吗?

一个简单的O(n)算法描述如下:

http://en.wikipedia.org/wiki/Reservoir_sampling

 array R[k]; // result integer i, j; // fill the reservoir array for each i in 1 to k do R[i] := S[i] done; // replace elements with gradually decreasing probability for each i in k+1 to length(S) do j := random(1, i); // important: inclusive range if j <= k then R[j] := S[i] fi done 

生成所有偏移量,然后单击文件。 假设你有偏移量的offsets (每行一个数),你可以像这样生成一个sed脚本:

 sed "s!.*!&{w $FILEtrg\nd;}!" offsets 

输出是一个sed脚本,你可以保存到一个临时文件,或者(如果你的sed方言支持它)管道到第二个sed实例:

 ... | sed -i -f - "$FILEsrc" 

生成offsets文件作为练习。

鉴于你有Linux的标签,这应该马上工作。 某些其他平台上的默认sed可能不理解\n和/或接受-f -从标准输入中读取脚本。

这里是一个完整的脚本,更新使用shuf (谢谢@Thor!),以避免可能重复的随机数字。

 #!/bin/sh FILEsrc=$1 FILEtrg=$2 MaxLines=$3 # Add a line number to each input line nl -ba "$FILEsrc" | # Rearrange lines shuf | # Pick out the line number from the first $MaxLines ones into sed script sed "1,${MaxLines}s!^ *\([1-9][0-9]*\).*!\1{w $FILEtrg\nd;}!;t;D;q" | # Run the generated sed script on the original input file sed -i -f - "$FILEsrc" 

[我已经更新了每个解决方案以从输入中删除选定的行,但是我不确定awk是正确的。 我自己偏爱bash解决方案,所以我不打算花时间调试它。 随意编辑任何错误。]

下面是一个简单的awk脚本(使用浮点数来管理的概率比较简单,而这些数字不能与bash混合使用):

 tmp=$(mktemp /tmp/XXXXXXXX) awk -v total=$(wc -l < "$FILEsrc") -v maxLines=$MaxLines ' BEGIN { srand(); } maxLines==0 { exit; } { if (rand() < maxLines/total--) { print; maxLines--; } else { print $0 > /dev/fd/3 } }' "$FILEsrc" > "$FILEtrg" 3> $tmp mv $tmp "$FILEsrc" 

当你打印一行到输出,你减少maxLines减少选择更多的行的概率。 但是,当你消耗的输入,你减少total增加的概率。 在极端情况下,当maxLines执行时概率为零,因此您可以停止处理输入。 在另一个极端,一旦total小于或等于maxLines ,概率就会达到1,并且您将接受所有更多的行。


这是相同的算法,使用整数算法在(几乎)纯粹的bash实现:

 FILEsrc=$1 FILEtrg=$2 MaxLines=$3 tmp=$(mktemp /tmp/XXXXXXXX) total=$(wc -l < "$FILEsrc") while read -r line && (( MaxLines > 0 )); do (( MaxLines * 32768 > RANDOM * total-- )) || { printf >&3 "$line\n"; continue; } (( MaxLines-- )) printf "$line\n" done < "$FILEsrc" > "$FILEtrg" 3> $tmp mv $tmp "$FILEsrc" 

这是一个完整的Go程序:

 package main import ( "bufio" "fmt" "log" "math/rand" "os" "sort" "time" ) func main() { N := 10 rand.Seed( time.Now().UTC().UnixNano()) f, err := os.Open(os.Args[1]) // open the file if err!=nil { // and tell the user if the file wasn't found or readable log.Fatal(err) } r := bufio.NewReader(f) var lines []string // this will contain all the lines of the file for { if line, err := r.ReadString('\n'); err == nil { lines = append(lines, line) } else { break } } nums := make([]int, N) // creates the array of desired line indexes for i, _ := range nums { // fills the array with random numbers (lower than the number of lines) nums[i] = rand.Intn(len(lines)) } sort.Ints(nums) // sorts this array for _, n := range nums { // let's print the line fmt.Println(lines[n]) } } 

假如你把go文件放在你的GOPATH中的一个名为randomlines的目录中,你可以像这样构建它:

 go build randomlines 

然后像这样调用它:

  ./randomlines "path_to_my_file" 

这将在您的文件中打印N(这里是10)个随机行,但不改变顺序。 当然,即使是大文件,也几乎是即时的。

这里有一个与coreutils,sed和awk有趣的双向选项:

 n=5 total=$(wc -l < infile) seq 1 $total | shuf | head -n $n \ | sed 's/^/NR == /; $! s/$/ ||/' \ | tr '\n' ' ' \ | sed 's/.*/ & { print >> "rndlines" }\n!( &) { print >> "leftover" }/' \ | awk -f - infile 

随机数字列表被传递给sed,它生成一个awk脚本。 如果awk从上面的管道中被移除,这将是输出:

 { if(NR == 14 || NR == 1 || NR == 11 || NR == 20 || NR == 21 ) print > "rndlines"; else print > "leftover" } 

所以随机线路保存在rndlines ,剩下的就leftover

提到“10数百”行应该很快排序,所以这是装饰,排序,未装订模式的一个很好的例子。 它实际上创建了两个新文件,删除原来的行可以通过重命名来模拟。

注意:head和tail不能用来代替awk,因为它们会在给定的行数后关闭文件描述符,导致tee退出,从而导致.rest文件中丢失的数据。

 FILE=input.txt SAMPLE=10 SEP=$'\t' <$FILE nl -s $"SEP" -nln -w1 | sort -R | tee \ >(awk "NR > $SAMPLE" | sort -t"$SEP" -k1n,1 | cut -d"$SEP" -f2- > $FILE.rest) \ >(awk "NR <= $SAMPLE" | sort -t"$SEP" -k1n,1 | cut -d"$SEP" -f2- > $FILE.sample) \ >/dev/null # check the results wc -l $FILE* # 'remove' the lines, if needed mv $FILE.rest $FILE 

这可能适用于你(GNU sed,sort和seq):

 n=10 seq 1 $(sed '$=;d' input_file) | sort -R | sed $nq | sed 's/.*/&{w output_file\nd}/' | sed -i -f - input_file 

$n是要提取的行数。