PHP: zvýraznění výrazu a diakritika

Peterson

PHP: zvýraznění výrazu a diakritika
« kdy: 24. 01. 2011, 16:13:21 »
Dobry den,

rad bych zvyraznil hledany vyraz v textu tak, aby preg_replace() nebral v potaz diakritiku v $subject, ale nevim si s tim rady...

Napr.:
Uzivatel zadal jako vyraz pro hledani "pocitac".
Pomoci MySQL LIKE '%pocitac%' dostanu text, kde se vyskytuje slovo "počítač".
Nyni bych rad v tomto textu zvyraznil ono slovo "počítač", ovsem uzivatel pro hledani nepouzil diakritiku.

Opacny pripad mi problemy nedela (tzn. zvyraznit v textu "pocitac", pokud uzivatel hleda "počítač").

Resil uz toto nekdo? Poradite?

Dekuji
--
P.
« Poslední změna: 25. 01. 2011, 10:57:28 od Petr Krčmář »


Logik

  • *****
  • 1 022
    • Zobrazit profil
    • E-mail
Re: PHP: zvyrazneni vyrazu, preg_replace vs. diakritika
« Odpověď #1 kdy: 24. 01. 2011, 16:29:52 »
No jedna možnost je pocčítač přepsat na něco typu
po[cč][ií]ta[cč]
písmen s diakritikou není tolik, takže výslednej regexp nebude tak dlouhej.

Druhá možnost je převést text na text bez diakritiky, vyhledat tam pocitac
a zvýrazňovat pak v orioginálním textu pomocí indexu.

ghost

Re: PHP: zvyrazneni vyrazu, preg_replace vs. diakritika
« Odpověď #2 kdy: 24. 01. 2011, 16:45:33 »
krkolomne reseni,
1. udelat si duplikat retezce bez diakritiky - jak vzor tak vysledek - treba iconvem z utf8 -> ascii
2. pouzit strpos na zjisteni pozice, kde retezec je
3. delku hledaneho vis
4. nahradit od indexu do indexu + delky retezce

Re: PHP: zvyrazneni vyrazu, preg_replace vs. diakritika
« Odpověď #3 kdy: 24. 01. 2011, 17:58:05 »
Dobry den,

rad bych zvyraznil hledany vyraz v textu tak, aby preg_replace() nebral v potaz diakritiku v $subject, ale nevim si s tim rady...

Napr.:
Uzivatel zadal jako vyraz pro hledani "pocitac".
Pomoci MySQL LIKE '%pocitac%' dostanu text, kde se vyskytuje slovo "počítač".
Nyni bych rad v tomto textu zvyraznil ono slovo "počítač", ovsem uzivatel pro hledani nepouzil diakritiku.

Opacny pripad mi problemy nedela (tzn. zvyraznit v textu "pocitac", pokud uzivatel hleda "počítač").

Resil uz toto nekdo? Poradite?

Dekuji
--
P.

zkus: php.net/manual/en/book.mbstring.php
Jestliže máte Windows tak si nafackujte, protože Váš počítač právě útočí na některý server. Děkujeme

Peterson

Re: PHP: zvyrazneni vyrazu, preg_replace vs. diakritika
« Odpověď #4 kdy: 25. 01. 2011, 08:56:03 »
Napad s pocitanim pozice a pote nahrazeni od indexu do delky slova zvyraznenim byl sice funkcni, ale opravdu dosti krkolomny.

Nakonec jsem se na celou vec podival z druhe strany a to se mi zda ponekud jednodussi jak na psani, tak i na vykon...

Ve vysledku tedy rozdelim jednotliva slova z vyhledaneho textu do pole. Kazde takoveto slovo potom prevadim do ASCII//TRANSLIT a porovnavam vuci hledanemu vyrazu (take ASCII//TRANSLIT). Pokud se vyraz shoduje, doplnim ho o zvyraznovaci kod.
Vyhoda tohoto reseni spociva v tom, ze lze zvyraznit vsechny nalezene shody a zaroven cele slovo kde ke shode doslo.

Snad se nekomu bude hodit, prip. uvitam diskuzi vedouci ke zlepseni:

Kód: [Vybrat]
// Nutne nastavit locale jinak se iconv chova divne
setlocale(LC_ALL, 'cs_CZ.UTF8');

