Фильтр (поиск и замена) массива байтов в InputStream - PullRequest
18 голосов
/ 12 октября 2011

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

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

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

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

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

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

Ответы [ 6 ]

29 голосов
/ 12 октября 2011

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

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

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

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

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
4 голосов
/ 03 декабря 2016

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

Вот как вы его используете. В этом случае я использую вложенный экземпляр для замены двух шаблонов, двух фиксированных 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 голосов
/ 22 июня 2012

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

  1. Оберните InputStream оберткой InputStreamReader,
  2. InputStreamReader с FilterReader, который заменяет строки, затем
  3. оберните FilterReader ReaderInputStream.

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

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

2 голосов
/ 12 октября 2011

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

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

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

1 голос
/ 12 декабря 2018

Я придумал этот простой кусок кода, когда мне нужно было передать файл шаблона в сервлете, заменив определенное ключевое слово значением. Это должно быть довольно быстро и мало памяти. Тогда, используя 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]);
    }
}
1 голос
/ 09 сентября 2018

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

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

В моем случае максимальный размер обрабатываемого нами файла составляет около 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;
    }

}
...