Java Mengganti beberapa substring berbeda dalam sebuah string sekaligus (atau dengan cara yang paling efisien)


97

Saya perlu mengganti banyak sub-string yang berbeda dalam sebuah string dengan cara yang paling efisien. apakah ada cara lain selain cara brute force untuk mengganti setiap bidang menggunakan string.replace?

Jawaban:


102

Jika string yang Anda operasikan sangat panjang, atau Anda beroperasi pada banyak string, maka sebaiknya gunakan java.util.regex.Matcher (ini membutuhkan waktu di muka untuk mengompilasi, jadi tidak akan efisien. jika masukan Anda sangat kecil atau pola pencarian Anda sering berubah).

Di bawah ini adalah contoh lengkap, berdasarkan daftar token yang diambil dari peta. (Menggunakan StringUtils dari Apache Commons Lang).

Map<String,String> tokens = new HashMap<String,String>();
tokens.put("cat", "Garfield");
tokens.put("beverage", "coffee");

String template = "%cat% really needs some %beverage%.";

// Create pattern of the format "%(cat|beverage)%"
String patternString = "%(" + StringUtils.join(tokens.keySet(), "|") + ")%";
Pattern pattern = Pattern.compile(patternString);
Matcher matcher = pattern.matcher(template);

StringBuffer sb = new StringBuffer();
while(matcher.find()) {
    matcher.appendReplacement(sb, tokens.get(matcher.group(1)));
}
matcher.appendTail(sb);

System.out.println(sb.toString());

Setelah ekspresi reguler dikompilasi, pemindaian string input biasanya sangat cepat (meskipun jika ekspresi reguler Anda rumit atau melibatkan mundur, Anda masih perlu melakukan tolok ukur untuk mengonfirmasi ini!)


1
Ya, perlu tolok ukur untuk jumlah iterasi sekalipun.
techzen

5
Saya pikir Anda harus melepaskan karakter khusus di setiap token sebelum melakukannya"%(" + StringUtils.join(tokens.keySet(), "|") + ")%";
Pengembang Marius Žilėnas

Perhatikan bahwa seseorang dapat menggunakan StringBuilder untuk sedikit lebih cepat. StringBuilder tidak disinkronkan. edit whoops hanya bekerja dengan java 9 sekalipun
Tinus Tate

3
Pembaca mendatang: untuk regex, "(" dan ")" akan menyertakan grup yang akan ditelusuri. "%" Dihitung sebagai literal dalam teks. Jika istilah Anda tidak dimulai DAN diakhiri dengan "%", istilah tersebut tidak akan ditemukan. Jadi sesuaikan prefiks dan sufiks pada kedua bagian (teks + kode).
linuxunil

66

Algoritma

Salah satu cara paling efisien untuk mengganti string yang cocok (tanpa ekspresi reguler) adalah menggunakan algoritme Aho-Corasick dengan kinerja Trie (dibaca "coba"), algoritme hashing cepat , dan implementasi koleksi yang efisien .

Kode Sederhana

Solusi sederhana memanfaatkan Apache StringUtils.replaceEachsebagai berikut:

  private String testStringUtils(
    final String text, final Map<String, String> definitions ) {
    final String[] keys = keys( definitions );
    final String[] values = values( definitions );

    return StringUtils.replaceEach( text, keys, values );
  }

Ini memperlambat teks besar.

Kode Cepat

Implementasi algoritma Aho-Corasick Bor memperkenalkan sedikit lebih banyak kompleksitas yang menjadi detail implementasi dengan menggunakan façade dengan tanda tangan metode yang sama:

  private String testBorAhoCorasick(
    final String text, final Map<String, String> definitions ) {
    // Create a buffer sufficiently large that re-allocations are minimized.
    final StringBuilder sb = new StringBuilder( text.length() << 1 );

    final TrieBuilder builder = Trie.builder();
    builder.onlyWholeWords();
    builder.removeOverlaps();

    final String[] keys = keys( definitions );

    for( final String key : keys ) {
      builder.addKeyword( key );
    }

    final Trie trie = builder.build();
    final Collection<Emit> emits = trie.parseText( text );

    int prevIndex = 0;

    for( final Emit emit : emits ) {
      final int matchIndex = emit.getStart();

      sb.append( text.substring( prevIndex, matchIndex ) );
      sb.append( definitions.get( emit.getKeyword() ) );
      prevIndex = emit.getEnd() + 1;
    }

    // Add the remainder of the string (contains no more matches).
    sb.append( text.substring( prevIndex ) );

    return sb.toString();
  }