function highlight($haystack,$needle) {

// Hledany vyraz prevest na transliterovany ASCII
$ASCII_needle=iconv("UTF-8","ASCII//TRANSLIT",$needle);

// Rozdelit slova z $haystack do pole; Odstranit duplicity z pole
$words=preg_split("/[\s,\.]+/",$haystack); array_unique($words);

// Prochazet pole slov
foreach($words as $word) {

// Slovo prevest na transliterovany ASCII
$ASCII_word=iconv("UTF-8","ASCII//TRANSLIT",$word);

// Porovnat shodu (pokud chci zvyraznit cele slovo kde je shoda, jinak pouzit /^$ASCII_needle$/ pro absolutni shodu)
if(preg_match("/".$ASCII_needle."/ui",$ASCII_word)) {

// Zvyrazneni hledaneho slova
$haystack=str_replace("$word","<span class=\"highlight\">$word</span>",$haystack);

}
}
return $haystack;
}


Logik

  • *****
  • 1 022
    • Zobrazit profil
    • E-mail
Re: PHP: zvyrazneni vyrazu, preg_replace vs. diakritika
« Odpověď #5 kdy: 25. 01. 2011, 09:25:07 »
ehm.... jednodušší? výkonější? To tvoje řešení je kulantně řečeno jedno z nejhorších. Jedinou devizu má, že je vcelku krátký, i když hromada lepších řešení by nebylo o moc delších.

O co je výkonější rozdělit text do pole a zkonvertovat každej prvek pole, než zkonvertovat celej text najednou? (A už vůbec nemluvim o tom, že text v ASCII si lze
připravit jednou a načítat z databáze už zkonvertovanej....) Už jen to zbytešný rozdělování do pole...
O co je výkonější provnávat každej prvek pole, než vyhledávat najednou v celym řetězci? Nemluvě o x-násobný kompilaci regulárního řetězce.

A každý nalezený slovo v řetězci navíc v každé podobě (s diakritikou nebo bez) hledáš v řetězci znovu. Takže pokud se ti tam vyskytuje Počítač, počítač, pocitac a Pocitac, tak místo jednoho převedení textu na lowercase ascii a vyhledání pomocí jednoduchého strpos (značně levnější než preg_match) to vyhledáváš 5x... To je výkonnost :-)

PS: Jako detail už je, že když už používáš str_replace, tak bys moh to aspoň nahradit vše najednou, viz argument typu pole u str_replace, nebudeš tak x-krát sestavovat dlouhej řetězec.

PPS: Kdyby každej programátor povinně rok psal v C/C++,. to by byl svět krásnej :-)

« Poslední změna: 25. 01. 2011, 09:35:54 od Logik »

Peterson

Re: PHP: zvyrazneni vyrazu, preg_replace vs. diakritika
« Odpověď #6 kdy: 25. 01. 2011, 09:39:21 »
Logik:

uznavam, ze to moje reseni neni uplne koser, ale na druhou stranu ma velkou vyhodu v tom, ze muzu zvyraznit cele slovo kde je vyskyt hledaneho vyrazu a zaroven vsechny vyskyty v celem textu.
Ano, index je jiste nejjednodussi orientace v retezci. Ne, ze by se mi ho nechtelo pouzit, ale pomoci indexu jsem neprisel na zpusob jak docilit toho co mam za lubem...

ghost

Re: PHP: zvyrazneni vyrazu, preg_replace vs. diakritika
« Odpověď #7 kdy: 25. 01. 2011, 11:01:00 »
Kód: [Vybrat]
setlocale(LC_ALL, 'cs_CZ.UTF8');
function highlight($search, $string) {
$_search = strtolower(iconv("UTF-8","ASCII//TRANSLIT", $search));
$_string = strtolower(iconv("UTF-8","ASCII//TRANSLIT", $string));
$result = "";
$end = "";

while(($lastIndex = strpos($_string, $_search)) !== false) {
// zacatek vyrazu
$start = mb_substr($string, 0, $lastIndex, "UTF-8");
// hledany vyraz
$middle = "<b>".mb_substr($string, $lastIndex, strlen($_search), "UTF-8")."</b>";
// novy zbytek vyrazu
$end = mb_substr($string, $lastIndex + strlen($_search), strlen($_string), "UTF-8");

// pripojime do vysledku
$result .= $start.$middle;

// hledame jen dopredu
$string = $end;
$_string = substr($_string, $lastIndex + strlen($_search), strlen($_string));
}

$result .= $end;

return mb_strlen($result, "UTF-8") ? $result : $string;
}

