Случайные строки Python из подпапок

У меня много задач в файлах .txt в нескольких подпапках. Я пытаюсь случайно собрать 10 заданий из этих папок, их содержащихся файлов и, наконец, текстовую строку внутри файла. Выбранную строку следует удалить или пометить, чтобы она не была выбрана при следующем выполнении. Это может быть слишком широкий вопрос, но я был бы признателен за любой ввод или направление.

Вот код, который у меня есть до сих пор:

#!/usr/bin/python  
import random   
with open('C:\\Tasks\\file.txt') as f:  
    lines = random.sample(f.readlines(),10)    
print(lines)

Ответ 1

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

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

Первое индексирование:

import os

root_path = r'C:\Tasks\\'
total_lines = 0
file_indices = dict()

# Based on https://stackoverflow.com/q/845058, bufcount function
def linecount(filename, buf_size=1024*1024):
    with open(filename) as f:
        return sum(buf.count('\n') for buf in iter(lambda: f.read(buf_size), ''))

for dirpath, dirnames, filenames in os.walk(root_path):
    for filename in filenames:
         if not filename.endswith('.txt'):
             continue
         path = os.path.join(dirpath, filename)
         file_indices[total_lines] = path
         total_lines += linecount(path)

offsets = list(file_indices.keys())
offsets.sort()

Теперь мы имеем отображение смещений, указывая на имена файлов и общее количество строк. Теперь мы выбираем десять случайных индексов и читаем их из ваших файлов:

import random
import bisect

tasks = list(range(total_lines))
task_indices = random.sample(tasks, 10)

for index in task_indices:
     # find the closest file index
     file_index = offsets[bisect.bisect(offsets, index) - 1]
     path = file_indices[file_index]
     curr_line = file_index
     with open(path) as f:
         while curr_line <= index:
             task = f.readline()
             curr_line += 1
     print(task)
     tasks.remove(index)

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

Также обратите внимание, что ваши задачи теперь "сохранены" в списке tasks; это индексы строк в ваших файлах, и я удаляю индекс из этой переменной при печати выбранной задачи. В следующий раз, когда вы запустите выбор random.sample(), ранее выбранные задачи больше не будут доступны для выбора в следующий раз. Эта структура нуждается в обновлении, если ваши файлы когда-либо будут меняться, так как индексы должны быть пересчитаны. file_indices поможет вам с этой задачей, но это выходит за рамки этого ответа.: -)

Если вам нужен только один образец из 10 предметов, используйте Blckknght solution, так как он будет проходить через файлы один раз, в то время как мне требуется 10 дополнительных открытий файлов. Если вам нужно несколько образцов, это решение требует только 10 дополнительных открываний файлов каждый раз, когда вам нужен ваш образец, он не будет сканировать все файлы снова. Если у вас меньше 10 файлов, все равно используйте ответ Blckknght.: -)

Ответ 2

Здесь простое решение, которое делает только один проход через файлы на образец. Если вы точно знаете, сколько элементов вы будете отбирать из файлов, это, вероятно, оптимально.

Прежде всего, это функция выборки. Это использует тот же алгоритм, с которым @NedBatchelder связан с комментарием о более раннем ответе (хотя показанный там код Perl выбрал только одну строку, а не несколько). Он выбирает значения из итерируемого строк и требует только, чтобы текущие выделенные строки сохранялись в памяти в любой момент времени (плюс следующая строка кандидата). Он поднимает значение ValueError, если итерабельность имеет меньше значений, чем запрошенный размер выборки.

import random

def random_sample(n, items):
    results = []

    for i, v in enumerate(items):
        r = random.randint(0, i)
        if r < n:
            if i < n:
                results.insert(r, v) # add first n items in random order
            else:
                results[r] = v # at a decreasing rate, replace random items

    if len(results) < n:
        raise ValueError("Sample larger than population.")

    return results

edit: В другом вопросе пользователь @DzinX заметил, что использование insert в этом коде делает производительность плохой (O(N^2)), если вы отбираете очень большое количество значений, Его улучшенная версия, которая позволяет избежать этой проблемы, здесь. /изменить

Теперь нам просто нужно сделать подходящий итеративный элемент для нашей функции для выборки. Вот как я буду делать это с помощью генератора. Этот код будет поддерживать только один файл одновременно, и ему не требуется больше одной строки в памяти за раз. Необязательный параметр exclude, если он присутствует, должен содержать строки set, которые были выбраны в предыдущем прогоне (и поэтому не должны возвращаться).

import os

def lines_generator(base_folder, exclude = None):
    for dirpath, dirs, files in os.walk(base_folder):
        for filename in files:
            if filename.endswith(".txt"):
                fullPath = os.path.join(dirpath, filename)
                with open(fullPath) as f:
                     for line in f:
                         cleanLine = line.strip()
                         if exclude is None or cleanLine not in exclude:
                             yield cleanLine

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

_seen = set()

def get_sample(n, count = None):
    base_folder = r"C:\Tasks"
    if count is None:
        sample = random_sample(n, lines_generator(base_folder, _seen))
        _seen.update(sample)
        return sample
    else:
        sample = random_sample(count * n, lines_generator(base_folder, _seen))
        _seen.update(sample)
        return [sample[i * n:(i + 1) * n] for i in range(count)]

Вот как это можно использовать:

def main():
    s1 = get_sample(10)
    print("Sample1:", *s1, sep="\n")

    s2, s3 = get_sample(10,2) # get two samples with only one read of the files
    print("\nSample2:", *s2, sep="\n")
    print("\nSample3:", *s3, sep="\n")

    s4 = get_sample(5000) # this will probably raise a ValueError!

Ответ 3

EDIT: При внимательном рассмотрении этот ответ не соответствует законопроекту. Переработка привела меня к алгоритму выборки коллектора, который @Blckknght использовал в его ответе. Поэтому игнорируйте этот ответ.

Несколько способов сделать это. Здесь один...

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

Код...

import os
import random

def file_iterator(top_dir):
    """Gather all task files"""
    files = []
    for dirpath, dirnames, filenames in os.walk(top_dir):
        for filename in filenames:
            if not filename.endswith('.txt'):
                continue
            path = os.path.join(dirpath, filename)
            files.append(path)
    return files


def random_lines(files, number=10):
    """Select a random file, select a random line until we have enough
    """
    selected_tasks = []

    while len(selected_tasks) < number:
        f = random.choice(files)
        with open(f) as tasks:
            lines = tasks.readlines()
            l = random.choice(lines)
            selected_tasks.append(l)
    return selected_tasks


## Usage
files = file_iterator(r'C:\\Tasks')
random_tasks = random_lines(files)