【问题标题】:What is the fastest way to search the csv file?搜索 csv 文件的最快方法是什么?
【发布时间】:2019-11-30 20:40:06
【问题描述】:

任务:检查文件中序列号和护照号的可用性。

我的决定如下:

def check_passport(filename, series: str, number: str) -> dict:
    """
    Find passport number and series
    :param filename:csv filename path
    :param series: passport series
    :param number: passport number
    :return:
    """
    print(f'series={series}, number={number}')
    find = False
    start = datetime.datetime.now()
    with open(filename, 'r', encoding='utf_8_sig') as csvfile:
        reader = csv.reader(csvfile, delimiter=',')
        try:
            for row in reader:
                if row[0] == series and row[1] == num:
                    print(row[0])
                    print(row[1])
                    find = True
                    break
        except Exception as e:
            print(e)
    print(datetime.datetime.now() - start)
    if find:
        return {'result': False, 'message': f'Passport found'}
    else:
        return {'result': False, 'message': f'Passport not found in Database'}

这是 csv 文件的一部分

PASSP_SERIES,PASSP_NUMBER
3604,015558 
6003,711925 
6004,461914 
6001,789369

如果文件中没有护照,时间会更糟,因为您需要检查所有行。我的最佳时间是 53 秒。

【问题讨论】:

  • 为什么不将 csv 作为 pandas DataFrame 读取?你可以简单地做df['columnname'].str.contains(series) 而不是遍历每一行。
  • 总是针对性能问题:您可以接受的时间是多少? 52可以吗?您需要 10 秒、1 秒还是 100 毫秒? CSV 文件的大小是多少?如果它是 100 GB,那么 53 秒是相当快的。你有多少个处理器? 1、4 还是 32?
  • 您始终可以在单独的文件中对数字进行分组 - 所有系列 3xxx3.csv 中,所有系列 6xxx6.csv 中 - 然后您必须读取和检查更少的数据行。或者您可以按系列对数据进行排序并创建第二个文件,其中包含从系列3xxx6xxx 开始的信息(如数据库中的索引),然后从文件中仅读取部分数据。或者您应该将其写入数据库并为此使用数据库。
  • CSV 文件为 1.5 Gb。我有 2 个处理器

标签: python csv


【解决方案1】:

考察了三个解决方案

  1. 原帖的CSV逐行方法
  2. 使用原始文本而不是使用 CSV 阅读器进行分区 CSV 字段
  3. 使用 Pandas 读取和处理块中的数据

结果 使用 1000 万到 3000 万行执行测试。

总结 使用 Pandas 是最慢的方法。当考虑到this article 的观察时,这并不奇怪(即,由于开销,Pandas 是读取 CSV 文件的较慢方法之一)。 最快的是将文件作为原始文本文件处理并在原始文本中查找数字(比最初发布的使用 CSV 阅读器的方法快约 2 倍)。 Pandas 比原来的方法慢了约 30%。

测试代码

import timeit
import time
import random
import numpy as np
import pandas as pd
import csv
import matplotlib.pyplot as plt
import math
import itertools

def wrapper(func, *args, **kwargs):
    " Use to produced 0 argument function for call it"
    # Reference https://www.pythoncentral.io/time-a-python-function/
    def wrapped():
        return func(*args, **kwargs)
    return wrapped

def method_line_by_line(filename, series: str, number: str) -> dict:
    """
    Find passport number and series
    :param filename:csv filename path
    :param series: passport series
    :param number: passport number
    :return:
    """
    find = False
    with open(filename, 'r', encoding='utf_8_sig') as csvfile:
        reader = csv.reader(csvfile, delimiter=',')
        try:
            for row in reader:
                if row[0] == series and row[1] == num:
                    find = True
                    break
        except Exception as e:
            pass

    if find:
        return {'result': True, 'message': 'Passport found'}
    else:
        return {'result': False, 'message': 'Passport not found in Database'}

def method_raw_text(filename, series: str, number: str) -> dict:
    """
    Find passport number and series by interating through text records
    :param filename:csv filename path
    :param series: passport series
    :param number: passport number
    :return:
    """
    pattern = series + "," + number
    with open(filename, 'r', encoding='utf_8_sig') as csvfile:
        if any(map(lambda x: pattern == x.rstrip(), csvfile)): # iterates through text looking for match
            return {'result': True, 'message': 'Passport found'}
        else:
            return {'result': False, 'message': 'Passport not found in Database'}