Zvyrazní všechno v textu, ignoruje diakritiku a velikost pismen. Ale nebere prekryvy - muzes si to ale upravit :).
Kód: [Vybrat]
echo highlight("poc", "Pocitace počitace počítač tralala");
<b>Poc</b>itace <b>poč</b>itace <b>poč</b>ítač tralala;

echo highlight("pop", "popop");
<b>pop</b>op

ghost

Re: PHP: zvýraznění výrazu a diakritika
« Odpověď #8 kdy: 25. 01. 2011, 11:14:32 »
prosim nekritizovat za opakovane pouziti strlen, zejmena pak strlen($_search). Samozrejme spocitat jednou na zacatku ... psal jsem rychle ...

Logik

  • *****
  • 1 022
    • Zobrazit profil
    • E-mail
Re: PHP: zvýraznění výrazu a diakritika
« Odpověď #9 kdy: 25. 01. 2011, 12:40:31 »
Peterson: viz např. ghostovo řešení. Zvýrazňovat celý slovo - to je takovej problém získanej index (respektive interval indexů) "rozšířit" o všechny nebílý znaky na obě strany?

Ghost: ten strlen by tak nevadil, v php je dýlka stringu uložená, takže se nemusí projít celej string. Horší je, že v každym kroku cyklu vytváříš novej $_string, místo toho, co bys nechal původní a hledal od určitýho indexu. No a pak ještě to jde taky dělat "inplace", stačí si pamatovat délku vloženejch segmentů. U hodně dlouhýho řetězce (řádově stovky kilobajt) to může udělat znatelnej rozdíl, nejen ve spotřebě paměti, ale taky potažmo rychlosti díky procesorový cache.

Ale možná úplně nejlepší řešení je ne budovat ten string inkrementálně, ale jeho vytvářený kousky házet do pole a pak výslednej řetězec vytvořit pomocí implode. Odpadne vytváření spousty pracovních pomocnejch řetězců a v implode se vše naalokuje a zkopíruje najednou.
Dokonce některý knihovny by to tak zvládly bez jakýhokoli kopírování částí řetězců - někde je substr implementovaný tak, že nevytváří novej buffer, ale používá buffer
původního řetězce, takže by se opravdu řetězec kopíroval pouze jednou. To ale bohužel není případ php.
« Poslední změna: 25. 01. 2011, 12:47:27 od Logik »

ghost

Re: PHP: zvýraznění výrazu a diakritika
« Odpověď #10 kdy: 25. 01. 2011, 13:01:26 »
Ghost: ten strlen by tak nevadil, v php je dýlka stringu uložená, takže se nemusí projít celej string. Horší je, že v každym kroku cyklu vytváříš novej $_string, místo toho, co bys nechal původní a hledal od určitýho indexu. No a pak ještě to jde taky dělat "inplace", stačí si pamatovat délku vloženejch segmentů. U hodně dlouhýho řetězce (řádově stovky kilobajt) to může udělat znatelnej rozdíl, nejen ve spotřebě paměti, ale taky potažmo rychlosti díky procesorový cache.

Ale možná úplně nejlepší řešení je ne budovat ten string inkrementálně, ale jeho vytvářený kousky házet do pole a pak výslednej řetězec vytvořit pomocí implode. Odpadne vytváření spousty pracovních pomocnejch řetězců a v implode se vše naalokuje a zkopíruje najednou.
Dokonce některý knihovny by to tak zvládly bez jakýhokoli kopírování částí řetězců - někde je substr implementovaný tak, že nevytváří novej buffer, ale používá buffer
původního řetězce, takže by se opravdu řetězec kopíroval pouze jednou. To ale bohužel není případ php.

strlen - jasny, vim jak je to s delkou stringu, ale neni moc pekne volat stejnou funkci se stejnym parametrem vicekrat :)

spojovani - rozmyslel jsem, zda pouzit implode a chvili jsem jej tam i mel, ale nakonec jsem pouzil spojeni retezcu prislo mi to v dany okamzik nazornejsi :)

hledani az od indexu - samozrejme by to bylo vhodnejsi, ale psal jsem jen po pameti a pouzival tak fce, ktere znam.
Ted jsem se podival a zjistil, ze strpos ma i treti parametr offset :) - tim by se to krasne vyresilo a odpadlo by znovu vytvareni $_string

