从文件中随机抽样

时间:2018-01-01 14:53:46

标签: python bash sed

我有一个大约40gb和1800000行的csv文件。

我想随机抽样10,000行并将其打印到新文件中。

现在,我的方法是使用sed作为:

(sed -n '$vars' < input.txt) > output.txt

$vars是随机生成的行列表。 (例如:1p; 14p; 1700p; ......; 10203p)

虽然这有效,但每次执行大约需要5分钟。这不是一个很大的时间,但我想知道是否有人对如何更快地提出想法?

6 个答案:

答案 0 :(得分:5)

拥有相同长度的线条的最大好处是,您不需要找到换行符来了解每条线的起点。文件大小约为40GB,包含约1.8M行,您的行长度约为20KB /行。如果你想采样10K线,你的线之间有~40MB。这几乎可以肯定比磁盘上块的大小大三个数量级。因此,寻找下一个读取位置比读取文件中的每个字节要有效得多。

寻求将使用具有不等行长度的文件(例如,UTF-8编码中的非ascii字符),但需要对该方法进行微小的修改。如果您有不相等的线,您可以搜索估计的位置,然后扫描到下一行的开头。这仍然是非常有效的,因为你需要为每个~20KB的内容跳过~40MB。由于您将选择字节位置而不是行位置,因此您的采样均匀性会受到轻微影响,并且您无法确定您正在读取的行号。

您可以使用生成行号的Python代码直接实现解决方案。以下是如何处理所有具有相同字节数的行的示例(通常为ascii编码):

import random
from os.path import getsize

# Input file path
file_name = 'file.csv'
# How many lines you want to select
selection_count = 10000

file_size = getsize(file_name)
with open(file_name) as file:
    # Read the first line to get the length
    file.readline()
    line_size = file.tell()
    # You don't have to seek(0) here: if line #0 is selected,
    # the seek will happen regardless later.

    # Assuming you are 100% sure all lines are equal, this might
    # discard the last line if it doesn't have a trailing newline.
    # If that bothers you, use `math.round(file_size / line_size)`
    line_count = file_size // line_size
    # This is just a trivial example of how to generate the line numbers.
    # If it doesn't work for you, just use the method you already have.
    # By the way, this will just error out (ValueError) if you try to
    # select more lines than there are in the file, which is ideal
    selection_indices = random.sample(range(line_count), selection_count)
    selection_indices.sort()

    # Now skip to each line before reading it:
    prev_index = 0
    for line_index in selection_indices:
        # Conveniently, the default seek offset is the start of the file,
        # not from current position
        if line_index != prev_index + 1:
            file.seek(line_index * line_size)
        print('Line #{}: {}'.format(line_index, file.readline()), end='')
        # Small optimization to avoid seeking consecutive lines.
        # Might be unnecessary since seek probably already does
        # something like that for you
        prev_index = line_index

如果您愿意牺牲(非常)少量的行号分布均匀性,您可以轻松地将相似的技术应用于行长度不等的文件。您只需生成随机字节偏移,并跳过偏移后的下一个完整行。在以下实现中,假设您知道没有行的长度超过40KB。如果您的CSV具有以UTF-8编码的非ascii unicode字符,则必须执行此类操作,因为即使这些行包含相同数量的字符,它们也将包含不同数量的字节。在这种情况下,您必须以二进制模式打开文件,否则当您跳到随机字节时,如果该字节碰巧是中间字符,则可能会遇到解码错误:

import random
from os.path import getsize

# Input file path
file_name = 'file.csv'
# How many lines you want to select
selection_count = 10000
# An upper bound on the line size in bytes, not chars
# This serves two purposes:
#   1. It determines the margin to use from the end of the file
#   2. It determines the closest two offsets are allowed to be and
#      still be 100% guaranteed to be in different lines
max_line_bytes = 40000

file_size = getsize(file_name)
# make_offset is a function that returns `selection_count` monotonically
# increasing unique samples, at least `max_line_bytes` apart from each
# other, in the range [0, file_size - margin). Implementation not provided.
selection_offsets = make_offsets(selection_count, file_size, max_line_bytes)
with open(file_name, 'rb') as file:
    for offset in selection_offsets:
        # Skip to each offset
        file.seek(offset)
        # Readout to the next full line
        file.readline()
        # Print the next line. You don't know the number.
        # You also have to decode it yourself.
        print(file.readline().decode('utf-8'), end='')

这里的所有代码都是Python 3。

答案 1 :(得分:2)

如果所有行都具有相同的长度,您可以使用/usr/bin/tar -czvf /home/user/backup-`(date +%y-%m-%d)`.tar.gz /some/file.txt 无需解析整个文件或将其加载到内存中。

