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

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

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

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

person user1582596    schedule 26.08.2012    source источник
comment
Вы хотите 10 случайных строк из каждого файла или 10 строк всего?   -  person Rob Cowie    schedule 26.08.2012
comment
Спасибо, всего 10 случайных строк.   -  person user1582596    schedule 26.08.2012
comment
Являются ли строки в этих файлах уникальными? Ожидаете ли вы, что между прогонами будут добавляться строки/файлы? Эти файлы содержат десятки или миллионы строк?   -  person Henk Langeveld    schedule 26.08.2012
comment
возможный дубликат как мне создать СПИСОК уникальных случайных числа?   -  person Andreas Jung    schedule 26.08.2012
comment
да, строки в этих файлах уникальны. нет, я не ожидаю, что строки/файлы будут добавлены между прогонами. нет, файлы не содержат десятки или миллионы строк. но может быть около 1000 ~ 2000 строк. Спасибо.!   -  person user1582596    schedule 26.08.2012
comment
Рассматривали ли вы возможность переноса данных в настоящую базу данных, такую ​​как SQLite, вместо использования файловой системы в качестве специальной базы данных?   -  person jfs    schedule 27.08.2012
comment
@Дж.Ф. Себастьян, спасибо за вклад, я подумал о MySQL, но мне слишком сложно выразить то, что я пытаюсь решить.   -  person user1582596    schedule 27.08.2012
comment
@user1582596: Простые случайные выборки из базы данных (My)Sql   -  person jfs    schedule 28.08.2012


Ответы (3)


Чтобы получить правильное случайное распределение по всем этим файлам, вам нужно рассматривать их как один большой набор строк и выбирать 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, поскольку оно будет выполняться только файлы один раз, в то время как мои требуют 10 дополнительных открытий файлов. Если вам нужно несколько образцов, это решение требует только 10 дополнительных открытий файлов каждый раз, когда вам нужен образец, оно не будет снова сканировать все файлы. Если у вас менее 10 файлов, все равно используйте ответ Blckknght. :-)

person Martijn Pieters    schedule 26.08.2012
comment
Спасибо, при индексации возникла следующая ошибка. Трассировка (последний последний вызов): файл ‹stdin›, строка 1, в ‹module› AttributeError: объект 'dict_keys' не имеет атрибута 'sort'. кстати, я пробую это с Python 3.2.3 - person user1582596; 26.08.2012
comment
@ user1582596: А, важное отличие, я обновил код для вас. - person Martijn Pieters; 26.08.2012
comment
На самом деле вам не нужно знать, сколько всего строк, чтобы выбрать 10 случайным образом. Вы можете выбрать одну строку случайным образом, уменьшив для каждой строки вероятность того, что вы сохраните именно ее: perlmonks .org/?node_id=1910 . Для N строк вы сохраняете список из N и для каждой новой строки уменьшаете вероятность того, что вы его сохраните: perlmonks.org/?node_id=1910 (извините за весь Perl). - person Ned Batchelder; 26.08.2012
comment
@NedBatchelder: Рад видеть, что этот метод по-прежнему требует, чтобы вы прочитали все файлы. :-P Читая между строк, я почти уверен, что ОП хочет выбрать случайные 10 задач более одного раза. В моей настройке вам нужно сканировать файлы только один раз, а затем выбирать образцы по мере необходимости. - person Martijn Pieters; 26.08.2012
comment
@MartijnPieters: да, ОП был немного расплывчатым по поводу удаления. Метод случайной строки из файла представляет собой равномерное распределение, и это интересно в этой технике. - person Ned Batchelder; 26.08.2012

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

Во-первых, это функция выборки. Здесь используется тот же алгоритм, на который @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

