Javascript: отрицательный взгляд за эквивалентом?

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

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

РЕДАКТИРОВАТЬ: Это регулярное выражение, которое я хотел бы работать, но это не так:

(?<!([abcdefg]))m

Так что это будет соответствовать "м" в "Джим" или "м", но не "джем"

11 ответов

Решение

В 2018 году утверждения утверждений были приняты в спецификации ECMAScript. Это было реализовано в V8 и поставляется без флагов с Google Chrome v62 и в Node.js v6 за флагом и v9 без флага. Итак, если вы разрабатываете для среды Chrome (например, Electron) или Node, вы можете начать использовать lookbehinds уже сегодня!

Положительный взгляд за использование:

console.log(
  "$9.99  €8.47".match(/(?<=\$)\d+(\.\d*)?/) // Matches "9.99"
);

Отрицательный взгляд за использование:

console.log(
  "$9.99  €8.47".match(/(?<!\$)\d+(?:\.\d*)/) // Matches "8.47"
);

Поддержка на других платформах:

  • Mozilla Firefox работает над этим: отслеживается здесь.
  • Microsoft Edge тоже работает над этим: отслеживается здесь (голосовое предложение пользователя).

Поскольку Javascript поддерживает негативную перспективу, один из способов сделать это:

  1. перевернуть строку ввода

  2. совпадать с обратным регулярным выражением

  3. отменить и переформатировать матчи


const reverse = s => s.split('').reverse().join('');

const test = (stringToTests, reversedRegexp) => stringToTests
  .map(reverse)
  .forEach((s,i) => {
    const match = reversedRegexp.test(s);
    console.log(stringToTests[i], match, 'token:', match ? reverse(reversedRegexp.exec(s)[0]) : 'Ø');
  });

Пример 1:

После вопроса Эндрю Энсли:

test(['jim', 'm', 'jam'], /m(?!([abcdefg]))/)

Выходы:

jim true token: m
m true token: m
jam false token: Ø

Пример 2:

После комментария @neaumusic (соответствует max-height но нет line-heightзнаковое существо height):

test(['max-height', 'line-height'], /thgieh(?!(-enil))/)

Выходы:

max-height true token: height
line-height false token: Ø

Предположим, вы хотите найти все int не предшествует unsigned:

С поддержкой негативного поиска:

(?<!unsigned )int

Без поддержки негативных взглядов:

((?!unsigned ).{9}|^.{0,8})int

По сути, идея состоит в том, чтобы захватить n предшествующих символов и исключить совпадение с отрицательным прогнозом, но также сопоставить случаи, когда нет предшествующих n символов. (где n - длина оглядки назад).

Итак, регулярное выражение в вопросе:

(?<!([abcdefg]))m

будет переводить на:

((?!([abcdefg])).|^)m

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

Стратегия Mijoja работает для вашего конкретного случая, но не в целом:

js>newString = "Fall ball bill balll llama".replace(/(ba)?ll/g,
   function($0,$1){ return $1?$0:"[match]";});
Fa[match] ball bi[match] balll [match]ama

Вот пример, где цель состоит в том, чтобы соответствовать двойному l, но не если ему предшествует "ba". Обратите внимание на слово "balll" - истинный взгляд сзади должен был подавить первые 2 л, но соответствовать 2-й паре. Но, сопоставляя первые 2 л и затем игнорируя это совпадение как ложное срабатывание, механизм регулярных выражений исходит из конца этого совпадения и игнорирует все символы в ложном положительном результате.

Использование

newString = string.replace(/([abcdefg])?m/, function($0,$1){ return $1?$0:'m';});

Вы можете определить группу без захвата, отрицая ваш набор символов:

(?:[^a-g])m

... который будет соответствовать каждому m НЕ предшествует ни одна из этих букв.

Вот как я добился str.split(/(?<!^)@/) для Node.js 8 (который не поддерживает lookbehind):

str.split('').reverse().join('').split(/@(?!$)/).map(s => s.split('').reverse().join('')).reverse()

Работает? Да (Юникод не проверен). Неприятно? Да.

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

Бьюсь об заклад, что нет способа найти регулярное выражение без просмотра назад, которое дает точный результат. Все, что вы можете делать, это работать с группами. Предположим, у вас есть регулярное выражение(?<!Before)Wanted, где Wanted регулярное выражение, которое вы хотите сопоставить, и Beforeэто регулярное выражение, которое подсчитывает то, что не должно предшествовать совпадению. Лучшее, что вы можете сделать, это отрицать регулярное выражениеBefore и используйте регулярное выражение NotBefore(Wanted). Желаемый результат - первая группа$1.

В твоем случае Before=[abcdefg] что легко отрицать NotBefore=[^abcdefg]. Таким образом, регулярное выражение будет[^abcdefg](m). Если вам нужна должностьWantedвы должны сгруппировать NotBefore тоже, так что желаемый результат - вторая группа.

Если совпадения Before узор имеет фиксированную длину n, то есть, если шаблон не содержит повторяющихся токенов, вы можете избежать отрицания Before шаблон и используйте регулярное выражение (?!Before).{n}(Wanted), но все равно придется использовать первую группу или регулярное выражение (?!Before)(.{n})(Wanted)и воспользуйтесь второй группой. В этом примере шаблонBefore на самом деле имеет фиксированную длину, а именно 1, поэтому используйте регулярное выражение (?![abcdefg]).(m) или (?![abcdefg])(.)(m). Если вас интересуют все совпадения, добавьтеg flag, см. мой фрагмент кода:

