Эффективный способ прочитать конкретный номер строки файла. (БОНУС: Ошибка в руководстве по Python) - PullRequest
6 голосов
/ 28 августа 2010

У меня есть текстовый файл объемом 100 ГБ, который представляет собой дамп BCP из базы данных. Когда я пытаюсь импортировать его с BULK INSERT, я получаю загадочную ошибку в строке 219506324. Прежде чем решить эту проблему, я хотел бы увидеть эту строку, но, увы, мой любимый метод

import linecache
print linecache.getline(filename, linenumber)

выбрасывает MemoryError. Интересно, что в руководстве написано , что "Эта функция никогда не вызовет исключение." В этом большом файле он выдает единицу, когда я пытаюсь прочитать строку № 1, и у меня есть около 6 ГБ свободной оперативной памяти. ...

Я бы хотел знать, какой самый элегантный метод подходит для того, чтобы добраться до этой недоступной линии. Доступными инструментами являются Python 2, Python 3 и C # 4 (Visual Studio 2010). Да, я понимаю, что всегда могу сделать что-то вроде

var line = 0;
using (var stream = new StreamReader(File.OpenRead(@"s:\source\transactions.dat")))
{
     while (++line < 219506324) stream.ReadLine(); //waste some cycles
     Console.WriteLine(stream.ReadLine());
}

Что бы сработало, но я сомневаюсь, что это самый элегантный способ.

РЕДАКТИРОВАТЬ: Я жду, чтобы закрыть эту тему, потому что жесткий диск, содержащий файл, сейчас используется другим процессом. Я собираюсь проверить оба предложенных метода и сообщить о времени. Спасибо всем за ваши предложения и комментарии.

Результаты приведены в Я реализовал методы Gabes и Alexes, чтобы увидеть, какой из них был быстрее. Если я делаю что-то не так, скажи. Я использую 10-миллионную строку в моем 100-гигабайтном файле, используя метод, предложенный Гейбом, а затем метод, предложенный Алекс, который я свободно перевел на C # ... Единственное, что я добавляю от себя, - это сначала чтение в 300 Файл MB в память только для очистки кеша жесткого диска.

const string file = @"x:\....dat"; // 100 GB file
const string otherFile = @"x:\....dat"; // 300 MB file
const int linenumber = 10000000;

ClearHDDCache(otherFile);
GabeMethod(file, linenumber);  //Gabe's method

ClearHDDCache(otherFile);
AlexMethod(file, linenumber);  //Alex's method

// Results
// Gabe's method: 8290 (ms)
// Alex's method: 13455 (ms)

Реализация метода Гейба выглядит следующим образом:

var gabe = new Stopwatch();
gabe.Start();
var data = File.ReadLines(file).ElementAt(linenumber - 1);
gabe.Stop();
Console.WriteLine("Gabe's method: {0} (ms)",  gabe.ElapsedMilliseconds);

Пока метод Алекса слегка обманчив:

var alex = new Stopwatch();
alex.Start();
const int buffersize = 100 * 1024; //bytes
var buffer = new byte[buffersize];
var counter = 0;
using (var filestream = File.OpenRead(file))
{
    while (true) // Cutting corners here...
    {
        filestream.Read(buffer, 0, buffersize);
        //At this point we could probably launch an async read into the next chunk...
        var linesread = buffer.Count(b => b == 10); //10 is ASCII linebreak.
        if (counter + linesread >= linenumber) break;
        counter += linesread;
    }
}
//The downside of this method is that we have to assume that the line fit into the buffer, or do something clever...er
var data = new ASCIIEncoding().GetString(buffer).Split('\n').ElementAt(linenumber - counter - 1);
alex.Stop();
Console.WriteLine("Alex's method: {0} (ms)", alex.ElapsedMilliseconds);

Так что, если Алекс не захочет комментировать, я отмечу решение Гейба как принятое.

Ответы [ 5 ]

8 голосов
/ 28 августа 2010

Вот моя элегантная версия на C #:

Console.Write(File.ReadLines(@"s:\source\transactions.dat").ElementAt(219506323));

или более общая:

Console.Write(File.ReadLines(filename).ElementAt(linenumber - 1));

Конечно, вы можете показать некоторый контекст до и после данной строки:

Console.Write(string.Join("\n",
              File.ReadLines(filename).Skip(linenumber - 5).Take(10)));

или более бегло:

File
.ReadLines(filename)
.Skip(linenumber - 5)
.Take(10)
.AsObservable()
.Do(Console.WriteLine);

Кстати, модуль linecache не делает ничего умного с большими файлами.Он просто читает все это, сохраняя все в памяти.Единственное исключение, которое он отлавливает, связано с вводом / выводом (не может получить доступ к файлу, файл не найден и т. Д.).Вот важная часть кода:

    fp = open(fullname, 'rU')
    lines = fp.readlines()
    fp.close()

Другими словами, он пытается поместить весь файл размером 100 ГБ в 6 ГБ ОЗУ!Руководство должно сказать следующее: «Эта функция никогда не выдаст исключение , если не может получить доступ к файлу

