Autor Zpráva
návštěvník
Profil *
Když mám nějaký string, který obsahuje diakritiku a chci ho projít ve smyčce, tak abych dostal postupně jednotlivé znaky (resp. jejich offsetové pozice), jak to udělat?
Kajman
Profil
Žádná z mb funkcí nevyhovuje?
http://www.php.net/manual/en/ref.mbstring.php
návštěvník
Profil *
Přesněji řečeno: chtěl bych vytvořit smyčku, kde budu moci procházet jednotlivé byty stringu a rozhodnout se podle toho jestli ten aktuální a následující byte tvoří multibytový znak.


Kajman:
Teď když se tam dívám tak hádám, že je na to mb_substr()


Poznámka:
Když porovnávám tyto dva příkazy, oba dělající stejnou věc: hledám písmeno š ve slabice "šo".
 
$org="šo";$x=0;
die($org[$x].$org[$x+1]."");
die("/".mb_substr($org,$x-1,1,"utf8")."/");
Zjišťuji, že mb_substr počítá offset od jedničky. To je normální?
Joker
Profil
návštěvník:
Cituji odsud z User Contributed Notes:
„I figure most people will want a simple way to break-up a multibyte string into its individual characters. Here's a function I'm using to do that. Change UTF-8 to your chosen encoding method.

<?php 
function mbStringToArray ($string) { 
     $strlen = mb_strlen($string); 
     while ($strlen) { 
         $array[] = mb_substr($string,0,1,"UTF-8"); 
         $string = mb_substr($string,1,$strlen,"UTF-8"); 
         $strlen = mb_strlen($string); 
     } 
     return $array; 
 } 
?>


Přijde mi, že by šlo vymyslet i něco efektivnějšího, když si na to vzpomenu, budu o tom ještě přemýšlet.
juriad
Profil
Pokud je to UTF-8 můžeme se řídit podle nejvyšších dvou bitů. Podrobnosti viz http://en.wikipedia.org/wiki/UTF-8#Description.
<?php

function UTF8ToArray($string) {
        $array = array();
        $len = strlen($string);
        for ($start = 0; $start < $len;) {
                if ((ord($string[$start]) & 0x80) == 0x00) {
                        $array[] = $string[$start];
                        $start++;
                } else {
                        for ($next = 1; $start + $next < $len && (ord($string[$start + $next]) & 0xc0) == 0x80; $next++);
                        $array[] = substr($string, $start, $next);
                        $start += $next;
                }
        }
        return $array;
}

var_dump(UTF8ToArray("Příliš žluťoučký kůň úpěl ďábelské ódy!Ž"));
Pozor na to, že bitový AND má nižší prioritu než porovnání!

Rozezná jen řetězec, kde znaky s diakritikou jsou tvořeny jediným code pointem. Pokud je znak rozdělený na dva, třeba č = c následované háčkem, nelze snadno poznat, ze jde o jeden znak.

Tento příspěvek jsem zrevidoval, výše uvedený kód funguje.
Tori
Profil
Sice se jinak snažím upřednostňovat řetězcové funkce před reguláry, ale tady zrovna bych, už kvůli přehlednosti, použila:
$str = '+ěščřžýáíé';
$znaky = preg_split('//u', $str);
návštěvník
Profil *
Tori, juriad:
Dělám to tak, že vytáhnu jeden mb znak a zjistím jak je dlouhý pomocí strlen(). S binárními operacemi pracovat neumím, ani nerozumím té tabulce. Vím jen že běžný znak má jeden bajt, tj osum bitů, 256 možných znaků, mb tedy 2 bajty, 256*256 = 65536 možností... Které dva bity jsou nejvyšší , ty první dva zleva? Co konkrétně dělá ta ukázka od juriada? Co znamená tato část:

