Прочитайте n випадкових рядків з потенційно величезного файлу


16

Ця задача полягає в тому, щоб прочитати випадкові рядки з потенційно величезного файлу без зчитування всього файлу в пам'яті.

Вхідні дані

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

Вихідні дані

n рядки текстового файлу вибираються рівномірно випадково без заміни.

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

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

Правила та обмеження

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

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

  • Ці рядки він виводить.
  • Перший рядок, який визначить, скільки символів у рядку міститься в текстовому файлі.

Можна припустити, що кожен символ у текстовому файлі займає рівно один байт.

Розділювачі рядків передбачаються довжиною 1 байт. Рішення можуть використовувати 2-байтні роздільники довгих рядків, лише якщо вони визначають цю потребу. Ви також можете припустити, що останній рядок закінчується роздільником рядків.

Ваша відповідь повинна бути повноцінною програмою, але ви можете вказати вклад будь-яким зручним для вас способом.

Мови та бібліотеки

Ви можете використовувати будь-яку мову чи бібліотеку, яка вам подобається.

Примітки

Виникло занепокоєння щодо обчислення кількості рядків у файлі. Як німі вказує в коментарях, ви можете зробити це на основі розміру файлу та кількості знаків у рядку.

Мотивація

У чаті деякі люди запитували, чи це насправді питання "Зроби X без Y". Я інтерпретую це, щоб запитати, чи обмеження незвично штучні.

Завдання випадкового відбору проб з величезних файлів не є рідкістю, і насправді це те, що мені іноді доводиться робити. Один із способів зробити це в bash:

shuf -n <num-lines>

Однак для великих файлів це дуже повільно, оскільки він читається у всьому файлі.


Чому потік?

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

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

8
Do X without Y - це попередження, яке починається з "Це не завжди погано". Основна проблема - штучні обмеження типу "не використовувати +", що дає перевагу мовам, які мають sum(). Нечитання файлу в пам'яті - це чітке і послідовне обмеження, яке аж ніяк не є довільним. Його можна протестувати з файлом, більшим за об'єм пам'яті, який неможливо обійти за допомогою мовних відмінностей. Також трапляються додатки в реальному світі (хоча це не потрібно для гольфу ...).
трихоплакс

1
Схоже, це насправді гольф з обмеженою складністю, де використання пам'яті обмежене, незважаючи на потенційно величезні файли. Справа не в тому, що у вашому коді немає певних речей, а в обмеженні того, як може діяти код.
xnor

Відповіді:


5

Діалог APL , 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∘,¨Додайте [-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 символів для 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'

Як щодо повернення лише одного випадкового рядка, якщо кількість не вказана:

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-походження, що є стандартним для деяких систем APL: видалити ¯1+та вставити 1+раніше 10.


Ааа .. APL :) Чи є спосіб перевірити цей код у Linux?

@Lembik Звичайно, цей код є крос-платформою. Завантажити з dyalog.com
Adám

Оскільки я не читаю APL, ви могли б пояснити код? Складні частини - це відбір проб без заміни та стрибки прямо до потрібного місця у файлі, щоб прочитати рядки.

@Lembik Ця частина проста. Аргументом ⎕NREAD є TieNumber ConversionCode BytesToRead [StartByte]. Він читає лише необхідні байти. Решта - це лише з'ясувати, що читати.
Адам

@Lembik Мені цікаво, чому моя відповідь не виграла щедрості.
Adám

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 Це уникає повторів. Не кажи мені ... я повинен був повторювати?
Значення чорнила

Ні, ви ідеальні :)

Чи можете ви зберегти будь-які байти, читаючи зі stdin? ruby shuffle.rb 3 < a.txtдає видобаток stdin. ІДК Рубі, хоча.
Пітер Кордес

1
@PeterCordes Це має сенс, але, як згадувалося, справа в тому, що Ruby не може прочитати розмір файлу stdin, тому це не вийшло.
Значення чорнила

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 від stdin.

Як це працює:

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))
Олександр Nigl

Вам потрібна заява про друк.

друк знаходиться всередині списку.
Олександр Нігль

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

vs.

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
Я завжди люблю баш-відповідь, але чи можете ви пояснити, як це не читає весь файл?

2
@Lembik додав пояснення!
Дом Гастінгс

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

Я сподіваюся, що це не надто багато, але я люблю цю відповідь :) Просто перевірив її з файлом 1 Гб.

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

1

Пітон 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
Завдання вимагає повноцінної програми, тому, боюся, визначення функції недостатньо.
німі

Щоб зберегти байт, видаліть простір між імпортом та *.
mriklojn

1
@nimi Потрібна повна програма для цього виклику, здається, безперечно перекриває правила форматування коду за замовчуванням
pppery

@ppperry: так, можливо, але це просто так.
німі

Щоб отримати довжину файлу, ви можете 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());}}

Безумовно

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());}}

Безумовно

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 Ви праві, я не думав про дублікати. І я можу порахувати кількість рядків та витягнутих рядків за рядковим числом, ось чому рядки можуть бути змінними довжиною.
Майстер117

Але вам потрібно перейти до місця у файлі, лише знаючи номер рядка, чи не так? Ви не можете сказати, де це, якщо всі лінії не мають однакової довжини.

@Lembik File.ReadLines ("pathToFile") створить ледаче перерахування на всіх рядках файлу, File.ReadLines ("pathToFile"). ElementAt (19) повертає 19-й рядок файлу. Начебто як карта всіх Linestarts.
Майстер117

Я не думаю, що ледачий перелік сумно стрибає (або шукає) у файлі. Отже, це зараз не відповідає правилам.

1

Пітон (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.