6 голосов
/ 28 августа 2010

Ну, память может исчерпать себя в любое время, асинхронно и непредсказуемо - вот почему обещание «никогда не исключение» на самом деле не применимо там (как, скажем, в Java, где каждыйМетод должен указывать, какие исключения он может вызывать, некоторые исключения исключаются из этого правила, поскольку практически любой метод может непредсказуемо вызвать их в любое время из-за нехватки ресурсов или других системных проблем).

linecache пытается прочитать весь файл.Ваша единственная простая альтернатива (надеюсь, вы не спешите) - читать по одной строке за раз с самого начала ...:

def readoneline(filepath, linenum):
    if linenum < 0: return ''
    with open(filepath) as f:
        for i, line in enumerate(filepath):
            if i == linenum: return line
        return ''

Здесь linenum равно 0 (если выэто не нравится, и ваш Python равен 2.6 или лучше, передайте начальное значение от 1 до enumerate), а возвращаемое значение - пустая строка для недопустимых номеров строк.

Несколько быстрее (и лот более сложный) - это, скажем, 100 МБ за раз (в двоичном режиме) в буфер;подсчитать количество концов строк в буфере (просто вызов .count('\n') для строкового объекта буфера);как только итоговое итоговое значение конца строки превысит искомое количество, найдите N-ный конец строки, находящийся в данный момент в буфере (где N - это разница между linenum, здесь на основе 1 и предыдущим промежуточным итогомконец строки), прочитайте немного больше, если N+1 конец строки также не находится в буфере (поскольку это точка, где заканчивается ваша строка), извлеките соответствующую подстроку.Не просто пара строк за with и возвращается для аномальных случаев ...; -).

Редактировать : так как OP комментирует сомнение в том, что чтение буфером вместопострочно может иметь значение для производительности, я удалил старый фрагмент кода, где я измерял два подхода для несколько связанных задач - подсчет количества строк с буферным подходом, цикл по строкам или чтение всегофайл в памяти одним глотком (readlines).Целевой файл - kjv.txt, стандартный английский текст Библии короля Иакова, одна строка в стихе, ASCII:

$ wc kjv.txt 
  114150  821108 4834378 kjv.txt

Платформа представляет собой ноутбук MacOS Pro, OSX 10.5.8,Intel Core 2 Duo на частоте 2,4 ГГц, Python 2.6.5.

Модуль для теста, readkjv.py:

def byline(fn='kjv.txt'):
    with open(fn) as f:
        for i, _ in enumerate(f):
            pass
    return i +1

def byall(fn='kjv.txt'):
    with open(fn) as f:
        return len(f.readlines())

def bybuf(fn='kjv.txt', BS=100*1024):
    with open(fn, 'rb') as f:
        tot = 0
        while True:
            blk = f.read(BS)
            if not blk: return tot
            tot += blk.count('\n')

if __name__ == '__main__':
    print bybuf()
    print byline()
    print byall()

print s просто для подтверждения правильности курса.1040 *

Числа вполне повторяемы.Как вы видите, даже на таком крошечном файле (менее 5 МБ!) Построчные подходы медленнее, чем на основе буфера - просто слишком много напрасных усилий!

Чтобы проверить масштабируемость, я затем использовалфайл в 4 раза больше, как показано ниже:

$ cat kjv.txt kjv.txt kjv.txt kjv.txt >k4.txt
$ wc k4.txt
  456600 3284432 19337512 k4.txt
$ py26 -mtimeit -s'import readkjv' 'readkjv.bybuf()'
10 loops, best of 3: 25.4 msec per loop
$ py26 -mtimeit -s'import readkjv' 'readkjv.bybuf("k4.txt")'
10 loops, best of 3: 102 msec per loop

и, как и предполагалось, подход с использованием буфера масштабируется почти линейно.Экстраполяция (всегда рискованное занятие, конечно ;-), чуть менее 200 МБ в секунду кажется предсказуемой производительностью - назовите ее 6 секунд на ГБ, или, может быть, 10 минут на 100 ГБ.

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

Elegant?Не совсем ... но для скорости довольно трудно победить! -)

1 голос
/ 05 апреля 2011

Вы можете попробовать этот sed-one-liner: sed '42q;d', чтобы получить строку с номером 42. Это не в Python или C #, но я предполагаю, что вы использовали sed на вашем Mac.

0 голосов
/ 28 августа 2010

Если вы ожидаете, что эта операция часто требуется для одного и того же файла, имеет смысл создать индекс.

Вы создаете индекс, просматривая весь файл один раз и записывая позиции начала строки,например в базе данных sqlite.Затем, когда вам нужно перейти к определенной строке, вы запрашиваете индекс для нее, ищите эту позицию и читаете строку.

0 голосов
/ 28 августа 2010

Не элегантным, но более быстрым решением было бы использование нескольких потоков (или задач в .NET 4.0) для одновременного чтения и обработки нескольких фрагментов файла.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...