if ((ord($string[$start]) & 0x80) == 0x00)
a tato:
(ord($string[$start + $next]) & 0xc0) == 0x80?
juriad
Profil
UTF-8 má 1 až 6 bytů (to poznáš podle prvního bytu). Multi byte obecně znamená více než jeden byte. (Některé?) čínské znaky používají čtyři byty.
Ne UTF-8 znaky kódované dvěma bytu obsahují efektivně jen 11 bitů informace o znaku. Další bity slouží k určení, o kolikátý byte znaku se jedná a jak je celý znak dlouhý (v bytech).

Ano nejvyšší bity jsou ty vlevo.Nastuduj si něco o dvojkové (začínají 0b) a šestnáctkové soustavě (začínají 0x).

Můj skript funguje tak, že pokud byte začíná nulou (nula je v nejvyšším bitu), jde o znak o délce jednoho bytu. Skrz podmínku na řádku 7 se podle specifikace UTF-8 dostanou jen znaky, které jsou tvořeny jedním bytem. Tedy původní ASCII znaky.

Jinak počítej počet znaků následujících začínajících jedna-nula. Počet bytů, které jsou součásí znaku je v proměnné $next. Tady používám pár triků (jako, že $next po poslední iteraci je rovná délce celého znaku).

ord('a') = 97 v desítkové soustavě = 0x61 v šestnáctkové = 0b0110 0001 ve dvojkové.

Tedy 0xc0 znamená 0b1100 0000 (dvě jedničky a za nimi 6 nul). Operátor & se použivá jako maskovací: z původního bytu vyber ty bity, kterým v masce odpovídá bit s jedničkou (dva nejvyšší).
(0b1101 1101 & 0b1100 0000) je 0b1000 0000 (vše binarně)


Zkus si kód:
        $len = strlen($string);

        for ($i = 0; $i < $len; $i++) {
                echo (ord($string[$i]) & 0xc0), "\n";
        }
Na daném testovacím vstupu vypíše:
64 // P - Obyčejný znak
192 // ř - Začátek vícebytového
128 // Pokračování vícebytového
192 // í - Začátek vícebytového
128 // Pokračování vícebytového
64 // l - Obycejný znak
64 // i -Obyčejný znak
192 // š - Začátek vícebajtového
128 // Pokračování vícebajtového
0 // znak menší než 64 (v tomto případě mezera)
a tak dále.

  0 = 0b0000 0000 = 0x00
 64 = 0b0100 0000 = 0x40
128 = 0b1000 0000 = 0x80
192 = 0b1100 0000 = 0xc0

Ukázka pro čínské znaky:
var_dump(UTF8ToArray("添加了"));
192 // začátek
128 // pokračování
128 // pokračování
192 // začátek
128 // pokračování
128 // pokračování
192 // začátek
128 // pokračování
128 // pokračování

Po rozdělení:
array(3) {
  [0]=>
  string(3) "添"
  [1]=>
  string(3) "加"
  [2]=>
  string(3) "了"
}



Jak kromě modula určit, zda je číslo sudé, nebo liché?
($cislo & 0x01) == 1 // liché (nejnižší bit je jednička)
($cislo & 0x01) == 0 // sudé (nejnižší bit je nula)
Joker
Profil
juriad:
UTF-8 má 1 až 6 bytů (to poznáš podle prvního bytu).
Jen trochu mimotematické doplnění, minimálně v praxi se často nepočítá s více než čtyřmi bajty a mám za to, že to je „ospravedlněné“ i nějakým standardem.
Tzn. obvyklá odpověď na otázku „Kolik bajtů paměti může zabrat n-znakový UTF-8 řetězec?“ je 4*n.
návštěvník
Profil *
juriad:
Tisíceré díky za tvé kódy a vysvětlení, za ty dva příspěvky by sis zasloužil metál a jméno tvé vyrýt na tabuli cti :-).
juriad
Profil
Joker:
Ano, prakticky se setkáš maximálně s čtyřbajtovým UTF-8 znakem. A v Čechách se obvykle pro český text počítá, že může zabrat v paměti 2*n.

