2012-01-31 6 views
4

У меня есть небольшая поисковая система, делающая свое дело, и хочу выделить результаты. Я думал, что все это сработало до тех пор, пока набор ключевых слов, которые я использовал сегодня, не выдул его из воды.ключевое слово highlight выделяет основные моменты в PHP preg_replace()

Проблема заключается в том, что preg_replace() выполняет цикл по замене, а в дальнейшем замены заменяют текст, который я вставлял в предыдущие. Смущенный? Вот моя псевдо функция:

public function highlightKeywords ($data, $keywords = array()) { 
    $find = array(); 
    $replace = array(); 
    $begin = "<span class=\"keywordHighlight\">"; 
    $end = "</span>"; 
    foreach ($keywords as $kw) { 
     $find[] = '/' . str_replace("/", "\/", $kw) . '/iu'; 
     $replace[] = $begin . "\$0" . $end; 
    } 
    return preg_replace($find, $replace, $data); 
} 

КИ, так что он работает при поиске «Фред» и «Дагг» но, к сожалению, при поиске «класс» и «Lass» и «как» это наносит реальный вопрос при выделении «Class Group Джозефа»

Joseph's <span class="keywordHighlight">Cl</span><span <span c<span <span class="keywordHighlight">cl</span>ass="keywordHighlight">lass</span>="keywordHighlight">c<span <span class="keywordHighlight">cl</span>ass="keywordHighlight">lass</span></span>="keywordHighlight">ass</span> Group 

Как бы я получить последние замены для работы только на не-HTML компонентах, а также позволяет маркировать весь матч? например если бы я искал «cla» и «lass», я бы хотел, чтобы «класс» был полностью выделен, поскольку в нем присутствуют как условия поиска, даже если они перекрываются, а выделение, которое было применено к первому совпадению, имеет класс "в нем, но , что не следует выделять.

Вздох.

Я предпочел бы использовать PHP-решение, чем jQuery (или любой клиентский).

Примечание: Я попытался отсортировать ключевые слова по длине, сначала сделав длинные, но это означает, что поиск по пересечению не выделяется, что означает «cla» и «lass» только часть слова «class» «выделил бы, и он все же убил заменяющие метки :(

EDIT: Я испортил, начиная с карандаша & бумаги и диких блужданий, и придумал очень неглазурованный код, чтобы решить эту проблему. , поэтому предложения по отделке/ускорению этого по-прежнему будут по достоинству оценены :)

public function highlightKeywords ($data, $keywords = array()) { 
    $find = array(); 
    $replace = array(); 
    $begin = "<span class=\"keywordHighlight\">"; 
    $end = "</span>"; 
    $hits = array(); 
    foreach ($keywords as $kw) { 
     $offset = 0; 
     while (($pos = stripos($data, $kw, $offset)) !== false) { 
      $hits[] = array($pos, $pos + strlen($kw)); 
      $offset = $pos + 1; 
     } 
    } 
    if ($hits) { 
     usort($hits, function($a, $b) { 
      if ($a[0] == $b[0]) { 
       return 0; 
      } 
      return ($a[0] < $b[0]) ? -1 : 1; 
     }); 
     $thisthat = array(0 => $begin, 1 => $end); 
     for ($i = 0; $i < count($hits); $i++) { 
      foreach ($thisthat as $key => $val) { 
       $pos = $hits[$i][$key]; 
       $data = substr($data, 0, $pos) . $val . substr($data, $pos); 
       for ($j = 0; $j < count($hits); $j++) { 
        if ($hits[$j][0] >= $pos) { 
         $hits[$j][0] += strlen($val); 
        } 
        if ($hits[$j][1] >= $pos) { 
         $hits[$j][1] += strlen($val); 
        } 
       } 
      } 
     } 
    } 
    return $data; 
} 
+0

'$ hits [$ i] [0]' означает, что даны 0 '$ hits'? О, этот мой ум ... – Cyclone

+0

'$ hits [$ i] [0]' является начальной точкой ключевого слова, а '$ hits [$ i] [1]' является конечной точкой. Это менее запутанно на бумаге :) – CrazyChris

+0

Прочтите мой снова, заменив '$' на 'S' мысленно ... просто плохая шутка, я боюсь – Cyclone

ответ

0

Я использовал foll из-за решения этой проблемы:

<?php 

$protected_matches = array(); 
function protect(&$matches) { 
    global $protected_matches; 
    return "\0" . array_push($protected_matches, $matches[0]) . "\0"; 
} 
function restore(&$matches) { 
    global $protected_matches; 
    return '<span class="keywordHighlight">' . 
       $protected_matches[$matches[1] - 1] . '</span>'; 
} 

preg_replace_callback('/\x0(\d+)\x0/', 'restore', 
    preg_replace_callback($patterns, 'protect', $target_string)); 

Первый preg_replace_callback вытаскивает все матчи и заменяет их NUL-байтов обернутый заполнителей; второй проход заменяет их тегами span.

Редактировать: Забыл (а), что $patterns был отсортирован по длине строки, максимально длинный и короткий.

Редактировать; другое решение

