Время прочтения: 6 мин.

Перед нами была поставлена задача по анализу выгруженной из базы данных информации. Сложность задачи оказалась в том, что получен очень большой многострочный текстовый файл (около 8 Гб, 14,4 млн. строк), который невозможно открыть с помощью общедоступных инструментов (например, Excel, у которого есть ограничение на количество записей в рабочем листе). В связи с этим возникла необходимость разбить его на несколько файлов так, чтобы они содержали заданное количество строк. После успешного решения данной задачи мы решили поделиться использованными методами.

В данной статье мы разберем три метода «нарезки» текстового файла на несколько с заданным количеством строк с помощью языка программирования Python. Далее заданное количество строк будем называть «count_lines».

Идея первого метода нарезки файла, назовем его «sequence_cut (последовательная нарезка)», состоит в том, что считывается строка из входящего файла и записывается в очередной файл. Такая последовательность действий продолжается до тех пор, пока количество записанных строк не превосходит count_lines. Далее делается тоже самое, но уже в новый файл для записи. И так далее. Завершается процесс, когда достигается конец входящего файла.

def sequence_cut(path_file_in, path_dir_out, count_lines=500000):
    '''Функция sequence_cut разрезает файл с расширением .txt на файлы с расширением .txt c заданным количеством строк.
            Параметры: path_file_in : str
                    Абсолютный или относительный путь до файла с расширением .txt, который нужно разрезать.
                  path_dir_out : str
                    Абсолютный или относительный путь до папки, в которую будут помещаться нарезанные файлы.
                  count_lines :  int, default 500000
                    Количество строк, на которые разрезается исходный файл.
       Возвращаемое значение: None   
    '''
    
    def delete_line_feed_end(line):        
        if line[-1:] == '\n':
            return line[:-1]
        return line
    
    path_dir_out += '\\' + path_file_in.split('\\')[-1][:-4]
    stop_iteration = count_lines - 2
    try:
        file_number = 1
        with open(path_file_in) as file_read:
            line = file_read.readline()            
            while line:
                new_file_name = path_dir_out + '_' + str(file_number) + '.txt'
                with open(new_file_name, 'w') as file_write:
                    file_write.write(delete_line_feed_end(line))                                       
                    line = file_read.readline()
                    i = 0                
                    while line and (i < stop_iteration):
                        file_write.write('\n')
                        file_write.write(line[:-1])
                        line = file_read.readline()
                        i += 1
                    if line:                        
                        file_write.write('\n')
                        file_write.write(delete_line_feed_end(line))             
                file_number += 1
                line = file_read.readline()
    except Exception as err:
        print('Ошибка: ', err)

Идея второго метода, назовем его «batch_cut (пакетная нарезка)», состоит в том, что считывается строка из входящего файла и сохраняется эта информация в список. Продолжаются такие операции до тех пор, пока длина списка не превосходит count_lines. Когда длина списка-накопителя будет равна count_lines, необходимо сделать из списка одну большую строку и записать ее в очередной файл. Далее делается тоже самое, но уже в новый файл для записи, не забывая очистить список. И так далее. Завершается процесс, когда достигается конец входящего файла.

def batch_cut(path_file_in, path_dir_out, count_lines=500000):
    '''Функция batch_cut разрезает файл с расширением .txt на файлы с расширением .txt c заданным количеством строк.
       
      Параметры: path_file_in : str
                 Абсолютный или относительный путь до файла с расширением .txt, который нужно разрезать.
                 path_dir_out : str
                    Абсолютный или относительный путь до папки, в которую будут помещаться нарезанные файлы.
                  count_lines :  int, default 500000
                    Количество строк, на которые разрезается исходный файл.
       Возвращаемое значение: None   
    '''
    
    def delete_line_feed_end(line):        
        if line[-1:] == '\n':
            return line[:-1]
        return line        
    
    path_dir_out += '\\' + path_file_in.split('\\')[-1][:-4]
    stop_iteration = count_lines - 2 
    try:
        file_number = 1  
        with open(path_file_in) as file_read:
            line = file_read.readline()                      
            while line:                
                i = 0
                batch = [delete_line_feed_end(line)]
                line = file_read.readline()
                while line and (i < stop_iteration):                    
                    batch.append(line[:-1])
                    line = file_read.readline()
                    i += 1
                if line:
                    batch.append(delete_line_feed_end(line))
                new_file_name = path_dir_out + '_' + str(file_number) + '.txt'
                with open(new_file_name, 'w') as file_write:
                    file_write.write('\n'.join(batch))                
                file_number += 1
                line = file_read.readline()
    except Exception as err:
        print('Ошибка: ', err)

Идея третьего метода, назовем его «pandas_cut», базируется на использовании модуля pandas, который написан на языке Python и применяется для обработки и анализа данных. С помощью функции read_csv и параметров skiprows (пропустить заданное количество строк файла), nrows (взять заданное количество строк файла), sep=’/n’ (разделитель), encoding=’cp1251′ (кодировка файла), будем считывать очередные count_lines строк файла path_file_in и записывать полученный результат в новый файл с помощью метода to_csv.

