Эффективный способ чтения определенного содержимого с конца текстового файла с помощью Golang

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

...
this is the very large 
part, contains lots lines.

#SPECIAL CHARS START#

...
this is the small part the the end, 
contain several lines, but 
we do not know how many lines
this part contains

Мое требование - получить текстовое содержимое небольшой части, которое после #SPECIAL CHARS START# и до конца, как я могу эффективно получить его с помощью Golang?

ОБНОВЛЕНО: мое текущее решение - получить строку за строкой с конца файла и запомнить курсор, если строка содержит специальные символы, разорвать цикл и получить курсор

func getBackwardLine(file *os.File, start int64) (string, int64) {
    line := ""
    cursor :=start
    stat, _ := file.Stat()
    filesize := stat.Size()

    for { 
        cursor--
        file.Seek(cursor, io.SeekEnd)

        char := make([]byte, 1)
        file.Read(char)

        if cursor != -1 && (char[0] == 10 || char[0] == 13) { 
            break
        }

        line = fmt.Sprintf("%s%s", string(char), line) 

        if cursor == -filesize { 
            break
        }
    }
    return line, cursor

}

func main() {
    file, err := os.Open("some.log")
    if err != nil {
        os.Exit(1)
    }
    defer file.Close()

    var cursor int64 = 0
    var line = ""

    for {  
        line, cursor = getBackwardLine(file, cursor)
        fmt.Println(line)
        if(strings.Contains(line, "#SPECIAL CHARS START#")) {
            break
        }
    }


    fmt.Println(cursor)  //now we get the cursor for the start of special characters
}

person donnior    schedule 16.10.2019    source источник
comment
Это может помочь: заголовок stackoverflow.com/questions/17863821/   -  person Pavlo    schedule 16.10.2019
comment
@Pavlo Спасибо, вопрос о ссылке для получения последних нескольких строк из файла, я думаю, моя проблема в том, как сначала найти строку со специальными символами, а затем я мог бы использовать ответы из ссылки.   -  person donnior    schedule 16.10.2019
comment
Вы пробовали что-то?   -  person Зелёный    schedule 16.10.2019
comment
Если вы вообще не представляете, сколько байтов следует за разделителем, у вас нет другого выбора, кроме как прочитать весь файл. Если вы это сделаете, перейдите к некоторому смещению от конца и начните чтение оттуда. Или угадайте смещение (например, 70% от размера файла), и если вы не найдете разделитель после предполагаемого смещения, прибегните к поиску с самого начала.   -  person Peter    schedule 16.10.2019
comment
@Зелёный Да, мой текущий способ ищет \n с конца и назад, пока я не получу строку для своих специальных символов.   -  person donnior    schedule 16.10.2019


Ответы (2)


Это решение реализует обратное чтение.

Он читает файл, начиная с конца, блоком из b.Len байт, затем ищет вперед разделитель, в настоящее время \n внутри блока, затем сдвигает начальное смещение на SepIndex (это делается для предотвращения разделения строки поиска на два последовательных читать). Прежде чем перейти к чтению следующего блока, он ищет строку search в прочитанном блоке, если находит, возвращает свою начальную позицию в файле и останавливается. В противном случае он уменьшает начальное смещение на b.Len, а затем читает следующий блок.

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

Если ваша строка поиска находится в пределах последних 10%, я уверен, что вы выиграете.

main.go

package main

import (
    "bytes"
    "flag"
    "fmt"
    "io"
    "log"
    "os"
    "time"

    "github.com/mattetti/filebuffer"
)