<?php 
     function highlightKeywords($data, $keywords = array(), 
      $prefix = '<span class="hilite">', $suffix = '</span>') { 

     $datacopy = strtolower($data); 
     $keywords = array_map('strtolower', $keywords); 
     $start = array(); 
     $end = array(); 

     foreach ($keywords as $keyword) { 
      $offset = 0; 
      $length = strlen($keyword); 
      while (($pos = strpos($datacopy, $keyword, $offset)) !== false) { 
       $start[] = $pos; 
       $end[] = $offset = $pos + $length; 
      } 
     } 

     if (!count($start)) return $data; 

     sort($start); 
     sort($end); 

     // Merge and sort start/end using negative values to identify endpoints 
     $zipper = array(); 
     $i = 0; 
     $n = count($end); 

     while ($i < $n) 
      $zipper[] = count($start) && $start[0] <= $end[$i] 
       ? array_shift($start) 
       : -$end[$i++]; 

     // EXAMPLE: 
     // [ 9, 10, -14, -14, 81, 82, 86, -86, -86, -90, 99, -103 ] 
     // take 9, discard 10, take -14, take -14, create pair, 
     // take 81, discard 82, discard 86, take -86, take -86, take -90, create pair 
     // take 99, take -103, create pair 
     // result: [9,14], [81,90], [99,103] 

     // Generate non-overlapping start/end pairs 
     $a = array_shift($zipper); 
     $z = $x = null; 
     while ($x = array_shift($zipper)) { 
      if ($x < 0) 
       $z = $x; 
      else if ($z) { 
       $spans[] = array($a, -$z); 
       $a = $x; 
       $z = null; 
      } 
     } 
     $spans[] = array($a, -$z); 

     // Insert the prefix/suffix in the start/end locations 
     $n = count($spans); 
     while ($n--) 
      $data = substr($data, 0, $spans[$n][0]) 
      . $prefix 
      . substr($data, $spans[$n][0], $spans[$n][1] - $spans[$n][0]) 
      . $suffix 
      . substr($data, $spans[$n][1]); 

     return $data; 
    } 
+0

Похоже, это может сработать ... Я хочу использовать его в объекте, хотя , поэтому, возможно, нужно будет преобразовать в анонимные функции и перевернуть глобальные переменные. У меня будет игра! – CrazyChris

+0

Хммм .... кажется, терпит неудачу на тестах кроссовера, то есть при поиске нескольких ключевых слов, которые пересекаются в том же тексте, например. HELL и LLO как ключевые слова в тексте HELLO найдут только один хит, а не оба из-за недавно введенных символов, и поэтому выделяют только один удар в тексте, а не оба. – CrazyChris

+0

Извините, я пропустил это условие в исходном вопросе. Вторая функция должна делать то, что вы хотите. – Steve

0

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

<?php 

function highlight_range_sort($a, $b) { 
    $A = abs($a); 
    $B = abs($b); 
    if ($A == $B) 
     return $a < $b ? 1 : 0; 
    else 
     return $A < $B ? -1 : 1; 
} 

function highlightKeywords($data, $keywords = array(), 
     $prefix = '<span class="highlight">', $suffix = '</span>') { 

     $datacopy = strtolower($data); 
     $keywords = array_map('strtolower', $keywords); 
     // this will contain offset ranges to be highlighted 
     // positive offset indicates start 
     // negative offset indicates end 
     $ranges = array(); 

     // find start/end offsets for each keyword 
     foreach ($keywords as $keyword) { 
      $offset = 0; 
      $length = strlen($keyword); 
      while (($pos = strpos($datacopy, $keyword, $offset)) !== false) { 
       $ranges[] = $pos; 
       $ranges[] = -($offset = $pos + $length); 
      } 
     } 

     if (!count($ranges)) 
      return $data; 

     // sort offsets by abs(), positive 
     usort($ranges, 'highlight_range_sort'); 

     // combine overlapping ranges by keeping lesser 
     // positive and negative numbers 
     $i = 0; 
     while ($i < count($ranges) - 1) { 
      if ($ranges[$i] < 0) { 
       if ($ranges[$i + 1] < 0) 
        array_splice($ranges, $i, 1); 
       else 
        $i++; 
      } else if ($ranges[$i + 1] < 0) 
       $i++; 
      else 
       array_splice($ranges, $i + 1, 1); 
     } 

     // create substrings 
     $ranges[] = strlen($data); 
     $substrings = array(substr($data, 0, $ranges[0])); 
     for ($i = 0, $n = count($ranges) - 1; $i < $n; $i += 2) { 
      // prefix + highlighted_text + suffix + regular_text 
      $substrings[] = $prefix; 
      $substrings[] = substr($data, $ranges[$i], -$ranges[$i + 1] - $ranges[$i]); 
      $substrings[] = $suffix; 
      $substrings[] = substr($data, -$ranges[$i + 1], $ranges[$i + 2] + $ranges[$i + 1]); 
     } 

     // join and return substrings 
     return implode('', $substrings); 
} 

// Example usage: 
echo highlightKeywords("This is a test.\n", array("is"), '(', ')'); 
echo highlightKeywords("Classes are as hard as they say.\n", array("as", "class"), '(', ')'); 
// Output: 
// Th(is) (is) a test. 
// (Class)es are (as) hard (as) they say. 
0

ОП - то, что неясно в вопросе, могут ли данные $ содержать HTML из get-go. Можете ли вы это прояснить?

Если $ data может содержать сам HTML, вы попадаете в области, пытающиеся анализировать нерегулярный язык с помощью обычного парсера языка, и это не будет хорошо работать.

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

+0

Да, должно было сделать это ясно. Данные - это весь текст, а не HTML. Я понимаю, что это столкнутся с проблемами со специальными переводами символов, но выделение предназначено для ключевых слов, а не знаков препинания, а если & и «не выделены, я в порядке с этими словами. – CrazyChris