Ale pokud píšeš algoritmy pro práci s textem v UTF-8 na bytové úrovni, není nejlepší mít takové předpoklady, protože pak přijde někdo, kdo o problematice ví víc než ty a tobě může položit aplikaci. Například, tvoje implementace mb_strlen bude počítat špatně délku, nastanou nějaké ty buffer overflow nebo SegFault (těžko říct, co je horší, jestli změna chování aplikace, nebo její pád).

Budoucí Perl 6 to má, myslím, pěkně vyřešené, můžeš si „objednat“, jak chceš nahlížet na řetězce. Zda na úrovni bytů, code-unit, code-pointů nebo grafémů. Tím ti odpadne potřeba mb_* funkcí, protože všechny funkce budou fungovat na té správné vrstvě abstrakce. Vše vlastně vyřeší samotný jazyk. Jen aby někdy vyšla stabilní verze :)

Databáze (konkrétně MSSQL) ukládá data ve sloupcích NVARCHAR v UCS2 (jejich název pro UTF-16). Na reprezentaci všech znaků jim stačí 2 byty (ale potřebují je vždy) a sem-tam přepínač pro přechod do jiného plánu (surrogate pairs). Pro ně je to nejspíš výhodnější, neboť mohou počítat s pevnou velikostí; usnadňuje to vyhledávání a umožňuje zarovnání databázových záznamů.

Další možností k nafouknutí textu je používání normalizace, jak jsem naznačil výše (http://en.wikipedia.org/wiki/Unicode_normalization#Normalization). Pak jeden znak napsaný na obrazovce může být kódován několika code-pointy (základ a diakritika zvlášť).

Pak třeba můžeš pracovat s filesystémem od Applu (HFS Plus) a začneš se divit, jak se chová:
Max. filename length: 255 characters (255 UTF-16 encoding units, normalized to Apple-modified variant of Unicode Normalization Format D)
Tam znak č zabírá dva znaky kvůli dekompozici; nemám to potvrzené, ale myslím, že dekompozici provádí přímo filesystém. Pokud bys vytvořil soubor s nějakým názvem s diakritikou zapsanou komponovaně, vylistoval si adresář, tak ten soubor se bude jmenovat jinak (název vypadá stejně, ale sekvence bytů, kterou je zapsaný je odlišná).

Koukám, že Unicode už má nějaké „znaky“ v 14. plánu; k jejich adresaci je třeba 24 bitů, tedy pět bytů v UTF-8.

návštěvník:
Díky za chválu.
Pokud máš problém s pár operátory v PHP, nezoufej, moc jich není. Určitě jich není tolik jako v budoucím Perlu 6 (http://glyphic.s3.amazonaws.com/ozone/mark/periodic/Periodic%20Table%20of%20the%20Operators%20A4%20300dpi.jpg), kde si dokonce můžeš přidefinovat své vlastní. :)
návštěvník
Profil *
juriad:
Ještě mě napadlo - vzhledem k tomu že se při tom pokaždé musí volat funkce ord(), není pro detekci sudý / lichý integer rychlejší z hlediska výkonu použít operátor modulo? A ty dvě techniky porovnávání a) v šestnáctkové soustavě + ord() b) mb_substr() + strlen() je tam z hlediska výkonu nějaký podstatný rozdíl? Moje funkce je založená na jiném principu, takže má jinou, ale pro mě přehlednější strukturu:

for($x=$zacatek_slova_org;$x<=$konec_slova_org;$x++):
  if ( $tx[$y]=="*" ):
    if ($prv) {$prv=0; continue;} //skip this char (second part of mb)
    $char = mb_substr($org,$x-1,1,"utf8");
    @$originalWords[$i].= $org[$x];
    $org[$x]='*';
    if ( strlen($char)==1 ):
      $prv=0;
    else:
      $org[$x+1]='*';
      $prv=1; // skip next char
      $y--;
    endif; 
  endif;
  $y++;