func main() {

    var search string
    var sep string
    var verbose bool
    flag.StringVar(&search, "search", "findme", "search word")
    flag.StringVar(&sep, "sep", "\n", "separator for the search detection")
    flag.BoolVar(&verbose, "v", false, "verbosity")
    flag.Parse()

    d := make(chan struct{})
    b := &bytes.Buffer{}
    go func() {
        io.Copy(b, os.Stdin)
        d <- struct{}{}
    }()
    <-time.After(time.Millisecond)
    select {
    case <-d:
    default:
        os.Stdin.Close()
    }

    readSize := 1024
    if b.Len() < 1 {
        input := fmt.Sprintf("%s%stail content", bytes.Repeat([]byte(" "), readSize-5), search)
        input += input
        b.WriteString(input)
    }

    bsearch := []byte(search)
    s, err := bytesSearch(b.Bytes())
    if err != nil {
        log.Fatal(err)
    }
    if verbose {
        s.logger = log.New(os.Stderr, "", log.LstdFlags)
    }
    s.Buffer = make([]byte, readSize)
    s.Sep = []byte(sep)
    got, err := s.Index(bsearch)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Index ", got)
    got, err = s.Index2(bsearch)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Index ", got)

}

type tailSearch struct {
    F      io.ReadSeeker
    Buffer []byte
    Sep    []byte
    start  int64
    logger interface {
        Println(...interface{})
    }
}

func fileSearch(f *os.File) (ret tailSearch, err error) {
    ret.F = f
    st, err := f.Stat()
    if err != nil {
        return
    }
    ret.start = st.Size()
    ret.Sep = []byte("\n")
    return ret, nil
}

func bytesSearch(b []byte) (ret tailSearch, err error) {
    ret.F = filebuffer.New(b)
    ret.start = int64(len(b))
    ret.Sep = []byte("\n")
    return
}

func (b tailSearch) Index(search []byte) (int64, error) {

    if b.Buffer == nil {
        b.Buffer = make([]byte, 1024, 1024)
    }
    buf := b.Buffer
    blen := len(b.Buffer)

    hasended := false
    for !hasended {
        if b.logger != nil {
            b.logger.Println("a start", b.start)
        }
        offset := b.start - int64(blen)
        if offset < 0 {
            offset = 0
            hasended = true
        }
        _, err := b.F.Seek(offset, 0)
        if err != nil {
            hasended = true
        }
        n, err := b.F.Read(buf)
        if b.logger != nil {
            b.logger.Println("f n", n, "err", err)
        }
        if err != nil {
            hasended = true
        }
        buf = buf[:n]
        b.start -= int64(n)
        if b.logger != nil {
            b.logger.Println("g start", b.start)
        }
        if b.start > 0 {
            i := bytes.Index(buf, b.Sep)
            if b.logger != nil {
                b.logger.Println("h sep", i)
            }
            if i > -1 {
                b.start += int64(i)
                buf = buf[i:]
                if b.logger != nil {
                    b.logger.Println("i start", b.start)
                }
            }
        }
        if e := bytes.LastIndex(buf, search); e > -1 {
            return b.start + int64(e), nil
        }
    }

    return -1, nil
}

func (b tailSearch) Index2(search []byte) (int64, error) {

    if b.Buffer == nil {
        b.Buffer = make([]byte, 1024, 1024)
    }
    buf := b.Buffer
    blen := len(b.Buffer)

    hasended := false
    for !hasended {
        if b.logger != nil {
            b.logger.Println("a start", b.start)
        }
        offset := b.start - int64(blen)
        if offset < 0 {
            offset = 0
            hasended = true
        }
        _, err := b.F.Seek(offset, 0)
        if err != nil {
            hasended = true
        }

        n, err := b.F.Read(buf)
        if b.logger != nil {
            b.logger.Println("f n", n, "err", err)
        }
        if err != nil {
            hasended = true
        }
        buf = buf[:n]
        b.start -= int64(n)

        if b.logger != nil {
            b.logger.Println("g start", b.start)
        }

        for i := 1; i < len(search); i++ {
            if bytes.HasPrefix(buf, search[i:]) {
                e := i - len(search)
                b.start += int64(e)
                buf = buf[e:]
            }
        }
        if b.logger != nil {
            b.logger.Println("g start", b.start)
        }

        if e := bytes.LastIndex(buf, search); e > -1 {
            return b.start + int64(e), nil
        }
    }

    return -1, nil
}

