Zin opsplitsen in "deelproblemen" ivm vertaling
Een groot deel van deze "opmerkingen" is echter "algemene bagger" die niet vertaald hoeft/kan worden. Zo zijn "Foo Bar" en "Noot Mies" bijvoorbeeld merken (we hebben een lijst met merken), en die blijven natuurlijk gelijk. "1.6" is ook niet iets wat in het Engels heel anders zal worden (beter: ook niet moet worden). Een tekst als "Past ook op" komt regelmatig voor en hoeft dus niet elke keer vertaald te worden. Kortom: in bovenstaand voorbeeld zou dus eigenlijk alleen "krassen aan achterzijde" vertaald moeten worden.
Ik heb al een analyse methode om "algemene zinsdelen" er uit te halen (zoals "Past ook op"). Ook merken en nummers zijn eenvoudig te "detecteren". Kortom: ik ben al zover dat ik "weet" dat ik enkel nog "krassen aan achterzijde" hoef te vertalen. Het probleem is nu: hoe ga ik dit handig doen. Ik zit nu op de toer waarbij ik delen van de zin markeer (dmv "markers") als bijvoorbeeld zijnde "zo laten" (merken, nummers), "standaardzin 21", enz. Tijdens het vertalen (per doel-taal) kan ik dan de "zo laten" delen ... zo laten, en voor de standaardzinnen de juiste "reeds vertaalde" zinsneden ophalen (in de juiste taal). Maar ik doe dit letterlijk "in de zin". Bovenstaande zin wordt dus iets van "<~~Foo Bar~~> <~~1.6~~> / <~~algemeen=21~~> <~~Noot Mies~~> <~~1.6~~> / <~~algemeen=41~~> krassen aan achterzijde / <~~algemeen=28~~> <~~123ABC456~~>". Kortom: alles tussen <~~ ... ~~> is iets wat ik lokaal kan "vertalen" (of niet hoef te vertalen), en alleen het stukje "krassen aan achterzijde" hoeft naar de vertaalservice (23 ipv 89 karakters; in mijn analyse kwam ik zelfs tot 11% = 89% kosten reductie). Dit is veelal een regex gebeuren (bijvoorbeeld preg_replace('/\\b(\\w*\\d\\w*)\\b/', '<~~$1~~>', $str) om "nummers" tussen markers te krijgen).
Alleen: dit kraakt. Het voelt alsof je een beetje met een hamer net zolang ergens op staat te rammen tot het de goede vorm heeft. Niet bepaald subtiel dus (en het is natuurlijk wachten op de eerste opmerking met "<~~" er in ...). Het liefst zou ik de zin in een array splitsen met dan per zinsdeel de bijbehorende "methode":
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
$sentence = [
['str' => 'Foo Bar', 'type' => 'make'],
['str' => '1.6', 'type' => 'number'],
['str' => ' / ', 'type' => 'other'],
['str' => 'Past ook op ', 'type' => 'common', 'id' => 21'],
['str' => 'Noot Mies', 'type' => 'make'],
['str' => '1.6', 'type' => 'number'],
['str' => ' / ', 'type' => 'other'],
['str' => 'Let op: ', 'type' => 'common', 'id' => 41],
['str' => 'krassen aan achterzijde', 'type' => 'trans'],
['str' => ' / ', 'type' => 'other'],
['str' => 'OEM', 'type' => 'common', 'id' => 28],
['str' => '123ABC456', 'type' => 'number']
];
['str' => 'Foo Bar', 'type' => 'make'],
['str' => '1.6', 'type' => 'number'],
['str' => ' / ', 'type' => 'other'],
['str' => 'Past ook op ', 'type' => 'common', 'id' => 21'],
['str' => 'Noot Mies', 'type' => 'make'],
['str' => '1.6', 'type' => 'number'],
['str' => ' / ', 'type' => 'other'],
['str' => 'Let op: ', 'type' => 'common', 'id' => 41],
['str' => 'krassen aan achterzijde', 'type' => 'trans'],
['str' => ' / ', 'type' => 'other'],
['str' => 'OEM', 'type' => 'common', 'id' => 28],
['str' => '123ABC456', 'type' => 'number']
];
Vervolgens is het een kwestie van per taal de array doorlopen, en per type zinsdeel de juiste actie ondernemen (en vervolgens de boel weer aan elkaar plakken en ergens opslaan). Een stuk mooier dus.
Vraag is nu: hoe ga ik de originele zin zo mooi "tokenizen" op basis van alle verschillende "type" zinsdeel? Ik kan natuurlijk het "search & replace" resultaat wat ik nu al heb gaan splitsen op de markers, maar dan blijft dat "slaan met de hamer" gevoel hangen. Het liefst zou ik de zin direct in bovenstaande mootjes hakken.
translation memory (TM) is daarvoor een oplossing. Daarmee hoef je eerder vertaalde strings niet opnieuw te vertalen én kun je permanent menselijke corecties van machinevertaalfouten vastleggen.
Als je avontuurlijk bent aangelegd, lijkt me dit een ideaal experiment om de mogelijkheden van machine learning eens stevig aan de tand te voelen. ;)
Een Als je avontuurlijk bent aangelegd, lijkt me dit een ideaal experiment om de mogelijkheden van machine learning eens stevig aan de tand te voelen. ;)
Zou je niet op een andere manier mening/betekenis kunnen geven aan deze informatie zodat deze min of meer in "vakjes" past? Dus in de vorm van eigenschappen en bijbehorende waarden? Je hoeft dan "enkel" de labels te vertalen.
Heb je al eens bij wijze van experiment gekeken of je dit in een soort van indeling kunt gieten met tags oid?
Misschien zou je dit ook deels aan de koppeling-kant kunnen oplossen? Wellicht als je meer ruimte biedt voor invoer in plaats van de eerder genoemde afvoerput komt deze informatie misschien beter tot zijn recht? Kunnen er afspraken gemaakt worden over het meer standaardiseren van het formaat van aangeleverde informatie? Anders blijft het toch een beetje shit out/shit in.
Daarnaast zou je kunnen kijken welke informatie relevant is en welke niet. Als de informatie relevant is dan zou ik zeggen dat een eigen plekje gerechtvaardigd is en als deze niet relevant is waarom zou je dan moeite doen om deze op te slaan en/of te vertalen?
Je bent nu vooral bezig met de vraag "hoe ga ik dit aanpakken", maar hoe zit het met de vragen "heb ik deze informatie nodig", "hoe wordt deze (vervolgens) ingezet/gebruikt" en "is dit de enige/beste/eenvoudigste aanpak die leidt tot het gewenste eindresultaat"?
Mogelijk probeer je ook iets te hard een machine te laten doen waar een persoon mogelijk beter in is, het gaat namelijk ook over de interpretatie van informatie. Je zou dit werk in principe ook, I don't know, door een stagiair kunnen laten doen ofzo, om maar een dwarsstraat te noemen.
Ik hang aan het einde van het afvoerputje. De data komt vanuit heel Europa, en vaak over verschillende schijven. Hoe vaak we nu al moeten "vechten" om heel basale dingen in het juiste vakje te krijgen ... Dat ga ik niet redden om alles op de juiste plek te krijgen. Dat is wat mij betreft dus een gepasseerd station.
Ook gaat het om veel te veel data om "met het handje" te doen. Het gaat om miljoenen records, met elke dag duizenden updates. De kwaliteit van de vertaling is ook niet zo heel belangrijk. Als maar "enigszins" duidelijk is wat er staat (beter een kromme Nederlandse zin, dan de originele - ik noem maar een dwarsstraat - Portugese tekst).
Mbt dat laatste ook nog de afweging Yandex (goedkoop / slechte vertaling) / Azure (medium / redelijk) / Google (duur / goed). Tussen Yandex en Google zit een factor 4. Dat tikt ook weer aardig aan ...
@Ward,
Ah, fijn om te zien dat ik een bestaand wiel (deels) aan het opnieuw uitvinden ben. TM is inderdaad wat ik aan het doen ben (om kosten te besparen). Vraag blijft dan: hoe ga ik de tekst "knap" in segmenten verdelen. Als je een ML tip hebt wil ik daar wel eens naar kijken, maar ik heb het idee dat het geheel nu zo basaal is (vaste lijsten) dat dat misschien een beetje over de top is (en anders heb ik het zelf al "uitgevonden" met m'n "algemene zinsdelen" opsporing).
Maar hoe belangrijk is deze informatie? Is het echt de moeite waard om te proberen recht te buigen wat in wezen krom is?
Niet alles is altijd van belang ... maar soms wel. Soms is de informatie "ter info" (bijvoorbeeld ook al duidelijk te zien op de foto's, of duplicaat van wat in de algemene "vakjes" staat), maar soms gaat het ook om "belangrijke" informatie (het gaat om artikelen die niet altijd "in nieuwstaat" zijn, dus dan is het wel handig om te weten als er delen ontbreken / beschadigd zijn, of anderszins niet "aan de verwachtingen" kunnen voldoen).
Rob Doemaarwat op 21/08/2020 20:51:56:
Mbt dat laatste ook nog de afweging Yandex (goedkoop / slechte vertaling) / Azure (medium / redelijk) / Google (duur / goed). Tussen Yandex en Google zit een factor 4. Dat tikt ook weer aardig aan ...
Er zijn er nog meer. Bijvoorbeeld DeepL schijnt goed te zijn.
Rob Doemaarwat op 21/08/2020 20:51:56:
Ah, fijn om te zien dat ik een bestaand wiel (deels) aan het opnieuw uitvinden ben. TM is inderdaad wat ik aan het doen ben (om kosten te besparen). Vraag blijft dan: hoe ga ik de tekst "knap" in segmenten verdelen.
Ik denk dat je dan eerst moet gaan segmenteren aan de hand van wat je al weet. Bijvoorbeeld voor elektronica geldt een andere "woordenschat" dan voor damesschoenen. Omdat ook automatisch vertalen tegenwoordig vaak ergens een vorm van machine learning gebruikt, helpt het als je daarvoor gescheiden datasets kunt aanleveren.
Ten tweede zou ik kijken hoe je prioriteiten kunt formaliseren. Sommige productgegevens zijn van levensbelang, en dat soms zelfs letterlijk, andere zijn verwaarloosbare marketese. Wat wil je weten en wat kun je vergeten? Als je de ruis kunt kunt wegfilteren, is wat je overhoudt beter te behappen. Kijk daarvoor bijvoorbeeld naar de productattributen in Schema.org, want die overlappen wat Google graag wil weten over producten. Je kunt daarmee clusters in de woordenschat vormen: sommige attributen hebben te maken met kleur, andere met het gewicht, enzovoort.
Het is niet zo dat alles in de "opmerkingen" staat. Het meeste spul wat (bijvoorbeeld) van belang is voor de "structured data" voor Google staat gewoon netjes "in vakjes". Het zijn echt van die "losse flodders" waar je op voorhand ook niet altijd "een vakje" voor kunt verzinnen.
Ik heb vanmorgen even lopen klungelen, en voorlopig heb ik deze - toch vrij simpele / recht-toe-recht-aan "tokenizer":
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?php
namespace DoeMaarWat;
/**
* String helpers.
*/
class Str{
//... other functions, constants, ...
/**
* Tokenize a string according to the rules.
* @param string $str
* @param array $rules Array with records containig the rule type and extra parameters for the rule. See TOKEN_TYPE_*
* constants.
* @return array String divided into tokens (records), with 'type', 'str' (original string), rule parameters, and other info.
*/
public static function tokenize($str,$rules){
static $delimiter = null;
static $indicator = null;
if(!$delimiter) $delimiter = strrev($indicator = self::random(self::TOKEN_INDICATOR_LENGTH,'[a-z]'));
$rule = array_shift($rules);
$tokens = [];
switch($type = $rule[self::TOKEN_TYPE] ?? null){
case self::TOKEN_TYPE_LIST:
foreach($rule[$type] as $key => $item) $str = preg_replace(
'/\\b' . preg_quote($item,'/') . '\\b/' . ($rule['case'] ?? false ? '' : 'i'),
$delimiter . $indicator . $key . '=$0' . $delimiter,
$str
);
foreach(explode($delimiter,$str) as $str) $tokens[] = substr($str,0,self::TOKEN_INDICATOR_LENGTH) == $indicator
? array_combine(['key',self::TOKEN_STR],explode('=',substr($str,self::TOKEN_INDICATOR_LENGTH),2)) + $rule
: [self::TOKEN_TYPE => self::TOKEN_TYPE_OTHER,self::TOKEN_STR => $str];
break;
case self::TOKEN_TYPE_REGEX:
$str = preg_replace($rule[$type],$delimiter . $indicator . '$0' . $delimiter,$str);
foreach(explode($delimiter,$str) as $str) $tokens[] = substr($str,0,self::TOKEN_INDICATOR_LENGTH) == $indicator
? [self::TOKEN_STR => substr($str,self::TOKEN_INDICATOR_LENGTH)] + $rule
: [self::TOKEN_TYPE => self::TOKEN_TYPE_OTHER,self::TOKEN_STR => $str];
break;
default:
throw new \Exception("Unknown rule type '$type'");
}
for($i = count($tokens) - 1; $i >= 0; $i--) if(($token = $tokens[$i])[self::TOKEN_TYPE] == self::TOKEN_TYPE_OTHER){
if(!strlen($token[self::TOKEN_STR])) array_splice($tokens,$i,1); //remove empty
elseif($rules) array_splice($tokens,$i,1,self::tokenize($token[self::TOKEN_STR],$rules));
}
return $tokens;
}
}
?>
namespace DoeMaarWat;
/**
* String helpers.
*/
class Str{
//... other functions, constants, ...
/**
* Tokenize a string according to the rules.
* @param string $str
* @param array $rules Array with records containig the rule type and extra parameters for the rule. See TOKEN_TYPE_*
* constants.
* @return array String divided into tokens (records), with 'type', 'str' (original string), rule parameters, and other info.
*/
public static function tokenize($str,$rules){
static $delimiter = null;
static $indicator = null;
if(!$delimiter) $delimiter = strrev($indicator = self::random(self::TOKEN_INDICATOR_LENGTH,'[a-z]'));
$rule = array_shift($rules);
$tokens = [];
switch($type = $rule[self::TOKEN_TYPE] ?? null){
case self::TOKEN_TYPE_LIST:
foreach($rule[$type] as $key => $item) $str = preg_replace(
'/\\b' . preg_quote($item,'/') . '\\b/' . ($rule['case'] ?? false ? '' : 'i'),
$delimiter . $indicator . $key . '=$0' . $delimiter,
$str
);
foreach(explode($delimiter,$str) as $str) $tokens[] = substr($str,0,self::TOKEN_INDICATOR_LENGTH) == $indicator
? array_combine(['key',self::TOKEN_STR],explode('=',substr($str,self::TOKEN_INDICATOR_LENGTH),2)) + $rule
: [self::TOKEN_TYPE => self::TOKEN_TYPE_OTHER,self::TOKEN_STR => $str];
break;
case self::TOKEN_TYPE_REGEX:
$str = preg_replace($rule[$type],$delimiter . $indicator . '$0' . $delimiter,$str);
foreach(explode($delimiter,$str) as $str) $tokens[] = substr($str,0,self::TOKEN_INDICATOR_LENGTH) == $indicator
? [self::TOKEN_STR => substr($str,self::TOKEN_INDICATOR_LENGTH)] + $rule
: [self::TOKEN_TYPE => self::TOKEN_TYPE_OTHER,self::TOKEN_STR => $str];
break;
default:
throw new \Exception("Unknown rule type '$type'");
}
for($i = count($tokens) - 1; $i >= 0; $i--) if(($token = $tokens[$i])[self::TOKEN_TYPE] == self::TOKEN_TYPE_OTHER){
if(!strlen($token[self::TOKEN_STR])) array_splice($tokens,$i,1); //remove empty
elseif($rules) array_splice($tokens,$i,1,self::tokenize($token[self::TOKEN_STR],$rules));
}
return $tokens;
}
}
?>
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
<?php
$str='Foo Bar 1.6 / Past ook op Noot Mies 1.6 / Let op: krassen aan achterzijde / OEM 123ABC456';
print("$str\n\n");
var_dump(\DoeMaarWat\Str::tokenize($str,[
['type' => 'list','list' => [5 => 'Hello World',123 => 'foo bar',124 => 'noot mies'],'name' => 'brand'],
['type' => 'list','list' => [21 => 'past ook op',41 => 'let op: ',28 => 'OEM'],'name' => 'common'],
['type' => 'regex','regex' => '/\\b\\S*\\d\\S*\\b/','name' => 'number'],
['type' => 'regex','regex' => '/\\s*\\/\\s*/','name' => 'delimiter']
]));
?>
$str='Foo Bar 1.6 / Past ook op Noot Mies 1.6 / Let op: krassen aan achterzijde / OEM 123ABC456';
print("$str\n\n");
var_dump(\DoeMaarWat\Str::tokenize($str,[
['type' => 'list','list' => [5 => 'Hello World',123 => 'foo bar',124 => 'noot mies'],'name' => 'brand'],
['type' => 'list','list' => [21 => 'past ook op',41 => 'let op: ',28 => 'OEM'],'name' => 'common'],
['type' => 'regex','regex' => '/\\b\\S*\\d\\S*\\b/','name' => 'number'],
['type' => 'regex','regex' => '/\\s*\\/\\s*/','name' => 'delimiter']
]));
?>
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
Foo Bar 1.6 / Past ook op Noot Mies 1.6 / Let op: krassen aan achterzijde / OEM 123ABC456
array(16) {
[0] => array(5) {
["key"] => string(3) "123"
["str"] => string(7) "Foo Bar"
["type"] => string(4) "list"
["list"] => array(3) {...}
["name"] => string(5) "brand"
}
[1] => array(2) {
["type"] => string(5) "other"
["str"] => string(1) " "
}
[2] => array(4) {
["str"] => string(3) "1.6"
["type"] => string(5) "regex"
["regex"] => string(14) "/\b\S*\d\S*\b/"
["name"] => string(6) "number"
}
[3] => array(4) {
["str"] => string(3) " / "
["type"] => string(5) "regex"
["regex"] => string(10) "/\s*\/\s*/"
["name"] => string(9) "delimiter"
}
[4] => array(5) {
["key"] => string(2) "21"
["str"] => string(11) "Past ook op"
["type"] => string(4) "list"
["list"] => array(3) {...}
["name"] => string(6) "common"
}
[5] => array(2) {
["type"] => string(5) "other"
["str"] => string(1) " "
}
[6] => array(5) {
["key"] => string(3) "124"
["str"] => string(9) "Noot Mies"
["type"] => string(4) "list"
["list"] => array(3) {...}
["name"] => string(5) "brand"
}
[7] => array(2) {
["type"] => string(5) "other"
["str"] => string(1) " "
}
[8] => array(4) {
["str"] => string(3) "1.6"
["type"] => string(5) "regex"
["regex"] => string(14) "/\b\S*\d\S*\b/"
["name"] => string(6) "number"
}
[9] => array(4) {
["str"] => string(3) " / "
["type"] => string(5) "regex"
["regex"] => string(10) "/\s*\/\s*/"
["name"] => string(9) "delimiter"
}
[10] => array(5) {
["key"] => string(2) "41"
["str"] => string(8) "Let op: "
["type"] => string(4) "list"
["list"] => array(3) {...}
["name"] => string(6) "common"
}
[11] => array(2) {
["type"] => string(5) "other"
["str"] => string(23) "krassen aan achterzijde"
}
[12] => array(4) {
["str"] => string(3) " / "
["type"] => string(5) "regex"
["regex"] => string(10) "/\s*\/\s*/"
["name"] => string(9) "delimiter"
}
[13] => array(5) {
["key"] => string(2) "28"
["str"] => string(3) "OEM"
["type"] => string(4) "list"
["list"] => array(3) {...}
["name"] => string(6) "common"
}
[14] => array(2) {
["type"] => string(5) "other"
["str"] => string(1) " "
}
[15] => array(4) {
["str"] => string(9) "123ABC456"
["type"] => string(5) "regex"
["regex"] => string(14) "/\b\S*\d\S*\b/"
["name"] => string(6) "number"
}
}
array(16) {
[0] => array(5) {
["key"] => string(3) "123"
["str"] => string(7) "Foo Bar"
["type"] => string(4) "list"
["list"] => array(3) {...}
["name"] => string(5) "brand"
}
[1] => array(2) {
["type"] => string(5) "other"
["str"] => string(1) " "
}
[2] => array(4) {
["str"] => string(3) "1.6"
["type"] => string(5) "regex"
["regex"] => string(14) "/\b\S*\d\S*\b/"
["name"] => string(6) "number"
}
[3] => array(4) {
["str"] => string(3) " / "
["type"] => string(5) "regex"
["regex"] => string(10) "/\s*\/\s*/"
["name"] => string(9) "delimiter"
}
[4] => array(5) {
["key"] => string(2) "21"
["str"] => string(11) "Past ook op"
["type"] => string(4) "list"
["list"] => array(3) {...}
["name"] => string(6) "common"
}
[5] => array(2) {
["type"] => string(5) "other"
["str"] => string(1) " "
}
[6] => array(5) {
["key"] => string(3) "124"
["str"] => string(9) "Noot Mies"
["type"] => string(4) "list"
["list"] => array(3) {...}
["name"] => string(5) "brand"
}
[7] => array(2) {
["type"] => string(5) "other"
["str"] => string(1) " "
}
[8] => array(4) {
["str"] => string(3) "1.6"
["type"] => string(5) "regex"
["regex"] => string(14) "/\b\S*\d\S*\b/"
["name"] => string(6) "number"
}
[9] => array(4) {
["str"] => string(3) " / "
["type"] => string(5) "regex"
["regex"] => string(10) "/\s*\/\s*/"
["name"] => string(9) "delimiter"
}
[10] => array(5) {
["key"] => string(2) "41"
["str"] => string(8) "Let op: "
["type"] => string(4) "list"
["list"] => array(3) {...}
["name"] => string(6) "common"
}
[11] => array(2) {
["type"] => string(5) "other"
["str"] => string(23) "krassen aan achterzijde"
}
[12] => array(4) {
["str"] => string(3) " / "
["type"] => string(5) "regex"
["regex"] => string(10) "/\s*\/\s*/"
["name"] => string(9) "delimiter"
}
[13] => array(5) {
["key"] => string(2) "28"
["str"] => string(3) "OEM"
["type"] => string(4) "list"
["list"] => array(3) {...}
["name"] => string(6) "common"
}
[14] => array(2) {
["type"] => string(5) "other"
["str"] => string(1) " "
}
[15] => array(4) {
["str"] => string(9) "123ABC456"
["type"] => string(5) "regex"
["regex"] => string(14) "/\b\S*\d\S*\b/"
["name"] => string(6) "number"
}
}
Alle niet-whitspace "other" entries moet ik hierna dus (nog) vertalen (en analyseren op nieuwe "standaard teksten").
Gewijzigd op 22/08/2020 14:57:41 door Rob Doemaarwat
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<?php
protected static function tokenizeMatches($str,$matches,$extra){
$index = 0;
$tokens = [];
foreach($matches[0] as list($match,$offset)){
$tokens[] = [self::TOKEN_TYPE => self::TOKEN_TYPE_OTHER,self::TOKEN_STR => substr($str,$index,$offset - $index)];
$tokens[] = [self::TOKEN_STR => $match] + $extra;
$index = $offset + strlen($match);
}
$tokens[] = [self::TOKEN_TYPE => self::TOKEN_TYPE_OTHER,self::TOKEN_STR => substr($str,$index)];
return $tokens;
}
/**
* Tokenize a string according to the rules.
* @param string $str
* @param array $rules Array with records containig the rule type and extra parameters for the rule. See TOKEN_TYPE_*
* constants. The first rule is applied first. The next rule is applied to all parts of the string not matching the first
* rule, and so on.
* @return array String divided into tokens (records), with 'str' (matched part of string), 'rule' name, rule 'type', and
* other rule based info.
*/
public static function tokenize($str,$rules){
$extra = [
self::TOKEN_RULE => $key = Record::key($rules),
self::TOKEN_TYPE => $type = ($rule = $rules[$key])[self::TOKEN_TYPE]
];
unset($rules[$key]);
$tokens = [];
switch($type){
case self::TOKEN_TYPE_LIST:
$tokens[] = [self::TOKEN_TYPE => self::TOKEN_TYPE_OTHER,self::TOKEN_STR => $str];
$length = strlen($str);
$prefix = '/\\b';
$suffix = '\\b/' . (($rule['case'] ?? false) ? '' : 'i');
foreach($rule[$type] as $key => $item) if(($item_length = strlen($item)) <= $length) for($i = count($tokens) - 1; $i >= 0; $i--) if(
(($token = $tokens[$i])[self::TOKEN_TYPE] == self::TOKEN_TYPE_OTHER) &&
(strlen($str = $token[self::TOKEN_STR]) >= $item_length) &&
preg_match_all($prefix . preg_quote($item,'/') . $suffix,$str,$matches,PREG_OFFSET_CAPTURE)
) array_splice($tokens,$i,1,self::tokenizeMatches($str,$matches,compact('key') + $extra));
break;
case self::TOKEN_TYPE_REGEX:
preg_match_all($rule[$type],$str,$matches,PREG_OFFSET_CAPTURE);
$tokens = self::tokenizeMatches($str,$matches,$extra);
break;
case self::TOKEN_TYPE_FUNC:
foreach(call_user_func($rule[$type],$str,$rule) as $token)
$tokens[] = $token + (($token[self::TOKEN_TYPE] ?? null) != self::TOKEN_TYPE_OTHER) ? $extra : [];
break;
default:
throw new \Exception("Unknown rule type '$type'");
}
for($i = count($tokens) - 1; $i >= 0; $i--) if(($token = $tokens[$i])[self::TOKEN_TYPE] == self::TOKEN_TYPE_OTHER){
if(!strlen($token[self::TOKEN_STR])) array_splice($tokens,$i,1); //remove empty
elseif($rules) array_splice($tokens,$i,1,self::tokenize($token[self::TOKEN_STR],$rules));
}
return $tokens;
}
?>
protected static function tokenizeMatches($str,$matches,$extra){
$index = 0;
$tokens = [];
foreach($matches[0] as list($match,$offset)){
$tokens[] = [self::TOKEN_TYPE => self::TOKEN_TYPE_OTHER,self::TOKEN_STR => substr($str,$index,$offset - $index)];
$tokens[] = [self::TOKEN_STR => $match] + $extra;
$index = $offset + strlen($match);
}
$tokens[] = [self::TOKEN_TYPE => self::TOKEN_TYPE_OTHER,self::TOKEN_STR => substr($str,$index)];
return $tokens;
}
/**
* Tokenize a string according to the rules.
* @param string $str
* @param array $rules Array with records containig the rule type and extra parameters for the rule. See TOKEN_TYPE_*
* constants. The first rule is applied first. The next rule is applied to all parts of the string not matching the first
* rule, and so on.
* @return array String divided into tokens (records), with 'str' (matched part of string), 'rule' name, rule 'type', and
* other rule based info.
*/
public static function tokenize($str,$rules){
$extra = [
self::TOKEN_RULE => $key = Record::key($rules),
self::TOKEN_TYPE => $type = ($rule = $rules[$key])[self::TOKEN_TYPE]
];
unset($rules[$key]);
$tokens = [];
switch($type){
case self::TOKEN_TYPE_LIST:
$tokens[] = [self::TOKEN_TYPE => self::TOKEN_TYPE_OTHER,self::TOKEN_STR => $str];
$length = strlen($str);
$prefix = '/\\b';
$suffix = '\\b/' . (($rule['case'] ?? false) ? '' : 'i');
foreach($rule[$type] as $key => $item) if(($item_length = strlen($item)) <= $length) for($i = count($tokens) - 1; $i >= 0; $i--) if(
(($token = $tokens[$i])[self::TOKEN_TYPE] == self::TOKEN_TYPE_OTHER) &&
(strlen($str = $token[self::TOKEN_STR]) >= $item_length) &&
preg_match_all($prefix . preg_quote($item,'/') . $suffix,$str,$matches,PREG_OFFSET_CAPTURE)
) array_splice($tokens,$i,1,self::tokenizeMatches($str,$matches,compact('key') + $extra));
break;
case self::TOKEN_TYPE_REGEX:
preg_match_all($rule[$type],$str,$matches,PREG_OFFSET_CAPTURE);
$tokens = self::tokenizeMatches($str,$matches,$extra);
break;
case self::TOKEN_TYPE_FUNC:
foreach(call_user_func($rule[$type],$str,$rule) as $token)
$tokens[] = $token + (($token[self::TOKEN_TYPE] ?? null) != self::TOKEN_TYPE_OTHER) ? $extra : [];
break;
default:
throw new \Exception("Unknown rule type '$type'");
}
for($i = count($tokens) - 1; $i >= 0; $i--) if(($token = $tokens[$i])[self::TOKEN_TYPE] == self::TOKEN_TYPE_OTHER){
if(!strlen($token[self::TOKEN_STR])) array_splice($tokens,$i,1); //remove empty
elseif($rules) array_splice($tokens,$i,1,self::tokenize($token[self::TOKEN_STR],$rules));
}
return $tokens;
}
?>