Как элегантно фильтровать деликатные слова

Java задняя часть алгоритм XSS
Как элегантно фильтровать деликатные слова

Функция фильтрации конфиденциальных слов используется во многих местах.Теоретически, в веб-приложениях, если задействован пользовательский ввод, требуется проверка текста, например: проверка XSS, проверка внедрения SQL, фильтрация конфиденциальных слов и т. д. Сегодня я сосредоточусь на том, как элегантно и эффективно реализовать фильтрацию чувствительных слов.

Схема фильтрации деликатных слов 1

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


   @Test
    public void test1(){
        Set<String>  sensitiveWords=new HashSet<>();
        sensitiveWords.add("shit");
        sensitiveWords.add("傻逼");
        sensitiveWords.add("笨蛋");
        String text="你是傻逼啊";
        for(String sensitiveWord:sensitiveWords){
            if(text.contains(sensitiveWord)){
                System.out.println("输入的文本存在敏感词。——"+sensitiveWord);
                break;
            }
        }
    }

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

Схема фильтрации деликатных слов 2

Я проверил схему фильтрации чувствительных слов в Интернете и нашел алгоритм под названием DFA, то есть алгоритм детерминированного конечного автомата, который переводится на китайский язык для определения алгоритма конечного автомата. Его основная идея состоит в том, чтобы извлекать чувствительные слова на основе перехода состояния.Все чувствительные слова могут быть обнаружены только путем однократного сканирования текста, поэтому эффективность намного выше, чем у схемы 1.

Предположим, у нас есть следующие 5 чувствительных слов для обнаружения: идиот, дурак, глупый большой, плохой парень, плохой парень. Затем мы можем сначала объединить слова с одним и тем же префиксом в чувствительных словах в древовидную структуру, а слова с разными префиксами принадлежат разным ветвям дерева, Взяв в качестве примера пять вышеупомянутых чувствительных слов, можно инициализировать следующие два дерева:

В чем преимущество составления чувствительных слов в древовидную структуру? Самым большим преимуществом является то, что это может уменьшить количество запросов. Нам нужно только один раз просмотреть текст, который нужно обнаружить, а затем получить поддерево, соответствующее символу в чувствительном тезаурусе. Если соответствующего поддерева нет, это означает, что в настоящее время обнаруженный символ отсутствует. В чувствительном тезаурусе пропустить сразу и продолжить обнаружение следующего символа; если есть соответствующее поддерево, то проверить, является ли следующий символ дочерним узлом поддерева, соответствующего предыдущему символу, так что повторяя, вы можете найти ожидание Проверить, содержит ли текст конфиденциальные слова.

Возьмем в качестве примера текст «Ты идиот», мы определяем каждый символ по очереди, потому что первых 4 символов нет в чувствительном лексиконе, и соответствующее поддерево не может быть найдено, поэтому оно пропускается напрямую. Когда обнаруживается слово «глупый», обнаруживается, что в чувствительном тезаурусе есть соответствующее поддерево, мы записываем его как дерево-1, а затем ищем, является ли следующий символ «сила» дочерним узлом дерева поддерева. 1, и найдите это Точно. Далее мы оценим, является ли символ «Сила» конечным узлом. Если это так, это означает, что совпало чувствительное слово. Здесь символ «Сила» оказывается конечным узлом tree-1, поэтому чувствительное слово успешно извлечено Word: "stupid". Вы нашли его? В процессе поиска нам нужно просканировать обнаруженный текст только один раз, а для чувствительных слов, которых нет в обнаруженном тексте, таких как «плохой парень» и «плохой парень» в этом примере, мы полностью сканироваться не будет, поэтому эффективность значительно повышается по сравнению со схемой 1.

В Java мы можем использовать HashMap для хранения вышеуказанной древовидной структуры или взять в качестве примера приведенные выше конфиденциальные слова, мы разделяем каждую строку конфиденциального слова на символы, а затем сохраняем их в HashMap, который можно хранить следующим образом:


{
    "傻": {
        "逼": {
            "isEnd": "Y"
        },
        "子": {
            "isEnd": "Y"
        },
        "大": {
            "个": {
                "isEnd": "Y"
            }
        }
    }
}

Сначала в качестве ключа используется первый символ каждого слова, значением является другой HashMap, ключ HashMap, соответствующий значению, является вторым символом, и если есть третий символ, второй символ сохраняется в качестве ключа , Конечно, это значение по-прежнему является HashMap, и так далее, до последнего символа, конечно, значение, соответствующее последнему символу, также является HashMap, но этому HashMap нужно только хранить конечный флаг, как в приведенном выше Например, мы сохраняем HashMap {"isEnd","Y"}, чтобы указать, что ключ, соответствующий этому значению, является последним символом конфиденциального слова.

Точно так же два чувствительных слова «плохой парень» и «плохой парень» также хранятся таким образом и не будут здесь перечислены.

Каковы преимущества использования хранилища HashMap? Мы знаем, что HashMap можно запрашивать с временной сложностью O(1) в идеальных условиях, поэтому в процессе обхода обнаруживаемой строки мы можем узнать, находится ли текущий символ в чувствительном слове с временной сложностью O(1). библиотеки, эффективность намного выше, чем у схемы 1.

Код следующий.