def method_pandas_chunks(filename, series: str, number: str) -> dict:
    """
    Find passport number and series using Pandas in chunks
    :param filename:csv filename path
    :param series: passport series
    :param number: passport number
    :return:
    """
    chunksize = 10 ** 5
    for df in pd.read_csv(filename, chunksize=chunksize, dtype={'PASSP_SERIES': str,'PASSP_NUMBER':str}):

      df_search = df[(df['PASSP_SERIES'] == series) & (df['PASSP_NUMBER'] == number)]

      if not df_search.empty:
        break

    if not df_search.empty:
        return {'result': True, 'message': 'Passport found'}
    else:
        return {'result': False, 'message': 'Passport not found in Database'}

def generate_data(filename, number_records):
    " Generates random data for tests"
    df = pd.DataFrame(np.random.randint(0, 1e6,size=(number_records, 2)), columns=['PASSP_SERIES', 'PASSP_NUMBER'])
    df.to_csv(filename, index = None, header=True)
    return df

def profile():
    Nls = [x for x in range(10000000, 30000001, 5000000)] # range of number of test rows
    number_iterations = 3 # repeats per test
    methods = [method_line_by_line, method_raw_text, method_pandas_chunks]
    time_methods = [[] for _ in range(len(methods))]

    for N in Nls:
        # Generate CSV File with N rows
        generate_data('test.csv', N)

        for i, func in enumerate(methods):
            wrapped = wrapper(func, 'test.csv', 'x', 'y') # Use x & y to ensure we process entire
                                                          # file without finding a match
            time_methods[i].append(math.log(timeit.timeit(wrapped, number=number_iterations)))

    markers = itertools.cycle(('o', '+', '.'))
    colors = itertools.cycle(('r', 'b', 'g'))
    labels = itertools.cycle(('line-by-line', 'raw-text', 'pandas'))
    print(time_methods)
    for i in range(len(time_methods)):
        plt.plot(Nls,time_methods[i],marker = next(markers),color=next(colors),linestyle='-',label=next(labels))

    plt.xlabel('list size', fontsize=18)
    plt.ylabel('log(time)', fontsize=18)
    plt.legend(loc = 'upper left')
    plt.show()

# Run Test
profile()

