Как выбрать случайные строки из файла

У меня есть текстовый файл, содержащий 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/С++ для этого?

Ответ 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 script следующим образом:

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

Вывод представляет собой sed script, который вы можете сохранить во временный файл, или (если ваш диалект sed поддерживает его) труба на второй экземпляр sed:

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

Создание файла offsets, оставшегося в виде упражнения.

Учитывая, что у вас есть тег Linux, это должно работать сразу. По умолчанию sed на некоторых других платформах может не понимать \n и/или принимать -f - для чтения script со стандартного ввода.

Вот полный script, обновленный для использования 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"

Ответ 3

[Я обновил каждое решение, чтобы удалить выбранные строки из ввода, но я не уверен, что awk верен. Я частично отношусь к решению bash, поэтому я не буду тратить время на его отладку. Не стесняйтесь редактировать любые ошибки.]

Здесь простой awk script (вероятности проще обрабатывать с номерами с плавающей запятой, которые не хорошо сочетаются с 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"

Ответ 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 print the line
        fmt.Println(lines[n])
    }
}

Если вы поместите файл go в каталог с именем randomlines в свой GOPATH, вы можете создать его так:

go build randomlines

И затем назовите его следующим образом:

  ./randomlines "path_to_my_file"

Это приведет к печати N (здесь 10) случайных строк в ваших файлах, но без изменения порядка. Конечно, это почти мгновенно даже с большими файлами.

Ответ 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 script. Если awk были удалены из вышеприведенного конвейера, это будет выход:

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

Таким образом, случайные строки сохраняются в rndlines, а остальные - в leftover.

Ответ 6

Упомянутые строки "10 сотен" должны сортироваться довольно быстро, так что это хороший случай для оформления Украшения, Сортировки, Undecorate. Он фактически создает два новых файла, удаляя строки из оригинала, можно имитировать путем переименования.

Примечание: голова и хвост не могут использоваться вместо 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

Ответ 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 - количество строк для извлечения.