Глава 9
Регулярни изрази

Някои хора, когато се сблъскат с проблем си мислят „Аз знам, че трябва да използвам регулярни изрази.” Сега те имат два проблема.”

Jamie Zawinski

Ян-ма казал: „Когато сечете едно дърво, е необходима голяма сила. Когато се изправяте срещу проблем, е необходим много код.””

Master Yuan-Ma, Книга за програмиране

Програмните инструменти и техники, оцеляват и се разпространяват по хаотичен, еволюционен начин. Те не винаги са красиви и брилянтни, като онези, които функционират достатъчно добре в рамките на правилната ниша - например, като се интегрират с друга успешна част от технология.

В тази глава ще обсъдим още един такъв инструмент, regular expressions (регулярни изрази). Регулярните изрази са начин да се пишат модели в string данни. Те образуват един малък, отделен език, който е част от JavaScript и от много други езици и инструменти.

Регулярните изрази са едновременно ужасно неудобни и изключително полезни. Техният синтаксис е загадъчен и програмният интерфейс на JavaScript, предвиден за тях е тромав. Но са мощен инструмент за проверка и обработка на strings. Правилното разбиране на регулярните изрази може да направи един програмист по-ефективен.

Създаване на регулярен израз

Регулярният израз е тип обект. Той може да бъде конструиран с RegExp конструктора или написан, като стойност чрез ограждане на модела с характера за наклонена черта (/).

edit & run code by clicking it
var re1 = new RegExp("abc"); var re2 = /abc/;

И двата регулярни израза представляват един и същ модел: един характер a последван от b последван от c.

При използване на конструктора RegExp, моделът е написан, като нормален string, така че се прилагат обичайните правила за обратно наклонените черти.

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

var eighteenPlus = /eighteen\+/;

Знаейки, какво точно прави характера на обратно наклонената черта - escape (бягство) - при писане на регулярни изрази, изисква от вас да знаете всеки характер със специално предназначение. За момента, това не може да бъде реалистично, така че, когато се съмнявате, просто сложете обратно наклонена черта пред всеки характер, който не е буква, цифра или празно пространство.

Тест за съвпадение

Обекта на регулярните изрази има редица методи. Най-простият от тях е test. Ако му подадете string, той ще върне булева стойност, която ви казва дали този string съдържа съвпадение с модела в израза.

console.log(/abc/.test("abcde"));
// → true
console.log(/abc/.test("abxde"));
// → false

Регулярен израз, съставен само от не-специални характери просто означава поредица от характери. Ние тестваме съвпадението не само в началото и test ще върне true, ако abc е на всякъде в string.

Съвпадение на набор от характери

Намирането дали string съдържа abc, може да се направи също толкова добре с извикване на indexOf. Регулярните изрази ни позволяват да отидем отвъд това и да изразят по-сложни модели

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

И двата от следните изрази съответстват на всички strings, които съдържат цифри:

console.log(/[0123456789]/.test("in 1992"));
// → true
console.log(/[0-9]/.test("in 1992"));
// → true

В квадратните скоби, тирето (-) между два характера, може да се използва за указване на диапозон от характери, където редът се определя от Unicode номера за съответния характер. Характерите от 0 до 9 стоят в непосредствена близост един до друг в тази таблица (кодове от 48 до 57), така че [0-9] обхваща всички тях и съответства на всяка цифра.

Има редица общи групи от характери, които имат свои собствени вградени команди за бърз достъп. Цифрите са една от тях: \d означава едно и също нещо, като [0-9].

\d Всеки характер цифра
\w Всеки буквено-цифров характер (‘дума характер’)
\s Всеки характер за празно пространство (space, tab, newline и similar)
\D Всеки характер, който не е цифра
\W Всеки не буквено-цифров характер
\S Всеки характер който не е за празно пространство
. Всеки характер, освен характера за нов ред

Така може да откриете съвпаденията за формата на дата и час, например като 30-01-2003 15:20, със следния израз:

var dateTime = /\d\d-\d\d-\d\d\d\d \d\d:\d\d/;
console.log(dateTime.test("30-01-2003 15:20"));
// → true
console.log(dateTime.test("30-jan-2003 15:20"));
// → false

Това изглежда ужасно, нали? Има прекалено много обратно наклонени черти, които правят фонов шум, което го прави трудно място за действителния модел на израз. Ще видим леко подобрена версия на този израз по-късно.

Тези кодове с обратно наклонена черта могат да бъдат използвани в квадратни скоби. Например [\d.] означава всяка цифра или период от характери. Но имайте в предвид, че самия период, когато се използва в квадратни скоби, губи своето специално значение. Същото важи и за други специални характери, като например плюс +.

За да обърнете набор от характери, тоест искате да съвпадат с всеки характер с изключение на зададените, можете да напишете ксор ^ характер след отварящата скоба.

var notBinary = /[^01]/;
console.log(notBinary.test("1100100010100110"));
// → false
console.log(notBinary.test("1100100010200110"));
// → true

Повтаряне частите на модел

Вече знаем как да съвпада една цифра. Какво става, ако искаме да съвпада цяла последователност от една или повече цифри?

