Прочитать n случайных строк из потенциально огромного файла


16

Эта задача заключается в чтении случайных строк из потенциально огромного файла без чтения всего файла в память.

вход

Целое число nи имя текстового файла.

Выход

n Строки текстового файла выбираются равномерно, произвольно, без замены.

Вы можете предположить, что n находится в диапазоне от 1 до количества строк в файле.

Будьте осторожны, когда nслучайная выборка чисел из диапазона, который вы получите, является равномерным. rand()%nв C не является равномерным, например. Каждый результат должен быть одинаково вероятным.

Правила и ограничения

Каждая строка текстового файла будет иметь одинаковое количество символов, и это будет не более 80.

Ваш код не должен читать содержимое текстового файла, кроме:

  • Те строки это выводит.
  • Первая строка для определения количества символов в строке в текстовом файле.

Мы можем предположить, что каждый символ в текстовом файле занимает ровно один байт.

Предполагается, что разделители строк имеют длину 1 байт. Решения могут использовать двухбайтовые разделители длинных строк, только если они указывают эту необходимость. Вы также можете предположить, что последняя строка заканчивается разделителем строк.

Ваш ответ должен быть полной программой, но вы можете указать ввод любым удобным для вас способом.

Языки и библиотеки

Вы можете использовать любой язык или библиотеку, которая вам нравится.

Примечания

Была проблема с подсчетом количества строк в файле. Как указывает Ними в комментариях, вы можете определить это по размеру файла и количеству символов в строке.

мотивация

В чате некоторые люди спрашивали, действительно ли это вопрос «Делай Х без Y». Я интерпретирую это, чтобы спросить, являются ли ограничения необычно искусственными.

Задача случайной выборки строк из огромных файлов не является чем-то необычным, и на самом деле мне иногда приходится это делать. Один из способов сделать это в bash:

shuf -n <num-lines>

Это, однако, очень медленно для больших файлов, поскольку он читает весь файл.


Почему отрицательный голос?

3
Это тривиально в таких языках, как C fseek, и невозможно в других. Кроме того, что если nчисло строк в файле больше?
Мего

4
@Mego: относительно вашей точки b): вы можете рассчитать количество строк, разделив размер файла на длину строки.
Ними

8
Do X без Y - предупреждение, которое начинается с «Это не всегда плохо». Основная проблема - искусственные ограничения, такие как «не использовать +», что дает преимущество языкам, которые имеют sum(). Не чтение файла в память является четким и последовательным ограничением, которое ни в коем случае не является произвольным. Его можно протестировать с файлом, превышающим объем памяти, который нельзя обойти из-за языковых различий. Также бывает, что в реальных приложениях (хотя это не обязательно для гольфа ...).
Трихоплакс

1
Похоже, что это на самом деле гольф-код с ограниченной сложностью, где использование памяти ограничено, несмотря на потенциально большие файлы. Дело не в том, чтобы не иметь определенных вещей в вашем коде, а в ограничении того, как код может действовать.
xnor

Ответы:


5

Дьялог АПЛ , 63 байта

⎕NREAD¨t 82l∘,¨lׯ1+⎕?(⎕NSIZE t)÷l←10⍳⍨⎕NREAD 83 80,⍨t←⍞⎕NTIE 0

Запрашивает имя файла, затем сколько произвольных строк желательно.

объяснение

Запрос на ввод текста (имя файла)
⎕NTIE 0Свяжите файл, используя следующий доступный номер связи (-1 в чистой системе).
t←Сохраните выбранный номер связи как t
83 80,⍨Добавить [83,80], получив [-1,83,80].
⎕NREADСчитайте первые 80 байтов. файла -1 как 8-битные целые числа (код преобразования 83)
10⍳⍨Найти индекс первого числа 10 (LF)
l←Сохранить длину строки как l
(⎕NSIZE t)÷Делить размер файла -1 на длину
строки Запрашивать числовой ввод (нужное количество строк) )
?X случайные выборки (без замены) из первых натуральных чисел Y
¯1+Добавьте -1, чтобы получить номера строк 0-начала *
Умножьте на длину строки, чтобы получить начальные байты.
t 82l∘,¨Prepend [-1,82, LineLength] к каждому начальному байту (создает список аргументов для⎕NREAD )
⎕NREAD¨ Читайте каждую строку как 8-битный символ (код преобразования 82)

Практический пример

Файл /tmp/records.txt содержит:

Hello
Think
12345
Klaus
Nilad