【讨论】:

    【解决方案2】:

    CSV 文件格式是一种方便简单的文件格式。

    它不是用于分析/快速搜索,这从来都不是目标。 对于必须处理所有条目或条目数量不是很大的不同应用程序和任务之间的交换非常有用。

    如果您想加快速度,您应该读取 CSV 文件一次并将其转换为数据库,例如sqlite 然后在数据库中执行所有搜索。 如果密码编号是唯一的,那么您甚至可以只使用简单的 dbm 文件或 python 搁置。

    可以通过向您搜索的字段添加索引来优化数据库性能。

    这完全取决于 CSV 文件的更改频率以及您执行搜索的频率,但这种方法通常会产生更好的结果。

    我从未真正使用过 pandas,但也许它在搜索/过滤方面的性能更高,尽管它永远不会胜过在真实数据库中的搜索。

    如果您想走 sqlite 或 dbm 之路,我可以提供一些代码。

    附录(在使用 csv 阅读器阅读之前使用对分搜索在已排序的 csv 文件中搜索):

    如果 csv 文件中的第一个字段是序列号,那么还有另一种方法。 (或者如果您愿意转换 csv 文件,使其可以使用 gnu sort 进行排序)

    只需对您的文件进行排序(在 linux 系统上使用 gnu 排序很容易。它可以对大文件进行排序而不会“爆炸”内存),排序时间不应该比您所拥有的搜索时间高很多瞬间。

    然后在文件中使用 bisect / seek 搜索具有正确序列号的第一行。然后使用您现有的功能并稍作修改。

    这将在几毫秒内为您提供结果。 我尝试使用一个随机创建的 csv 文件,其中包含 3000 万个条目,大小约为 1.5G。

    如果在 linux 系统上运行,您甚至可以更改代码,以便在 csv 文件更改时创建您下载的 csv 文件的排序副本。 (在我的机器上排序大约需要 1 到 2 分钟)所以每周搜索 2 到 3 次后,这将是值得的。

    import csv
    import datetime
    import os
    
    def get_line_at_pos(fin, pos):
        """ fetches first complete line at offset pos
            always skips header line
        """
        fin.seek(pos)
        skip = fin.readline()
        # next line for debugging only
        # print("Skip@%d: %r" % (pos, skip))
        npos = fin.tell()
        assert pos + len(skip) == npos
        line = fin.readline()
        return npos, line
    
    
    def bisect_seek(fname, field_func, field_val):
        """ returns a file postion, which guarantees, that you will
            encounter all lines, that migth encounter field_val
            if the file is ordered by field_val.
            field_func is the function to extract field_val from a line
            The search is a bisect search, with a complexity of log(n)
        """
        size = os.path.getsize(fname)
        minpos, maxpos, cur = 0, size, int(size / 2)
    
        with open(fname) as fin:
            small_pos = 0
            # next line just for debugging
            state = "?"
            prev_pos = -1
            while True:  # find first id smaller than the one we search
                # next line just for debugging
                pos_str = "%s %10d %10d %10d" % (state, minpos, cur, maxpos)
                realpos, line = get_line_at_pos(fin, cur)
                val = field_func(line)
                # next line just for debugging
                pos_str += "# got @%d: %r %r" % (realpos, val, line)
                if val >= field_val:
                    state = ">"
                    maxpos = cur
                    cur = int((minpos + cur) / 2)
                else:
                    state = "<"
                    minpos = cur
                    cur = int((cur + maxpos) / 2)
                # next line just for debugging
                # print(pos_str)
                if prev_pos == cur:
                    break
                prev_pos = cur
        return realpos
    
    
    def getser(line):
        return line.split(",")[0]
    
    
    def check_passport(filename, series: str, number: str) -> dict:
        """
        Find passport number and series
        :param filename:csv filename path
        :param series: passport series
        :param number: passport number
        :return:
        """
        print(f'series={series}, number={number}')
        found = False
        start = datetime.datetime.now()
        # find position from which we should start searching
        pos = bisect_seek(filename, getser, series)
        with open(filename, 'r', encoding='utf_8_sig') as csvfile:
            csvfile.seek(pos)
            reader = csv.reader(csvfile, delimiter=',')
            try:
                for row in reader:
                    if row[0] == series and row[1] == number:
                        found = True
                        break
                    elif row[0] > series:
                        # as file is sorted we know we can abort now
                        break
            except Exception as e:
                print(e)
        print(datetime.datetime.now() - start)
        if found:
            print("good row", row)
            return {'result': True, 'message': f'Passport found'}
        else:
            print("bad row", row)
            return {'result': False, 'message': f'Passport not found in Database'}
    

    附录 2019-11-30: 这里有一个脚本可以将你的大文件分割成更小的块并对每个块进行排序。 (我不想实现完整的合并排序,因为在这种情况下,在每个块中搜索已经足够有效。如果对 mor 感兴趣,我建议尝试实现合并排序或发布有关在 windows 下对大文件进行排序的问题用python)

    split_n_sort_csv.py:

    import itertools
    import sys
    import time
    
    def main():
        args = sys.argv[1:]
        t = t0 = time.time()
        with open(args[0]) as fin:
            headline = next(fin)
            for idx in itertools.count():
                print(idx, "r")
                tprev = t
                lines = list(itertools.islice(fin, 10000000))
                t = time.time()
                t_read = t - tprev
                tprev = t
                print("s")
                lines.sort()
                t = time.time()
                t_sort = t - tprev
                tprev = t
                print("w")
                with open("bla_%03d.csv" % idx, "w") as fout:
                    fout.write(headline)
                    for line in lines:
                        fout.write(line)
                t = time.time()
                t_write = t - tprev
                tprev = t
    
                print("%4.1f %4.1f %4.1f" % (t_read, t_sort, t_write))
                if not lines:
                    break
        t = time.time()
        print("Total of %5.1fs" % (t-t0))
    
    if __name__ == "__main__":
        main()
    

    这里是一个修改版本,它在所有块文件中搜索。

    import csv
    import datetime
    import itertools
    import os
    
    ENCODING='utf_8_sig'
    
    def get_line_at_pos(fin, pos, enc_encoding="utf_8"):
        """ fetches first complete line at offset pos
            always skips header line
        """
        while True:
            fin.seek(pos)
            try:
                skip = fin.readline()
                break
            except UnicodeDecodeError:
                pos += 1
    
        # print("Skip@%d: %r" % (pos, skip))
        npos = fin.tell()
        assert pos + len(skip.encode(enc_encoding)) == npos
        line = fin.readline()
        return npos, line
    
    def bisect_seek(fname, field_func, field_val, encoding=ENCODING):
        size = os.path.getsize(fname)
        vmin, vmax, cur = 0, size, int(size / 2)
        if encoding.endswith("_sig"):
            enc_encoding = encoding[:-4]
        else:
            enc_encoding = encoding
        with open(fname, encoding=encoding) as fin:
            small_pos = 0
            state = "?"
            prev_pos = -1
            while True:  # find first id smaller than the one we search
                # next line only for debugging
                pos_str = "%s %10d %10d %10d" % (state, vmin, cur, vmax)
                realpos, line = get_line_at_pos(fin, cur, enc_encoding=enc_encoding)
                val = field_func(line)
                # next line only for debugging
                pos_str += "# got @%d: %r %r" % (realpos, val, line)
                if val >= field_val:
                    state = ">"
                    vmax = cur
                    cur = int((vmin + cur) / 2)
                else:
                    state = "<"
                    vmin = cur
                    cur = int((cur + vmax) / 2)
                # next line only for debugging
                # print(pos_str)
                if prev_pos == cur:
                    break
                prev_pos = cur
        return realpos
    
    def getser(line):
        return line.split(",")[0]
    
    def check_passport(filename, series: str, number: str,
            encoding=ENCODING) -> dict:
        """
        Find passport number and series
        :param filename:csv filename path
        :param series: passport series
        :param number: passport number
        :return:
        """
        print(f'series={series}, number={number}')
        found = False
        start = datetime.datetime.now()
        for ctr in itertools.count():
            fname = filename % ctr
            if not os.path.exists(fname):
                break
            print(fname)
            pos = bisect_seek(fname, getser, series)
            with open(fname, 'r', encoding=encoding) as csvfile:
                csvfile.seek(pos)
                reader = csv.reader(csvfile, delimiter=',')
                try:
                    for row in reader:
                        if row[0] == series and row[1] == number:
                            found = True
                            break
                        elif row[0] > series:
                            break
                except Exception as e:
                    print(e)
            if found:
                break
        print(datetime.datetime.now() - start)
        if found:
            print("good row in %s: %d", (fname, row))
            return {'result': True, 'message': f'Passport found'}
        else:
            print("bad row", row)
            return {'result': False, 'message': f'Passport not found in Database'}
    

    要测试,调用:

    check_passport("bla_%03d.csv", series, number)
    

    【讨论】:

    • 感谢您的回答。文件 csv 将每周更改。这个 csv 文件不是我的。我从另一个网站上拿的。
    • 您每周执行搜索的频率如何?如果这将是数十或数百次,那么您可以每周下载一次 csv,将其转换为另一种格式,然后进行更快的搜索,或者即时创建帮助文件。将轻松加速搜索 100x 1000x 或在第一次搜索期间创建助手。如果有兴趣,请告诉我,我会看看我是否可以展示基本想法。这可能会使后续搜索加速数倍(可能不到一秒)。文件大概有多少行,大概行长是多少?
    • 你的代码是在 windows 还是 linux (posix) 机器上运行的?
    • 我发布了一个解决方案,假设您在机器上运行,它可以对 csv 文件进行排序,并假设 PASSP_SERIES 是 csv 文件中的第一个字段。您的搜索时间应该远低于一秒。
    • 我每周搜索不超过 100 次。
    猜你喜欢
    • 2021-09-07
    • 2018-11-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-08-13
    • 1970-01-01
    • 2017-10-26
    相关资源
    最近更新 更多