Когато поставите харктера плюс (+) след нещо в регулярен израз, това означава, че елемента може да се повтори повече от веднъж. По този начин, /\d+/ съвпада с един или повече цифрови характери.

console.log(/'\d+'/.test("'123'"));
// → true
console.log(/'\d+'/.test("''"));
// → false
console.log(/'\d*'/.test("'123'"));
// → true
console.log(/'\d*'/.test("''"));
// → true

Звездата (*) има подобно значение, но също така позволява моделът да съвпада нула пъти. Нещо със звезда след него, никога не предпазва модела от съвпадение - той ще съвпада нула пъти, ако не може да намери подходящ текст за съвпадение.

Въпросителният знак прави част от един модел “по избор”, което означава, че може да се случи нула или едно пъти. В следващия пример, на u характера му е позволено да се случи, но модела същто така съвпада и когато липсва.

var neighbor = /neighbou?r/;
console.log(neighbor.test("neighbour"));
// → true
console.log(neighbor.test("neighbor"));
// → true

За да покажете, че един модел трябва да се случва точно брой пъти, използвайте фигурни скоби. Поставянето на {4} след елемент, изисква да се случи точно четири пъти. Възможно е също така да се определи диапазон, по този начин: {2,4} означава, че елемента трябва да се случи най-малко два и най-много четири пъти.

Тука е една версия на модела за дата и час, който позволява и двата - единични и двуцифрени дни, месеци и часове. Също така е по- разбираем.

var dateTime = /\d{1,2}-\d{1,2}-\d{4} \d{1,2}:\d{2}/;
console.log(dateTime.test("30-1-2003 8:45"));
// → true

Можете също да определите и отворени граници при използване на фигурни скоби, като не зададете номер от двете страни на запетаята. Така {,5} означава от нула до пет и {5,} означава пет или повече пъти.

Групиране на под-изрази

За да използвате оператор, като * или + на повече от един елемент в даден момент, може да използвате обикновени скоби. Една част от регулярен израз, който е заграден в скоби се брои за един елемент, доколкото операторите след това са засегнати.

var cartoonCrying = /boo+(hoo+)+/i;
console.log(cartoonCrying.test("Boohoooohoohooo"));
// → true

Първият и вторият характер + се прилагат само за второто o в boo и hoo съответно. Третият + се отнася за цялата група (hoo+) отговаряща на една или повече поредици, като тази.

i в края на израза в предишния пример прави този регулярен израз да се влияе от случая, което позволява да съвпада с главна буква B във входящия string, въпреки, че моделът е само за малки букви.

Съвпадения и групи

Метода test е абсолютно най-простия начин за съответствие с регулярен израз. Той ви казва само дали съвпада и нищо друго. Регулярните изрази имат също и exec метод, който ще върне null ако не е открито съвпадение или в противен случай ще върне обект с информация за съвпадението.

var match = /\d+/.exec("one two 100");
console.log(match);
// → ["100"]
console.log(match.index);
// → 8

Обекта, който се връща от exec има свойството index, което ни казва, къде в string успешно започва съвпадението. Освен това, обекта изглежда, като масив от strings (и всъщност е), чиито първи елемент е string-а, който съответства - в предишния пример, това е последователноста от цифри, която тръсехме.

String стойностите имат match метод, който се държи по подобен начин.

console.log("one two 100".match(/\d+/));
// → ["100"]

Когато регулярният израз съдържа групи под-изрази групирани в скоби, текстът, който съответства на тези групи също ще се появи в масива. Цялото съвпадение винаги е първият елемент. Следващият елемент е част от първата група (тази, чиято отварящата скоба е на първо място в израза), след това втората група и т.н.

var quotedText = /'([^']*)'/;
console.log(quotedText.exec("she said 'hello'"));
// → ["'hello'", "hello"]

Когато една група свърши без съвпадение (например, когато е последвана от въпросителен знак), позицията и в изходящия масив ще съдържа undefined. По същия начин, когато една група е съчетана няколко пъти, само последното съвпадение ще се отзове в масива.

console.log(/bad(ly)?/.exec("bad"));
// → ["bad", undefined]
console.log(/(\d)+/.exec("123"));
// → ["123", "3"]

Групите могат да бъдат полезни за извличане на части от string. Ако искаме не само да проверим дали string съдържа дата, но я извлечем и конструираме обект, който да я представя, можем да увием в скоби модела на цифрите и направо да вземем датата от резултата на exec.

Но първо, малко отклонение, в което ще обсъдим предпочитан начин за съхранение на дата и час стойности в JavaScript.

Типът Дата

JavaScript има стандартен тип обект за представяне на дати или по-скоро, точки във времето. Той се нарича Date. Ако просто създадете обект дата чрез new, ще получите текущата дата и час.

console.log(new Date());
// → Wed Dec 04 2013 14:24:57 GMT+0100 (CET)

Можете също така да създадете обект за определен период от време.

