1

Есть txt файл:

rama
mama
papa
deda
koza
dama
repa
и т.д.

Надо рандомно вытащить оттуда 3 слова, но так, чтобы первое слово было как в txt файле, на примере этого rama, а остальные 2 слова любые, но чтоб уже не повторялись со старыми словами.

Подскажите пожалуйста как все это реализовать на Python 3.

jfs
  • 52,361
zerox
  • 44

4 Answers4

6

Чтобы прочитать первую строчку и выбрать ещё две случайные строчки из небольшого файла:

#!/urs/bin/env python3
import random

with open('input.txt') as file:
    lines = [next(file)] + random.sample(list(file), 2)
print(*map(str.strip, lines))

next(file) читает первую строчку из файла (файлы являются итераторами над строками в Питоне). random.sample() выбирает пару элементов из списка без замещений. Если слова во входном файле не повторяются, то результат всегда содержит уникальные слова.


Если слова могут повторяться в файле, то можно использовать set(), чтобы только уникальные слова остались:

#!/urs/bin/env python3
import random

with open('input_with_dups.txt') as file:
    first_word = next(file).strip()
    words = set(map(str.strip, file)) - {first_word} # unique words
print(first_word, *random.sample(words, 2)) #NOTE: use random.sample()
                                            #to avoid relying on
                                            #PYTHONHASHSEED behavior

В этом случае, вероятность, что слово выбрано, не зависит от того как часто оно встречается в файле—все слова (кроме первого) имеют одинаковый вес.

str.strip() используется, чтобы удалить пробелы из входных строк, так чтобы в каждой строке только само слово осталось, иначе 'word', 'word\n', или 'word ' рассматривались бы как разные слова.


Если файл большой, но содержит только различающиеся слова, то можно использовать reservoir_sample() функцию, которая реализует линейный алгоритм R:

#!/urs/bin/env python3
with open('input.txt') as file:
    lines = [next(file)] + reservoir_sample(file, 2)
print(*map(str.strip, lines))

Это решение не читает весь файл в память сразу и поэтому может работать даже для больших файлов. Где reservoir_sample():

import itertools
import random

def reservoir_sample(iterable, k,
                     randrange=random.randrange, shuffle=random.shuffle):
    """Select *k* random elements from *iterable*.

    Use O(n) Algorithm R https://en.wikipedia.org/wiki/Reservoir_sampling
    """
    it = iter(iterable)
    sample = list(itertools.islice(it, k))  # fill the reservoir
    if len(sample) < k:
        raise ValueError("Sample larger than population")
    shuffle(sample)
    for i, item in enumerate(it, start=k+1):
        j = randrange(i) # random [0..i)
        if j < k:
            sample[j] = item # replace item with gradually decreasing probability
    return sample

Вероятность выбора произвольной строчки в файле постоянна и равна k / n, где n—кол-во строк в файле.


В общем случае (если слова могут повторяться во входом файле и он может быть большим). Необходимо модифицировать reservoir_sample() алгоритм, чтобы только ещё невыбранные элементы рассматривались:

#!/urs/bin/env python3
import itertools
import random


def choose_uniq(iterable, k, chosen, randrange=random.randrange):
    j0 = len(chosen)
    it = (x for x in iterable if x not in chosen)
    for x in itertools.islice(it, k):  # NOTE: add one by one
        chosen.append(x)
    if len(chosen) < (j0 + k):
        raise ValueError("Sample larger than population")
    for i, item in enumerate(it, start=k + 1):
        j = randrange(i)  # random [0..i)
        if j < k:  # replace item with gradually decreasing probability
            chosen[j0 + j] = item

with open('input_with_dups.txt') as file:
    chosen_words = [next(file).strip()]  # first word
    choose_uniq(map(str.strip, file), 2, chosen_words)
print(*chosen_words)

(x for x in iterable if x not in chosen) отсеивает уже выбранные элементы. Это работает, так как элементы генерируются «лениво»: по одному. Так как k == 2 в этом случае, то x not in chosen это быстрая операция даже для списка. Для больших к, можно set тип в этом выражении использовать, чтобы получить O(1) поведение.

choose_uniq() не ведёт себя как random.sample(), поэтому shuffle() убран. Получающееся распределение не совсем равномерное: в зависимости от порядка строк в исходном файле, часто повторяющаяся строка может быть выбрана чаще чем если бы только уникальные слова рассматривались бы (результат отличается от
set(map(str.strip, file)) - {first_word} решения).

Если требуется равномерное распределение (все уникальные слова выбираются с одинаковой вероятностью), то для больших файлов, не помещающихся в памяти, можно использовать внешнюю сортировку, что позже позволит отсеять дубликаты без дополнительных затрат памяти (в O(1) памяти), например, используя itertools.groupby() что в свою очередь позволит использовать снова reservoir_sample() без изменений.


Если не требуется строго равномерное распределение, то чтобы не читать весь потенциально большой файл (для скорости), можно выбирать слова со случайной позиции в файле. Для удобства можно использовать mmap модуль, который позволяет обращаться с файлом как со строкой (последовательность байтов), даже если размер файла больше доступной памяти:

#!/urs/bin/env python3
import locale
import mmap
import random
import re