Tolak ukur

Untuk tolok ukur, buffer dibuat menggunakan randomNumeric sebagai berikut:

  private final static int TEXT_SIZE = 1000;
  private final static int MATCHES_DIVISOR = 10;

  private final static StringBuilder SOURCE
    = new StringBuilder( randomNumeric( TEXT_SIZE ) );

Di mana MATCHES_DIVISORmenentukan jumlah variabel yang akan dimasukkan:

  private void injectVariables( final Map<String, String> definitions ) {
    for( int i = (SOURCE.length() / MATCHES_DIVISOR) + 1; i > 0; i-- ) {
      final int r = current().nextInt( 1, SOURCE.length() );
      SOURCE.insert( r, randomKey( definitions ) );
    }
  }

Kode patokan itu sendiri ( JMH sepertinya berlebihan):

long duration = System.nanoTime();
final String result = testBorAhoCorasick( text, definitions );
duration = System.nanoTime() - duration;
System.out.println( elapsed( duration ) );

1.000.000: 1.000

Tolok ukur mikro sederhana dengan 1.000.000 karakter dan 1.000 string yang ditempatkan secara acak untuk diganti.

  • testStringUtils: 25 detik, 25533 milis
  • testBorAhoCorasick: 0 detik, 68 milis

Tidak ada kontes.

10.000: 1.000

Menggunakan 10.000 karakter dan 1.000 string yang cocok untuk mengganti:

  • testStringUtils: 1 detik, 1402 milis
  • testBorAhoCorasick: 0 detik, 37 milis

Kesenjangan ditutup.

1.000: 10

Menggunakan 1.000 karakter dan 10 string yang cocok untuk mengganti:

  • testStringUtils: 0 detik, 7 milis
  • testBorAhoCorasick: 0 detik, 19 milis

Untuk string pendek, overhead pengaturan Aho-Corasick melampaui pendekatan brute-force StringUtils.replaceEach.

Pendekatan campuran berdasarkan panjang teks dimungkinkan, untuk mendapatkan yang terbaik dari kedua implementasi.

Implementasi

Pertimbangkan untuk membandingkan implementasi lain untuk teks dengan panjang lebih dari 1 MB, termasuk:

Dokumen

Makalah dan informasi yang berkaitan dengan algoritma:


5
Kudos untuk memperbarui pertanyaan ini dengan informasi baru yang berharga, itu sangat bagus. Menurut saya benchmark JMH masih sesuai, setidaknya untuk nilai yang masuk akal seperti 10.000: 1.000 dan 1.000: 10 (JIT terkadang dapat melakukan optimasi ajaib).
Tunaki

hapus builder.onlyWholeWords () dan akan bekerja sama dengan penggantian string.
Ondrej Sotolar

Terima kasih banyak atas jawaban yang luar biasa ini. Ini pasti sangat membantu! Saya hanya ingin berkomentar bahwa untuk membandingkan kedua pendekatan, dan juga untuk memberikan contoh yang lebih bermakna, seseorang harus membangun Trie hanya sekali dalam pendekatan kedua, dan menerapkannya ke banyak string input yang berbeda. Saya pikir ini adalah keuntungan utama memiliki akses ke Trie versus StringUtils: Anda hanya membangunnya sekali. Tetap saja, terima kasih banyak atas jawaban ini. Ini sangat
mirip

Poin yang sangat bagus, @VicSeedoubleyew. Mau memperbarui jawabannya?
Dave Jarvis

9

Ini berhasil untuk saya:

String result = input.replaceAll("string1|string2|string3","replacementString");

Contoh:

String input = "applemangobananaarefruits";
String result = input.replaceAll("mango|are|ts","-");
System.out.println(result);

Keluaran: apple-banana-frui-


Persis apa yang saya butuhkan teman saya :)
GOXR3PLUS

7

Jika Anda akan mengubah String berkali-kali, biasanya lebih efisien menggunakan StringBuilder (tetapi ukur kinerja Anda untuk mencari tahu) :

String str = "The rain in Spain falls mainly on the plain";
StringBuilder sb = new StringBuilder(str);
// do your replacing in sb - although you'll find this trickier than simply using String
String newStr = sb.toString();

