Задать вопрос

Помогите составить регулярное выражение для замены первого вхождения слова в HTML, но строго вне анкоров и атрибутов?

Друзья, регуляркой требуется найти и заменить в тексте, размеченном в HTML:

  1. Первое вхождение заданного слова
  2. Которое при этом не обёрнуто внутрь тега <a>…</a> (т.е. не попадает в анкор какой-либо ссылки)
  3. Которое при этом не является частью какого-либо атрибута (типа alt у тега img)

Ниже представляю текст для примера и тестов. Замена должна случиться лишь во фрагменте «… каждый огурец, только что...» (первое предложение последнего абзаца). Все остальные вхождения слова «огурец» нарушают какое-либо из перечисленных условий.

<p>Самые вкусные огурцы росли у меня на даче прошлым летом, это был отличный сезон. Когда я их срывал, то уплетал вот так: <br /> <img src="cucumber-eater.png" alt="Я ем огурец"></p>
<p>Кстати, вы знали, что <a href="http://example.com/super-facts/blue-cucumber" title="По ссылке рассказ про то, как вырастить синий огурец">обычный огурец может быть синим</a>? Я вот — не знал, думал, что они все только зелёные.</p>
<p>Лично мне аппетитным кажется каждый огурец, только что сорванный с грядки. Хотя свежий на вид огурец и зимой можно купить в любом супермаркете, я предпочитаю кушать только то, что выращено своими руками под бдительным контролем.</p>

Проблема решаема за счёт работы с DOM (обходятся отдельно иннертексты всех узлов с помощью simplehtmldom.sourceforge.net или иного парсера и замены просто не делаются для иннертекстов элементов <a>). Но намного удобнее было бы иметь решение в виде работающего регулярного выражения (производительность не принципиальна). Его осилить — не могу, ибо с регулярками слабо знаком.

Заранее спасибо за ваше внимание к такому нетривиальному случаю.

P.S. В комментариях — много интересного. Война регулярок и контрпримеров. Спасибо хабралюдям Jaguar_ko, yui_room9, dsd_corp за разыгравшуюся зарубу :-)
  • Вопрос задан
  • 7544 просмотра
Подписаться 12 Оценить Комментировать
Решения вопроса 1
Jaguar_ko
@Jaguar_ko
/(search)(?!.*(?:)|(".*>))/
Это чисто теоретически:)
Проверить в час ночи на телефоне нет возможности)
П.С: search — искомое слово :)
Ответ написан
Пригласить эксперта
Ответы на вопрос 3
@Ents
Сделать подобное невозможно никаким регулярным языком. Если интересно почему — читайте про конечные автоматы (частным случаем которых, являются регулярки)

Смотрите в сторону DOM
Ответ написан
Quiz
@Quiz
Когда человек сталкивается с проблемой, он думает «Я могу легко решить эту проблему при помощи регулярного выражения!». С тех пор у него две проблемы...
Ответ написан
@dsd_corp
По просьбам выше выкладываю.
Идем в этот репозиторий.
Тащим оттуда три файла: xmlp.inc, progress.inc и cucumbers.zip.
Сам пример по вашему вопросу в cucumbers.zip.
xmlp.inc — парсер типа DOM.
progress.inc — просто вспомогательный, используется примером для замера и вывода на экран времени работы.

Нужно распаковать zip и в получившуюся директорию скопировать остальные два файла.

Собственно запускаем пример example.php

Основная нужная вам функция: replace_text()
Первые два параметра понятны и так — это текст для поиска и искомое.
Четвертый параметр $ignore_tags — это массив имен тегов, которые мы пропускаем. В вашем случае по условию это 'A'. 'IMG' в примере можно исключить из этого массива — я просто так добавил )
Третий параметр — на что заменить найденные вхождения.
Но если этот третий параметр false(я так сделал опцию), то функция вернет не измененную строку, а массив оффсетов найденных вхождений.
Функция не останавливается на первом валидном вхождении — заменяет все, что найдет, и все, что подходит по условию.
Если вы не хотите, чтобы функция правила косяки HTML/XML ее собственному разумению, ну и заодно хотите заменить только определенные по счету вхождения, тогда вы можете получить оффсеты, и потом либо в цикле заменить все PHP-шной функцией substr_replace(т.к. оффсеты вхождений у вас есть, а длину искомой строки вы тоже знаете), либо заменить только первое вхождение по первому оффсету из возвращенного массива.

В примере функции frt1(), frt2() и frt3() идентичны по функционалу, frt1() работает рекурсивно, в остальных от рекурсии я избавился. frt3() от frt2() отличается только ассоциативной индексацией стека(в глазах не так рябит и понятнее). А так все три эти функции делают одно и то же, первые две можно удалить.
Фактически используется frt3() для поиска с заменой и frt4() для получения оффсетов.

Файл cucumbers.txt — это ваш пример, в cucumbers1.txt я напихал еще огурцов в разные места )))
Эти файлы используются как входные, ну там разберетесь, видно все по коду.
Результаты работы экзампла тоже в файлы выплевываются, вы их увидите в той же директории после отработки скрипта.
Будут вопросы — задавайте.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы