Удалить первые N строк файла

Мне было интересно, как я могу удалить первые N строк определенного файла эффективно.

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

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

Создание API ввода вопросов на разных языках программирования (Python, PHP, Go и Node.js)
Создание API ввода вопросов на разных языках программирования (Python, PHP, Go и Node.js)
API ввода вопросов - это полезный инструмент для интеграции моделей машинного обучения, таких как ChatGPT, в приложения, требующие обработки...
4
0
2 823
4

Ответы 4

Файловые системы не поддерживают удаление из произвольных мест файла, поэтому вам, по крайней мере, нужно перезаписать весь файл самостоятельно. Обычный способ реализовать то, что вам нужно, - это

  • Откройте временный файл,
  • читать небольшие фрагменты из исходного файла, например по одной строке за раз в вашем случае
  • Запишите во временный файл только те строки, которые вы хотите сохранить в исходном файле.
  • переименуйте временный файл в исходное имя, когда закончите.

Боюсь, что это единственное эффективное решение, я подумал, скорее всего, сразу после публикации вопроса.

Dygnus 09.09.2018 21:21

Вы пытаетесь сделать что-то вроде усечения и поворота журнала?

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

Например, у вас может быть 2018-09-09.log.txt или log.1.txt, и вы каждый час переходите к новому файлу журнала. Таким образом можно удалить старые файлы, чтобы освободить место.

Помимо этого, я не думаю, что есть особый способ удалить первые строки файла в Go без перезаписи оставшейся части файла.

Самый эффективный способ сделать это - прочитать файл в обратном направлении.

/*
    This package implements funcationality similar to the UNIX command tail.
    It prints out the last "x" many lines of a file, usually 10.
    The number of lines in a file is determined by counting newline (\n) characters.
    This can be use either from CLI or imported into other projects.
*/

package main

import (
    "errors"
    "flag"
    "fmt"
    "io"
    "os"
)

var (
    //DEFINE FLAG DEFAULTS
    filename = ""
    numLines = 10

    //ErrNoFilename is thrown when the path the the file to tail was not given
    ErrNoFilename = errors.New("You must provide the path to a file in the \"-file\" flag.")

    //ErrInvalidLineCount is thrown when the user provided 0 (zero) as the value for number of lines to tail
    ErrInvalidLineCount = errors.New("You cannot tail zero lines.")
)

//GET FLAGS FROM CLI
func init() {
    flag.StringVar(&filename, "file", "", "Filename to get last lines of text from.")
    flag.IntVar(&numLines, "n", numLines, "Number of lines to get from end of file.")
    flag.Parse()
}

//MAIN FUNCTIONALITY OF APP
//make sure filename (path to file) was given
//run it through the tailing function
//print the output to stdout
func main() {
    //TAIL
    text, err := GoTail(filename, numLines)
    if err != nil {
        fmt.Println(err)
        return
    }

    //DONE
    fmt.Print(text)
    return
}

//GoTail IS THE FUNCTION THAT ACTUALLY DOES THE "TAILING"
//this can be used this package is imported into other golang projects
func GoTail(filename string, numLines int) (string, error) {
    //MAKE SURE FILENAME IS GIVEN
    //actually, a path to the file
    if len(filename) == 0 {
        return "", ErrNoFilename
    }

    //MAKE SURE USER WANTS TO GET AT LEAST ONE LINE
    if numLines == 0 {
        return "", ErrInvalidLineCount
    }

    //OPEN FILE
    file, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    defer file.Close()

    //SEEK BACKWARD CHARACTER BY CHARACTER ADDING UP NEW LINES
    //offset must start at "-1" otherwise we are already at the EOF
    //"-1" from numLines since we ignore "last" newline in a file
    numNewLines := 0
    var offset int64 = -1
    var finalReadStartPos int64
    for numNewLines <= numLines-1 {
        //seek to new position in file
        startPos, err := file.Seek(offset, 2)
        if err != nil {
            return "", err
        }

        //make sure start position can never be less than 0
        //aka, you cannot read from before the file starts
        if startPos == 0 {
            //set to -1 since we +1 to this below
            //the position will then start from the first character
            finalReadStartPos = -1
            break
        }

        //read the character at this position
        b := make([]byte, 1)
        _, err = file.ReadAt(b, startPos)
        if err != nil {
            return "", err
        }

        //ignore if first character being read is a newline
        if offset == int64(-1) && string(b) == "\n" {
            offset--
            continue
        }

        //if the character is a newline
        //add this to the number of lines read
        //and remember position in case we have reached our target number of lines
        if string(b) == "\n" {
            numNewLines++
            finalReadStartPos = startPos
        }

        //decrease offset for reading next character
        //remember, we are reading backward!
        offset--
    }

    //READ TO END OF FILE
    //add "1" here to move offset from the newline position to first character in line of text
    //this position should be the first character in the "first" line of data we want
    endPos, err := file.Seek(int64(-1), 2)
    if err != nil {
        return "", err
    }
    b := make([]byte, (endPos+1)-finalReadStartPos)
    _, err = file.ReadAt(b, finalReadStartPos+1)
    if err == io.EOF {
        return string(b), nil
    } else if err != nil {
        return "", err
    }

    //special case
    //if text is read, then err == io.EOF should hit
    //there should *never* not be an error above
    //so this line should never return
    return "**No error but no text read.**", nil
}

В консоли необходимо ввести: gotail -file <filename> -n=<number of lines> Таким образом, вы можете прочитать последнее количество строк с конца файла. Если вам нужны все строки, кроме первых 10, я изменю этот код.

Интересно, не пропустите ли вы изменения, когда лог-файл сильно записан, при перезаписи файла без первых N строк. За время, необходимое этому сценарию для обработки входного файла, сам файл мог быть изменен. Это может привести к потере некоторых данных. Поскольку вы возвращаете содержимое хвостовых данных с помощью GoTail(), я могу представить (в зависимости от проверяемого файла), это может сильно потреблять память.

Dygnus 11.09.2018 10:13

@Dygnus Я обновил код, решив проблему, на которую вы мне указали. Спасибо!

Dippo 11.09.2018 23:25

Вы можете найти общее количество строк в файле с помощью wc -l <fileName>, пусть это будет x, и предположим, что вы хотите прочитать все строки, кроме первой 1000, затем используйте tail -n<x-1000> <fileName>.

Эффективным способом было бы использовать sed. См. Ответ это

Другие вопросы по теме