Регулярная ловушка

    Допустим, мы хотим проверить регулярным выражением, что в строке есть хотя бы одна цифра. Пишем код:

    <script>

    var digits = /([0-9])+/g;

    function has_digit(s) { return digits.test(s); }

    </script>


    Этот код неверен.
    Вам очевидно — почему? Если нет — добро пожаловать под кат!



    Кстати, убедитесь сами:

    <script>

    document.write(has_digit("abc0xyz") + ' ' + has_digit("abc0xyz"));

    </script>


    Получаем:

    true false

    А должны, конечно:

    true true

    Функция has_digit() регулярно дает неверный ответ. Обратимся к всемирному разуму Сети.

    http://javascript.ru

    Чтобы просто узнать, подходит ли строка под регулярное выражение regexp, используйте regexp.test(string).

    Двойка, ответ неверный.

    http://www.regular-expressions.info

    The test() function of the RegExp object is a shortcut to exec() != null. It takes the subject string as a paarameter and returns true or false depending on whether the regex matches part of the string or not.

    Двойка, ответ неверный.

    И т.д.

    Long story short. Читаем стандарт.

    RegExp.prototype.test(string)
    Equivalent to the expression RegExp.prototype.exec(string)!=null.


    Значит дело в волшебных пузырьках функции RegExp.prototype.exec(string).

    Ее алгоритм таков: при наличии флага g у регулярного выражения поиск совпадений начинается со свойства lastIndex (это свойство регулярного выражения). Вначале оно равно нулю, но после удачного поиска обновляется позицией для поиска очередного совпадения.

    Вот и решение. У глобальных регулярных выражений функция test() не обязательно ищет совпадения с начала строки.

    Проблема усугубляется тем, что обычно регулярное выражение применяют в форме:

    /([0-9])+/g.test("abc")


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

    Разумеется, не я первый наступил на эти грабли.

    http://stackoverflow.com

    Удивляет, что большинство сетевых ресурсов по JavaScript оказались бесполезны.

    Надеюсь, кому-то эта информация сбережет драгоценное время…

    UDP

    Да, global flag в данном случае не нужен.

    Еще раз — если Вы поняли, что не так — под кат можно было и не ходить :-)

    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 54
    • +12
      Читайте «Mozilla Developer Network», там обычно полная информация. Вот и в данном случае developer.mozilla.org/en/JavaScript/Reference/Global_Objects/RegExp/test:
      As with exec (or in combination with it), test called multiple times on the same global regular expression instance will advance past the previous match.
      • 0
        Я обычно смотрю стандарт, но за ссылку спасибо.
        • +2
          Автор не раскрыл темы с позицией поиска.
          На самом деле, вполне возможно работать не пересоздавая объект регулярного выражения.

          Достаточно перед очередным поискoм lastIndex присвоить нулю, и всё будет работать как ожидается.
        • +16
          А зачем понадобилось вписвать сюда параметр /g?
          Разве не логичнее было его не писать вообще?
          • +30
            Если продолжить эту мысль, то вместо
            /([0-9])+/g
            можно записать сперва
            /([0-9])+/
            а затем
            /([0-9])/
            и наконец
            /[0-9]/
            • +1
              Также скобки в начальном выражении совершенно не нужны, первое упрощение могло выглядеть как /[0-9]+/.
              • +31
                /\d+/ ;)
                • +38
                  Там и "+" не нужен. В условии ведь было «хотя бы одна цифра». Значит, после нахождения первой цифры можно остановиться и вернуть true.
                  • +7
                    А если ещё убрать объявление регулярки перед функцией:

                    function has_digit(s) { return /\d/.test(s); }
                    


                    то автор вообще не столкнулся бы с проблемой :)
                    • +3
                      автор хотел показать пример с кэшированием регулярки, чтобы при каждом вызове не происходил повторный парсинг и создание объекта RegExp
                      • 0
                        Вот до чего доводят преждевременные «оптимизации»
                      • +4
                        Кое-где — столкнулся бы.
            • –38
              Очевидная вещь же. Если вы этого не знали, значит это лишь ваша оплошность и нехватка опыта. Таких вещей бесчисленное множество, зачем о каждой писать на Хабре? =)
              • +3
                Может оказатсья так, что он будет не единственным с данной проблемой. В этом то и смысл — набраться опыта на чужих ошибках :)
                • 0
                  Я бы не сказал, что подобных несоответствий так много.

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

                • –2
                  А теперь запускаем код в FF 3.6 или любом достаточно старом браузере и любуемся на результаты.
                  javascript: function test(d){return /([0-9])+/g.test(d)}; alert( [test('5'), test('5')] )


                  Автор так бежал, так спешил поделиться своей находкой, что до самой сути и не дошел. А если бы умел пользоваться поиском, нашел бы замечательную статью на javascript.ru.
                  • +4
                    Статью дальше ката вообще читали? Или так спешили поделиться своей находкой, что не хватило времени?
                    • 0
                      Нет, что Вы! Мне
                      /([0-9])+/g.test("abc")

                      Этот код не вызывает проблем.
                      с неба свалилось.
                      • 0
                        Зря умничаете. Отличие вашего кода и того кода, что привёл автор в том, что у вас при каждом тесте создаётся новое регулярное выражение, а у автора — используется одно и то же. Попробуйте что-то типа такого:
                        javascript: function test(d){return test.regexp.test(d)}; test.regexp =  /([0-9])+/g; alert( [test('5'), test('5')] )
                        
                        • +5
                          Эх, TheShock, а я считал Вас разбирающимся в JS…
                          Почитали бы хотя бы статью по ссылке
                          То есть, простыми словами, литеральный регэксп не создается каждый раз при вызове var r = /regexp/.
                          Вместо этого браузер возвращает уже существующий объект, со всеми свойствами, оставшимися от предыдущего запуска.
                          Это баг спецификации ES3, и в ES5 он уже исправлен (соответственно, и некоторые браузеры подтянулись).
                          • –2
                            Я говорю не про теоретические изыскания, а про то, как есть на практике.
                            • +1
                              Как бы там ни было, даже в Хроме восьмом этот «баг спецификации» проявляется. Хотя, с тем, что это баг, я согласен.
                      • –3
                        Firefox 3.6.16 true, false.
                        Что я делаю не так?

                        Комментатор так бежал, так спешил поделиться своей находкой, что даже запустить свой код не удосужился?
                        • +5
                          *facepalm*
                          Вы на код посмотрели перед запуском? Автор утверждает, что код
                          > /([0-9])+/g.test(«abc»)
                          Не вызывает проблем. Внимание, вопрос: чем таким '5' во втором вызове отличается от '5' в первом, что второй вызов вернет отличный от первого результат?
                          • +2
                            А, прошу прощения, я не так вас понял. Действительно, в старых и новых браузерах этот код работает иначе.
                        • 0
                          Довольно интересно. Google Chrome (10.0.648.6 dev): true, true
                          • –1
                            Это неправильный код. Смотрите моё объяснение выше.
                        • +12
                          Эта статья демонстрирует полное незнание регулярных выражений. Зачем этой проверке флаг g? Зачем скобки вокруг символьного класса?
                          • –7
                            Скобки не нужны. Регулярку можно упростить еще сильней. Суть не в этом, а в том, что Вы можете это выражение взять из другой части кода. Где оно реально что-то выделяло. И забыть убрать флаг global.
                            И получить эту трудноотлавливаемую проблему.
                            • +5
                              Копипаст — одно из самых больших зол в программировании.
                              • +5
                                Третье после отсутствия мозгов и второе после отсутствия практики.
                                • +2
                                  Отсутствие практики — не грех.
                                  • +5
                                    Угу. Это только зло.

                                    А вот нежелание учиться — грех.
                          • 0
                            Забавный факт. Не обращал на него внимания.
                            На ActionScript, который основан на том же стандарте, что и JavaScript, данное поведение повторяется.
                            • 0
                              Это не поведение, это продолжение поиска
                            • +1
                              Как раз недавно писал по этому поводу: habrahabr.ru/blogs/javascript/112518/#comment_3613835
                              Автору: текст немного сумбурен.
                              • +21
                                Сколько пафоса в статье, а толком объяснить так и не смог. Столько всякой воды вокруг одной строки с ответом, что хрен найдешь ее там.
                                • +5
                                  эээ, я вот одного не пойму — сделав банальную ошибку с инициализацией, зачем обвинять в этом хорошие ресурсы и документацию?
                                  • +4
                                    Спасибо за эту статью, но если внимательно читать книги, вот например у Флэнегана написано «Когда exec() вызывается для регулярного выражения, содержащего флаг g, метод устанавливает свойство lastIndex объекта регулярного выражения равным номеру позиции символа, следующего непосредственно за найденной подстрокой. Когда метод exec() вызывается для того же регулярного выражения второй раз, он начинает поиск с символа, позиция которого указана в свойстве lastIndex.»

                                    Читайте книги внимательно, и не будет таких вот трудновылавливаемых ошибок
                                    • +2
                                      Ресурсы все правильно пишут, единственно что про lastIndex умалчивают. Кстати, автор javascript.ru абсолютно в курсе этого поведения, в частности на мастер-классах про это рассказывает.
                                      • +13
                                        Вы такими темпами JavaScript изучите :)
                                        • 0
                                          Да, интересное исследование, сам натыкался на такой «финт», просто не думал, что можно из этого написать заметку на Хабре :)
                                          • +5
                                            >Удивляет, что большинство сетевых ресурсов по JavaScript оказались бесполезны.

                                            2006 год — обсуждалось на Винграде
                                            2007 год — обсуждалось на ДКлабе

                                            Обсуждалось и задолго раньше, ссылок из глубокого прошлого под рукой нет, поверьте на слово. И конечно же это поведение описано и на более молодых ресурсах, и в ньюсах англоязычных, и в книгах (того же Флэнагана), и в спеках языка, и в самой экме… Но вдумчиво не читали, не читаем и не будем читать. Всё как всегда — ни дня без чудесного открытия… ;-)
                                            • 0
                                              … мы хотим проверить регулярным выражением, что в строке есть хотя бы одна цифра ...

                                              /\d/.test(str);

                                              Ну вот так как-то.
                                              • 0
                                                а я вот так делаю:

                                                function has_digit(s) { return (s.search(digits) != -1) }
                                                • –3
                                                  bool b;

                                                  if (Convert.ToString(b).Length == 4) //True
                                                  {

                                                  }
                                                  else
                                                  • +1
                                                    ?
                                                    • 0
                                                      Хотите казаться умным? Молчите.
                                                      • 0
                                                        парсер — лох. я хотел написать код от коллег-индусов.
                                                        не надо делать поспешных выводов насчёт ума
                                                        • +3
                                                          Парсер не лох, юзайте кнопочку «предпросмотр». А раз Вы ответили «кодом от коллег-индусов», то похоже вы сравнивали его с кодом человека выше. Код человека выше нормален.
                                                  • +6
                                                    Очередной пост на главной про изучение студентом регулярных выражений
                                                    • +1
                                                      Блин… прочитал почти все комментарии и вообще запутался что верно, а что нет
                                                    • –1
                                                      Какие регулярки? Автор, учи русский.
                                                      «Вам очевидно — почему?» :)

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