子文件夹中的Python随机行

时间:2012-08-26 09:17:47

标签: python python-3.x random-sample

我在多个子文件夹中的.txt文件中有很多任务。我试图从这些文件夹,它们包含的文件以及文件中的文本行中随机选取总共10个任务。应删除或标记选定的行,以便在下次执行时不会选择它。这可能是一个太宽泛的问题,但我很欣赏任何意见或指示。

这是我到目前为止的代码:

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

3 个答案:

答案 0 :(得分:14)

这是一个简单的解决方案,每个样本只传递一次文件。如果您确切知道将从文件中采样的项目数量,则可能是最佳选择。

首先是示例功能。这使用了@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))大量的价值观。他避免这个问题的改进版本是here。的 /修改

现在我们只需要为我们的函数制作一个合适的可迭代项来进行采样。以下是我使用发电机的方法。此代码一次只保持一个文件打开,并且一次不需要多行内存。可选的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!

答案 1 :(得分:4)

要在所有这些文件中获得正确的随机分布,您需要将它们视为一组大行并随机选择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's solution,因为它只会通过文件一次,而我需要10个额外的文件开头。如果您需要多个样本,此解决方案每次需要样本时只需要10个额外的文件开头,它不会再次扫描所有文件。如果您的文件少于10个,仍然使用Blckknght的答案。 : - )

答案 2 :(得分:0)

编辑:经过仔细审查,这个答案不符合要求。重新加工使我得到了水库采样算法,@ Blckknght在他的回答中使用了该算法。所以忽略这个答案。

很少有这样做。这是一个...

  1. 获取所有任务文件的列表
  2. 随机选择一个
  3. 随机选择该文件中的一行
  4. 重复,直到我们有所需的行数
  5. 代码......

    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)