endfor;

Tohle je čistě ukázka v praxi. Porovnávám dva texty, jeden obsahuje pouze znaky základní abecedy $tx[$y] a případné hvězdičky v místě cenzurovaných slov. Druhý je originál $org[$x], a tam kopíruju hvězdičky. Když zjistím, že je to s diakritikou, dávám tam dvě hvězdičky jinak jednu. Je tam navíc proměnná $prv, která indikuje jestli jde o první nebo druhý bajt. Myslím, že je to dost přehledný i když nepoužívám porovnávání s šestnáctkovou soustavou.
juriad
Profil
návštěvník:
Ten sudý/lichý integer nikde ve svém kódu nepoužívám, byla to jen ukázka bitového operátoru mimo téma. Modulo je pomalejší (jako instrukce na procesoru), PHP však může podle situace klidně modulo nahradit za bitovou „magii“.

Funkce ord bude extrémně rychlá, vrátí první byte interní reprezentace řetězce.
To jestli napíšeš číslo v desítkové soustavě, dvojkové nebo šestnáctkové nemá na rychlost vůbec vliv, je to asi stejné jako rozdíl mezi čísly 10.0 a 10.00.
Tu šestnáctkovou soustavu používám z důvodu toho, že 0x80 je přehlednější než číslo 128. Hned je vidět, že jedničku má jen nejvyšší bit.
Naopak mb_* funkce musí být z principu pomalejší, protože pracují s obecným kódováním, já pracuji s ASCII (to nejjedodušší možné).

K tvému kódu:
Předpokládat, že multi-byte znamená 2 bytu je špatně; tento znak má velikost tři byty: string(3) "添".

Lepší by bylo něco jako:
for ($x = $zacatek_slova_org; $x <= $konec_slova_org; $y++) { # $y se zvětší vždy o jedna
        $char = mb_substr($org, $x - 1, 1, "utf8"); # proč je tam $x - 1? Lepší by bylo číslovat od nuly.
        if ($tx[$y] == "*") {
                @$originalWords[$i] .= $org[$x]; # toto nechápu, ale asi to má nějaký význam
                for ($l = 0; $l <= strlen($char); $l++) { # projde všechny byty znaku
                        $org[$x + $l] = '*'; # nahradí je hvědičkou
                }
        }
        $x += strlen($char); # posuneš se vždy o celý znak
}
návštěvník
Profil *
juriad:
"# proč je tam $x - 1? Lepší by bylo číslovat od nuly."

V druhém příspěvku jsem psal:
Zjišťuji, že mb_substr počítá offset od jedničky. To je normální?

Do $originalWords[$i] se uchovává původní slovo. Sice provedu záměnu za hvězdičky, ale vygeneruju pole, abych věděl která slova jsem změnil.

Rozhodoval jsem se mezi dvěma možnostma, buď projet celé slovo hvězdičkama nebo jenom tu část kde mají být hvězdičky. Tak asi by bylo lepší udělat to na té bázi, abych to přepnul v době kdy se tam objeví začátek vícebytového.


Tak jsem to vylepšíl:

      $prv=1;
      for($x=$zacatek_slova_org;$x<=$konec_slova_org;$x++):
        if ( $tx[$y]=="*" ):
          @$originals[$i].= $org[$x]; // save the original word
          // echo (ord($org[$x]) & 0xc0 ).";";
          $org[$x]='*';
          if (ord($org[$x]) & 0xc0 <> 192): // začátek nového mb
            $prv=0;
          else:
            if (!$prv) $y--; // transponované slovo n je kratší o m bytů = ... proto musím y upravit tak, aby se vešel do délky slova s diakritikou
            $prv=1;
          endif; 
        endif;
        $y++;
      endfor;
juriad
Profil
Na 7. řádku máš špatně závorky. Napřed se provede porovnání 0xc0 <> 192, a až pak ten bitový AND.
Prozkoumej pořadí operátorů: http://www.php.net/manual/en/language.operators.precedence.php