main_test.go

package main

import (
    "bytes"
    "fmt"
    "strings"
    "testing"
)

func TestOne(t *testing.T) {

    type test struct {
        search  []byte
        readLen int
        input   string
        sep     []byte
        want    int64
    }

    search := []byte("find me")
    blockLen := 1024
    tests := []test{
        test{
            search:  search,
            sep:     []byte("\n"),
            readLen: blockLen,
            input:   fmt.Sprintf("%stail content", search),
            want:    0,
        },
        test{
            search:  search,
            sep:     []byte("\n"),
            readLen: blockLen,
            input:   fmt.Sprintf(""),
            want:    -1,
        },
        test{
            search:  search,
            sep:     []byte("\n"),
            readLen: blockLen,
            input:   strings.Repeat("nop\n", 10000),
            want:    -1,
        },
        test{
            search:  search,
            sep:     []byte("\n"),
            readLen: blockLen,
            input:   fmt.Sprintf("%s%stail content", bytes.Repeat([]byte(" "), blockLen-5), search),
            want:    1019,
        },
        test{
            search:  search,
            sep:     []byte("\n"),
            readLen: blockLen,
            input:   fmt.Sprintf("%s%stail content", bytes.Repeat([]byte(" "), blockLen), search),
            want:    1024,
        },
        test{
            search:  search,
            sep:     []byte("\n"),
            readLen: blockLen,
            input:   fmt.Sprintf("%s%stail content", bytes.Repeat([]byte(" "), blockLen+10), search),
            want:    1034,
        },
        test{
            search:  search,
            sep:     []byte("\n"),
            readLen: blockLen,
            input:   fmt.Sprintf("%s%stail content", bytes.Repeat([]byte(" "), (blockLen*2)+10), search),
            want:    2058,
        },
        test{
            search:  search,
            sep:     []byte("\n"),
            readLen: blockLen,
            input:   fmt.Sprintf("%s%s%stail content", bytes.Repeat([]byte(" "), (blockLen*2)+10), search, search),
            want:    2065,
        },
    }

    for i, test := range tests {
        s, err := bytesSearch([]byte(test.input))
        if err != nil {
            t.Fatal(err)
        }
        s.Buffer = make([]byte, test.readLen)
        s.Sep = test.sep
        got, err := s.Index(test.search)
        if err != nil {
            t.Fatal(err)
        }
        if got != test.want {
            t.Fatalf("invalid index at %v got %v wanted %v", i, got, test.want)
        }
        got, err = s.Index2(test.search)
        if err != nil {
            t.Fatal(err)
        }
        if got != test.want {
            t.Fatalf("invalid index at %v got %v wanted %v", i, got, test.want)
        }
    }

}

Bench_test.go

package main

import (
    "bytes"
    "fmt"
    "testing"

    "github.com/mattetti/filebuffer"
)

func BenchmarkIndex(b *testing.B) {
    search := []byte("find me")
    blockLen := 1024
    input := fmt.Sprintf("%s%stail content", bytes.Repeat([]byte(" "), blockLen-5), search)
    input += input
    s := tailSearch{}
    s.F = filebuffer.New([]byte(input))
    s.Buffer = make([]byte, blockLen)
    for i := 0; i < b.N; i++ {
        s.start = int64(len(input))
        _, err := s.Index(search)
        if err != nil {
            b.Fatal(err)
        }
    }
}

func BenchmarkIndex2(b *testing.B) {
    search := []byte("find me")
    blockLen := 1024
    input := fmt.Sprintf("%s%stail content", bytes.Repeat([]byte(" "), blockLen-5), search)
    input += input
    s := tailSearch{}
    s.F = filebuffer.New([]byte(input))
    s.Buffer = make([]byte, blockLen)
    for i := 0; i < b.N; i++ {
        s.start = int64(len(input))
        _, err := s.Index2(search)
        if err != nil {
            b.Fatal(err)
        }
    }
}