Setiap kali Anda melakukan penggantian pada String, objek String baru dibuat, karena String tidak dapat diubah. StringBuilder bisa berubah, artinya, dapat diubah sebanyak yang Anda inginkan.


Saya takut, itu tidak membantu. Setiap kali penggantian berbeda dengan panjang aslinya, Anda perlu beberapa pergeseran, yang bisa lebih mahal daripada membangun tali baru. Atau apakah saya melewatkan sesuatu?
maaartinus

4

StringBuilderakan melakukan penggantian dengan lebih efisien, karena buffer array karakternya dapat ditentukan ke panjang yang diperlukan. StringBuilderdirancang untuk lebih dari sekadar menambahkan!

Tentu pertanyaan sebenarnya adalah apakah ini merupakan optimasi yang terlalu jauh? JVM sangat baik dalam menangani pembuatan beberapa objek dan pengumpulan sampah berikutnya, dan seperti semua pertanyaan pengoptimalan, pertanyaan pertama saya adalah apakah Anda telah mengukur ini dan menentukan bahwa ini adalah masalah.


2

Bagaimana jika menggunakan metode replaceAll () ?


4
Banyak substring yang berbeda dapat ditangani dalam regex (/substring1i>substring2i>.../). Itu semua tergantung pada jenis penggantian apa yang OP coba lakukan.
Avi

4
OP mencari sesuatu yang lebih efisien daripadastr.replaceAll(search1, replace1).replaceAll(search2, replace2).replaceAll(search3, replace3).replaceAll(search4, replace4)
Kip

2

Rythm a java template engine sekarang dirilis dengan fitur baru yang disebut mode interpolasi String yang memungkinkan Anda melakukan sesuatu seperti:

String result = Rythm.render("@name is inviting you", "Diana");

Kasus di atas menunjukkan Anda dapat mengirimkan argumen ke templat berdasarkan posisi. Rythm juga memungkinkan Anda untuk menyampaikan argumen dengan nama:

Map<String, Object> args = new HashMap<String, Object>();
args.put("title", "Mr.");
args.put("name", "John");
String result = Rythm.render("Hello @title @name", args);

Catatan Rythm SANGAT CEPAT, sekitar 2 hingga 3 kali lebih cepat dari format dan kecepatan String, karena ia mengkompilasi template ke dalam kode byte java, kinerja runtime sangat dekat dengan konsentrasi dengan StringBuilder.

Tautan:


Ini adalah kemampuan yang sangat tua yang tersedia dengan banyak bahasa template seperti kecepatan, bahkan JSP. Juga tidak menjawab pertanyaan yang tidak memerlukan string pencarian dalam format yang telah ditentukan sebelumnya.
Angsuman Chakraborty

Menarik, jawaban yang diterima memberikan contoh :, "%cat% really needs some %beverage%."; bukankah %token yang dipisahkan itu merupakan format yang ditentukan sebelumnya? Poin pertama Anda lebih lucu lagi, JDK menyediakan banyak "kemampuan lama", beberapa di antaranya dimulai dari tahun 90-an, mengapa orang repot-repot menggunakannya? Komentar dan downvoting Anda tidak masuk akal
Gelin Luo

Apa gunanya memperkenalkan mesin templat Rythm ketika sudah ada banyak mesin templat yang sudah ada sebelumnya, dan banyak digunakan seperti Velocity atau Freemarker untuk boot? Juga mengapa memperkenalkan produk lain ketika fungsionalitas inti Java lebih dari cukup. Saya sangat meragukan pernyataan Anda tentang kinerja karena Pattern juga dapat dikompilasi. Ingin melihat beberapa bilangan real.
Angsuman Chakraborty

Hijau, Anda melewatkan intinya. Penanya ingin mengganti string arbitrer sedangkan solusi Anda hanya akan mengganti string dalam format yang telah ditentukan seperti @ sebelumnya. Ya, contoh tersebut menggunakan% tetapi hanya sebagai contoh, bukan sebagai faktor pembatas. Jadi jawaban Anda tidak menjawab pertanyaan dan karenanya poin negatifnya.
Angsuman Chakraborty

2