console.log(new Date(2009, 11, 9));
// → Wed Dec 09 2009 00:00:00 GMT+0100 (CET)
console.log(new Date(2009, 11, 9, 12, 59, 59, 999));
// → Wed Dec 09 2009 12:59:59 GMT+0100 (CET)

JavaScript използва конвенция, където номера на месеца започва от нула (така, че м.декември е 11), но номерата на дните започват от едно. Това е объркващо и глупаво. Бъдете внимателни.

Последните четири аргумента( часове, минути, секунди и милисекунди) не са задължителни и се отчитат като нулеви, когато не се зададат.

Времеви отрязъци се съхраняват, като брой от милисекунди от началото на 1970г., и с помощта на отрицателни числа за времето преди 1970г.(след конвенция определена от “Unix time”, която е изработена по това време). Метода getTime на обекта дата връща този номер. Той е голям, както можете да си представите.

console.log(new Date(2013, 11, 19).getTime());
// → 1387407600000
console.log(new Date(1387407600000));
// → Thu Dec 19 2013 00:00:00 GMT+0100 (CET)

Ако дадете на Dateконструктора един единствен аргумент, той ще бъде третиран , като такъв брой милисекунди. Можете да получите текущия брой милисекунди, чрез създаване на нов Date обект и извикате getTime върху него, но също и чрез извикване на Date.now функцията.

Дата обекта предоставя методи, като getFullYear, getMonth, getDate, getHours, getMinutes и getSeconds за извличане на техните компоненти. Има също така и getYear, който ви дава безполезна двуцифрена годишна стойност (като 93 или 14).

Поставянето на скоби около частите на израза, показва че се интересуваме от това и сега може лесно да се създаде дата само със string.

function findDate(string) {
  var dateTime = /(\d{1,2})-(\d{1,2})-(\d{4})/;
  var match = dateTime.exec(string);
  return new Date(Number(match[3]),
                  Number(match[2]) - 1,
                  Number(match[1]));
}
console.log(findDate("30-1-2003"));
// → Thu Jan 30 2003 00:00:00 GMT+0100 (CET)

Думи и string граници

За съжаление, findDate също успешно ще извлече безсмислена дата 00-1-3000 от string “100-1-30000”. Съвпадението може да се случи навсякъде в string, така че в този случай просто ще започне с втория характер и ще завърши в последния втори характер.

Ако искаме да наредим, че съвпадението трябва да обхване целия string, можем да добавим маркерите ^ и $. Ксор съвпада с началото на входния string, а харктера за долар съвпада с края. Така че, /^\d+$/ съвпада със string, състоящ се изцяло от една или повече цифри, /^!/ съвпада с всеки string, който започва с удивителен знак и /x^/ не съвпада с всеки string (не може да има x преди началото на string-а).

Ако, от друга страна, просто искаме да се уверим, че започва с начална и завършва с дума граница, можем да използваме маркер \b. Граничната дума може да бъде в началото, в края или всяка точка на string, който има дума характер в него, (като в \w) за едната страна и nonword характер за другата.

console.log(/cat/.test("concatenate"));
// → true
console.log(/\bcat\b/.test("concatenate"));
// → false

Имайте в предвид, че маркера за граница, не представлява действителен характер. Той просто се прилага на регулярния израз за съвпадне само, когато дадено условие държи мястото, където той се появява в модела.

Избор на модели

Да кажем, че искаме да знаем дали част от текст съдържа не само номера, а номера последвани от думи, като прасе, крава или пиле, или някоя от техните форми за множествено число.

Бихме могли да напишем три регулярни израза и да ги тестваме по ред, но има и по-добър начин. Характера (|), означава избор между лявата и дясната страна на модела. Така, че можем да направим това:

var animalCount = /\b\d+ (pig|cow|chicken)s?\b/;
console.log(animalCount.test("15 pigs"));
// → true
console.log(animalCount.test("15 pigchickens"));
// → false

Скобите могат да бъдат използвани, за да се ограничи част от модела, когато операторът (|) се отнася до него и също може да сложите няколко такива оператора един до друг, за да направят своя избор между повече от два модела.

Механизми за съвпадение

Регулярните изрази могат да бъдат замислени, като технологични схеми. Това е схемата за изразяване на животновъдството в предишния пример:

Visualization of /\b\d+ (pig|cow|chicken)s?\b/

Нашият израз съвпада със string, ако можем да намерим пътя от лявата страна на диаграмата към дясната страна. Ние държим текущата позиция в string и всеки път, когато се движим през кутия, проверяваме за тази част от string, след като сегашната ни позиция съвпада с тази кутия.

Така че, ако се опитаме да сравним "the 3 pigs" с нашия регулярен израз, нашият напредък през диаграмата ще изглежда така:

Концептуално, двигателя на регулярния израз търси съвпадение в string по следния начин: той започва в началото на string и търси съвпадение там. В този случай там има дума граница, така че ще премине през първата кутия - но няма никаква цифра, така че ще се провали във втората кутия. После се премества на втория характер в string и се опита да започне ново съвпадение там .... и т.н., докато не открие съвпадение или не достигне края на string и реши, че наистина няма съвпадение.

Връщане на зад