тестирование

$ go test -v
=== RUN   TestOne
--- PASS: TestOne (0.00s)
PASS
ok      test/backwardsearch 0.002s
$ go test -bench=. -benchmem -v
=== RUN   TestOne
--- PASS: TestOne (0.00s)
goos: linux
goarch: amd64
pkg: test/backwardsearch
BenchmarkIndex-4        20000000           108 ns/op           0 B/op          0 allocs/op
BenchmarkIndex2-4       10000000           167 ns/op           0 B/op          0 allocs/op
PASS
ok      test/backwardsearch 4.129s
$ echo "rrrrfindme" | go run main.go -v
2019/10/17 12:17:04 a start 11
2019/10/17 12:17:04 f n 11 err <nil>
2019/10/17 12:17:04 g start 0
Index  4
2019/10/17 12:17:04 a start 11
2019/10/17 12:17:04 f n 11 err <nil>
2019/10/17 12:17:04 g start 0
2019/10/17 12:17:04 g start 0
Index  4
$ cat bench_test.go | go run main.go -search main
Index  8
Index  8
$ go run main.go 
Index  2056
Index  2056
person mh-cbon    schedule 16.10.2019
comment
для полноты в этом коде спрятана ошибка в i := bytes.Index(buf, b.Sep) и есть версия этого алгоритма, не требующая разделителя. Оба оставлены заинтересованному читателю в качестве упражнений. - person mh-cbon; 16.10.2019
comment
Спасибо большое, завтра проверю и отпишусь. - person donnior; 16.10.2019

Обратите внимание, что я неправильно понял ваш вопрос и подумал, что речь идет о чтении из строки. Я обновил свой ответ, но мне также нравится мой метод чтения строк. Таким образом, я буду держать его здесь. Перейдите ниже, чтобы увидеть новый ответ.

Подпрограммы для решения вашей проблемы просты, если специальные символы всегда одинаковы.

1-й: Найдите индекс этой строки специальных символов.
2-й: Если он найден, добавьте этот индекс к длине этой строки специальных символов.
3-й: Затем получите все содержимое из этого (индекс строки специальных символов + длина строки специальных символов) до конца содержимого.

package main

import "fmt"
import "strings"

func main() {
    var str = `
this is the very large 
part, contains lots lines.

#SPECIAL CHARS START#
a
...
this is the small part the the end, 
contain several lines, but 
we do not know how many lines
this part contains
`   
    var specialStr = "#SPECIAL CHARS START#";
    var lengthOfSpecial = len(specialStr);
    var indexOf = strings.Index(str, specialStr);
    var contentAfter string;

    if ( indexOf != -1 ){
        // If found get content
        indexOf += lengthOfSpecial;
        contentAfter = str[indexOf:];    
    } else {
        // If not content after empty.
        contentAfter = "";
    }

    fmt.Print(contentAfter);
}

Новый ответ

Извините, я так долго не писал о Golang. Таким образом, я могу придумать только код ниже. Я даже забыл, что вам не нужно ";" в конце строки :).

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

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

Звучит просто? Есть одна проблема и как я с ней справился, я очень подробно написал в примечании к коду той части. Надо подумать, что там делать. Так в чем проблема? Если вы читаете файл по частям, ожидайте, что то, что вы ищете, может быть разделено на две части. Таким образом, это необходимо учитывать.

Обратите внимание, что я использую байтовые срезы и работаю непосредственно в байтах. Во-вторых, байт сначала содержит состав размера индекса среза, и его индекс используется от высокого к низкому, потому что вы не хотите добавлять, а затем переворачивать его. Кроме того, "perRead" не может быть меньше длины строки поиска.

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