Di bawah ini berdasarkan jawaban Todd Owen . Solusi tersebut memiliki masalah bahwa jika pengganti berisi karakter yang memiliki arti khusus dalam ekspresi reguler, Anda bisa mendapatkan hasil yang tidak diharapkan. Saya juga ingin dapat secara opsional melakukan penelusuran tidak peka huruf besar / kecil. Inilah yang saya dapatkan:

/**
 * Performs simultaneous search/replace of multiple strings. Case Sensitive!
 */
public String replaceMultiple(String target, Map<String, String> replacements) {
  return replaceMultiple(target, replacements, true);
}

/**
 * Performs simultaneous search/replace of multiple strings.
 * 
 * @param target        string to perform replacements on.
 * @param replacements  map where key represents value to search for, and value represents replacem
 * @param caseSensitive whether or not the search is case-sensitive.
 * @return replaced string
 */
public String replaceMultiple(String target, Map<String, String> replacements, boolean caseSensitive) {
  if(target == null || "".equals(target) || replacements == null || replacements.size() == 0)
    return target;

  //if we are doing case-insensitive replacements, we need to make the map case-insensitive--make a new map with all-lower-case keys
  if(!caseSensitive) {
    Map<String, String> altReplacements = new HashMap<String, String>(replacements.size());
    for(String key : replacements.keySet())
      altReplacements.put(key.toLowerCase(), replacements.get(key));

    replacements = altReplacements;
  }

  StringBuilder patternString = new StringBuilder();
  if(!caseSensitive)
    patternString.append("(?i)");

  patternString.append('(');
  boolean first = true;
  for(String key : replacements.keySet()) {
    if(first)
      first = false;
    else
      patternString.append('|');

    patternString.append(Pattern.quote(key));
  }
  patternString.append(')');

  Pattern pattern = Pattern.compile(patternString.toString());
  Matcher matcher = pattern.matcher(target);

  StringBuffer res = new StringBuffer();
  while(matcher.find()) {
    String match = matcher.group(1);
    if(!caseSensitive)
      match = match.toLowerCase();
    matcher.appendReplacement(res, replacements.get(match));
  }
  matcher.appendTail(res);

  return res.toString();
}

Berikut adalah kasus pengujian unit saya:

@Test
public void replaceMultipleTest() {
  assertNull(ExtStringUtils.replaceMultiple(null, null));
  assertNull(ExtStringUtils.replaceMultiple(null, Collections.<String, String>emptyMap()));
  assertEquals("", ExtStringUtils.replaceMultiple("", null));
  assertEquals("", ExtStringUtils.replaceMultiple("", Collections.<String, String>emptyMap()));

  assertEquals("folks, we are not sane anymore. with me, i promise you, we will burn in flames", ExtStringUtils.replaceMultiple("folks, we are not winning anymore. with me, i promise you, we will win big league", makeMap("win big league", "burn in flames", "winning", "sane")));

  assertEquals("bcaacbbcaacb", ExtStringUtils.replaceMultiple("abccbaabccba", makeMap("a", "b", "b", "c", "c", "a")));
  assertEquals("bcaCBAbcCCBb", ExtStringUtils.replaceMultiple("abcCBAabCCBa", makeMap("a", "b", "b", "c", "c", "a")));
  assertEquals("bcaacbbcaacb", ExtStringUtils.replaceMultiple("abcCBAabCCBa", makeMap("a", "b", "b", "c", "c", "a"), false));

  assertEquals("c colon  backslash temp backslash  star  dot  star ", ExtStringUtils.replaceMultiple("c:\\temp\\*.*", makeMap(".", " dot ", ":", " colon ", "\\", " backslash ", "*", " star "), false));
}

private Map<String, String> makeMap(String ... vals) {
  Map<String, String> map = new HashMap<String, String>(vals.length / 2);
  for(int i = 1; i < vals.length; i+= 2)
    map.put(vals[i-1], vals[i]);
  return map;
}