function TestSORegEx() {
  var s = "Donald Trump doesn't like jam, but Homer Simpson does.";
  var reg = /(?![abcdefg])(.{1})(m)/gm;
  var out = "Matches and groups of the regex " + 
            "/(?![abcdefg])(.{1})(m)/gm in \ns = \"" + s + "\"";
  var match = reg.exec(s);
  while(match) {
    var start = match.index + match[1].length;
    out += "\nWhole match: " + match[0] + ", starts at: " + match.index
        +  ". Desired match: " + match[2] + ", starts at: " + start + ".";   
    match = reg.exec(s);
  }
  out += "\nResulting string after statement s.replace(reg, \"$1*$2*\")\n"
         + s.replace(reg, "$1*$2*");
  alert(out);
}

Следуя идее Mijoja и опираясь на проблемы, выявленные JasonS, у меня появилась эта идея; я немного проверил, но не уверен в себе, так что проверка кем-то более опытным, чем я, в js regex была бы отличной:)

var re = /(?=(..|^.?)(ll))/g
         // matches empty string position
         // whenever this position is followed by
         // a string of length equal or inferior (in case of "^")
         // to "lookbehind" value
         // + actual value we would want to match

,   str = "Fall ball bill balll llama"

,   str_done = str
,   len_difference = 0
,   doer = function (where_in_str, to_replace)
    {
        str_done = str_done.slice(0, where_in_str + len_difference)
        +   "[match]"
        +   str_done.slice(where_in_str + len_difference + to_replace.length)

        len_difference = str_done.length - str.length
            /*  if str smaller:
                    len_difference will be positive
                else will be negative
            */

    }   /*  the actual function that would do whatever we want to do
            with the matches;
            this above is only an example from Jason's */



        /*  function input of .replace(),
            only there to test the value of $behind
            and if negative, call doer() with interesting parameters */
,   checker = function ($match, $behind, $after, $where, $str)
    {
        if ($behind !== "ba")
            doer
            (
                $where + $behind.length
            ,   $after
                /*  one will choose the interesting arguments
                    to give to the doer, it's only an example */
            )
        return $match // empty string anyhow, but well
    }
str.replace(re, checker)
console.log(str_done)

мой личный вывод:

Fa[match] ball bi[match] bal[match] [match]ama

принцип заключается в том, чтобы позвонить checker в каждой точке строки между любыми двумя символами, когда эта позиция является начальной точкой:

--- любая подстрока размером с то, что не требуется (здесь 'ba'таким образом ..) (если этот размер известен; в противном случае, возможно, сделать это будет сложнее)

--- --- или меньше, чем это, если это начало строки: ^.?

и, после этого,

--- что нужно искать (здесь 'll').

При каждом вызове checker, будет тест, чтобы проверить, если значение до ll это не то, что мы не хотим (!== 'ba'); если это так, мы вызываем другую функцию, и она должна быть этой (doer), который внесет изменения в str, если целью является эта, или, в более общем смысле, будет вводить во входные данные, необходимые для ручной обработки результатов сканирования str,

здесь мы меняем строку, поэтому нам нужно было отслеживать разницу в длине, чтобы сместить места, заданные replace, все рассчитано на str, который сам по себе никогда не меняется.

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

я думаю, что с точки зрения производительности это должно быть довольно резким: все эти бессмысленные замены "в", this str.length-1 время, плюс здесь ручная замена делающим, что означает много нарезки... вероятно, в этом конкретном вышеупомянутом случае, который можно сгруппировать, разрезая строку только один раз на части вокруг того места, где мы хотим вставить [match] а также .join()с этим [match] сам.

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

И в checker, в случае множественных возможностей нежелательных значений для $ позади, мы должны будем проверить его с помощью еще одного регулярного выражения (которое будет кэшировано (создано) снаружи). checker Лучше всего избегать создания одного и того же объекта регулярного выражения при каждом вызове checker) знать, стоит ли этого избегать.

надеюсь, я был чист; если не стесняйтесь, я постараюсь лучше.:)

Используя ваш случай, если вы хотите заменить m с чем-то, например, преобразовать его в верхний регистр MМожно отменить набор в группу захвата.

матч ([^a-g])m, заменить $1M

"jim jam".replace(/([^a-g])m/g, "$1M")
\\jiM jam

([^a-g]) будет соответствовать любой символ не (^) в a-g диапазон и сохранить его в первой группе захвата, так что вы можете получить к нему доступ с $1,

Итак, мы находим im в jim и заменить его на iM что приводит к jiM,

Это эффективно делает это

"jim".match(/[^a-g]m/)
> ["im"]
"jam".match(/[^a-g]m/)
> null

Пример поиска и замены

"jim jam".replace(/([^a-g])m/g, "$1M")
> "jiM jam"

Обратите внимание, что отрицательная строка поиска должна быть длиной 1 символ, чтобы это работало.

/(?![abcdefg])[^abcdefg]m/giда, это трюк

Это может помочь, в зависимости от контекста:

Это соответствует m в jim, но не jam:

"jim jam".replace(/[a-g]m/g, "").match(/m/g)
Другие вопросы по тегам