Как преобразовать HTML сущностей через io.Reader - PullRequest
2 голосов
/ 23 февраля 2020

Моя Go программа делает HTTP-запросы с большими телами ответов JSON документов, строки которых кодируют символ амперсанда & как & (предположительно из-за какой-то странности платформы Microsoft?). Моя программа должна преобразовать эти объекты обратно в символ амперсанда таким образом, чтобы это было совместимо с json.Decoder.

Пример ответа может выглядеть следующим образом:

{"name":"A&B","comment":"foo&bar"}

Чей соответствующий объект будет выглядеть следующим образом:

pkg.Object{Name:"A&B", Comment:"foo&bar"}

Документы имеют различную форму, поэтому невозможно преобразовать HTML объекты после декодирования. В идеале это можно сделать, поместив считыватель тела ответа в другое средство чтения, которое выполняет преобразование.

Существует ли простой способ обернуть http.Response.Body в некоторый io.ReadCloser, который заменяет все экземпляры & на & (или в общем случае заменяет любую строку X на строку Y)?

Я подозреваю, что это возможно с x/text/transform, но не сразу видите, как. В частности, меня беспокоят крайние случаи, когда сущность охватывает пакеты байтов. То есть одна партия заканчивается &am, а следующая начинается, например, p;. Есть какая-нибудь библиотека или идиома, которая изящно справляется с такой ситуацией?

Ответы [ 2 ]

1 голос
/ 24 февраля 2020

Если вы не хотите полагаться на внешний пакет, такой как transform.Reader, вы можете написать пользовательскую оболочку io.Reader.

Следующее будет обрабатывать крайний случай, когда элемент find может охватывать два Read() вызова:

type fixer struct {
    r        io.Reader // source reader
    fnd, rpl []byte    // find & replace sequences
    partial  int       // track partial find matches from previous Read()
}

// Read satisfies io.Reader interface
func (f *fixer) Read(b []byte) (int, error) {
    off := f.partial
    if off > 0 {
        copy(b, f.fnd[:off]) // copy any partial match from previous `Read`
    }

    n, err := f.r.Read(b[off:])
    n += off

    if err != io.EOF {
        // no need to check for partial match, if EOF, as that is the last Read!
        f.partial = partialFind(b[:n], f.fnd)
        n -= f.partial // lop off any partial bytes
    }

    fixb := bytes.ReplaceAll(b[:n], f.fnd, f.rpl)

    return copy(b, fixb), err // preserve err as it may be io.EOF etc.
}

Наряду с этим помощником (который, возможно, мог бы использовать некоторую оптимизацию):

// returns number of matched bytes, if byte-slice ends in a partial-match
func partialFind(b, find []byte) int {
    for n := len(find) - 1; n > 0; n-- {
        if bytes.HasSuffix(b, find[:n]) {
            return n
        }
    }
    return 0 // no match
}

Работает Пример игровой площадки .


Примечание: для проверки логики пограничного регистра c можно использовать narrowReader для обеспечения коротких Read и принудительного разделения совпадения на Read, например: пример игровой площадки

1 голос
/ 24 февраля 2020

Вам необходимо создать transform.Transformer, который заменит ваши символы.

Поэтому нам нужен тот, который преобразует старый []byte в новый []byte, сохраняя при этом все остальные данные , Реализация может выглядеть следующим образом:

type simpleTransformer struct {
    Old, New []byte
}

// Transform transforms `t.Old` bytes to `t.New` bytes.
// The current implementation assumes that len(t.Old) >= len(t.New), but it also seems to work when len(t.Old) < len(t.New) (this has not been tested extensively)
func (t *simpleTransformer) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
    // Get the position of the first occurance of `t.Old` so we can replace it
    var ci = bytes.Index(src[nSrc:], t.Old)

    // Loop over the slice until we can't find any occurances of `t.Old`
    // also make sure we don't run into index out of range panics
    for ci != -1 && nSrc < len(src) {
        // Copy source data before `nSrc+ci` that doesn't need transformation
        copied := copy(dst[nDst:nDst+ci], src[nSrc:nSrc+ci])
        nDst += copied
        nSrc += copied

        // Copy new data with transformation to `dst`
        nDst += copy(dst[nDst:nDst+len(t.New)], t.New)

        // Skip the rest of old bytes in the next iteration
        nSrc += len(t.Old)

        // search for the next occurance of `t.Old`
        ci = bytes.Index(src[nSrc:], t.Old)
    }

    // Mark the rest of data as not completely processed if it contains a start element of `t.Old`
    // (e.g. if the end is `&amp` and we're looking for `&amp;`)
    // This data will not yet be copied to `dst` so we can work with it again
    // If it is at the end (`atEOF`), we don't need to do the check anymore as the string might just end with `&amp` 
    if bytes.Contains(src[nSrc:], t.Old[0:1]) && !atEOF {
        err = transform.ErrShortSrc
        return
    }

    // Copy rest of data that doesn't need any transformations
    // The for loop processed everything except this last chunk
    copied := copy(dst[nDst:], src[nSrc:])
    nDst += copied
    nSrc += copied

    return nDst, nSrc, err
}

// To satisfy transformer.Transformer interface
func (t *simpleTransformer) Reset() {}

Реализация должна быть уверена, что имеет дело с символами, которые разбиты на несколько вызовов метода Transform, поэтому она возвращает transform.ErrShortSrc, чтобы сообщить transform.Reader, что ему нужно больше информации о следующих байтах.

Теперь это можно использовать для замены символов в потоке:

var input = strings.NewReader(`{"name":"A&amp;B","comment":"foo&amp;bar"}`)
r := transform.NewReader(input, &simpleTransformer{[]byte(`&amp;`), []byte(`&`)})
io.Copy(os.Stdout, r) // Instead of io.Copy, use the JSON decoder to read from `r`

Вывод:

{"name":"A&B","comment":"foo&bar"}

Вы также можете увидеть это в действии на Go Playground .

...