Регулярния израз /\b([01]+b|\d+|[\da-f]+h)\b/ търси съвпадение за бинарно число последвано от b, обикновено десетично число без никакъв суфикс характер или шестнадестетично число (т.е. основа 16 с буквите от a до f, постоянно за цифри от 10 до 15) следвано от h. Това е съответната схема:

Visualization of /\b([01]+b|\d+|[\da-f]+h)\b/

При съвпадение на този израз, често се случва горното бинарно (binary) разклонение да е въведено въпреки, че входът всъщност не съдържа бинарно число. Когато съвпадне със string “103”, например, става ясно, че само в 3 сме в грешния клон. Тоест string съвпада с израза, само клона в който сме в момента не.

Това съответствие е backtracks (връщане на зад). При влизане в клон, то си спомня текущата позиция (в този случай началото на string, само през първото поле на границата в схемата) така че, да може да се върне и да пробва друг клон, ако сегашния не работи. За string "103" след, като се натъкне на 3, то пробва на клона за десетични числа. Той съвпада, така че, даденото съвпадение се съобщава в края на краищата.

Съпоставянето спира веднага след, като установи пълно съвпадение. Това означава, че много клонове потенциално биха могли да съответстват на string, но се използва само първия (от подредбата, по която клоните се появяват в регулярния израз).

Връщането на зад също се случва с оператори за повторение, като + и *. Ако съвпадне /^.*x/ със "abcxe", часта .* ще се опита първо да консумира целия string. След което, двигателя ще осъзнае, че се нуждае от х, за да съвпадне с модела. Тъй като няма х на края на string, операторът * се опитва да съответства на един характер по-малко. Но съгласувателя пак не намира х след abcx, така че се връща на зад отново, за съвпадение на оператора звезда само с abc. Сега намира х, от който има нужда и отчита успешно съвпадение с позиции от 0 до 4.

Възможно е да се напишат регулярни изрази, които да направят много връщания на зад. Този проблем възниква, когато един модел може да се сравнява с парче от въвеждането по много различни начини. Например, ако се объркате докато пишете бинарен номер за регулярен израз, вие можете да напишете нещо такова: /([01]+)+b/.

Visualization of /([01]+)+b/

Ако това се опита да съответства на някаква дълга поредица от нули и единици, без да следи b характера, съгласувателя първо ще премине през вътрешната линия, докато не прочете всички цифри. След това забелязва, че няма b, така че се връща една позиция на зад и минава през външната линия веднъж, излиза отново, опитвайки се с връщане на зад да излезе от вътрешната линия още веднъж. Той ще продължи да опитва всеки възможен маршрут през тези две вериги. Това означава, че обема на работа се удвоява с всеки допълнителен характер. Дори само на дузина характери, резултата за съвпадението ще отнеме много време - практически завинаги.

Метод replace

String стойностите имат метод replace, който се използва за замяна на част от string с друг string.

console.log("papa".replace("p", "m"));
// → mapa

Първият аргумент може да бъде регулярен израз, като в този случай първото съвпадение на регулярния израз се заменя. Когато опцията g (за глобално) е добавена към регулярния израз, всички съвпадения в string ще бъдат заменени, а не само първото.

console.log("Borobudur".replace(/[ou]/, "a"));
// → Barobudur
console.log("Borobudur".replace(/[ou]/g, "a"));
// → Barabadar

Би било разумно, ако изборът между заменяне на едно съвпадение или всички съвпадения, беше направен чрез допълнителен аргумент на replace или чрез предоставяне на различен метод replaceAll. Но за съжаление, по някаква причина изборът се основава на свойство на регулярния израз.

Истинската сила на използване на регулярни изрази с replace идва от факта, че можем да се върнем към съвпадащи групи в заменения string. Например, да кажем, имаме голям string съдържащ имена на хора, по едно име на ред във формат Lastname, Firstname. Ако искаме да сменим тези имена и да премахнем запетаята, за да получим друг формат Firstname Lastname, можем да използваме следния код:

console.log(
  "Hopper, Grace\nMcCarthy, John\nRitchie, Dennis"
    .replace(/([\w ]+), ([\w ]+)/g, "$2 $1"));
// → Grace Hopper
//   John McCarthy
//   Dennis Ritchie

$1 и $2 в заместващия string се отнасят за групите в скобите в модела. $1 се заменя с текста, който съвпада с първата група, $2 с втората и т. н. до $9. Цялото съвпадение може да бъде посочено с $&.

Също така може да се подаде функция вместо string, като втори аргумент към replace. За всяка замяна, функцията се извиква със съвпадащи групи (или цялото съвпадение), като аргумент и нейната върната стойност ще бъде вмъкната в новия string.

Ето един прост пример:

var s = "the cia and fbi";
console.log(s.replace(/\b(fbi|cia)\b/g, function(str) {
  return str.toUpperCase();
}));
// → the CIA and FBI

А тука още по-интересно:

