Фильтр (поиск и замена) массива байтов в InputStream

У меня есть InputStream, который принимает html файл в качестве входного параметра. Я должен получить байты из входного потока.

У меня есть строка: "XYZ". Я хотел бы преобразовать эту строку в формат байта и проверить, есть ли соответствие для строки в последовательности байтов, которую я получил из InputStream. Если есть, то я должен заменить совпадение байтовой последовательностью для некоторой другой строки.

Есть ли кто-нибудь, кто мог бы мне помочь? Я использовал регулярное выражение для поиска и замены. но обнаруживая и заменяя поток байтов, я не знаю.

Раньше я использовал jsoup для разбора html и замены строки, однако из-за некоторых проблем с кодировкой utf файл кажется поврежденным, когда я это делаю.

TL; DR: Мой вопрос:

Является ли способ найти и заменить строку в байтовом формате в сыром InputStream в Java?

Ответы

Ответ 1

Не уверен, что вы выбрали лучший подход для решения своей проблемы.

Тем не менее, мне не нравится (и, как политика не), отвечать на вопросы "не надо", поэтому здесь идет...

Посмотрите FilterInputStream.

Из документации:

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


Это было веселое упражнение, чтобы написать его. Вот вам полный пример:

import java.io.*;
import java.util.*;

class ReplacingInputStream extends FilterInputStream {

    LinkedList<Integer> inQueue = new LinkedList<Integer>();
    LinkedList<Integer> outQueue = new LinkedList<Integer>();
    final byte[] search, replacement;

    protected ReplacingInputStream(InputStream in,
                                   byte[] search,
                                   byte[] replacement) {
        super(in);
        this.search = search;
        this.replacement = replacement;
    }

    private boolean isMatchFound() {
        Iterator<Integer> inIter = inQueue.iterator();
        for (int i = 0; i < search.length; i++)
            if (!inIter.hasNext() || search[i] != inIter.next())
                return false;
        return true;
    }

    private void readAhead() throws IOException {
        // Work up some look-ahead.
        while (inQueue.size() < search.length) {
            int next = super.read();
            inQueue.offer(next);
            if (next == -1)
                break;
        }
    }

    @Override
    public int read() throws IOException {    
        // Next byte already determined.
        if (outQueue.isEmpty()) {
            readAhead();

            if (isMatchFound()) {
                for (int i = 0; i < search.length; i++)
                    inQueue.remove();

                for (byte b : replacement)
                    outQueue.offer((int) b);
            } else
                outQueue.add(inQueue.remove());
        }

        return outQueue.remove();
    }

    // TODO: Override the other read methods.
}

Пример использования

class Test {
    public static void main(String[] args) throws Exception {

        byte[] bytes = "hello xyz world.".getBytes("UTF-8");

        ByteArrayInputStream bis = new ByteArrayInputStream(bytes);

        byte[] search = "xyz".getBytes("UTF-8");
        byte[] replacement = "abc".getBytes("UTF-8");

        InputStream ris = new ReplacingInputStream(bis, search, replacement);

        ByteArrayOutputStream bos = new ByteArrayOutputStream();

        int b;
        while (-1 != (b = ris.read()))
            bos.write(b);

        System.out.println(new String(bos.toByteArray()));

    }
}

Учитывая байты для строки "Hello xyz world", она печатает:

Hello abc world

Ответ 2

Будет работать следующий подход, но я не знаю, насколько сильно это влияет на производительность.

  • Оберните InputStream с помощью InputStreamReader,
  • оберните InputStreamReader с помощью FilterReader, который заменяет строки, затем
  • завершите FilterReader с помощью ReaderInputStream.

Крайне важно выбрать подходящую кодировку, иначе содержимое потока станет поврежденным.

Если вы хотите использовать регулярные выражения для замены строк, вы можете использовать Streamflyer, мой инструмент, который удобной альтернативой FilterReader. Вы найдете пример для потоков байтов на веб-странице Streamflyer. Надеюсь, это поможет.

Ответ 3

Мне тоже было нужно что-то подобное, и я решил использовать свое собственное решение вместо того, чтобы использовать приведенный выше пример @aioobe. Посмотрите код. Вы можете вытащить библиотеку из центра maven или просто скопировать исходный код.

Вот как вы его используете. В этом случае я использую вложенный экземпляр для замены двух шаблонов двух окончаний fix dos и mac.

new ReplacingInputStream(new ReplacingInputStream(is, "\n\r", "\n"), "\r", "\n");

Здесь полный исходный код:

/**
 * Simple FilterInputStream that can replace occurrances of bytes with something else.
 */
public class ReplacingInputStream extends FilterInputStream {