1
public String replace(String input, Map<String, String> pairs) {
  // Reverse lexic-order of keys is good enough for most cases,
  // as it puts longer words before their prefixes ("tool" before "too").
  // However, there are corner cases, which this algorithm doesn't handle
  // no matter what order of keys you choose, eg. it fails to match "edit"
  // before "bed" in "..bedit.." because "bed" appears first in the input,
  // but "edit" may be the desired longer match. Depends which you prefer.
  final Map<String, String> sorted = 
      new TreeMap<String, String>(Collections.reverseOrder());
  sorted.putAll(pairs);
  final String[] keys = sorted.keySet().toArray(new String[sorted.size()]);
  final String[] vals = sorted.values().toArray(new String[sorted.size()]);
  final int lo = 0, hi = input.length();
  final StringBuilder result = new StringBuilder();
  int s = lo;
  for (int i = s; i < hi; i++) {
    for (int p = 0; p < keys.length; p++) {
      if (input.regionMatches(i, keys[p], 0, keys[p].length())) {
        /* TODO: check for "edit", if this is "bed" in "..bedit.." case,
         * i.e. look ahead for all prioritized/longer keys starting within
         * the current match region; iff found, then ignore match ("bed")
         * and continue search (find "edit" later), else handle match. */
        // if (better-match-overlaps-right-ahead)
        //   continue;
        result.append(input, s, i).append(vals[p]);
        i += keys[p].length();
        s = i--;
      }
    }
  }
  if (s == lo) // no matches? no changes!
    return input;
  return result.append(input, s, hi).toString();
}

1

Periksa ini:

String.format(str,STR[])

Misalnya:

String.format( "Put your %s where your %s is", "money", "mouth" );

0

Ringkasan: Implementasi kelas tunggal dari jawaban Dave, untuk secara otomatis memilih yang paling efisien dari dua algoritma.

Ini adalah implementasi kelas tunggal yang lengkap berdasarkan jawaban luar biasa dari Dave Jarvis di atas . Kelas secara otomatis memilih di antara dua algoritme yang disediakan berbeda, untuk efisiensi maksimum. (Jawaban ini untuk orang yang hanya ingin menyalin dan menempel dengan cepat.)

Kelas ReplaceStrings:

package somepackage

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.ahocorasick.trie.Emit;
import org.ahocorasick.trie.Trie;
import org.ahocorasick.trie.Trie.TrieBuilder;
import org.apache.commons.lang3.StringUtils;

/**
 * ReplaceStrings, This class is used to replace multiple strings in a section of text, with high
 * time efficiency. The chosen algorithms were adapted from: https://stackoverflow.com/a/40836618
 */
public final class ReplaceStrings {

    /**
     * replace, This replaces multiple strings in a section of text, according to the supplied
     * search and replace definitions. For maximum efficiency, this will automatically choose
     * between two possible replacement algorithms.
     *
     * Performance note: If it is known in advance that the source text is long, then this method
     * signature has a very small additional performance advantage over the other method signature.
     * (Although either method signature will still choose the best algorithm.)
     */
    public static String replace(
        final String sourceText, final Map<String, String> searchReplaceDefinitions) {
        final boolean useLongAlgorithm
            = (sourceText.length() > 1000 || searchReplaceDefinitions.size() > 25);
        if (useLongAlgorithm) {
            // No parameter adaptations are needed for the long algorithm.
            return replaceUsing_AhoCorasickAlgorithm(sourceText, searchReplaceDefinitions);
        } else {
            // Create search and replace arrays, which are needed by the short algorithm.
            final ArrayList<String> searchList = new ArrayList<>();
            final ArrayList<String> replaceList = new ArrayList<>();
            final Set<Map.Entry<String, String>> allEntries = searchReplaceDefinitions.entrySet();
            for (Map.Entry<String, String> entry : allEntries) {
                searchList.add(entry.getKey());
                replaceList.add(entry.getValue());
            }
            return replaceUsing_StringUtilsAlgorithm(sourceText, searchList, replaceList);
        }
    }

    /**
     * replace, This replaces multiple strings in a section of text, according to the supplied
     * search strings and replacement strings. For maximum efficiency, this will automatically
     * choose between two possible replacement algorithms.
     *
     * Performance note: If it is known in advance that the source text is short, then this method
     * signature has a very small additional performance advantage over the other method signature.
     * (Although either method signature will still choose the best algorithm.)
     */
    public static String replace(final String sourceText,
        final ArrayList<String> searchList, final ArrayList<String> replacementList) {
        if (searchList.size() != replacementList.size()) {
            throw new RuntimeException("ReplaceStrings.replace(), "
                + "The search list and the replacement list must be the same size.");
        }
        final boolean useLongAlgorithm = (sourceText.length() > 1000 || searchList.size() > 25);
        if (useLongAlgorithm) {
            // Create a definitions map, which is needed by the long algorithm.
            HashMap<String, String> definitions = new HashMap<>();
            final int searchListLength = searchList.size();
            for (int index = 0; index < searchListLength; ++index) {
                definitions.put(searchList.get(index), replacementList.get(index));
            }
            return replaceUsing_AhoCorasickAlgorithm(sourceText, definitions);
        } else {
            // No parameter adaptations are needed for the short algorithm.
            return replaceUsing_StringUtilsAlgorithm(sourceText, searchList, replacementList);
        }
    }

