如何从文件中选择随机行

时间:2012-09-10 15:18:39

标签: linux bash text random sed

我有一个包含10百行不同长度的文本文件。现在我想随机选择N行,将它们保存在另一个文件中,然后从原始文件中删除它们。 我已经找到了这个问题的一些答案,但是大多数都使用了一个简单的想法:对文件进行排序并选择第一行或最后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 ++代码吗?

7 个答案:

答案 0 :(得分:6)

简单的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

答案 1 :(得分:3)

生成所有偏移量,然后单次传递文件。假设您在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"

答案 2 :(得分:3)

[我已更新每个解决方案以从输入中删除选定的行,但我不肯定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"

答案 3 :(得分:1)

这是一个完整的围棋计划:

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文件放在randomlines中名为GOPATH的目录中,您可以像这样构建它:

go build randomlines

然后像这样称呼它:

  ./randomlines "path_to_my_file"

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

答案 4 :(得分:0)

这是一个有趣的两遍选项,包括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

答案 5 :(得分:0)

提到的“10个”行应该很快排序,所以这对于Decorate,Sort,Undecorate模式来说是一个很好的例子。它实际上创建了两个新文件,可以通过重命名来模拟从原始文件中删除行。

注意:不能使用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

答案 6 :(得分:-1)

这可能对你有用(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是要提取的行数。