Пытаетесь найти все экземпляры ключевого слова НЕ в комментариях или литералах?

Я пытаюсь найти все экземпляры ключевого слова "public" в некотором Java-коде (со скриптом Python), которые не находятся в комментариях или строках, то есть не найдены после //, между /* и */, а не между ними двойные или одинарные кавычки, и которые не являются частью имен переменных, т. е. им должен предшествовать пробел, табуляция или новая строка, а за ними должно следовать то же самое.

Итак, что у меня есть на данный момент -

//.*\spublic\s.*\n
/\*.*\spublic\s.*\*/
".*\spublic\s.*"
'.*\spublic\s.*'

Я вообще все путаю?

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

Я понял, что это, вероятно, использует отрицательный взгляд вперед и взгляд назад, но я все еще не могу собрать его воедино. Кроме того, для регулярного выражения /**/ меня беспокоит, что .* не соответствует новой строке, поэтому он не сможет распознать, что это public находится в комментарии:

/*
public
*/

Все, что ниже этого пункта, является моим размышлением на бумаге и может быть проигнорировано. Эти мысли не совсем точны.


Редактировать:

Я осмелюсь сказать, что (?<!//).*public.* будет соответствовать чему угодно, кроме однострочных комментариев, так что я немного разобрался. Я думаю. Но все равно непонятно, как все совместить.

Редактировать2:

Итак, следуя этой идее, я |проверил их все, чтобы получить--

(?<!//).*public.*|(?<!/\*).*public.\*/(?!\*/)|(?<!").*public.*(?!")|(?<!').*public.*(?!')

Но я не уверен в этом. //public не будет соответствовать первому альтернативному варианту, но будет соответствовать второму. Мне нужно И смотреть вперед и смотреть назад, а не ИЛИ все это.


person temporary_user_name    schedule 11.12.2012    source источник
comment
Это может немного помочь. (?(id) yes|no) в основном является регулярным выражением Python, если оператор. Если это может быть более простой способ проверить, каким было первое совпадение. Вы также можете сопоставить предыдущее совпадение, используя (?P=name) или просто \number, если вы не используете именованные группы.   -  person aquavitae    schedule 11.12.2012
comment
Вы можете ограничить, какая их комбинация может быть проигнорирована для вас. Пример: " /* \" " + public + " \" \*/ \\\" " Является ли слово общедоступным внутри или снаружи? Отсутствие подстроки является безопасным признаком начала/конца комментария/строки, если вы не отслеживаете состояние с начала файла. (Я внутри строки/комментария или кода программы? Количество обратных слэшей перед кавычками четное или нечетное?) Если чудесным образом все реализовано регулярным выражением, то оно может рекурсивно повторять что-то сложное в каждой позиции и быть в тысячу раз медленнее, чем обычно.   -  person hynekcer    schedule 11.12.2012
comment
будет ли решение в виде: [m.span('keyword') for m in some_regex.finditer(text) if m.groupdict()['keyword'] is not None] приемлемым?   -  person jfs    schedule 14.12.2012


Ответы (4)


Извините, но я должен сообщить вам новость, что то, что вы пытаетесь сделать, невозможно. Причина в основном в том, что Java не является обычным языком. Как мы все уже знаем, большинство движков регулярных выражений предоставляют нерегулярные функции, но в Python, в частности, не хватает чего-то вроде рекурсии (PCRE) или групп балансировки (.NET), которые могли бы помочь. Но давайте рассмотрим это более подробно.

Прежде всего, почему ваши паттерны не так хороши, как вы думаете? (для задачи сопоставления public внутри этих литералов; аналогичные проблемы будут применяться к обращению логики)

Как вы уже поняли, у вас будут проблемы с разрывами строк (в случае /*...*/). Это можно решить, используя модификатор/опцию/флаг re.S (который изменяет поведение .) или используя [\s\S] вместо . (поскольку первый соответствует любому символу).

Но есть и другие проблемы. Вам нужно только найти окружающие вхождения строки или литералы комментариев. На самом деле вы не гарантируете, что они специально обернуты вокруг рассматриваемого public. Я не уверен, сколько вы можете поместить в одну строку в Java, но если у вас есть произвольная строка, затем public, а затем другая строка в одной строке, тогда ваше регулярное выражение будет соответствовать public, потому что оно может найти " до и после него. Даже если это невозможно, если у вас есть два блочных комментария на одном входе, то любой public между этими двумя блочными комментариями вызовет совпадение. Таким образом, вам нужно найти способ утверждать только то, что ваш public действительно внутри "..." или /*...*/, а не только то, что эти литералы можно найти где угодно слева или справа от него.

Следующее: совпадения не могут перекрываться. Но ваше совпадение включает в себя все, от начального до конечного литерала. Итак, если бы у вас было "public public", это привело бы только к одному совпадению. И захват здесь не поможет. Обычно хитрость, позволяющая избежать этого, заключается в использовании обходных путей (которые не включены в сопоставление). Но (как мы увидим позже) просмотр назад работает не так хорошо, как вы думаете, потому что он не может быть произвольной длины (это возможно только в .NET).

Теперь хуже всего. Что делать, если у вас есть " внутри комментария? Это не должно считаться, верно? Что делать, если у вас есть // или /* или */ внутри строки? Это не должно считаться, верно? Как насчет ' внутри "-строк и " внутри '-строк? Хуже того, как насчет \" внутри "-строки? Таким образом, для 100% надежности вам придется выполнить аналогичную проверку и для окружающих разделителей. И обычно именно здесь регулярные выражения достигают предела своих возможностей, и именно поэтому вам нужен правильный синтаксический анализатор, который проходит по входной строке и строит целое дерево вашего кода.

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

Но осталось еще одно. Что (?<!//).*public.* не работает? Для этого достаточно, чтобы (?<!//) совпадало в любой отдельной позиции. например если бы вы только что ввели // public, движок попробовал бы отрицательный поиск назад прямо в начале строки (слева от начала строки), не нашел бы //, затем использовал бы .* для использования // и пробела, а затем соответствует public. На самом деле вам нужно (?<!//.*)public. Это запустит просмотр назад с начальной позиции public и просмотрит всю текущую строку влево. Но... это просмотр назад переменной длины, который поддерживается только .NET.

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

public(?=[^"]*("[^"]*"[^"]*)*$)

Теперь, если мы очень постараемся, мы также можем игнорировать экранированные кавычки внутри строки:

public(?=[^"]*("(?:[^"\\]|\\.)*"[^"]*)*$)

Поэтому, как только мы столкнемся с ", мы примем либо символы без кавычек, символы без обратной косой черты, либо символ обратной косой черты и все, что следует за ним (это также позволяет экранировать символы обратной косой черты, так что в "a string\\" мы не будем обрабатывать закрывающий " как сбежавший). Мы можем использовать это с многострочным режимом (re.M), чтобы избежать перехода к концу ввода (потому что конца строки достаточно):

public(?=[^"\r\n]*("(?:[^"\r\n\\]|\\.)*"[^"\r\n]*)*$)

(re.M подразумевается для всех последующих паттернов)

Вот что он ищет для строк в одинарных кавычках:

public(?=[^'\r\n]*('(?:[^'\r\n\\]|\\.)*'[^'\r\n]*)*$)

Для блочных комментариев это немного проще, потому что нам нужно искать только /* или конец строки (на этот раз действительно конец всей строки), никогда не встречая */ на пути. Это делается с отрицательным просмотром каждой позиции до конца поиска:

public(?=(?:(?![*]/)[\s\S])*(?:/[*]|\Z))

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

public(?=[^"\r\n]*("(?:[^"\r\n\\]|\\.)*"[^"\r\n]*)*$)(?=[^'\r\n]*('(?:[^'\r\n\\]|\\.)*'[^'\r\n]*)*$)(?=(?:(?![*]/)[\s\S])*(?:/[*]|\Z))

А как насчет однострочных комментариев? Трюк для эмуляции просмотра назад с переменной длиной обычно состоит в том, чтобы поменять местами строку и шаблон, что делает просмотр назад:

cilbup(?!.*//)

Конечно, это означает, что мы должны изменить и все остальные шаблоны. Хорошая новость заключается в том, что если мы не заботимся об экранировании, они выглядят точно так же (поскольку и кавычки, и блочные комментарии симметричны). Таким образом, вы можете запустить этот шаблон на обратном вводе:

cilbup(?=[^"\r\n]*("[^"\r\n]*"[^"\r\n]*)*$)(?=[^'\r\n]*('[^'\r\n]*'[^'\r\n]*)*$)(?=(?:(?![*]/)[\s\S])*(?:/[*]|\Z))(?!.*//)

Затем вы можете найти соответствующие позиции в вашем фактическом вводе, используя inputLength -foundMatchPosition - foundMatchLength.

Что теперь с побегом? Теперь это довольно раздражает, потому что нам приходится пропускать кавычки, если за ними после стоит обратная косая черта. Из-за некоторых проблем с возвратом нам нужно позаботиться об этом в пяти местах. Три раза при использовании символов без кавычек (поскольку теперь нам также нужно разрешить "\. И дважды при использовании символов в кавычках (используя отрицательный просмотр вперед, чтобы убедиться, что после них нет обратной косой черты). Давайте посмотрим на двойные кавычки:

cilbup(?=(?:[^"\r\n]|"\\)*(?:"(?!\\)(?:[^"\r\n]|"\\)*"(?!\\)(?:[^"\r\n]|"\\)*)*$)

(Это выглядит ужасно, но если вы сравните его с паттерном, не учитывающим бегство, вы заметите несколько отличий.)

Итак, включив это в приведенный выше шаблон:

cilbup(?=(?:[^"\r\n]|"\\)*(?:"(?!\\)(?:[^"\r\n]|"\\)*"(?!\\)(?:[^"\r\n]|"\\)*)*$)(?=(?:[^'\r\n]|'\\)*(?:'(?!\\)(?:[^'\r\n]|'\\)*'(?!\\)(?:[^'\r\n]|'\\)*)*$)(?=(?:(?![*]/)[\s\S])*(?:/[*]|\Z))(?!.*//)

Так что это может на самом деле сделать это во многих случаях. Но, как видите, это ужасно, почти невозможно читать и определенно невозможно поддерживать.

Какие были предостережения? Никаких литералов комментариев внутри строк, никаких строковых литералов внутри строк другого типа, никаких строковых литералов внутри комментариев. Кроме того, у нас есть четыре независимых просмотра вперед, которые, вероятно, займут некоторое время (по крайней мере, я думаю, что у меня аннулирована большая часть обратного отслеживания).

В любом случае, я считаю, что это максимально приближено к регулярным выражениям.

ИЗМЕНИТЬ:

Я только что понял, что забыл условие, согласно которому public не должно быть частью более длинного литерала. Вы включили пробелы, но что, если это первое, что во входных данных? Проще всего было бы использовать \b. Это соответствует положению (без включения окружающих символов), которое находится между символом слова и символом, не являющимся словом. Однако идентификаторы Java могут содержать любую букву или цифру Unicode, и я не уверен, поддерживает ли Python \b Unicode. Кроме того, идентификаторы Java могут содержать $. Который в любом случае сломает это. Осмотры спешат на помощь! Вместо того, чтобы утверждать, что с каждой стороны есть пробел, давайте утверждаем, что не пробела нет. Поскольку для этого нам нужны отрицательные обходные пути, мы получим преимущество в том, что не будем включать эти символы в совпадение бесплатно:

(?<!\S)cilbup(?!\S)(?=(?:[^"\r\n]|"\\)*(?:"(?!\\)(?:[^"\r\n]|"\\)*"(?!\\)(?:[^"\r\n]|"\\)*)*$)(?=(?:[^'\r\n]|'\\)*(?:'(?!\\)(?:[^'\r\n]|'\\)*'(?!\\)(?:[^'\r\n]|'\\)*)*$)(?=(?:(?![*]/)[\s\S])*(?:/[*]|\Z))(?!.*//)

И поскольку просто прокручивая этот фрагмент кода вправо, невозможно понять, насколько смехотворно велико это регулярное выражение, вот оно в режиме свободного пространства (re.X) с некоторыми аннотациями:

(?<!\S)      # make sure there is no trailing non-whitespace character
cilbup       # public
(?!\S)       # make sure there is no leading non-whitespace character
(?=          # lookahead (effectively lookbehind!) to ensure we are not inside a
             # string
  (?:[^"\r\n]|"\\)*
             # consume everything except for line breaks and quotes, unless the
             # quote is followed by a backslash (preceded in the actual input)
  (?:        # subpattern that matches two (unescaped) quotes
    "(?!\\)  # a quote that is not followed by a backslash
    (?:[^"\r\n]|"\\)*
             # we've seen that before
    "(?!\\)  # a quote that is not followed by a backslash
    (?:[^"\r\n]|"\\)*
             # we've seen that before
  )*         # end of subpattern - repeat 0 or more times (ensures even no. of ")
  $          # end of line (start of line in actual input)
)            # end of double-quote lookahead
(?=(?:[^'\r\n]|'\\)*(?:'(?!\\)(?:[^'\r\n]|'\\)*'(?!\\)(?:[^'\r\n]|'\\)*)*$)
             # the same horrible bastard again for single quotes
(?=          # lookahead (effectively lookbehind) for block comments
  (?:        # subgroup to consume anything except */
    (?![*]/) # make sure there is no */ coming up
    [\s\S]   # consume an arbitrary character
  )*         # repeat
  (?:/[*]|\Z)# require to find either /* or the end of the string
)            # end of lookahead for block comments
(?!.*//)     # make sure there is no // on this line
person Martin Ender    schedule 13.12.2012
comment
О Боже. Я не буду читать это до завтра. Но я взволнован. - person temporary_user_name; 13.12.2012
comment
@Lindrian, пожалуйста, попробуйте с этим регулярным выражением;). Сайт не поддерживает обходы (пока?) - person Martin Ender; 14.12.2012
comment
@m.buettner Ах! Это потому, что вы используете неэкранированную косую черту в регулярном выражении. Поскольку используемые разделители представляют собой косую черту, это приводит к тому, что они раздвигаются. Вот исправленная версия: regex101.com/r/hC1uE4. - person Firas Dib; 14.12.2012
comment
@ Линдриан, ага. ошибка, которую я получил, казалось, указывала на просмотр вперед - person Martin Ender; 14.12.2012
comment
@Aerovistae: имейте в виду, что, как прямо сказано в ответе, то, что вы на самом деле хотите сделать, невозможно; это показывает вам, как пройти около 80% пути, и объясняет, как вы могли бы получить часть остатка, если бы использовали замену библиотеки регулярных выражений, но нет никакого способа пройти весь путь. (Кроме того, как только вы начнете работать с этими функциями, вы должны быть очень осторожны, чтобы не достичь экспоненциальной производительности.) Таким образом, это действительно неправильный способ решить вашу проблему. Но пока вы это понимаете, читать интересно. :) - person abarnert; 15.12.2012
comment
@abarnert о да, я прекрасно об этом знаю. - person temporary_user_name; 15.12.2012
comment
@ m.buettner Я пытался это прочитать. Но ничего себе, я заблудился. Я могу следить за любым отдельным компонентом, но сумма просто поражает меня. Ты чертовски хороший заклинатель регулярных выражений. - person temporary_user_name; 15.12.2012
comment
@Aerovistae, если вы понимаете отдельные прогнозы, в их объединении действительно нет никакой магии. x(?=.*y) находит x, за которым следует y´ (at any later point in the string). x(?=.*z)` делает то же самое для x, за которым следует z. но после выполнения предпросмотра движок возвращается туда, где он начался (вот почему это просмотрнаперед). так и условия, вы можете просто использовать x(?=.*y)(?=.*z). - person Martin Ender; 15.12.2012
comment
должна быть возможность разделить ввод на последовательность комментариев, строковых литералов, public ключевых слов и игнорировать остальные без рекурсивных возможностей регулярных выражений, т. е. лексическая структура Java регулярна (или подмножество, необходимое для извлечения public, равно обычный). - person jfs; 15.12.2012
comment
@ J.F.Sebastian J.F.Sebastian, хм, да, подход, который также собирает комментарии и строки, может сработать. затем вы можете отсортировать все комментарии и строки на втором этапе, и у вас останется public совпадений. - person Martin Ender; 15.12.2012

Рассматривали ли вы замену всех комментариев и строковых литералов в одинарных и двойных кавычках нулевыми строками с помощью метода re sub(). Затем просто выполните простой поиск/сопоставление/нахождение полученного файла для слова, которое вы ищете?

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

person Don O'Donnell    schedule 11.12.2012
comment
Отличная идея. Но на данный момент я хочу сделать это с помощью регулярного выражения. Я хочу знать, как это сделать. - person temporary_user_name; 11.12.2012
comment
Похоже, вам нужно будет сделать отрицательный взгляд назад и вперед, но это работает только для фиксированного количества символов. Вы можете кое-что узнать, взглянув на модуль tokenize в stdlib. Если бы вы анализировали код Python, а не Java, было бы проще использовать tokenize напрямую, чтобы найти слова без кавычек и без комментариев. - person Don O'Donnell; 11.12.2012
comment
замена многострочных комментариев может привести к искажению номеров строк. - person jfs; 11.12.2012

Вы можете использовать pyparsing, чтобы найти ключевое слово public вне комментария или строки в двойных кавычках:

from pyparsing import Keyword, javaStyleComment, dblQuotedString

keyword = "public"
expr = Keyword(keyword).ignore(javaStyleComment | dblQuotedString)

Пример

for [token], start, end in expr.scanString(r"""{keyword} should match
    /*
    {keyword} should not match "
    */
    // this {keyword} also shouldn't match
    "neither this \" {keyword}"
    but this {keyword} will
    re{keyword} is ignored
    '{keyword}' - also match (only double quoted strings are ignored)
    """.format(keyword=keyword)):
    assert token == keyword and len(keyword) == (end - start)
    print("Found at %d" % start)

Вывод

Found at 0
Found at 146
Found at 187

Чтобы игнорировать строку с одинарными кавычками, вы можете использовать quotedString вместо dblQuotedString.

Чтобы сделать это только с регулярными выражениями, см. тег regex-negation на SO, например, Регулярное выражение для сопоставления строки, не содержащей слова? или даже с меньшими возможностями регулярных выражений Регулярное выражение: сопоставление путем исключения, без упреждения - возможно ли это?. Простым способом было бы использовать положительное совпадение и пропускать совпадающие комментарии, строки в кавычках. Результат – остальные матчи.

person jfs    schedule 11.12.2012

Это поиск противоположного, потому что это именно то, о чем вы просите. :)

Я не знаю способа сопоставить их все в одном регулярном выражении (хотя теоретически это должно быть возможно, поскольку обычные языки закрыты для дополнений и пересечений). Но вы определенно можете искать все экземпляры public, а затем удалять любые экземпляры, которые соответствуют одному из ваших «плохих» регулярных выражений. Попробуйте использовать, например, set.difference для свойств match.start и match.end из re.finditer.

person Danica    schedule 11.12.2012
comment
Ах, но теперь это стало проблемой для моих навыков регулярных выражений. Я должен понять, как победить эту штуку. - person temporary_user_name; 11.12.2012