Сделайте так, чтобы программа RandLines содержала вышеуказанный код дословно, введя в сеанс APL следующее:

∇RandLines
⎕NREAD¨t 82l∘,¨lׯ1+⎕?(⎕NSIZE t)÷l←10⍳⍨⎕NREAD 83 80,⍨t←⍞⎕NTIE 0
∇

В сеансе APL введите RandLinesи нажмите Enter.

Система перемещает курсор на следующую строку, которая является подсказкой длины 0 для символьных данных; войти /tmp/records.txt.

Теперь система выводит ⎕:и ожидает числовой ввод; войти 4.

Система выводит четыре случайные строки.

Реальная жизнь

В действительности вы можете указать имя файла и считать его аргументами и получить результат в виде таблицы. Это можно сделать, введя:

RandLs←{↑⎕NREAD¨t 82l∘,¨lׯ1+⍺?(⎕NSIZE t)÷l←10⍳⍨⎕NREAD 83 80,⍨t←⍵⎕NTIE 0}

Теперь вы делаете MyLines содержать три случайные строки с:

MyLines←3 RandLs'/tmp/records.txt'

Как насчет возврата только одной случайной строки, если count не указан:

RandL←{⍺←1 ⋄ ↑⎕NREAD¨t 82l∘,¨lׯ1+⍺?(⎕NSIZE t)÷l←10⍳⍨⎕NREAD 83 80,⍨t←⍵⎕NTIE 0}

Теперь вы можете сделать оба:

MyLines←2 RandL'/tmp/records.txt'

и (обратите внимание на отсутствие левого аргумента):

MyLine←RandL'/tmp/records.txt'

Делаем код читабельным

Гольф-однострочные APL - плохая идея. Вот как я бы написал в производственной системе:

RandL←{ ⍝ Read X random lines from file Y without reading entire file
    ⍺←1 ⍝ default count
    tie←⍵⎕NTIE 0 ⍝ tie file
    length←10⍳⍨⎕NREAD 83 80,⍨tie ⍝ find first NL
    size←⎕NSIZE tie ⍝ total file length
    starts←lengthׯ1+⍺?size÷length ⍝ beginning of each line
    ↑⎕NREAD¨tie 82length∘,¨starts ⍝ read each line as character and convert list to table
}

* Я мог бы сохранить байт, запустив его в режиме 0-origin, который является стандартным для некоторых систем APL: удалить ¯1+и вставить 1+до 10.


Ааа .. APL :) Есть ли способ проверить этот код в Linux?

@Lembik Конечно, этот код кроссплатформенный. Скачать с dyalog.com
Адам

Поскольку я не читаю APL, не могли бы вы объяснить код? Сложные части представляют собой выборочные строки без замены и переходят прямо в нужное место в файле, чтобы прочитать строки.

@Lembik Эта часть проста. Аргумент NREAD - TieNumber ConversionCode BytesToRead [StartByte]. Он читает только необходимые байты. Остальное только выясняет, что читать.
Адам

@Lembik Мне любопытно, почему мой ответ не выиграл награду.
Адам

7

Рубин, 104 94 92 90 байтов

Имя файла и количество строк передаются в командную строку. Например, если программа имеет shuffle.rbимя файла a.txt, запуститеruby shuffle.rb a.txt 3 три случайные строки.

-4 байта от обнаружения openсинтаксиса в Ruby вместоFile.new

f=open$*[0]
puts [*0..f.size/n=f.gets.size+1].sample($*[1].to_i).map{|e|f.seek n*e;f.gets}

Кроме того, вот 85-байтовое решение анонимной функции, которое принимает строку и число в качестве аргументов.

->f,l{f=open f;puts [*0..f.size/n=f.gets.size+1].sample(l).map{|e|f.seek n*e;f.gets}}

Ниже 100 байтов! Возможно, Руби - лучший язык для игры в гольф в конце концов. «Образец» избегает повторений?

@Lembik ruby-doc.org/core-2.2.0/Array.html#method-i-sample Это позволяет избежать повторений. Не говори мне ... я должен был повторяться?
Value Ink

Нет, ты идеален :)

Можете ли вы сохранить какие-либо байты, читая из стандартного ввода? ruby shuffle.rb 3 < a.txtдает вам поиск стандартного ИДК Руби, правда.
Питер Кордес

1
@PeterCordes Это имеет смысл, но, как уже упоминалось, причиной ошибки является то, что Ruby не может прочитать размер файла стандартного ввода, поэтому это не сработало.
Value Ink