    /**
     * replaceUsing_StringUtilsAlgorithm, This is a string replacement algorithm that is most
     * efficient for sourceText under 1000 characters, and less than 25 search strings.
     */
    private static String replaceUsing_StringUtilsAlgorithm(final String sourceText,
        final ArrayList<String> searchList, final ArrayList<String> replacementList) {
        final String[] searchArray = searchList.toArray(new String[]{});
        final String[] replacementArray = replacementList.toArray(new String[]{});
        return StringUtils.replaceEach(sourceText, searchArray, replacementArray);
    }

    /**
     * replaceUsing_AhoCorasickAlgorithm, This is a string replacement algorithm that is most
     * efficient for sourceText over 1000 characters, or large lists of search strings.
     */
    private static String replaceUsing_AhoCorasickAlgorithm(final String sourceText,
        final Map<String, String> searchReplaceDefinitions) {
        // Create a buffer sufficiently large that re-allocations are minimized.
        final StringBuilder sb = new StringBuilder(sourceText.length() << 1);
        final TrieBuilder builder = Trie.builder();
        builder.onlyWholeWords();
        builder.ignoreOverlaps();
        for (final String key : searchReplaceDefinitions.keySet()) {
            builder.addKeyword(key);
        }
        final Trie trie = builder.build();
        final Collection<Emit> emits = trie.parseText(sourceText);
        int prevIndex = 0;
        for (final Emit emit : emits) {
            final int matchIndex = emit.getStart();

            sb.append(sourceText.substring(prevIndex, matchIndex));
            sb.append(searchReplaceDefinitions.get(emit.getKeyword()));
            prevIndex = emit.getEnd() + 1;
        }
        // Add the remainder of the string (contains no more matches).
        sb.append(sourceText.substring(prevIndex));
        return sb.toString();
    }

    /**
     * main, This contains some test and example code.
     */
    public static void main(String[] args) {
        String shortSource = "The quick brown fox jumped over something. ";
        StringBuilder longSourceBuilder = new StringBuilder();
        for (int i = 0; i < 50; ++i) {
            longSourceBuilder.append(shortSource);
        }
        String longSource = longSourceBuilder.toString();
        HashMap<String, String> searchReplaceMap = new HashMap<>();
        ArrayList<String> searchList = new ArrayList<>();
        ArrayList<String> replaceList = new ArrayList<>();
        searchReplaceMap.put("fox", "grasshopper");
        searchReplaceMap.put("something", "the mountain");
        searchList.add("fox");
        replaceList.add("grasshopper");
        searchList.add("something");
        replaceList.add("the mountain");
        String shortResultUsingArrays = replace(shortSource, searchList, replaceList);
        String shortResultUsingMap = replace(shortSource, searchReplaceMap);
        String longResultUsingArrays = replace(longSource, searchList, replaceList);
        String longResultUsingMap = replace(longSource, searchReplaceMap);
        System.out.println(shortResultUsingArrays);
        System.out.println("----------------------------------------------");
        System.out.println(shortResultUsingMap);
        System.out.println("----------------------------------------------");
        System.out.println(longResultUsingArrays);
        System.out.println("----------------------------------------------");
        System.out.println(longResultUsingMap);
        System.out.println("----------------------------------------------");
    }
}

Dependensi Maven yang dibutuhkan:

(Tambahkan ini ke file pom Anda jika perlu.)

    <!-- Apache Commons utilities. Super commonly used utilities.
    https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.10</version>
    </dependency>

    <!-- ahocorasick, An algorithm used for efficient searching and 
    replacing of multiple strings.
    https://mvnrepository.com/artifact/org.ahocorasick/ahocorasick -->
    <dependency>
        <groupId>org.ahocorasick</groupId>
        <artifactId>ahocorasick</artifactId>
        <version>0.4.0</version>
    </dependency>
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.