Ale stejně se mi to nelíbí, ta nerovnost je asi opačně, navíc musíš rozlišovat znaky kódované jediným bytem.
Dělej jak uznáš za vhodné ty, ona to asi nebude úplně výkonově kritická část aplikace, takže si můžeš dovolit nějaké to pohodlí.
Večer/zítra se pokusím (nic neslibuji) poslat funkční hvězdičkovač pro UTF-8.
návštěvník
Profil *
juriad:
Asi máš pravdu. Na tu prioritu operátorů jsem zapomněl, špatně jsem inicioval $prv a ono se zdálo, že s $prv=0; to funguje. Přehodil jsem prioritu operátorů a změnil <> na == a ono to funguje na oko "úplně" stejně jako před tím (ale je jasné, že v tom bude ve skutečnosti rozdíl).

      $prv=0;
      $badwords[$alastkey_corr]='';
      for($x=$zacatek_slova_org;$x<=$konec_slova_org;$x++):
        if ( $tx[$y]=="*" ):
          $badwords[$alastkey_corr].= $org[$x]; // save the original word
          $org[$x]='*';
          if ( (ord($org[$x]) & 0xc0) == 192): // začátek nového mb
            $prv=0;
          else:
            if (!$prv) $y--; // transponované slovo n je kratší o m bytů = $a[$n][1] ... proto musím y upravit tak, aby se vešel do délky slova s diakritikou
            $prv=1; // skip next char
          endif; 
        endif;
        $y++;
      endfor;
      }         
    endfor;

Snad je to tak OK a už s tím nebudou další problémy.
návštěvník
Profil *
Nevíte co může být chybou na řádku 5?
Notice: Uninitialized string offset: 27


Vztaženo ke kódu: $badwords[$alastkey_corr].=
Jan Tvrdík
Profil
návštěvník:
Když mám nějaký string, který obsahuje diakritiku a chci ho projít ve smyčce, tak abych dostal postupně jednotlivé znaky (resp. jejich offsetové pozice), jak to udělat?
Nečetl jsem celé vlákno, ale nestačilo by (v případě, že chci i ty offsetové pozice)

$str = "třešeň";
preg_match_all('#.#u', $str, $m, PREG_OFFSET_CAPTURE);
foreach ($m[0] as $match) {
    list($letter, $offset) = $match;
    printf("%d - %s\n<br>", $offset, $letter);
}

Případě, pokud bych nechtěl ty offsetové pozice, tak:
$str = "třešeň";
preg_match_all('#.#u', $str, $m);
foreach ($m[0] as $letter) {
    echo "$letter\n<br>";
}

// nebo
$str = "třešeň";
$letters = array_slice(preg_split('##u', $str), 1, -1);
foreach ($letters as $letter) {
    echo "$letter<br>\n";
}
návštěvník
Profil *
Už to mám vyřešené (hotové), jen sem naposledy dostával nějakou chybu. V projektu jsem pracoval se dvěma druhy textu. Jeden byl text s diakritikou a druhý ten samý, ale bez diakritiky, zbavený všech znaků, které nejsou součástí slova (@ a 0 tvoří vyjímku a interpretuji je jako "a" a "o"). Potřeboval jsem zapamatovat si offsety "neslov" v původním řetězci a offsety slov v transponovaném řetězci, včetně rozdílů délek slov mezi utf8 a ne-utf 8. Pak jsem provedl hvězdičkování v transponovaném řetězci a převedl ty hvězdičky na ten původní řetězec. Nakonec jsem to zvládl hlavně díky Tori a juriad, kteří mi dali dobré tipy.

Vaše odpověď

Mohlo by se hodit


Prosím používejte diakritiku a interpunkci.

Ochrana proti spamu. Napište prosím číslo dvě-sta čtyřicet-sedm:

0