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


142

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

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

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

(?<!([abcdefg]))m

Таким образом, это будет соответствовать «м» в «Джим» или «м», но не «джем»


Подумайте о том, чтобы опубликовать регулярное выражение так, чтобы оно выглядело негативно; это может облегчить ответ.
Даниэль ЛеЧеминант

1
Те, кто хочет отслеживать усыновление и т. Д., Обращаются к таблице совместимости ECMAScript 2016+
Wiktor Stribiżew

@ WiktorStribiżew: в спецификации 2018 года были добавлены осмотры. Chrome поддерживает их, но Firefox до сих пор не реализовал спецификацию .
Лонни Бест

Нужно ли это оглядываться назад? Как насчет (?:[^abcdefg]|^)(m)? Как в"mango".match(/(?:[^abcdefg]|^)(m)/)[1]
slebetman

Ответы:


58

Взгляд за утверждениями был принят в спецификации ECMAScript в 2018 году.

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

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

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

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

Поддержка платформы:


2
есть ли полифилл?
Килли

1
@ Килли, насколько я знаю, не так далеко, и я сомневаюсь, что когда-либо будет, так как его создание было бы потенциально очень непрактичным (IE пишет полную реализацию Regex в JS)
Okku

Как насчет использования плагина babel, возможно ли его скомпилировать до ES5 или уже поддерживаемого ES6?
Стефан Джей

1
@IlpoOksanen Я думаю, что вы имеете в виду расширение реализации RegEx ... что и делают полифиллы .... и нет ничего плохого в написании логики в JavaScript
neaumusic

1
О чем ты говоришь? Почти все предложения основаны на других языках, и они всегда предпочитают сопоставлять синтаксис и семантику других языков, если это имеет смысл в контексте идиоматической JS и обратной совместимости. Я думаю, что совершенно ясно заявил, что в спецификации 2018 года в 2017 году были приняты как положительные, так и отрицательные взгляды, и я дал ссылки на источники. Кроме того, я подробно описал, какие платформы реализуют указанную спецификацию и каков статус других платформ - и даже обновлял ее с тех пор. Естественно, это не последняя функция Regexp, которую мы увидим
Okku

83

Начиная с 2018 года утверждения типа «Сзади» являются частью спецификации языка ECMAScript .

// positive lookbehind
(?<=...)
// negative lookbehind
(?<!...)

Ответ до 2018 года

Поскольку 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: Ø

36
проблема с этим подходом состоит в том, что он не работает, когда у вас есть и lookahead, и lookbehind
kboom

3
Можете ли вы показать рабочий пример, скажем, я хочу соответствовать, max-heightно нет, line-heightи я только хочу, чтобы матч былheight
neaumusic

Не помогает, если задача состоит в замене двух последовательных идентичных символов (и не более 2), которым не предшествует какой-либо символ. ''(?!\()заменит апострофы ''(''test'''''''testс другого конца, оставив, (''test'NNNtestа не (''testNNN'test.
Wiktor Stribi'ew

61

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

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

(?<!unsigned )int

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

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

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

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

(?<!([abcdefg]))m

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

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

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


2
Это должен быть правильный ответ. Смотрите: "So it would match the 'm' in 'jim' or 'm', but not 'jam'".replace(/(j(?!([abcdefg])).|^)m/g, "$1[MATCH]") возвращается "So it would match the 'm' in 'ji[MATCH]' or 'm', but not 'jam'" Это довольно просто, и это работает!
Asrail

41

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

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 л и затем игнорируя это совпадение как ложное срабатывание, механизм регулярных выражений исходит из конца этого совпадения и игнорирует все символы в ложном положительном результате.


5
Ах, вы правы. Тем не менее, это намного ближе, чем я был раньше. Я могу принять это, пока не появится что-то лучшее (например, JavaScript, реализующий lookbehinds).
Эндрю Энсли

33

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

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

10
Это ничего не делает: newStringвсегда будет равным string. Почему так много голосов?
MikeM

@MikeM: потому что дело в том, чтобы просто продемонстрировать подходящую технику.
ошибка

57
@bug. Демонстрация, которая ничего не делает, является странной демонстрацией. Ответ выглядит так, как если бы он был просто скопирован и вставлен без какого-либо понимания того, как он работает. Таким образом, отсутствие сопутствующего объяснения и неспособность продемонстрировать, что что-либо было сопоставлено.
MikeM

2
@MikeM: правило SO гласит: если ответ на вопрос в том виде , в котором он написан , это правильно. ОП не указал вариант использования
ошибка

7
Концепция верна, но да, это не очень хорошо. Попробуйте запустить это в консоли JS ... "Jim Jam Momm m".replace(/([abcdefg])?m/g, function($0, $1){ return $1 ? $0 : '[match]'; });. Это должно вернуться Ji[match] Jam Mo[match][match] [match]. Но также обратите внимание, что, как упоминал Джейсон ниже, он может потерпеть неудачу в некоторых случаях.
Саймон Ист

11

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

(?:[^a-g])m

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


2
Я думаю, что матч на самом деле также охватит предыдущий символ.
Сэм

4
^ это правда. Класс персонажа представляет ... персонажа! Все, что делает ваша группа без захвата, не делает это значение доступным в контексте замены. Ваше выражение не говорит «каждому m НЕ предшествует ни одна из этих букв», оно говорит «каждому m предшествует символ, который НЕ является ни одной из этих букв»
theflowers of time

5
Чтобы ответ также решал исходную задачу (начало строки), он также должен включать параметр, поэтому полученное регулярное выражение будет (?:[^a-g]|^)m. См. Regex101.com/r/jL1iW6/2 для запуска примера.
Джонни Сковдал

Использование пустотной логики не всегда дает желаемый эффект.
GoldBishop

2

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

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

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


1

следуя идее 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случае множественных возможностей нежелательных значений для $ behind, нам нужно будет проверить его с еще одним регулярным выражением ( checkerлучше кэшировать (создавать) снаружи ), чтобы избежать создания того же самого объекта регулярного выражения. при каждом вызове checker) знать, действительно ли это то, чего мы стремимся избежать.

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


1

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

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

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

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

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


1

Как уже упоминалось ранее, 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флаг, посмотрите мой фрагмент кода:

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

0

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

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

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

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

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


1
Не совсем. В «Джиме» я не хочу «я»; просто "м". И "m".match(/[^a-g]m/)да, nullтоже. Я тоже хочу "м" в этом случае.
Эндрю Энсли

-1

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


5
Проверка (?![abcdefg])полностью избыточна, поскольку [^abcdefg]уже выполняет свою работу, чтобы предотвратить сопоставление этих символов.
nhahtdh

2
Это не будет соответствовать 'm' без предшествующих символов.
Эндрю Энсли
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.