var stock = "1 lemon, 2 cabbages, and 101 eggs";
function minusOne(match, amount, unit) {
  amount = Number(amount) - 1;
  if (amount == 1) // only one left, remove the 's'
    unit = unit.slice(0, unit.length - 1);
  else if (amount == 0)
    amount = "no";
  return amount + " " + unit;
}
console.log(stock.replace(/(\d+) (\w+)/g, minusOne));
// → no lemon, 1 cabbage, and 100 eggs

Това взема string, намира всички появявания на номер, следван от буквено-цифрова дума и връща string, където за всеки такъв случай се отнема по едно.

Групата (\d+) завършва, като аргумент amount към функцията, а групата (\w+) получава обвързване към unit. Функцията преобразува amount към номер, което винаги работи, тъй като съвпада с \d+ и прави някои корекции в случай, че има само едно или нула от ляво.

Ненаситност

Не е трудно, когато се използва replace да се напише функция, която премахва всички коментари в парче JavaScript код. Това е първи опит:

function stripComments(code) {
  return code.replace(/\/\/.*|\/\*[^]*\*\//g, "");
}
console.log(stripComments("1 + /* 2 */3"));
// → 1 + 3
console.log(stripComments("x = 10;// ten!"));
// → x = 10;
console.log(stripComments("1 /* a */+/* b */ 1"));
// → 1  1

Частта преди оператора или (|) просто съвпада с две наклонени черти, последвана от произволен брой характери за нов ред. Частта за многоредови коментари е по-ангажираща. Ние използваме [^] (за всеки характер, който не е в празен набор от характери), като начин да съответства на всеки характер. Не можем просто да използваме точка тук, защото блоковите коментари могат да продължат на нов ред, а точките не съвпадат с характера за нов ред.

Но изходът на последния пример изглежда е грешен. Защо?

Частта от израза [^]*, както описах в раздела за връщане на зад, ще е първото съвпадение, толкова колкото може. Ако това е причината следващата част на модела да се провали, съгласувателя се връща на зад един характер и се опитва отново там. В примера, съгласувателя се опитва да съответства на цялата останала част от string-а и след това се връща обратно от там. Той ще намери поява на */ след връщане на зад с четири характера и съвпада с него. Това не е онова, което искахме - намерението беше да съответства на един единствен коментар, за да не вървим по целия път до края на кода и да намерим края на последния блок коментари.

Заради това поведение, ние казваме, че операторите за повторение (+, *, ?, и {}) са greedy (ненаситни), това означава, че те съвпадат толкова колкото могат и се връщат от там. Ако сложим въпросителен знак след тях (+?, *?, ??, {}?), те стават nongreed (не-лакоми) и започват да съответстват на най-малко съвпадения, само когато оставащия модел не се побира в по-малко съвпадение.

И това е точно онова, което искаме в този случай. Докато звездата * съответства на най-малкия участък от характери, което ни води до */, ние консумираме един блок с коментари и нищо повече.

function stripComments(code) {
  return code.replace(/\/\/.*|\/\*[^]*?\*\//g, "");
}
console.log(stripComments("1 /* a */+/* b */ 1"));
// → 1 + 1

Много грешки в регулярните изрази могат да бъдат проследени до неволно използване на ненаситни оператори, където nongreed ще работят една идея по-добре. Когато използвате оператор за повторение, помислете първо за nongreed вариант.

Динамично създаване на RegExp обекти

Има случаи, в които може да не знаете точният модел, който трябва да съответства насреща, когато пишете кода си. Да речем, че търсите името на потребител в част от текст и му прилагате долна черта за да се откроява. Тъй като, вие ще разберете името само след, като програмата действително работи, не може да използвате наклонена черта базирана на нотация.

Но можете да изградите един string и да използвате RegExp конструктора върху него. Ето един пример:

var name = "harry";
var text = "Harry is a suspicious character.";
var regexp = new RegExp("\\b(" + name + ")\\b", "gi");
console.log(text.replace(regexp, "_$1_"));
// → _Harry_ is a suspicious character.

Когато създаваме маркери за граница \b, ние трябва да използваме две обратно наклонени черти, защото ги пишем в нормален string, нормално наклонената черта, затваря регулярния израз. Вторият аргумент на RegExp конструктора съдържа вариантите за регулярния израз - в този случай "gi" за глобално и да се влияе от случая.

Но какво правим, ако името е "dea+hl[]rd", защото нашият потребител е изперкал тинейджър? Това ще доведе до безсмислен регулярен израз, който всъщност няма да съвпада с името на потребителя.

За да заобиколим този проблем, можем да добавим обратно наклонени черти пред всеки характер, в който се съмняваме. Добавянето на обратни наклонени черти, пред буквени характери е лоша идея, защото такива неща, като \b и \n имат специално значение. Но escaping на всичко, което не е буквено-цифрово или празно пространство е безопасно.

var name = "dea+hl[]rd";
var text = "This dea+hl[]rd guy is super annoying.";
var escaped = name.replace(/[^\w\s]/g, "\\$&");
var regexp = new RegExp("\\b(" + escaped + ")\\b", "gi");
console.log(text.replace(regexp, "_$1_"));
// → This _dea+hl[]rd_ guy is super annoying.

Метод за търсене

Метода indexOf за strings не може да се извика с регулярен израз. Но има друг метод search, който очаква регулярен израз. Както indexOf, той връща първия индекс, на който е намерен израза или -1, когато не е намерен.

console.log("  word".search(/\S/));
// → 2
console.log("    ".search(/\S/));
// → -1

За съжаление, няма начин да се посочи, от къде трябва да започне съвпадението (както можем да направим с втория аргумент на indexOf), което често е полезно.

Свойството lastIndex

Метода exec също не предоставя удобен начин за започване на търсенето от дадена позиция в string. Но предоставя неудобен начин.

Обекта на регулярните изрази има свойства. Едно такова свойство е source, който съдържа string създаден от израза. Друго свойство е lastIndex, който контролира в някои ограничени случаи, къде да започне следващото съвпадение.

При тези обстоятелства, регулярния израз трябва да има глобална (g) поддръжка и съвпадението трябва да се случи чрез метода exec. Отново по-разумно решение би било, да се разреши допълнителен аргумент, който да бъде приет от exec, но здравият разум не е определяща характеристика за интерфейса на регулярните изрази в JavaScript.

var pattern = /y/g;
pattern.lastIndex = 3;
var match = pattern.exec("xyzzy");
console.log(match.index);
// → 4
console.log(pattern.lastIndex);
// → 5

Ако съвпадението е било успешно, извикването на exec автоматично актуализира свойството lastIndex, до точката след съвпадението. Ако не е открито съвпадение, lastIndex се настройва обратно към нула, което също така е стойността, която има в обекта на новопостроения регулярния израз.

Когато използвате глобален регулярен израз, за няколко извиквания на exec, тези автоматични актуализации на свойството lastIndex може да предизвикат проблеми. Регулярният израз може случайно да започне от индекс, който е останал от предното извикване.

var digit = /\d/g;
console.log(digit.exec("here it is: 1"));
// → ["1"]
console.log(digit.exec("and now: 1"));
// → null

Друг интересен ефект на глобалната опция е, че тя променя начина на работа на метода match в string. Когато се извика с глобален израз, вместо да връща масив подобен на този върнат от exec, match ще намери всички съвпадения на модела в string-а и ще върне масив, съдържащ съвпадащите strings.

console.log("Banana".match(/an/g));
// → ["an", "an"]

Така че, бъдете внимателни с глобалните регулярни изрази. В случаите, когато те са необходими, извиквайте replace и в местата, където изрично искате, използвате lastIndex ( обикновено това са единствените случаи, където искате да ги използвате).

Цикъл за съвпадения

Един общ модел е да се сканират всички съвпадения на модел в string, по начин, който ни дава достъп до обекта на съвпадението в тялото на цикъл с помощта на lastIndex и exec.

var input = "A string with 3 numbers in it... 42 and 88.";
var number = /\b(\d+)\b/g;
var match;
while (match = number.exec(input))
  console.log("Found", match[1], "at", match.index);
// → Found 3 at 14
//   Found 42 at 33
//   Found 88 at 40

Това се прави с използването на факта, че стойността на израза за присвояване (=) е присвоената стойност. Така, че с помоща на match = number.exec(input), като условие в while цикъла, извършваме съвпадението в началото на всяка итерация, съхраняваме резултата в променлива и спираме цикъла, когато няма намерени повече съвпадения.

Разбор на INI файла

В заключение на главата, ще разгледаме един проблем, който се отнася до регулярните изрази. Представете си, че пишем програма за автоматично събиране на информация за нашите враговете от Интернет. (Ние всъщност няма да напишем тази програма тук, и по-точно, частта, която чете конфигурационния файл. Съжалявам за пропуска.) Конфигурационния файл, изглежда така:

searchengine=http://www.google.com/search?q=$1
spitefulness=9.7

; comments are preceded by a semicolon...
; each section concerns an individual enemy
[larry]
fullname=Larry Doe
type=kindergarten bully
website=http://www.geocities.com/CapeCanaveral/11451

[gargamel]
fullname=Gargamel
type=evil sorcerer
outputdir=/home/marijn/enemies/gargamel

Точните правила за този формат (който, всъщност е широко използван формат, обикновено се нарича INI файл) са както следва:

Нашата задача е да се преобразува string, като този в масив от обекти, всеки с name свойство и масив от настройки. Ще ни трябва един такъв обект за всяка секция и един за глобалните настройки в горната част.

Тъй като, формата трябва да бъде обработен ред по ред, разделянето на файла на отделни редове е добро начало. Ние използвахме string.split("\n") за да направим това в Глава 6. Някои операционни системи обаче, не използват само характера за нов ред при разделяне на редове, а използват характера за връщане последван от този за нов ред ("\r\n"). Като се има в предвид, че метода split позволява също и регулярен израз, като негов аргумент, можем да разделим регулярния израз на /\r?\n/, което позволява едновременно използването на "\n" и "\r\n" между редовете.

function parseINI(string) {
  // Start with an object to hold the top-level fields
  var currentSection = {name: null, fields: []};
  var categories = [currentSection];

  string.split(/\r?\n/).forEach(function(line) {
    var match;
    if (/^\s*(;.*)?$/.test(line)) {
      return;
    } else if (match = line.match(/^\[(.*)\]$/)) {
      currentSection = {name: match[1], fields: []};
      categories.push(currentSection);
    } else if (match = line.match(/^(\w+)=(.*)$/)) {
      currentSection.fields.push({name: match[1],
                                  value: match[2]});
    } else {
      throw new Error("Line '" + line + "' is invalid.");
    }
  });

  return categories;
}

Този код минава през всеки ред във файла и актуализира обекта на “текущата секция”, който върви заедно. Първо се проверява дали редът може да се игнорира, използвайки израза /^\s*(;.*)?$/. Виждате ли как работи? Частта между скобите ще съвпадне с коментарите, а въпросителния знак ? ще се увери, че също съвпада с редове съдържащи само интервали.

Ако реда не е коментар, кода след това проверява дали с този ред започва нова секция. Ако е така, той създава обект за нова текуща секция, към който ще бъдат добавени следващите настройки.

Последната смислена възможност е, че редът е нормален и кода го добавя към обекта на текущата секция.

Ако реда не съвпада с някоя от тези форми, функцията хвърля грешка.

Обърнете внимание на повтарящото се използване на ^ и $, което ни уверява, че изразът съвпада с цялия ред, а не само с част от него. Оставянето на тези извън резултата в кода, който общо взето работи, но се държи странно за някои входове, може да създаде трудности при проследяването на грешки.

Моделът if (match = string.match(...)) е подобен на трика за използване на присвояването, като условие за while цикъла. Ако не сте сигурни, че извикването на match ще успее, може да получите достъп до получения обект само вътре в if изявлени, за тестване на това. За да не се прекъсне веригата на if формите, ще присвоим резултата от съвпадението към променлива и веднага ще използваме това присвояване, като тест в if изявлението.

Международни характери

Поради първоначално опростеното прилагане на JavaScript и на факта, че този опростен подход беше непоклатим, като стандартно поведение, регулярните изрази в JavaScript са доста тъпи като характери, които не са включени в английския език. Например, що се отнася до регулярните изрази в JavaScript, “думата характер” е само един от 26-те характера в латинската азбука (главни и малки букви) и по някаква причина също и характера за долна черта. Неща като é или β, които най-категорично са думи характери, няма да съвпаднат с \w, а ще съответстват на главна \W на nonword категорията.

По странна историческа случайност \s за празно пространство (whitespace) няма такъв проблем и съвпада с всички знаци, които Unicode счита за празно пространство, включително и неща, като непрекъснато пространство и Монголската гласна за сепаратор.

Някои реализации на регулярни изрази в други програмни езици имат синтаксис, който да съответства на специфични категории характери в Unicode, например “всички главни букви”, ”всички препинателни знаци” или ”контролни характери”. Има планове за добавяне на поддръжка за такива категории в JavaScript, но за съжаление изглежда, че няма да се реализират в близко бъдеще.

Резюме

Регулярните изрази са обекти, които представляват модели в strings. Те използват собствения си синтаксис за да изразят тези модели.

/abc/ Поредица от характери
/[abc]/ Всеки характер от набора с характери
/[^abc]/ Всеки характер, който не е в набора от характери
/[0-9]/ Всеки характер в диапазона от характери
/x+/ Едно или повече съответствия на модела x
/x+?/ Едно или повече съответствия, nongreedy
/x*/ Нула или повече съответствия
/x?/ Нула или едно съответствие
/x{2,4}/ Между две и четири съответствия
/(abc)/ Група
/a|b|c/ Всеки един от няколко модела
/\d/ Всяка цифра характер
/\w/ Един буквено-цифров характер (“дума характер”)
/\s/ Всеки интервал характер
/./ Всеки характер, с изключение на характера за нов ред
/\b/ Дума граница
/^/ Начало на вход
/$/ Край на вход

Регулярният израз има метод test за тестване дали даден string съвпада с него. Той има също и exec метод, който когато установи съвпадение, връща масив съдържащ всички съвпадащи групи. Такъв масив има свойството index, което показва къде е започнало съвпадението.

Strings имат match метод, за съпоставянето им с регулярн израз и search метод за търсене, връщайки само началната позиция в съвпадението. Техният replace метод, може да замени съвпаденията на един модел със заменящ string. Алтернативно, можете да подадете функция към replace, която да бъде използвана за изграждане на заместващ string на базата на текста за съвпадението и съвпадащите групи.

Регулярни изрази могат да имат опции, които са написани след наклонената черта за затваряне. Опцията i прави съвпадението да се влияе от случая, докато опцията g прави израза глобален, което наред с други неща прави метода replace да заменя всички инстанции, вместо само първата.

Конструктора RegExp може да се използва за създаване на регулярен израз със стойност string.

Регулярните изрази са остър инструмент с неудобна дръжка. Те опростяват някои задачи неимоверно, но могат бързо да станат трудни, когато се прилагат за сложни проблеми. Част от програмистите не занят как да ги използват и се съпротивляват на желанието да опитат да опростят нещата, когато не могат нормално да се изразяват с тях.

Упражнения

Това е почти неизбежно, че в хода на работа по тези упражнения, вие ще се объркате и разочаровате от необяснимото поведение на някои регулярни изрази. Понякога помага да въведете вашия израз в онлайн инструмент, като debuggex.com за да видите дали неговата визуализация съответства на това, което сте искали и да експериментирате с начина, по който реагира на различни входни strings.

RegExp голф

Код голф е термин използван за игра и се опитва да изрази конкретна програма в няколко характера, ако е възможно. По същия начин regexp golf е практика при писане на малък регулярен израз, който ако е възможно да съвпадне с определен модел и само с този модел.

За всяка от следните позиции, напишете регулярен израз, който да тества дали някой от дадените substrings се среща в string. Регулярният израз трябва да съвпада само с strings, съдържащ един от описаните substrings. Не се притеснявайте за думи граници, освен ако изрично не е описано. Когато вашия израз работи, вижте дали можете да гo направите по-малък.

  1. car and cat

  2. pop and prop

  3. ferret, ferry, and ferrari

  4. Any word ending in ious

  5. A whitespace character followed by a dot, comma, colon, or semicolon

  6. A word longer than six letters

  7. A word without the letter e

Вижте таблицата в резюмето на главата за помощ. Тествайте всяко решение с няколко тест strings.

// Fill in the regular expressions

verify(/.../,
       ["my car", "bad cats"],
       ["camper", "high art"]);

verify(/.../,
       ["pop culture", "mad props"],
       ["plop"]);

verify(/.../,
       ["ferret", "ferry", "ferrari"],
       ["ferrum", "transfer A"]);

verify(/.../,
       ["how delicious", "spacious room"],
       ["ruinous", "consciousness"]);

verify(/.../,
       ["bad punctuation ."],
       ["escape the dot"]);

verify(/.../,
       ["hottentottententen"],
       ["no", "hotten totten tenten"]);

verify(/.../,
       ["red platypus", "wobbling nest"],
       ["earth bed", "learning ape"]);


function verify(regexp, yes, no) {
  // Ignore unfinished exercises
  if (regexp.source == "...") return;
  yes.forEach(function(s) {
    if (!regexp.test(s))
      console.log("Failure to match '" + s + "'");
  });
  no.forEach(function(s) {
    if (regexp.test(s))
      console.log("Unexpected match for '" + s + "'");
  });
}

Цитати - стил

Представете си, че сте написали една история и използвате единични кавички за да отбележите парчета диалог. Сега искате да замените всички цитати на диалога с двойни кавички, като запазите единичните кавички използвани, като знак за съкращение.

Помислете за модел, който разграничава тези два вида ползване на цитати и направете извикване към replace метода, който да направи правилно замяната.

var text = "'I'm the cook,' he said, 'it's my job.'";
// Change this call.
console.log(text.replace(/A/g, "B"));
// → "I'm the cook," he said, "it's my job."

Най-очевидното решение е да се заменят само цитатите с nonword характери поне на едната страна. Нещо, подобно на /\W'|'\W/. Но вие също трябва да се вземе началото и края на реда под внимание.

В допълнение, трябва да сте сигурни, че замяната включва характери, които съответстват на \W модела, така че да не се пропуснат. Това може да се направи, като ги опаковате в скоби включително и техните групи в заместващ string ($1, $2). Групи, които не се покриват няма да бъдат заменени с нищо.

Отново номера

Серия от цифри могат да бъдат съчетани с прост регулярен израз /\d+/.

Напишете израз, който съвпада само с JavaScript-стил номера. Той трябва да подържа незадължителните характери плюс + и минус - в предната част на числото, след десетичната точка и експонента нотацията - 5e-3 или 1E10 - отново с допълнителен характер пред експонентата. Също така, имайте предвид, че не е необходимо да има цифри пред или след точката, но броят им не може да бъде само една точка. Това означава, че .5 и 5. са валидни номера в JavaScript, но само точка не е.

// Fill in this regular expression.
var number = /^...$/;

// Tests:
["1", "-1", "+15", "1.55", ".5", "5.", "1.3e2", "1E-4",
 "1e+12"].forEach(function(s) {
  if (!number.test(s))
    console.log("Failed to match '" + s + "'");
});
["1a", "+-1", "1.2.3", "1+1", "1e4.5", ".5.", "1f5",
 "."].forEach(function(s) {
  if (number.test(s))
    console.log("Incorrectly accepted '" + s + "'");
});

На първо място, не забравяйте обратно наклонената черта преди точката.

Съвпадението с незадължителен характер преди номер, както и преди експонента, може да се направи със [+\-]? или (\+|-|) (плюс, минус или нищо).

Най-сложната част от упражнението е проблема със съвпадението на двете "5." и ".5" без да съвпада с точката ".". За тази цел, добро решение е да използвате оператора | да се разделят двата случая - една или повече цифри по желание последвани от точка и нула или повече цифри или точка, последвани от една или повече цифри.

И накрая, за да се влияе от случая или добавете i опция за регулярния израз или използвайте [eE].