如何从一个文件中读取一行随机的内容?

56

有没有内置的方法可以做到这一点?如果没有,我该如何在不造成太多额外开销的情况下完成?


1
@Greg 这是 Perl,不是 Python。 - quantumSoup
4
问题中使用了Perl的例子,但问题本身不限于特定编程语言。最有用的答案使用伪代码,可以轻松转换成您选择的编程语言。 - Greg Hewgill
1
谢谢,我也发现这个链接非常有用:http://mail.python.org/pipermail/tutor/2007-July/055635.html 但你需要将它们读入内存中。 - Shane
2
@Greg 这并不适用于文件 I/O,因为它在不同的编程语言中可能会有很大的不同。 - quantumSoup
1
@quantumSoup 在所有编程语言中,逐行读取文件基本上是相同的。 - P Shved
如何在不将整个文件加载到内存中的情况下索引随机行? - Charlie Parker
12个回答

81

虽然没有内置函数,但是可以使用算法R(3.4.2)(Waterman的“Reservoir Algorithm”),该算法被收录在Knuth的著作“计算机程序设计艺术”中,并且这个算法还经过了大量的简化处理。

import random

def random_line(afile):
    line = next(afile)
    for num, aline in enumerate(afile, 2):
        if random.randrange(num):
            continue
        line = aline
    return line
num, ... in enumerate(..., 2) 迭代器会产生序列 2,3,4...。因此,randrange 函数的返回值为0的概率是 1.0/num -- 而我们必须用这个概率来替换当前选定的行(参考算法样本大小为1的特殊情况 -- 参见 Knuth 的书籍以证明正确性 == 当然,我们也处于足够小的“水库”情况,能够适应内存;-))...并且正是我们执行此操作的概率。

10
我一直认为random.choice()函数应该能够在任意迭代器上运行,而不仅仅是序列,并实现与上述算法完全相同的功能。 - Greg Hewgill
3
@Greg Hewgill,那是很好的建议,但每十个问题中就会出现一个“我的迭代器去哪了”的问题。 - aaronasterling
2
@aaron,对的——同样的原因,例如,迭代器没有len...算法并不难看出,但是消耗迭代器被认为是一个太容易令人惊讶的效果。当然,这是一系列艰难的设计决策(例如,sum确实会消耗迭代器——决策在于求和可能是用户所需的全部,而长度或一个随机项则不太可能如此...总是两难的决策——如果我们有一种明确标记名称为“具有副作用”的方法,就像Ruby的尾随感叹号一样,设计选择可能会有所不同)。 - Alex Martelli
1
@Henry,没错 - 我编辑了A标签以正确地设置属性,谢谢你的提醒。 - Alex Martelli
在 Python 3.8 中的第二次调用时,line = next(afile) 抛出 StopIteration 异常。 - Ali Tou
当您在同一迭代器上第二次调用函数(例如,相同的打开文件)时,迭代器将已被第一次调用完全消耗,因此现在它将为空,并且StopIteration是正确的,就像任何其他空迭代器一样。如果您需要重复从迭代器中获取随机项,则必须首先将整个迭代器复制到项目列表中,然后在列表上使用random.choice最简单。 - Alex Martelli

53
import random
lines = open('file.txt').read().splitlines()
myline =random.choice(lines)
print(myline)

针对非常长的文件:

根据文件长度随机定位到文件的某个位置,并在该位置之后找到两个换行符(或者是一个换行符和文件结尾)。如果原始查找位置小于100并且我们最终在最后一行内,则从文件开头或末尾开始再次查找100个字符。

然而,这样做过于复杂,因为文件可以迭代。所以将其转化为列表,然后使用 random.choice(如果需要多个,请使用random.sample )。

import random
print(random.choice(list(open('file.txt'))))

22
如果任务只是阅读一行内容,那么将整个文件加载到内存中就没有意义。 - iankit
1
这个解决方案非常简单易懂。我建议将此解决方案作为最终答案。 - Francisco Maria Calisto
2
这是一个有效的解决方案,但它不会去除\r\n或EOL。你需要添加.rstrip()来清理它。 - Payam
你是否将整个文件加载到内存中?如果@iankit已经评论了这个问题,那么对此进行评论会很好。 - Charlie Parker
我喜欢这个,因为如果你的电脑是2000年之后制造的,那么在内存中加载整个文件并不重要。 - eeeeeeeeeeeeeeeeeeeeeeeeeeeeee

18

这取决于您对“过多”的开销有何理解。如果可以将整个文件存储在内存中,那么可以考虑以下代码:

import random

random_lines = random.choice(open("file").readlines())

用这种方法可以解决问题。


17

虽然我晚了四年,但是我认为我有最快的解决方案。最近我写了一个名为linereader的Python软件包,它允许你操作文件句柄的指针。