def pandas_cut(path_file_in, path_dir_out, count_lines=500000):
'''Функция pandas_cut разрезает файл с расширением .txt на файлы с расширением .txt c заданным количеством строк.
       
       Параметры: path_file_in : str
                    Абсолютный или относительный путь до файла с расширением .txt, который нужно разрезать.
                  path_dir_out : str
                    Абсолютный или относительный путь до папки, в которую будут помещаться нарезанные файлы.
                  count_lines :  int, default 500000
                    Количество строк, на которые разрезается исходный файл.
       Возвращаемое значение: None   
    '''
    
    import pandas as pd    
    df = pd.DataFrame([0])
    path_out = path_dir_out + '\\' + path_file_in.split('\\')[-1][:-4]
    file_number = 1
    skiprows = 0
    try:
        while True:
            df = pd.read_csv(path_file_in, header=None, skiprows=skiprows, nrows=count_lines, sep='\n', encoding='cp1251'),        
            new_file_name = path_out + '_' + str(file_number) + '.txt'
            df.to_csv(new_file_name, header=None, index=False)
            skiprows += count_lines            
            file_number += 1            
    except:
        pass

Протестируем функции sequence_cut, batch_cut на время выполнения, соблюдая некоторые условия:

  1. Читаемый файл и директория для файлов, полученных после нарезки, хранятся на рабочей машине пользователя, то есть не будет траты времени на передачу данных на удаленный сервер.
  2. Процессор на рабочей машине AMD Ryzen 3 (3.60 GHz); накопитель SSD; оперативная память 8 Гб; операционная система Windows 1064x.
  3. Читаемый файл будет большого объема: интересно сравнить время выполнения функций на файлах с большим количеством строк, так как для файла с маленьким количеством строк разница по времени не ощутима.
  4. Входящий файл будет нарезан по 500 000 строк.
  5. В функции batch_cut длина списка batch будет равна count_lines.
  6. Функции будут протестированы на файле, полученном с помощью функции generate_data (реализация ниже), и «боевом файле».
def generate_data(path_file, count_lines):
    '''Функция generate_data создает файл path_file c заданным количеством строк count_lines.
    
       Параметры: path_file_in : str
                    Абсолютный или относительный путь до файла с расширением .txt для генерации.
                  count_lines :  int, default 500000
                    Количество строк, которое будет содержаться в сгенерированном файле.
       Возвращаемое значение: None     
    '''
    
    from random import randint
    dict_rand = {0: '', 1: '****', 2: str(randint(1,9999)).zfill(4), 3: str(randint(100000, 999999999)).zfill(9), 4: 'Привет', 5: 'Пока'}

    with open(path_file, 'w') as file:
      for i in range(count_lines - 1):
        string = '~'.join([dict_rand[randint(0,4)] for _ in range(90)]) + '\n'
        file.write(string)
      file.write('~'.join([dict_rand[randint(0,4)] for _ in range(90)]))

Сделаем два тестовых файла на 10 000 000 и 20 000 000 строк, приближенные по качеству содержания к боевому файлу.

generate_data('test10^7.txt', 10 ** 7)
generate_data('test10^7 prod 2.txt', 10 ** 7 * 2)

Засечем время выполнения функций sequence_cut, batch_cut, pandas_cut на файлах ‘battle_file.txt’, ‘test10^7.txt’, ‘test10^7 prod 2.txt’. Нарезанные файлы сохраним в соответствующие директории.

%%time 
sequence_cut('battle_file.txt', 'Folder battle_file sequence_cut', count_lines=500000)

Wall time: 4min

%%time
batch_cut('battle_file.txt', 'Folder battle_file batch_cut', count_lines=500000)

Wall time: 3min 50s

%%time
pandas_cut('battle_file.txt', 'Folder battle_file pandas_cut', count_lines=500000)

Wall time: 13min 14s

%%time 
sequence_cut('test10^7.txt', 'Folder test10^7 sequence_cut', count_lines=500000)

Wall time: 2min 40s

%%time
batch_cut('test10^7.txt', 'Folder test10^7 batch_cut', count_lines=500000)

Wall time: 2min 20s

%%time
pandas_cut('test10^7.txt', 'Folder test10^7 pandas_cut', count_lines=500000)

Wall time: 7min 30s

%%time 
sequence_cut('test10^7 prod 2.txt', 'Folder test10^7 prod 2 sequence_cut', count_lines=500000)

Wall time: 5min 22s

%%time
batch_cut('test10^7 prod 2.txt', 'Folder test10^7 prod 2 batch_cut', count_lines=500000)

Wall time: 4min 25s

%%time
pandas_cut('test10^7 prod 2.txt', 'Folder test10^7 prod 2 pandas_cut', count_lines=500000)

Wall time: 19min 39s

В ходе нашего тестирования получили следующие результаты:

Название файлаРазмер файлаКоличество строк в файлеВремя
sequence_cut
Время batch_cutВремя pandas_cut
battle_file.txt7 982 472 КБ14 400 0004min3min 50s13min 14s
test10^7.txt4 931 645 КБ10 000 0002min 40s2min 20s7min 30s
test10^7 prod 2.txt9 863 322 КБ20 000 0005min 22s4min 25s19min 39s

Видно, что метод пакетной нарезки (batch_cut) дает преимущество во времени и работает быстрее на файлах с очень большим объемом данных. Стоит учитывать, что пакетный способ нарезки использует список, который хранит большое количество данных в памяти, чего нет у последовательного способа нарезки (sequence_cut).

В любом случае выбор за вами!