Первый заключается в инициализации чувствительного тезауруса:


   private Map<Object,Object> sensitiveWordsMap;

    private static final String END_FLAG="end";

    private void initSensitiveWordsMap(Set<String> sensitiveWords){
        if(sensitiveWords==null||sensitiveWords.isEmpty()){
            throw new IllegalArgumentException("Senditive words must not be empty!");
        }
        sensitiveWordsMap=new HashMap<>(sensitiveWords.size());
        String currentWord;
        Map<Object,Object> currentMap;
        Map<Object,Object> subMap;
        Iterator<String> iterator = sensitiveWords.iterator();
        while (iterator.hasNext()){
            currentWord=iterator.next();
            if(currentWord==null||currentWord.trim().length()<2){  //敏感词长度必须大于等于2
                continue;
            }
            currentMap=sensitiveWordsMap;
            for(int i=0;i<currentWord.length();i++){
                char c=currentWord.charAt(i);
                subMap=(Map<Object, Object>) currentMap.get(c);
                if(subMap==null){
                    subMap=new HashMap<>();
                    currentMap.put(c,subMap);
                    currentMap=subMap;
                }else {
                    currentMap= subMap;
                }
                if(i==currentWord.length()-1){
                    //如果是最后一个字符,则put一个结束标志,这里只需要保存key就行了,value为null可以节省空间。
                    //如果不是最后一个字符,则不需要存这个结束标志,同样也是为了节省空间。
                    currentMap.put(END_FLAG,null);
                }
            }
        }
    }

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

Далее следует сканирование чувствительных слов:


public enum MatchType {

    MIN_MATCH("最小匹配规则"),MAX_MATCH("最大匹配规则");

    String desc;

    MatchType(String desc) {
        this.desc = desc;
    }
}

    public Set<String> getSensitiveWords(String text,MatchType matchType){
        if(text==null||text.trim().length()==0){
            throw new IllegalArgumentException("The input text must not be empty.");
        }
        Set<String> sensitiveWords=new HashSet<>();
        for(int i=0;i<text.length();i++){
            int sensitiveWordLength = getSensitiveWordLength(text, i, matchType);
            if(sensitiveWordLength>0){
                String sensitiveWord = text.substring(i, i + sensitiveWordLength);
                sensitiveWords.add(sensitiveWord);
                if(matchType==MatchType.MIN_MATCH){
                    break;
                }
                i=i+sensitiveWordLength-1;
            }
        }
        return sensitiveWords;
    }

    public int getSensitiveWordLength(String text,int startIndex,MatchType matchType){
        if(text==null||text.trim().length()==0){
            throw new IllegalArgumentException("The input text must not be empty.");
        }
        char currentChar;
        Map<Object,Object> currentMap=sensitiveWordsMap;
        int wordLength=0;
        boolean endFlag=false;
        for(int i=startIndex;i<text.length();i++){
            currentChar=text.charAt(i);
            Map<Object,Object> subMap=(Map<Object,Object>) currentMap.get(currentChar);
            if(subMap==null){
                break;
            }else {
                wordLength++;
                if(subMap.containsKey(END_FLAG)){
                    endFlag=true;
                    if(matchType==MatchType.MIN_MATCH){
                        break;
                    }else {
                        currentMap=subMap;
                    }
                }else {
                    currentMap=subMap;
                }
            }
        }
        if(!endFlag){
            wordLength=0;
        }
        return wordLength;
    }

Среди них MatchType представляет собой правило сопоставления. Иногда нам нужно только найти секретное слово, а иногда нам нужно знать, сколько конфиденциальных слов содержится в тексте, который нужно обнаружить. Первое соответствует принципу минимального соответствия, а второе соответствует максимальному соответствию в принципе.

Функция метода getSensitiveWordLength состоит в том, чтобы вычислить длину чувствительного слова в обнаруживаемом тексте на основе данного обнаруживаемого текста, начального нижнего индекса и правил сопоставления.Если он не существует, он вернет 0, и если он существует, он вернет совпавшее чувствительное слово длины.

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

Наконец, напишите тестовый пример для проверки:


   public static void main(String[] args) {
        Set<String> sensitiveWords=new HashSet<>();
        sensitiveWords.add("你是傻逼");
        sensitiveWords.add("你是傻逼啊");
        sensitiveWords.add("你是坏蛋");
        sensitiveWords.add("你个大笨蛋");
        sensitiveWords.add("我去年买了个表");
        sensitiveWords.add("shit");

        TextFilter textFilter=new TextFilter();
        textFilter.initSensitiveWordsMap(sensitiveWords);
        String text="你你你你是傻逼啊你,说你呢,你个大笨蛋。";
        System.out.println(textFilter.getSensitiveWords(text,MatchType.MAX_MATCH));
    }

В результате получается следующее:

Как видите, мы успешно отфильтровали деликатные слова.

Схема фильтрации деликатных слов 3

Второе решение может удовлетворить требования с точки зрения производительности, но его легко взломать, например, если я добавлю пробел между чувствительными словами в тексте, который необходимо обнаружить, его можно будет успешно обойти. Эту проблему несложно решить.Простым методом является инициализация библиотеки недопустимых символов, таких как пробелы, *, #, @ и другие символы, а затем удаление недопустимых символов в тексте, который необходимо обнаружить, до обнаружения текста. , так что если обнаруженные символы не существуют, эти недопустимые символы, поэтому вы можете продолжать использовать вторую схему для фильтрации. Пока обнаруженный текст не слишком длинный, нам нужно только еще раз отсканировать обнаруженный текст, чтобы удалить недопустимые символы на основе схемы 2, и эта потеря производительности все еще приемлема.

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

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

Полный код:GitHub.com/или Джеймс/TE…