изменить: В другом вопросе пользователь @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!
person Blckknght    schedule 26.08.2012
comment
вы могли бы написать: (letter for word in sentence for letter in word if good(letter)) вместо chain.from_iterable((for letter in word if good(letter)) for word in sentence) - person jfs; 27.08.2012
comment
Хм, ты прав. Я думаю, что начал использовать chain.from_iter, когда пробовал что-то другое, и это не нужно в версии, которую я закончил и опубликовал. Прямое генераторное выражение понятнее, поэтому я попробую его вместо этого (я думаю, что это также сэкономит мне строку, так как мне не нужно будет разделять строки по отдельности). - person Blckknght; 27.08.2012
comment
Вы также можете написать явные циклы for и yield line в файле task_pipeline(). Он должен производить наиболее удобочитаемую версию. Кроме того, в этом случае естественно использовать with open(filename) as file: (это нужно, если дерево содержит большое количество текстовых файлов, чтобы избежать ошибки «Слишком много открытых файлов») - person jfs; 27.08.2012
comment
@MartijnPieters: вы пропустили проверку if r < n в предыдущей строке. Это представляет собой уменьшение вероятности замены после того, как вы получили первые n значений. Вы правы в том, что возможно, что алгоритм вернет менее n значений, но это произойдет только в том случае, если в итерируемых элементах меньше n значений (он вернет их все в случайном порядке). - person Blckknght; 27.08.2012
comment
Правильно, действительно, r < n предотвратит IndexErrors, я пропустил это. :-P Оба комментария отозваны. - person Martijn Pieters; 27.08.2012
comment
Если вы должны были сохранить номер строки со строкой ((i, v)), вы можете получить их и пропустить эти строки в будущем запуске. - person Martijn Pieters; 27.08.2012
comment
@ J.F.Sebastian: Да, версия с явным циклом может быть проще, но мое сердце настроено на генераторные выражения. Они могут быть не идеальными. Что касается предотвращения дублирования, одно из решений — просто взять большую выборку в начале и использовать ее фрагменты. Поскольку выборочные значения расположены в случайном порядке, все подмножества также являются случайными выборками. Конечно, вам нужно точно знать, сколько всего предметов вам понадобится, чтобы это сработало. - person Blckknght; 27.08.2012
comment
@Blckknght, я получаю сообщение об ошибке из-за двоеточия в коде, sample = random_sample(10, task_pipeline(base_folder)): - person user1582596; 27.08.2012
comment
@DavidFraser: Да, если вы введете менее n строк. В противном случае будет создано ровно n элементов. - person Martijn Pieters; 27.08.2012
comment
Спасибо всем за отзывы. Я обновил ответ кучей изменений, включая преобразование функции конвейера в явный генератор, а не использование набора выражений генератора (я также переименовал его). Я также добавил некоторый код, чтобы избежать дублирования, если несколько образцов запрашиваются подряд, но эта часть, вероятно, намного медленнее, чем базовый алгоритм выборки. Если вы заранее знаете, сколько сэмплов потребуется, получите их все сразу для наилучшей производительности! Я также заставил пробную функцию вызывать ValueError, если она не получает достаточного количества значений. - person Blckknght; 27.08.2012
comment
@user1582596. Ага, это была опечатка. Извини за это. Он должен исчезнуть из нового кода. - person Blckknght; 27.08.2012
comment
В Python 3.3: with open(os.path.join(dirpath, filename)) as file: yield from file Примечание. Он удаляет line.strip(), проверку exclude. - person jfs; 28.08.2012
comment
@Blckknght, 10x, я пытался заставить это работать, но каким-то образом я попал в статистику, в которой нет ни ошибок, ни вывода. пытаюсь отладить... - person user1582596; 28.08.2012

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

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

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

Код...

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)
person Rob Cowie    schedule 26.08.2012
comment
Это может привести к дублированию выбора, и я сомневаюсь, что распределение выборки будет однородным. Как вы запоминаете или удаляете выбранные задачи при будущем запуске? Из OP: Выбранная строка должна быть удалена или отмечена, чтобы она не выбиралась при следующем выполнении. - person Martijn Pieters; 27.08.2012
comment
Дох, надо было внимательнее читать. Немного поздно изменить мой ответ сейчас. Я займусь этим завтра. Я подозреваю, что простое решение состоит в том, чтобы превратить список строк в набор - person Rob Cowie; 27.08.2012
comment
10x, @Martijn Pieters, получил следующие ошибки, Traceback (последний последний вызов): Файл C:\Dropbox\Python\testr1.py, строка 31, в ‹module› files = file_iterator(r'C:\\Dropbox\ \ans7i\\') Файл C:\Dropbox\Python\testr1.py, строка 11, в file_iterator path = os.path.join(dirpath, filename) UnboundLocalError: ссылка на локальную переменную 'filename' перед назначением - person user1582596; 27.08.2012
comment
Эти две строки должны иметь отступ еще на один уровень; Я исправлю их. - person Martijn Pieters; 27.08.2012
comment
@Martijn Pieters, так приятно видеть это в действии, интересно, есть ли какой-нибудь быстрый способ добавить папку, иерархию имен файлов в качестве префикса, чтобы было легко найти задачу, откуда она исходит. Пример [Task][Do1][Do2][DL.txt] [задача]; [Task][Do3][Do5][DL20.txt] [task] Также добавлен оператор «print (random_tasks)», но вывод выглядит как один абзац и делает его немного нечитаемым. - person user1582596; 27.08.2012
comment
У вас есть путь в dirpath, и вы можете перебирать random_tasks для печати отдельных элементов. - person Martijn Pieters; 27.08.2012
comment
@Martijn Pieters, пока не могу понять, интересно, встраиваете ли вы код. ТИА. мне также нужна помощь, чтобы каждая задача начиналась с новой строки. - person user1582596; 27.08.2012
comment
Мех, этот ответ не очень полезен. Blckknght и Martijn оба дали лучшие ответы. Посмотрите на эти ответы, а не на этот. - person Rob Cowie; 27.08.2012
comment
10x @RobCowie, пока я рассматриваю все другие варианты как возможный ответ. Мне не менее любопытно узнать, как можно реализовать dirpath в этом случае, так как я потратил время, чтобы решить сам, и я не смог, также это было бы хорошим опытом обучения. ТИА. - person user1582596; 27.08.2012
comment
Извините, я не уверен, что вы подразумеваете под «dirpath». Пожалуйста объясните далее. - person Rob Cowie; 28.08.2012
comment
@RobCowie, в 10 раз на данный момент, в настоящее время этот код помогает мне вытягивать текстовые строки, но мне интересно, как я могу включить путь к каталогу (включая имя файла) в качестве префикса. поэтому будет полезно определить, откуда исходит задача. Например [Task][Do1][Do2][DL.txt][task1]; [Task][Do3][Do5][DL20.txt] [task2] Также добавлен оператор «print (random_tasks)», но вывод выглядит как абзац и делает его немного нечитаемым. Мне также нужна помощь, чтобы каждая задача начиналась с новой строки. - person user1582596; 28.08.2012