如何选择从文件中随机行(How to select random lines from a file

2019-09-22 06:26发布

我有一个包含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 ++代码,这样做呢?

Answer 1:

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


Answer 2:

生成所有的偏移量,则会使整个文件一次通过。 假设你有偏移的在期望数量的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"


Answer 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做,这样你就可以停止处理该输入。 在另一个极端,概率命中1一旦total小于或等于maxLines ,你会被接受所有进一步线。


下面是相同的算法,在(几乎)纯实施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"


Answer 4:

这里有一个完整的围棋程序:

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])
    }
}

只要你把旅途中文件的目录名为randomlinesGOPATH ,你可以建立这样的:

go build randomlines

然后调用它是这样的:

  ./randomlines "path_to_my_file"

这将打印N(在这里10)随机线在你的文件,但没有改变的顺序。 当然,它甚至与大文件几乎瞬时的。



Answer 5:

下面是用的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



Answer 6:

提到“10个数百名”行应排得相当快,所以这是为装饰,排序,去除装饰图案漂亮的情况下。 它实际上是创建两个新的文件,从原来的一个可以重新命名模拟删除线。

注:头尾不能用来代替awk的,因为他们关闭给行数后的文件描述符,使得三通退出从而在.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


Answer 7:

这可能会为你工作(GNU sed的,排序和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是行提取的数目。



文章来源: How to select random lines from a file