Обратите внимание, что в итоге я нашел решение для правильной проверки небольшого фрагмента из двух последовательных прочитанных фрагментов. Я задокументировал это в коде перед объявлением «var halfpart».

package main

import "fmt"
import "os"
import "bytes"

func getLog(fileName string, findStr string) string {
  const perRead int64 = 512

  file, err := os.Open(fileName)
  if err != nil {
    // error code go here for open file error.
      os.Exit(1)
  }
  stat, err := file.Stat()
  if err != nil {
    // error code go here for getting file stat.
      os.Exit(1)
  }

  // Convert specialChar to find to bytes for fast searching.
  var findBytes = []byte(findStr)
  var findLength = len(findBytes)
  // The length of findStr can't be larger than a read.
  if int64(findLength) > perRead { os.Exit(1) }

  var lastRead = stat.Size()
  var contents = make([][]byte, lastRead / perRead + 1)
  var lastIndex = len(contents) - 1
  var saveIndex = lastIndex

  for {
    var readBytes []byte

    if ( lastRead == 0 ){ break }
    if ( lastRead - perRead > -1 ){
      readBytes =  make([]byte, perRead )
      lastRead = lastRead - perRead
    } else {
      readBytes = make([]byte, lastRead - 0)
      lastRead = 0
    }

    _, err = file.ReadAt(readBytes, lastRead)
    if ( err != nil ){
      // error code go here for reading error
      // This method can't never encounter an eof error
      os.Exit(1);
    }

    var indexOf = bytes.Index(readBytes, findBytes)

    if indexOf != -1 {
      contents[saveIndex] = readBytes[indexOf + findLength:]
      saveIndex -= 1
      break
    } else {
      if saveIndex < lastIndex {
        // So for here, take a small chunk of the beginning of last found(equal to findStr's length) 
        // add to a small ended chunk of this found(equal to findStr's length)
        // However, if this found is less than findStr length,// Then grab whatever available.
        var halfpart []byte
        if len(readBytes) < findLength {
          halfpart = append(readBytes, contents[saveIndex + 1][:findLength]...)
        } else {
          halfpart = append(readBytes[len(readBytes) - findLength:], contents[saveIndex + 1][:findLength]...)
        }

        var indexOf2 = bytes.Index(halfpart, findBytes)
        if indexOf2 != -1 {
          saveIndex = saveIndex + 1
          contents[saveIndex] = append(halfpart[indexOf2 + findLength:], contents[saveIndex][findLength:]...)
          saveIndex -= 1
          break
        }
      }
      contents[saveIndex] = readBytes
      saveIndex -= 1
    }
  }

  for i := saveIndex; i > -1; i-- {
    contents[saveIndex] = []byte{}
  }

  return string(bytes.Join(contents,[]byte{}))
}

func main() {
  var fileName = "test.txt"
  var findStr = "#SPECIAL CHARS START#"
  fmt.Println(getLog(fileName, findStr))
}

Содержимое test.txt:

Note that, I misread your question and thought that it was about reading from string. I will update this answer tomorrow.

The routines to solve your problem is simple if the special chars are always the same.

1st: Look for the Index of that special chars string.
2nd: If found, add that index to the length of that special chars string.
3rd: Then get all the content from that (index of special chars string + length of special chars string) to the end of the content.
#SPECIAL CHARS START#
The header lines were kept separate because they looked like mail
headers and I have mailmode on.  The same thing applies to Bozo's
quoted text.  Mailmode doesn't screw things up very often, but since
most people are usually converting non-mail, it's off by default.

Paragraphs are handled ok.  In fact, this one is here just to
demonstrate that.

