【问题标题】:How to select random lines from a file如何从文件中选择随机行
【发布时间】:2012-09-03 11:52:49
【问题描述】:

我有一个包含 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++ 代码吗?

【问题讨论】:

  • 这可以在真正的编程语言中非常快地完成,而不仅仅是一个 shell。
  • “快速方法”包括将适当的内容读入内存、操作和一次写入或读取行偏移量、执行增量计算和一次写入......两者在其他环境中都简单得多,而且两者都充分利用一次写作。
  • @dystroy:定义“真正的编程语言”。
  • @phresnel 任何足够完整的语言,以便您可以使用标准 API 而不是寻找黑客和您计算机上可能拥有的所有实用程序(sed、awk 等)的选项。两种方法都有效,但一种方法是在可预测的开发和执行时间内完成。不要误会:我对在 shell 上执行此操作的巧妙方法很感兴趣,但我只是说键入 30 行编程语言可能更容易、更有效。
  • @dystroy 这有点愚蠢——sed、awk 和其他以这种方式使用的工具本身可以被视为 API,只要有定义的公开接口(和“标准”对于许多 Linux 发行版)。并且 API 可以被指定为要求.. 当然我同意使用 perl 或 ruby​​ 或 python 可能会更容易(所有这些都是一个“可能在 [他们的] 计算机上拥有的程序” )。

标签: linux bash text random sed


【解决方案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

【讨论】:

【解决方案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"

【讨论】:

  • 既然我很着急,请问您可以发给我完整的 c/c++ 代码吗?
  • 你在开玩笑吗? sed 的来源可在 savannah.gnu.org/projects/sed 获得
  • 我的意思是我想要的代码的 c++ 实现,而不是 sed 的源代码......!!!
  • @Hakim first:尝试更准确地提出您的问题。你想要 BASH 还是 C/C++ 解决方案?这里有很多方法可以做到这一点,例如。使用 bash、perl、python、c、c++(替代任何语言)——所以:你想要什么?!?第二:stackoverflow 不是您的个人编程服务!所以,如果你赶时间 - 聘请付费程序员。
【解决方案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"

【讨论】:

  • 您仍然需要从源文件中删除选定的行。如果它们都是唯一的,则为简单的fgrep -vxf "$FILEtrg" "$FILEsrc",或者您可以内联到脚本中。
【解决方案4】:

这是一个完整的 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)随机行,但不会更改顺序。当然,即使是大文件,它也几乎是瞬时的。

【讨论】:

  • 您能详细介绍一下您使用的算法吗?特别是对于不熟悉 go 的读者?
  • +1 不是因为我喜欢这种方法,而是为了反驳莫名其妙的反对意见。选民,请解释一下?
  • 它只是提取行,构建一个介于 0 和行号之间的整数数组,对其进行排序,然后使用该数组打印行。避免寻找 shell hack 的重点是您可以轻松地根据需要更改它。您最喜欢的语言会很容易地做到这一点(java、python 等)。
【解决方案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中。

【讨论】:

  • +1 用于seq | shuf 技巧。但是,使用head 而不是tail 不是很有意义吗?
【解决方案6】:

提到的“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

【讨论】:

    【解决方案7】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-03-03
      • 1970-01-01
      • 2016-04-29
      • 2020-10-18
      • 1970-01-01
      • 2020-08-01
      • 2018-04-03
      相关资源
      最近更新 更多