Kazdopadne jsem chtel jen lehce nastinit jak to udelat pomoci zakladnich funkci pro praci se stringem a vyhnout se regexpum ...

Efficient

Re: PHP: zvýraznění výrazu a diakritika
« Odpověď #11 kdy: 25. 01. 2011, 13:18:03 »
Tady jsou sami PHP heroes jak tak vidim... Zkuste trochu algoritmicky myslet drive nez neco zprasite v PHP...

ghost

Re: PHP: zvýraznění výrazu a diakritika
« Odpověď #12 kdy: 25. 01. 2011, 13:27:26 »
Tady jsou sami PHP heroes jak tak vidim... Zkuste trochu algoritmicky myslet drive nez neco zprasite v PHP...
Tak nam navrhni vlastni reseni a hlavne u toho algoritmicky mysli ... navic tazatel to chtel v PHP ... to mu to mame napsat v Cecku?
Nebo jsme mu meli napsat algoritmus na vyhledávaní v textu - treba Rabin–Karp?

Peterson

Re: PHP: zvýraznění výrazu a diakritika
« Odpověď #13 kdy: 25. 01. 2011, 18:02:40 »
Jeste jsem si s tim nejakou dobu hral a zkousel ...

Priklad od Ghosta funguje perfektne. Trochu jsem ho upravil na zaklade navrhu Logika, ale bud jsem to neudelal dobre nebo je chyba jinde (je rozhodne pomalejsi).
Mozna nakonec to, ze Ghost "$_string" ukusuje, je pro vykon lepsi nez prochazeni celeho textu pomoci indexu a offsetu.
Sazeni vysledku do pole a nasledny implode (namisto rozsirovani stavajiciho stringu) nemelo temer zadny vliv na pamet i vykon (nebo opravdu velmi zanedbatelny).

Nakonec jsem otestoval celkem 4 reseni na textu dlouhem 1.5MB (1.564.135 slov, 172.109 slov), testovaci podminky byly shodne a pred kazdym testem jsem restartoval apache.
Vysledky jsou nasledujici:

# Ghostovo originalni reseni
highlight    MEM: 1707,292 kB, Time: 13,8758 s

# Uprava Ghostova reseni
highlight2  MEM: 1707,360 kB, Time: 17,7329 s

Kód: [Vybrat]
function highlight2($search, $string) {
$_search = strtolower(iconv("UTF-8","ASCII//TRANSLIT", $search));
$_string = strtolower(iconv("UTF-8","ASCII//TRANSLIT", $string));
$result = "";
$len=strlen($_search);

// Vychozi offset
$offset=0;

// Hledame pozici prvniho vyskytu hledaneho; bereme v potaz offset
while(($lastIndex = strpos($_string, $_search, $offset)) !== false) {

// zacatek vyrazu (zaciname na offsetu)
$start = mb_substr($string, $offset, ($lastIndex-$offset), "UTF-8");
// hledany vyraz
$middle = "<b>".mb_substr($string, $lastIndex, $len, "UTF-8")."</b>";

// pripojime do vysledku
$result .= $start.$middle;

// Nastavime novy offset
$offset=($lastIndex+$len);
}

// Pripojime konec textu (bereme v potaz offset)
$result.=mb_substr($string,$offset,mb_strlen($string,"UTF-8"),"UTF-8");

return mb_strlen($result, "UTF-8") ? $result : $string;
}

# Index pozic pomoci preg_match_all()
highlight3  MEM: 1794,860 kB, Time: 15,4054 s

Kód: [Vybrat]
function highlight3($needle,$haystack,$type="need") {

// Transliterace
$ASCII_haystack=strtolower(iconv("UTF-8","ASCII//TRANSLIT",$haystack));
$ASCII_needle=strtolower(iconv("UTF-8","ASCII//TRANSLIT",$needle));

// Delka hledaneho
$len=strlen($ASCII_needle);

// preg_match_all nam naplni pole pozicemi hledaneho vyrazu
preg_match_all("/$ASCII_needle/",$ASCII_haystack,$m,PREG_OFFSET_CAPTURE);

// Vychozi offset
$offset=0;

// Prochazeni pole s pozicemi kazdeho vyskytu
foreach($m[0] as $p) {

// Pozice vyskytu
$pos=$p[1];

// Zacatek vyrazu
$start=mb_substr($haystack,$offset,($pos-$offset),"UTF-8");

// Zvyrazneni
$word="<strong>".mb_substr($haystack,$pos,$len,"UTF-8")."</strong>";

// Pripojeni do vysledku
$out.=$start.$word;

// Novy offset
$offset=($pos+$len);
}

// Konec vyrazu do vysledku
$out.=mb_substr($haystack,$offset,mb_strlen($haystack,"UTF-8"),"UTF-8");

return $out;

}