    // while matching, this is where the bytes go.
    int[] buf=null;
    int matchedIndex=0;
    int unbufferIndex=0;
    int replacedIndex=0;

    private final byte[] pattern;
    private final byte[] replacement;
    private State state=State.NOT_MATCHED;

    // simple state machine for keeping track of what we are doing
    private enum State {
        NOT_MATCHED,
        MATCHING,
        REPLACING,
        UNBUFFER
    }

    /**
     * @param is input
     * @return nested replacing stream that replaces \n\r (DOS) and \r (MAC) line endings with UNIX ones "\n".
     */
    public static InputStream newLineNormalizingInputStream(InputStream is) {
        return new ReplacingInputStream(new ReplacingInputStream(is, "\n\r", "\n"), "\r", "\n");
    }

    /**
     * Replace occurances of pattern in the input. Note: input is assumed to be UTF-8 encoded. If not the case use byte[] based pattern and replacement.
     * @param in input
     * @param pattern pattern to replace.
     * @param replacement the replacement or null
     */
    public ReplacingInputStream(InputStream in, String pattern, String replacement) {
        this(in,pattern.getBytes(StandardCharsets.UTF_8), replacement==null ? null : replacement.getBytes(StandardCharsets.UTF_8));
    }

    /**
     * Replace occurances of pattern in the input.
     * @param in input
     * @param pattern pattern to replace
     * @param replacement the replacement or null
     */
    public ReplacingInputStream(InputStream in, byte[] pattern, byte[] replacement) {
        super(in);
        Validate.notNull(pattern);
        Validate.isTrue(pattern.length>0, "pattern length should be > 0", pattern.length);
        this.pattern = pattern;
        this.replacement = replacement;
        // we will never match more than the pattern length
        buf = new int[pattern.length];
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        // copy of parent logic; we need to call our own read() instead of super.read(), which delegates instead of calling our read
        if (b == null) {
            throw new NullPointerException();
        } else if (off < 0 || len < 0 || len > b.length - off) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return 0;
        }

        int c = read();
        if (c == -1) {
            return -1;
        }
        b[off] = (byte)c;

        int i = 1;
        try {
            for (; i < len ; i++) {
                c = read();
                if (c == -1) {
                    break;
                }
                b[off + i] = (byte)c;
            }
        } catch (IOException ee) {
        }
        return i;

    }

    @Override
    public int read(byte[] b) throws IOException {
        // call our own read
        return read(b, 0, b.length);
    }

    @Override
    public int read() throws IOException {
        // use a simple state machine to figure out what we are doing
        int next;
        switch (state) {
        case NOT_MATCHED:
            // we are not currently matching, replacing, or unbuffering
            next=super.read();
            if(pattern[0] == next) {
                // clear whatever was there
                buf=new int[pattern.length]; // clear whatever was there
                // make sure we start at 0
                matchedIndex=0;

                buf[matchedIndex++]=next;
                if(pattern.length == 1) {
                    // edgecase when the pattern length is 1 we go straight to replacing
                    state=State.REPLACING;
                    // reset replace counter
                    replacedIndex=0;
                } else {
                    // pattern of length 1
                    state=State.MATCHING;
                }
                // recurse to continue matching
                return read();
            } else {
                return next;
            }
        case MATCHING:
            // the previous bytes matched part of the pattern
            next=super.read();
            if(pattern[matchedIndex]==next) {
                buf[matchedIndex++]=next;
                if(matchedIndex==pattern.length) {
                    // we've found a full match!
                    if(replacement==null || replacement.length==0) {
                        // the replacement is empty, go straight to NOT_MATCHED
                        state=State.NOT_MATCHED;
                        matchedIndex=0;
                    } else {
                        // start replacing
                        state=State.REPLACING;
                        replacedIndex=0;
                    }
                }
            } else {
                // mismatch -> unbuffer
                buf[matchedIndex++]=next;
                state=State.UNBUFFER;
                unbufferIndex=0;
            }
            return read();
        case REPLACING:
            // we've fully matched the pattern and are returning bytes from the replacement
            next=replacement[replacedIndex++];
            if(replacedIndex==replacement.length) {
                state=State.NOT_MATCHED;
                replacedIndex=0;
            }
            return next;
        case UNBUFFER:
            // we partially matched the pattern before encountering a non matching byte
            // we need to serve up the buffered bytes before we go back to NOT_MATCHED
            next=buf[unbufferIndex++];
            if(unbufferIndex==matchedIndex) {
                state=State.NOT_MATCHED;
                matchedIndex=0;
            }
            return next;

        default:
            throw new IllegalStateException("no such state " + state);
        }
    }

    @Override
    public String toString() {
        return state.name() + " " + matchedIndex + " " + replacedIndex + " " + unbufferIndex;
    }

}

