web-dev-qa-db-ja.com

InputStreamのバイトの配列をフィルター(検索および置換)

HTMLファイルを入力パラメーターとして取るInputStreamがあります。入力ストリームからバイトを取得する必要があります。

文字列があります:"XYZ"。この文字列をバイト形式に変換し、InputStreamから取得したバイトシーケンス内の文字列と一致するかどうかを確認したいと思います。その場合は、他の文字列の一致をbyeシーケンスに置き換える必要があります。

これを手伝ってくれる人はいますか?検索と置換に正規表現を使用しました。ただし、バイトストリームを見つけて置換することは知らない。

以前は、jsoupを使用してhtmlを解析して文字列を置き換えていましたが、utfエンコードの問題のため、ファイルを破損するとファイルが破損しているように見えます。

TL; DR:私の質問は:

Javaの生のInputStreamでバイト形式の文字列を見つけて置き換える方法はありますか?

20
user471450

問題を解決するための最良のアプローチを選択したかどうかはわかりません。

とはいえ、質問に「しない」で答えるのは好きではありません(ポリシーとしては持ちません)ので、ここにいきます...

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
29
aioobe

私もこのようなものが必要で、@ aioobeによる上記の例を使用する代わりに、独自のソリューションを導入することにしました。 code をご覧ください。ライブラリをMaven Centralからプルするか、ソースコードをコピーすることができます。

これがあなたの使い方です。この場合、ネストされたインスタンスを使用して、2つのパターン、2つの修正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
Jilles van Gurp

次のアプローチは機能しますが、パフォーマンスへの影響はそれほど大きくありません。

  1. InputStreamInputStreamReaderで囲み、
  2. 文字列を置き換えるInputStreamReaderFilterReaderをラップし、次に
  3. FilterReaderReaderInputStreamでラップします。

適切なエンコーディングを選択することが重要です。そうしないと、ストリームのコンテンツが破損します。

正規表現を使用して文字列を置換する場合は、FilterReaderの便利な代替手段である Streamflyer を使用できます。 Streamflyerのウェブページにバイトストリームの例があります。お役に立てれば。

4
rwitzel

バイトストリーム(InputStream)の検索と置換のための組み込み機能はありません。

また、このタスクを効率的かつ正しく完了する方法はすぐにはわかりません。ストリームにBoyer-Mooreアルゴリズムを実装しましたが、うまく機能しますが、少し時間がかかりました。このようなアルゴリズムがない場合、ブルートフォースアプローチに頼る必要があります ストリーム内のすべての位置から始まるパターンを探す これは低速になる可能性があります。

HTMLをテキストとしてデコードする場合でも、HTMLは「通常の」言語ではないため、 正規表現を使用してパターンを照合することはお勧めできません

したがって、いくつかの困難に直面したとしても、HTMLをドキュメントとして解析するという独自のアプローチを追求することをお勧めします。文字エンコーディングで問題が発生している間は、長い目で見れば、正しいソリューションを修正する方が、間違ったソリューションを審査するよりも簡単でしょう。

2
erickson

特定のキーワードを値で置き換えるサーブレットでテンプレートファイルを提供する必要があるときに、この簡単なコードを思いつきました。かなり高速で、メモリが少ないはずです。次に、パイプストリームを使用して、あらゆる種類のものに使用できると思います。

/ 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
J.C

私はこれに対する解決策を必要としていましたが、ここでの答えはメモリやCPUのオーバーヘッドを引き起こしすぎていることがわかりました。以下のソリューションは、単純なベンチマークに基づいて、これらの用語で他のソリューションを大幅に上回っています。

このソリューションは特にメモリ効率が高く、GBストリーム以上でも測定可能なコストは発生しません。

つまり、これはCPUコストがゼロのソリューションではありません。 CPU /処理時間のオーバーヘッドは、最も要求の厳しい/リソースに敏感なシナリオを除くすべての場合におそらく妥当ですが、オーバーヘッドは現実的であり、特定のコンテキストでこのソリューションを採用する価値を評価するときに考慮する必要があります。

私の場合、処理している実際のファイルの最大サイズは約6 MBであり、44のURL置換で約170ミリ秒の遅延が追加されています。これは、単一のCPUシェア(1024)を使用してAWS ECSで実行されるZuulベースのリバースプロキシ用です。ほとんどのファイル(100KB未満)では、追加される待ち時間はサブミリ秒です。同時実行性が高い(したがってCPUの競合)と、追加のレイテンシが増加する可能性がありますが、現時点では、人間が気付くようなレイテンシの影響なしに、単一ノードで数百のファイルを同時に処理できます。

私たちが使用しているソリューション:

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;
    }

}
1
rees