你必须知道已经执行dd的行号,以及每行的精确字节长度,当然还要测试并确保所有行真正具有相同的长度。即使wc -l也会很慢,因为它会读取整个文件。

例如,如果每行是20000字节

wc

这样我们循环并运行10K进程,我不确定它是否可以一次完成,所以虽然dd更快,但使用Python和#!/bin/bash for i in `shuf -n 10000 -i 0-1799999 | sort -n` do dd if=file bs=20000 skip="$i" count=1 of=output status=none \ oflag=append conv=notrunc done 方法之类的语言(如@tripleee所说的那样) @Mad Physicist暗示评论)将具有一个过程的优势。

seek()

再保存几秒钟,如果输出足够小,可以将其保存在bytearray中并在结束时立即写入。

#!/usr/bin/python3
import random

randoms = random.sample(range(0, 1800000), 10000)
randoms.sort()

lsize = 20000

with open("file", "rb") as infile, open('output', 'wb') as outfile:
    for n in randoms:
        infile.seek(lsize * n)
        outfile.write(infile.read(lsize))

答案 2 :(得分:1)

如果您的行的长度都相同,那么您的Python脚本可以在文件中提前seek(),并且您知道要准确找到哪个索引,以便在换行符后准确定位到字符上。< / p>

为您的sed脚本生成随机索引的Python脚本应该很容易适应这种方法。基本上,当您生成123p以进入sed时,请寻找122 *行长并读取您登陆的行。

一个复杂的问题是Python 3禁止在文本模式下打开的文件中随机搜索(因为它需要知道编码字符的开始和结束位置)。对于快速而脏的脚本,只需读取和写入字节就可以了(通常建议将字节解码为Unicode,然后在写入之前再次编码;但由于您根本不在Python中处理这些行,所以是不必要的。)

答案 3 :(得分:1)

出于测试目的,让我们创建一个1,800,000行的文件:

$ awk 'BEGIN {for (i=1; i<=1800000; i++) print "line " i}' >file
$ ls -l file
-rw-r--r--  1 dawg  wheel  22288896 Jan  1 09:41 file

假设您不知道该文件中的行数,获取总行数的最快方法是使用POSIX实用程序wc

$ time wc -l file
 1800000 file

real    0m0.018s
user    0m0.012s
sys 0m0.004s

因此,要获得1,800,000行的文本文件的总行数非常快。

现在您已知道总行数,您可以使用awk打印这些行的随机样本:

#!/bin/bash

lc=($(wc -l file))
awk -v lc="$lc" -v c=10000 '
BEGIN{srand()}
int(lc*rand())<=c{print; i++}
i>=c{exit}
' file >rand_lines

在我的旧款iMac上运行大约200毫秒。请注意,总计关闭到10,000但可能更少,因为在达到10,000行之前经常会到达文件的末尾。

如果你想要真正随机性的罚款10,000,你可以这样做:

awk -v lc="$lc" -v c=10000 '
BEGIN{srand()}
int(lc*rand())<c * (1.01 or a factor to make sure that 10,000 is hit before EOF) {print; i++}
i>=c{exit}
' file >rand_lines

或者,或者,在1和行数之间生成10,000个唯一数字:

awk -v lc="$lc" -v c=10000 '
BEGIN{srand()
      while (i<c) {
        x=int(lc*rand())
        if (x in rl) continue  # careful if c is larger than or close to lc
        else {
        rl[x]
        i++} 
        }
     }
NR in rl' file >rand_lines

答案 4 :(得分:0)

您需要将数据插入数据库(例如sqlite或mysql),然后在SQL中重复您的想法

select * from your_table where id in (1, 14, 1700, ...)

您还可以阅读如何从这个优秀教程http://jan.kneschke.de/projects/mysql/order-by-rand/

中选择随机样本

没有办法设计一个运行得更快的shell脚本,因为您的代码最终依赖于文件系统从根本上运行的方式。也就是说,为了获得良好的性能,您需要按顺序和块状访问磁盘。数据库旨在通过将数据在硬盘中的布局方式存储在名为 index 的单独文件中来解决此问题。它的工作方式与书的索引相同。

这是一个丰富的主题,需要一些学习。如果您不熟悉数据库编程,那么40 gb数据集是一个很好的起点。

答案 5 :(得分:0)

借鉴蒙特卡罗模拟世界的另一个想法是在每次迭代中循环并生成随机数。现在,如果你想要一组180k行中的10k行,你的理由如下。您想要包含相关行的10/180更改。如果随机数小于或等于10/180,则接受该行。否则,如果已经收集了所需的行数,则拒绝它或中断循环。

这种方法的缺点是无法保证正好采样10k行。我也怀疑这种方法存在偏见,并且它不够随意。