Ответ 4

Нет встроенных функций для поиска и замены в потоках байтов (InputStream).

И, метод для выполнения этой задачи эффективно и правильно не сразу становится очевидным. Я реализовал алгоритм Boyer-Moore для потоков, и он работает хорошо, но это заняло некоторое время. Без такого алгоритма вам нужно прибегнуть к подходу с грубой силой, когда вы ищите шаблон, начинающийся с каждой позиции в потоке, который может быть медленным.

Даже если вы декодируете HTML как текст, используя регулярное выражение для соответствия шаблонам, может быть плохой идеей,, поскольку HTML не является "обычным" языком.

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

Ответ 5

Мне нужно было решить эту проблему, но я обнаружил, что ответы здесь привели к слишком большой загрузке памяти и/или ЦП. Приведенное ниже решение значительно превосходит другие здесь в этих условиях на основе простого сравнительного анализа.

Это решение особенно эффективно использует память и не требует измеримых затрат даже при потоках> ГБ.

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

В моем случае наш максимальный размер файла, который мы обрабатываем, составляет около 6 МБ, где мы видим добавленную задержку около 170 мс с 44 заменами URL. Это для обратного прокси-сервера на базе Zuul, работающего на AWS ECS с одним общим ресурсом ЦП (1024). Для большинства файлов (до 100 КБ) добавленная задержка составляет менее миллисекунды. При высоком параллелизме (и, следовательно, конфликте с процессором) дополнительная задержка может увеличиться, однако в настоящее время мы можем обрабатывать сотни файлов одновременно на одном узле без заметного влияния задержки на человека.

Решение, которое мы используем:

import java.io.IOException;
import java.io.InputStream;

public class TokenReplacingStream extends InputStream {

    private final InputStream source;
    private final byte[] oldBytes;
    private final byte[] newBytes;
    private int tokenMatchIndex = 0;
    private int bytesIndex = 0;
    private boolean unwinding;
    private int mismatch;
    private int numberOfTokensReplaced = 0;

    public TokenReplacingStream(InputStream source, byte[] oldBytes, byte[] newBytes) {
        assert oldBytes.length > 0;
        this.source = source;
        this.oldBytes = oldBytes;
        this.newBytes = newBytes;
    }

    @Override
    public int read() throws IOException {

        if (unwinding) {
            if (bytesIndex < tokenMatchIndex) {
                return oldBytes[bytesIndex++];
            } else {
                bytesIndex = 0;
                tokenMatchIndex = 0;
                unwinding = false;
                return mismatch;
            }
        } else if (tokenMatchIndex == oldBytes.length) {
            if (bytesIndex == newBytes.length) {
                bytesIndex = 0;
                tokenMatchIndex = 0;
                numberOfTokensReplaced++;
            } else {
                return newBytes[bytesIndex++];
            }
        }

        int b = source.read();
        if (b == oldBytes[tokenMatchIndex]) {
            tokenMatchIndex++;
        } else if (tokenMatchIndex > 0) {
            mismatch = b;
            unwinding = true;
        } else {
            return b;
        }

        return read();

    }

    @Override
    public void close() throws IOException {
        source.close();
    }

    public int getNumberOfTokensReplaced() {
        return numberOfTokensReplaced;
    }

}

Ответ 6

Я придумал этот простой кусок кода, когда мне нужно было передать файл шаблона в сервлете, заменив определенное ключевое слово значением. Это должно быть довольно быстро и мало памяти. Тогда, используя Piped Streams, я думаю, вы можете использовать его для всех видов вещей.

/JC

public static void replaceStream(InputStream in, OutputStream out, String search, String replace) throws IOException
{
    replaceStream(new InputStreamReader(in), new OutputStreamWriter(out), search, replace);
}

public static void replaceStream(Reader in, Writer out, String search, String replace) throws IOException
{
    char[] searchChars = search.toCharArray();
    int[] buffer = new int[searchChars.length];

    int x, r, si = 0, sm = searchChars.length;
    while ((r = in.read()) > 0) {

        if (searchChars[si] == r) {
            // The char matches our pattern
            buffer[si++] = r;

            if (si == sm) {
                // We have reached a matching string
                out.write(replace);
                si = 0;
            }
        } else if (si > 0) {
            // No match and buffered char(s), empty buffer and pass the char forward
            for (x = 0; x < si; x++) {
                out.write(buffer[x]);
            }
            si = 0;
            out.write(r);
        } else {
            // No match and nothing buffered, just pass the char forward
            out.write(r);
        }
    }

    // Empty buffer
    for (x = 0; x < si; x++) {
        out.write(buffer[x]);
    }
}