# Diakritika / alternativy pomoci preg_replace()
Nejvice naroku na pamet i cas. Zase na druhou stranu to je jednoduche a diky regexpum se to da velice dobre rozsirovat.
highlight4  MEM: 3261,516 kB, Time: 17,8068 s

Kód: [Vybrat]
function highlight4($needle,$haystack,$type="need") {

// Priprava na slozeni regexpu (obsazeni temer vsech moznych akcentu)
$map1[]="/[aãǎâăåąàȧáä]/iu";
$map1[]="/[bḃ]/iu";
$map1[]="/[cčĉċćç]/iu";
$map1[]="/[dďḋḑ]/iu";
$map1[]="/[eěêĕęèéëȩ]/iu";
$map1[]="/[fḟ]/iu";
$map1[]="/[gǧĝġǵģ]/iu";
$map1[]="/[hȟĥḣḧḩ]/iu";
$map1[]="/[iĩǐîĭįìıíï]/iu";
$map1[]="/[jǰĵ]/iu";
$map1[]="/[kǩḱķ]/iu";
$map1[]="/[lľĺļ]/iu";
$map1[]="/[mṁḿ]/iu";
$map1[]="/[nñňǹṅńņ]/iu";
$map1[]="/[oõǒôŏǫòȯóőö]/iu";
$map1[]="/[pṗṕþ]/iu";
$map1[]="/[rřṙŕŗ]/iu";
$map1[]="/[sšŝṡśş]/iu";
$map1[]="/[tťṫẗţ]/iu";
$map1[]="/[uũǔûŭůųùúűü]/iu";
$map1[]="/[vṽ]/iu";
$map1[]="/[wŵẘẁẇẃẅ]/iu";
$map1[]="/[xẋ]/iu";
$map1[]="/[yỹŷẙỳẏýÿ]/iu";
$map1[]="/[zžẑżź]/iu";

// Slozeni regexpu
$map2[]="[aãǎâăåąàȧáä]";
$map2[]="[bḃ]";
$map2[]="[cčĉċćç]";
$map2[]="[dďḋḑ]";
$map2[]="[eěêĕęèéëȩ]";
$map2[]="[fḟ]";
$map2[]="[gǧĝġǵģ]";
$map2[]="[hȟĥḣḧḩ]";
$map2[]="[iĩǐîĭįìıíï]";
$map2[]="[jǰĵ]";
$map2[]="[kǩḱķ]";
$map2[]="[lľĺļ]";
$map2[]="[mṁḿ]";
$map2[]="[nñňǹṅńņ]";
$map2[]="[oõǒôŏǫòȯóőö]";
$map2[]="[pṗṕþ]";
$map2[]="[rřṙŕŗ]";
$map2[]="[sšŝṡśş]";
$map2[]="[tťṫẗţ]";
$map2[]="[uũǔûŭůųùúűü]";
$map2[]="[vṽ]";
$map2[]="[wŵẘẁẇẃẅ]";
$map2[]="[xẋ]";
$map2[]="[yỹŷẙỳẏýÿ]";
$map2[]="[zžẑżź]";

// Nahrazeni hledaneho vyrazu regexpem
// "pocitac" se bude hledat jako: [pṗṕþ][oõǒôŏǫòȯóőö][cčĉċćç][iĩǐîĭįìıíï][tťṫẗţ][aãǎâăåąàȧáä][cčĉċćç]
$needle=preg_replace($map1,$map2,$needle);

// Finalni nahrazeni hledaneho vyrazu v textu
$haystack=preg_replace("/[^\s]*${needle}[^\s\.]*/iu","<strong>\$0</strong>",$haystack);

return $haystack;
}

ghost

Re: PHP: zvýraznění výrazu a diakritika
« Odpověď #14 kdy: 25. 01. 2011, 19:19:31 »
Pekne srovnani.
Jen mala poznamka k highlight3:
mel bys jeste v $ASCII_needle backslashovat vsechny specialni znaky, ktere jsou ridici pro regularni vyraz - teda pokud to nedelas o uroven vys nebo s tim zamerne nepocitac ...