with open('input_with_dups.txt', 'rb') as file, \
        mmap.mmap(file.fileno(), 0, access=mmap.ACCESS_READ) as s:
    first_nonspace_pos = re.search(br'\S', s).start()  # skip leading space
    chosen = set([get_word(s, first_nonspace_pos), b''])  # get 1st word
    while len(chosen) != 4:  # add two more random non-empty words
        chosen.add(get_word(s, random.randrange(len(s))))
encoding = locale.getpreferredencoding(False)
print(*[w.decode(encoding) for w in chosen if w])

где get_word() возвращает слово из строки около указанной позиции в файле:

def get_word(s, position, newline=b'\n'):
    """Return a word from a line in *s* at *position*."""
    i = s.rfind(newline, 0, position)  # find newline on the left
    start = (i + 1) if i != -1 else 0
    i = s.find(newline, position)  # find newline on the right
    end = i if i != -1 else len(s)
    return s[start:end].strip()  # a space is not part of a word, strip it

В файле могут быть пустые (содержащие только пробелы строки)—код с first_nonspace_index и b'' позволяет избежать выбора пустого слова. Код предполагает, что во входном файле больше двух различных слов иначе возможен бесконечный цикл. Юникодный пробелы (такие как U+00A0) не рассматриваются.

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

jfs
  • 52,361
  • на большом файле возможно читать кусками, отбрасываем первую и последнюю строку из куска, т.к. может быть не полной, остальные стандартным random.sample из set – Igor Aug 04 '16 at 21:48
  • @Igor не вижу как это поможет выбрать слова случайно, не нарушая равномерного распределения (если комментарий о choose_uniq() случае). Или вы надеетесь улучшить производительность reservoir_sample() решения? (которое вероятно ограничено только скоростью диска—IO bound). Попробуйте в виде ответа представить решение для ясности. – jfs Aug 04 '16 at 23:19
  • В исходной задаче о равномерном распределении ничего не было сказано, приведение данных к нормализованному виду считаем что не требуется. Иначе нужно понимать, нужны ли нам числа или их удаляем, сколько букв в слове и т.п. В задаче указана только необходимость выборки 3 слов из файла = строки. Если файл огромный то построчное чтение не требуется в полном объеме. Погрешность в выборке будет допустима. – Igor Aug 08 '16 at 06:54
  • @Igor фраза "рандомно выбрать" подразумевает равномерное распределение иначе можно было бы просто прочитать три первых попавшихся различающихся слова из файла. Не нужно игнорировать условия без оснований и *не упоминая что вы их игнорируете. Ещё раз: что вы хотели достичь (скорость, простоту кода)? – jfs Aug 08 '16 at 07:30
  • больше простоту чем скорость. – Igor Aug 08 '16 at 07:57
  • @Igor: попробуйте реализовать ваше предложение, перечислив явно когда предположения, используемые в решении, и посмотрите насколько код простым получится (или нет). – jfs Aug 08 '16 at 08:21
  • Посмотрел, но задача не совсем ясна, полный перебор всех строк в файле или выдернуть кусок текста размером в 50мб и сделать выборку из него. Решения разные. – Igor Aug 08 '16 at 08:31
3
import random
with open('source.txt', 'r') as source:
   l = source.readlines()
   word1 = l[0]
   word2 = ''
   word3 = ''
   while True:
      word2 = l[random.randint(1, len(l) - 1)]
      if word2 != word1:
         break
   while True:
      word3 = l[random.randint(1, len(l) - 1)]
      if word3 != word1 and word3 != word2:
         break
   print(word1, word2, word3)

1) Предполагается, что в файле одно слово в каждой строке.

2) Уйдет в бесконечный цикл, если в файле нет трех различных слов.

3) Крайне неэффективно по памяти при большом объеме исходного файла.

andy.37
  • 7,461
  • Поскольку randint работает с отрезком [a;b], при получении значения равного len(l), Вы получите ошибку выхода за диапазон. Из документации: random.randint(a, b) => return a random integer N such that a <= N <= b.
  • – Alex Krass Jan 11 '16 at 15:26
  • @AlexKrass, спасибо, поправил. Почему то считал, что randint аналогично range работает. – andy.37 Jan 11 '16 at 15:28
  • как-то вы совсем сложно подходите к решению: words = [next(source)] + random.sample(set(source), 2) – jfs Jan 12 '16 at 09:34
  • @jfs, ну да, по кол-ву кода Ваш вариант явно короче и элегантней). Вопрос в цене set(source) по сравнению с моими циклами... – andy.37 Jan 12 '16 at 09:42
  • @andy.37: как set() так и readlines() читают весь файл. – jfs Jan 12 '16 at 09:44
  • @jfs, насколько я понимаю, set должен сделать из списка множество без повторений, я даже не уверен, что это O(n). По памяти наши варианты неэффективны одинаково (Ваш чуть лучше, вероятно). Зато мой будет чудно работать для файла из 10000 строк с тремя различными словами... – andy.37 Jan 12 '16 at 09:46
  • source у вас это файл, а не список (иначе бы next(source) не работал). Гарантированно (если специальным образом ввод не сконфигурирован (PYTHONHASHSEED) — случайно так не получится с заметной вероятностью) что амортизированное O(n) время. Меня больше смущает излишняя многословность вашего кода без веских оснований: нужно писать самый простой код, который справляется с задачей — всё остальное вторично. – jfs Jan 12 '16 at 09:54