以下是使用这个软件包获取随机行的简单解决方案:

from random import randint
from linereader import dopen

length = #lines in file
filename = #directory of file

file = dopen(filename)
random_line = file.getline(randint(1, length))

第一次执行这个操作是最糟糕的,因为linereader必须以特殊格式编译输出文件。完成后,无论文件大小如何,linereader都可以快速访问文件中的任何行。

如果您的文件非常小(足够小,适合MB),则可以将dopen替换为copen,并在内存中创建文件的缓存条目。这不仅更快,而且在加载到内存中时,您可以获得文件中的行数;它已经为您完成了。您只需要生成随机行号即可。以下是一些示例代码。

from random import randint
from linereader import copen

file = copen(filename)
lines = file.count('\n')
random_line = file.getline(randint(1, lines))

我刚看到有人可以从我的包中受益,感到非常高兴!抱歉没有及时回复,但这个包肯定可以用于许多其他问题。


1
我遇到了ValueError,提示找不到行号,但实际上文件的大小比行号小。 - kakarukeys
1
很酷的东西!你为什么要从1开始索引文件行呢?(getline(file, 0)会返回最后一行) - Jura Brazdil

7
如果你不想通过 f.read() 或者 f.readlines() 加载整个文件到内存中,你可以按照以下的方式获取随机行:
import os
import random


def get_random_line(filepath: str) -> str:
    file_size = os.path.getsize(filepath)
    with open(filepath, 'rb') as f:
        while True:
            pos = random.randint(0, file_size)
            if not pos:  # the first line is chosen
                return f.readline().decode()  # return str
            f.seek(pos)  # seek to random position
            f.readline()  # skip possibly incomplete line
            line = f.readline()  # read next (full) line
            if line:
                return line.decode()  
            # else: line is empty -> EOF -> try another position in next iteration

补充:是的,Ignacio Vazquez-Abrams在他上面的答案中提出了这个方法,但a)他的答案中没有代码,b)我自己想到了这个实现方式;它可以返回第一行或最后一行。希望对某人有用。

然而,如果你关心分发问题,这段代码对你不是一个选择。


5

这是Alex Martelli的答案的稍微改进版本,它可以处理空文件(通过返回一个default值):

from random import randrange

def random_line(afile, default=None):
    line = default
    for i, aline in enumerate(afile, start=1):
        if randrange(i) == 0:  # random int [0..i)
            line = aline
    return line

使用这种 方法,可以在O(n)时间和O(1)空间的条件下从任何迭代器中获取随机项目。


4
如果您不想阅读整个文件,可以将其定位到文件中间,然后向后查找换行符,并调用readline函数。
这是一个执行此操作的Python3脚本,
这种方法的一个缺点是短行出现的可能性较低。
def read_random_line(f, chunk_size=16):
    import os
    import random
    with open(f, 'rb') as f_handle:
        f_handle.seek(0, os.SEEK_END)
        size = f_handle.tell()
        i = random.randint(0, size)
        while True:
            i -= chunk_size
            if i < 0:
                chunk_size += i
                i = 0
            f_handle.seek(i, os.SEEK_SET)
            chunk = f_handle.read(chunk_size)
            i_newline = chunk.rfind(b'\n')
            if i_newline != -1:
                i += i_newline + 1
                break
            if i == 0:
                break
        f_handle.seek(i, os.SEEK_SET)
        return f_handle.readline()

1
import random

with open("file.txt", "r") as f:
    lines = f.readlines()
    print (random.choice(lines))

1
这可能很笨重,但我猜它能起作用?(至少对于txt文件而言)
import random
choicefile=open("yourfile.txt","r")
linelist=[]
for line in choicefile:
    linelist.append(line)
choice=random.choice(linelist)
print(choice)

它会读取文件的每一行,并将其追加到列表中。然后从列表中选择一个随机行。 如果您想在选择后删除该行,只需执行以下操作。
linelist.remove(choice)

希望这可以帮到你,但至少没有额外的模块和导入(除了随机)而且相对较轻。

1

随机定位到一个位置,读取一行并将其丢弃,然后再读取另一行。行的分布可能不是正常的,但这并不总是重要的。


4
特别是,这使得选择第一行(以及选择其他行的概率与每个前一行的长度成比例)变得不可能。我的 A 也不会产生正态分布(那将很奇怪——什么均值,什么方差?!),而是产生一个均匀分布,这似乎更有可能符合原帖中“随机”的含义。 - Alex Martelli
2
为了解决@AlexMartelli指出的问题,如果随机查找导致您到达了最后一行,请选择第一行。但另一个问题在于,相对于其他行,单行文字较多的行被选中的概率更高。 - Ashwin Surana

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接