5

Haskell, 240 224 236 байт

import Test.QuickCheck
import System.IO
g=hGetLine
main=do;f<-getLine;n<-readLn;h<-openFile f ReadMode;l<-(\x->1+sum[1|_<-x])<$>g h;s<-hFileSize h;generate(shuffle[0..div s l-1])>>=mapM(\p->hSeek h(toEnum 0)(l*p)>>g h>>=putStrLn).take n

Читает имя файла и n из стандартного ввода.

Как это устроено:

main=do
  f<-getLine                   -- read file name from stdin
  n<-readLn                    -- read n from stdin
  h<-openFile f ReadMode       -- open the file
  l<-(\x->1+sum[1|_<-x])<$>g h -- read first line and bind l to it's length +1
                               -- sum[1|_<-x] is a custom length function
                               -- because of type restrictions, otherwise I'd have
                               -- to use "toInteger.length"
  s<-hFileSize h               -- get file size
  generate(shuffle[0..div s l-1])>>=
                               -- shuffle all possible line numbers 
  mapM (\->p  ...  ).take n    -- for each of the first n shuffled line numbers 
     hSeek h(toEnum 0).(l*p)>> -- jump to that line ("toEnum 0" is short for "AbsoluteSeek")
     g h>>=                    -- read a line from current position
     putStrLn                  -- and print

Запуск этой программы для файлов с несколькими строками занимает много времени и памяти из-за ужасной неэффективности shuffle функции.

Редактировать: я пропустил часть "случайно без замены" (спасибо @feersum за то, что заметил!).


Haskell качается :)

1
Как избежать выбора линии, которая уже была выбрана?
feersum

@feersum: о, я пропустил эту часть. Исправлена.
Ними,

Я вижу stackoverflow.com/questions/13779630/… несколько многословно!

1
Возможно, должна быть отдельная задача по отбору проб без замены в небольшом пространстве.

3

PowerShell v2 +, 209 байт

param($a,$n)
$f=New-Object System.IO.FileStream $a,"Open"
for(;$f.ReadByte()-ne10){$l++}
$t=$f.Length/++$l-1
[byte[]]$z=,0*$l
0..$t|Get-Random -c $n|%{$a=$f.Seek($l*$_,0);$a=$f.Read($z,0,$l-1);-join[char[]]$z}

Принимает ввод $aкак имя файла и $nкак количество строк. Обратите внимание, что это $aдолжно быть полное имя файла и предполагается, что это кодировка ANSI.

Затем мы создаем новый FileStreamобъект и сообщаем ему доступ к файлу $aс Openпривилегиями.

forцикл.Read() с через первую линию , пока мы не ударили \nхарактер, увеличивающийся наш линейный счетчик длины каждого символ. Затем мы устанавливаем $tравный размеру файла (т. Е. Длине потока), деленному на количество символов в строке (плюс один, чтобы он считал терминатор), минус один (поскольку мы проиндексированы нулями). Затем мы строим наш буфер так же, $zчтобы он был длиной строки.

Последняя строка создает динамический массив с ..оператором диапазона. 1 Мы передаем этот массив Get-Randomсо -Cмножеством, $nчтобы случайным образом выбрать $nномера строк без повторения. Счастливые числа заключены в петлю с |%{...}. Каждую итерацию мы сохраняем в .Seekопределенном месте, а затем .Readв строке символов $z. Мы повторно бросили$z как char-массив и -joinвсе вместе, оставляя результирующую строку в конвейере и вывод неявно в конце программы.

технически мы должны завершить $f.Close()вызов, чтобы закрыть файл, но это стоит байтов! :п

пример

a.txt:
a0000000000000000000000000000000000000000000000001
a0000000000000000000000000000000000000000000000002
a0000000000000000000000000000000000000000000000003
a0000000000000000000000000000000000000000000000004
a0000000000000000000000000000000000000000000000005
a0000000000000000000000000000000000000000000000006
a0000000000000000000000000000000000000000000000007
a0000000000000000000000000000000000000000000000008
a0000000000000000000000000000000000000000000000009
a0000000000000000000000000000000000000000000000010

PS C:\Tools\Scripts\golfing> .\read-n-random-lines.ps1 "c:\tools\scripts\golfing\a.txt" 5
a0000000000000000000000000000000000000000000000002 
a0000000000000000000000000000000000000000000000001 
a0000000000000000000000000000000000000000000000004 
a0000000000000000000000000000000000000000000000010 
a0000000000000000000000000000000000000000000000006 