THIS LINE IS VERY IMPORTANT!
(Ok, it wasn't *that* important)


EXAMPLE HEADER
==============

Since this is the first header noticed (all caps, underlined with an
"="), it will be a level 1 header.  It gets an anchor named
"section_1".
person Kevin Ng    schedule 16.10.2019
comment
это совсем не эффективно. Вы не должны загружать все содержимое файла в память, а обрабатывать его кусками. Ваш алгоритм — O(n), где n — размер файла. - person mh-cbon; 16.10.2019
comment
@ mh-cbon ты читал мой ответ в начале? И да, не для файла, а для строки, что является одним из самых эффективных методов. Кстати, я, как и вы, создаю обратное чтение, но ваш код выглядит запутанным и требует чего-то простого. - person Kevin Ng; 16.10.2019
comment
Я буду более чем счастлив прочитать ваше окончательное решение и сравнить с моим. - person mh-cbon; 16.10.2019
comment
@ mh-cbon - Вы должны посмотреть все мои ответы для C и VB, многие из них читают файл задом наперед. Это будет не первый. - person Kevin Ng; 16.10.2019
comment
тогда для вас не должно быть проблемой показать что-то, что мы можем прочитать и сравнить. все еще жду. - person mh-cbon; 16.10.2019
comment
@mh-cbon - я закончил свой метод. Хотя все в порядке. Не беспокойтесь о том, что я сказал ранее. Ваш код в порядке, это просто еще одна разновидность. Я дал вам большой палец вверх, хотя я еще даже не проверил его. - person Kevin Ng; 16.10.2019
comment
эй, я проверил, это новое решение намного лучше. Он проходит мои собственные тесты. Однако отмечает, что, поскольку ваша функция возвращает хвостовое содержимое, этот алгоритм требует хранить в памяти столько содержимого. Таким образом, в худшем случае, если файл большой, а поиск находится в начале, а не в конце, большая часть файла окажется в памяти. Кроме того, на мой взгляд, вы должны читать блоки размером более 512 байт. IO дешевый, но медленный, лучше читать большие куски. Мне также интересно, как часто он вызывает bytes.Index, трудно сказать, более глубокая проверка с использованием pprof даст некоторые правильные детали. - person mh-cbon; 16.10.2019
comment
@ mh-cbon - в итоге я нашел решение для правильной проверки между двумя фрагментами. Как вы думаете, я должен изменить ответ как общий код или оставить его как сейчас? - person Kevin Ng; 16.10.2019
comment
я думаю, что лучше отредактировать и оставить только последнюю версию. - person mh-cbon; 16.10.2019
comment
@mh-cbon - спасибо. Я согласен, и я сделаю это прямо сейчас. - person Kevin Ng; 16.10.2019
comment
@KevinNg, ваш код работает хорошо, я проверил его на своей машине с одним файлом ›80 000 строк, специальной строкой в ​​строке 70, 000 и 79 900 (означает, что небольшая часть содержит 10 000 строк или 100 строк), оба имеют хорошую производительность, (~200 мс и ~100 мс), Спасибо! - person donnior; 17.10.2019
comment
@donnior - Спасибо за ваш комментарий, добро пожаловать. - person Kevin Ng; 18.10.2019
comment
@donnior - я забыл упомянуть, что мой код был написан для общего использования. Если вы хотите, чтобы он работал быстрее, есть варианты, зависящие от вашего варианта использования. Если вы работаете с небольшими файлами, на самом деле лучше просто загрузить весь файл в память. Если вы работаете с большим файлом, вы всегда можете изменить значение на чтение на 1024, 2048, 4096 в зависимости от того, сколько памяти вы хотите потратить. Кроме того, если вы хотите сразу же записать найденное на диск, не нуждаясь в найденном в строковом формате, просто верните объединенный массив байтов вместе, не преобразовывая его в строку. Затем запишите это на диск. - person Kevin Ng; 18.10.2019
comment
@donnior - еще один способ сделать это намного быстрее: вместо вызова Index вы ищете массив байтов самостоятельно. Но это потребует обширной отладки. - person Kevin Ng; 18.10.2019