1 Технически это означает, что мы можем поддерживать не более 50 000 строк, поскольку это самый большой диапазон, который можно динамически создать таким образом. : - / Но мы не можем просто зациклить время Get-Randomкоманды $n, отбрасывая дубликаты каждого цикла, так как это не является детерминированным ...


2

Python 3, 146 139 байт

from random import*
i=input
f=open(i())
l=len(f.readline())
[(f.seek(v*l),print(f.read(l)))for v in sample(range(f.seek(0,2)//l),int(i()))]
#print is here^

Ввод: [имя файла] \ n [строки] \ n

Это решение в значительной степени украдено у @pppery, но является только python3.5 и представляет собой законченную программу.

Редактировать: Спасибо @Mego за встроенный диапазон и совместимость с python3.x. Edit2: разъяснение, где печать, потому что я получил два комментария об этом. (Комментарий, очевидно, не является частью кода или количества байтов.)


Спасибо! Какая часть является только Python 3.5?

2
r=range(f.seek(0,2)//l)будет работать, что сбрасывает 3 байта и устраняет необходимость в 3,5. Еще лучше, сбрейте еще 3 байта, вставив rangeвызов в sampleвызове. Кроме того, это не полная программа - вам нужно распечатать список.
Мего

@Lembik: Это было 3,5 только потому, что я использовал, r=[*range(f.seek(0,2)//l)]потому что я думал, что я не мог sampleгенератор. Оказывается, я мог. @Mega: он завершен, потому что печатает каждую строку внутри спискаprint(f.read(l))
Александр Нигл,

Вам нужно печатать заявление, хотя.

печать находится внутри понимания списка.
Александр Нигл,

2

Луа, 126 122

r=io.read;f=io.open(r())c=2+f:read():len()for i=1,r()do f:seek("set",c*math.random(0,f:seek("end")/c-1))print(f:read())end

Использует 2 байта для переносов строк. Измените 2 на 1 для 1. У меня только 2, потому что это то, что было в моем тестовом файле.

Попал под запись PHP, но все же 2-е место (в настоящее время). Проклинаю тебя, рубиновая запись!


1
Lua был первым языком программирования, который я выучил, и даже после всего, что я выучил с тех пор, он все еще мой любимый. Он настолько универсален, что его легко писать.
Blab

2

Bash (ну, coreutils), 100 байт

n=`head -1 $2|wc -c`;shuf -i0-$[`stat -c%s $2`/$n] -n$1|xargs -i dd if=$2 bs=$n skip={} count=1 2>&-

объяснение

Это позволяет избежать чтения всего файла, используя ddдля извлечения частей файла, которые нам нужны, без чтения файла полностью, к сожалению, он оказывается довольно большим со всеми опциями, которые мы должны указать:

ifявляется ли входной файл
bsразмером блока (здесь мы устанавливаем его, $nдлина первой строки которого
skipравна случайным целым числам, извлеченным из него shufи равняется ibsпропускаемому значению skip* ibsбайтов
count- количество ibsотрезков длины, которое требуется вернуть
status=none, необходимо вырезать информация обычно выводитсяdd

Мы получаем длину строки с помощью head -1 $2|wc -cи размер файла с помощью stat -c%s $2.

использование

Сохранить выше как file.shи запустить с помощью file.sh n filename.

Задержки

time ~/randlines.sh 4 test.txt
9412647
4124435
7401105
1132619

real    0m0.125s
user    0m0.035s
sys     0m0.061s

против

time shuf -n4 test.txt
1204350
3496441
3472713
3985479

real    0m1.280s
user    0m0.287s
sys     0m0.272s

Время выше для файла 68MiB, созданного с использованием seq 1000000 9999999 > test.txt.

Спасибо @PeterCordes за его -1 совет!


1
Мне всегда нравится ответ bash, но не могли бы вы объяснить, как он не читает весь файл?

2
@Lembik добавил объяснение!
Дом Гастингс

1
Вы можете bs=вместо этого ibs=, так obsкак настройка тоже хорошо. Я предполагаю , что вы не можете заменить if=$2с <$2хотя, так как это все еще xargs«s командной строки. \<$2тоже не работает (xargs использует exec напрямую, без оболочки).
Питер Кордес

Я надеюсь, что это не так уж и много, но мне нравится этот ответ :) Только что проверил его с файлом 1GB.

1
re: перенаправление stderr в stdin: вы также можете закрыть stderr 2>&-, чтобы не было никакой опасности, что вывод куда-либо будет (например, если stdin оказался дескриптором файла для чтения и записи). Он работает с GNU dd: он все еще выдает его stdoutдо того, как попытается и не сможет записать stderr.
Питер Кордес

1

Python 3 - 161 160 149 байт

from random import*;
def f(n,g):f=open(g);l=len(f.readline());r=list(range(f.seek(0,2)/l));shuffle(r);[(f.seek(v*l),print(f.read(l)))for v in r[:k]]

Этот код определяет функцию, которая называется как f(10,'input.txt')


1
Задача требует полной программы, поэтому, боюсь, определения функции недостаточно.
Ними

Чтобы сохранить байт, удалите пробел между импортом и *.
Миклойн

1
@nimi Требование полной программы для этой задачи, кажется,
произвольно отменяет

@ppperry: да, может быть, но это просто так.
Nimi

Чтобы получить длину файла, вы можете использовать f.seek (0,2) , что делает импорт os и os.stat устаревшим.
Александр Нигл,

1

C # 259 байт без дубликатов

class Program{static void Main(string[]a){int c=Convert.ToInt32(a[1]);var h=File.ReadLines(a[0]);HashSet<int>n=new HashSet<int>();while(n.Count<c)n.Add(new Random().Next(0,h.Count()));for(;c>0;c--)Console.WriteLine(h.Skip(n.ElementAt(c-1)).Take(1).First());}}

Ungolfed

class Program{static void Main(string[] a)
{
        int c = Convert.ToInt32(a[1]);
        var h = File.ReadLines(a[0]);
        HashSet<int> n = new HashSet<int>();
        while (n.Count < c)
            n.Add(new Random().Next(0, h.Count()));           
        for (; c > 0; c--)
            Console.WriteLine(h.Skip(n.ElementAt(c-1)).Take(1).First());
    }
}

File.ReadLines является Ленивым. Это дает дополнительное преимущество: все строки могут иметь разную длину.

Запуск это будет:

sample.exe a.txt 10000

C # 206 байт с дубликатами

class Program{static void Main(string[]a){var n=new Random();int c=Convert.ToInt32(a[1]);var h=File.ReadLines(a[0]);for(;c>0;c--)Console.WriteLine(h.Skip((int)(n.NextDouble()*h.Count())).Take(1).First());}}

Ungolfed

class Program
{
    static void Main(string[] a)
    {
        Random n = new Random();
        int c = Convert.ToInt32(a[1]);
        var h = File.ReadLines(a[0]);
        for (; c > 0; c--)
            Console.WriteLine(h.Skip((int)(n.NextDouble()*h.Count())).Take(1).First());
    }
}

Я не полностью следую вашему решению. Если все строки имеют разную длину, то задача невозможна. Кроме того, как вы случайным образом выбираете линии без замены точно? Я извиняюсь, мой C # недостаточно хорош.

@Lembik Вы правы, я не думал о дубликатах. И я могу посчитать количество строк и извлечь строки по количеству белья, поэтому строки могут быть переменными по длине.
Master117

Но вы должны перейти к месту в файле, только зная номер строки, не так ли? Вы не можете сказать, где это, если все строки не имеют одинаковую длину.

@Lembik File.ReadLines ("pathToFile") создает перечисление Lazy для всех строк файла, File.ReadLines ("pathToFile"). ElementAt (19) возвращает 19-ю строку файла. Вроде как карта всех Linestarts.
Master117

Я не думаю, что Lazy перечисление скачет (или ищет) в файле, к сожалению. Так что это не соответствует правилам в настоящее время.

1

Python (141 байт)

Сохраняет каждую строку с равной вероятностью, используйте также с трубами. Это не отвечает на пропущенное ограничение вопроса, хотя ...

Использование cat largefile | python randxlines.py 100или python randxlines 100 < largefile(как указано @petercordes)

import random,sys
N=int(sys.argv[1])
x=['']*N
for c,L in enumerate(sys.stdin):
    t=random.randrange(c+1)
    if(t<N):x[t] = L
print("".join(x))

3
Весь смысл этого вопроса в том, что вы должны искать в потоке ввода. Вы, вероятно, должны сказать, что это часть ограничений вопроса, которые вы игнорируете (хотя использование примера чтения из трубы делает это довольно ясным). Чтение с stdin с python ./randxlines.py 100 < largefileбыло бы хорошо для правильного ответа, однако: в этом случае stdinбудет искать.
Питер Кордес
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.