File Coverage

blib/lib/Text/Util/Chinese.pm
Criterion Covered Total %
statement 124 128 96.8
branch 34 44 77.2
condition 34 40 85.0
subroutine 22 22 100.0
pod 8 10 80.0
total 222 244 90.9


line stmt bran cond sub pod time code
1             package Text::Util::Chinese;
2 7     7   240605 use strict;
  7         48  
  7         204  
3 7     7   36 use warnings;
  7         11  
  7         161  
4 7     7   35 use utf8;
  7         13  
  7         30  
5              
6 7     7   195 use Exporter 5.57 'import';
  7         216  
  7         355  
7 7     7   7370 use Unicode::UCD qw(charscript);
  7         376317  
  7         9811  
8              
9             our $VERSION = '0.08';
10             our @EXPORT_OK = qw(sentence_iterator phrase_iterator presuf_iterator word_iterator extract_presuf extract_words tokenize_by_script looks_like_simplified_chinese);
11              
12             my $RE_simplified_chinese_characters = qr/[厂几儿亏与万亿个勺么广门义尸卫飞习马乡丰开无专扎艺厅区历车冈贝见气长仆币仅从仓风匀乌凤为忆订计认队办劝书击扑节术厉龙灭轧东业旧帅归叶电号叹们仪丛乐处鸟务饥闪兰汇头汉宁讨写让礼训议讯记辽边发圣对纠丝动执巩扩扫扬场亚朴机权过协压厌页夺达夹轨迈毕贞师尘当吓虫团吗屿岁岂刚则网迁乔伟传优伤价华伪会杀众爷伞创肌杂负壮冲庄庆刘齐产决闭问闯并关汤兴讲军许论农讽设访寻迅尽导异孙阵阳阶阴妇妈戏观欢买红纤级约纪驰寿麦进远违运抚坛坏扰坝贡抢坟坊护壳块声报苍严芦劳苏极杨两丽医辰励还歼来连坚时吴县园旷围吨邮员听呜岗帐财针钉乱体伶彻余邻肠龟犹条饭饮冻状亩况库疗应这弃冶闲间闷灶灿沃沟怀忧穷灾证启评补识诉诊词译灵层迟张际陆陈劲鸡驱纯纱纳纲驳纵纷纸纹纺驴纽环责现规拢拣担顶拥势拦拨择苹茎柜枪构杰丧画枣卖矿码厕奋态欧垄轰顷转斩轮软齿虏肾贤国畅鸣咏罗帜岭凯败贩购图钓侦侧凭侨货质径贪贫肤肿胀胁鱼备饰饱饲变庙剂废净闸闹郑单炉浅泪泻泼泽怜学宝审帘实试诗诚衬视话诞询该详肃录隶届陕限驾参艰线练组细驶织终驻驼绍经贯帮挂项挠赵挡垫挤挥荐带茧荡荣药标栋栏树咸砖砌牵残轻鸦战点临览竖削尝显哑贵虾蚁蚂虽骂哗响峡罚贱钞钟钢钥钩选适种复俩贷顺俭须剑胆胜脉狭狮独狱贸饶蚀饺饼弯将奖疮疯亲闻阀阁养类逆总炼烂洁洒浇浊测济浑浓恼举觉宪窃语袄误诱说诵垦昼险娇贺垒绑绒结绕骄绘给络骆绝绞统艳蚕顽捞载赶盐损捡换热恐壶莲获恶档桥础顾轿较顿毙虑监紧党晒晓晕唤罢圆贼贿钱钳钻铁铃铅牺敌积称笔笋债倾舰舱爱颂胳脏胶脑皱饿恋桨浆离资阅烦烧烛递涛涝润涨烫涌宽宾请诸读袜课谁调谅谈谊剥恳剧难预绢验继掠职萝营梦检聋袭辅辆虚悬崭铜铲银笼偿衔盘鸽领脸猎馅馆痒盖断兽渐渔渗惭惊惨惯窑谋谎祸谜弹隐婶颈绩绪续骑绳维绵绸绿趋搁搂搅联确暂辈辉赏喷践遗赌赔铸铺链销锁锄锅锈锋锐筐筑筛储惩释腊鲁馋蛮阔粪湿湾愤窜窝裤谢谣谦属屡缎缓编骗缘摄摆摊鹊蓝献楼赖雾输龄鉴错锡锣锤锦键锯矮辞筹签简腾触酱粮数满滤滥滚滨滩誉谨缝缠墙愿颗蜡蝇赚锹锻稳箩馒赛谱骡缩嘱镇颜额聪樱飘瞒题颠赠镜赞篮辩懒缴辫骤镰仑讥邓卢叽尔冯迂吁吆伦凫妆汛讳讶讹讼诀驮驯纫玛韧抠抡坞拟芜苇杈轩卤呕呛岖佃狈鸠庐闰兑沥沦汹沧沪诅诈坠纬坯枢枫矾殴昙咙账贬贮侠侥刽觅庞疟泞宠诡屉弥叁绅驹绊绎贰挟荚荞荠荤荧栈砚鸥轴勋哟钙钝钠钦钧钮氢胧饵峦飒闺闽娄烁炫洼诫诬诲逊陨骇挚捣聂荸莱莹莺栖桦桩贾砾唠鸯赃钾铆秫赁耸颁脐脓鸵鸳馁斋涡涣涤涧涩悯窍诺诽谆骏琐麸掷掸掺萤萧萨酝硕颅晤啰啸逻铐铛铝铡铣铭矫秸秽躯敛阎阐焕鸿渊谍谐裆袱祷谒谓谚颇绰绷综绽缀琼揽搀蒋韩颊雳翘凿喳晾畴鹃赋赎赐锉锌牍惫痪滞溃溅谤缅缆缔缕骚鹉榄辐辑频跷锚锥锨锭锰颓腻鹏雏馍馏禀痹誊寝褂裸谬缤赘蔫蔼碱辕辖蝉镀箫舆谭缨撵镊镐篓鲤瘪瘫澜谴鹤缭辙鹦篱鲸濒缰赡镣鳄嚣鳍癞攒鬓躏镶]/;
13              
14             sub exhaust {
15 2     2 0 6 my ($iter, $cb) = @_;
16 2         4 my @list;
17 2         7 while(defined(my $x = $iter->())) {
18 2         6 push @list, $x;
19 2 50       20 $cb->($x) if defined($cb);
20             }
21 2         86 return @list;
22             }
23              
24             sub grep_iterator {
25 5     5 0 121 my ($iter, $cb) = @_;
26             return sub {
27 40     40   77 local $_;
28 40         55 do {
29 45         94 $_ = $iter->();
30 45 100       136 return undef unless defined($_);
31             } while (! $cb->());
32 35         119 return $_;
33             }
34 5         23 }
35              
36             sub phrase_iterator {
37 5     5 1 149 my ($input_iter, $opts) = @_;
38 5         9 my @phrases;
39             return sub {
40 1429   100 1429   350112 while(! @phrases && defined(my $text = $input_iter->())) {
41             @phrases = grep {
42 713 100 100     15026 (! /\A\s+\z/) && (! /\p{General_Category=Punctuation}/) && /\p{Han}/
  3110         19111  
43             } split / ( \r?\n | \p{General_Category: Other_Punctuation} )+ /x, $text;
44             }
45 1429         3462 return shift @phrases;
46             }
47 5         39 }
48              
49             sub sentence_iterator {
50 3     3 1 135018 my ($input_iter, $opts) = @_;
51 3         8 my @sentences;
52             return sub {
53 773   100 773   189123 while(! @sentences && defined(my $text = $input_iter->())) {
54 692         12173 @sentences = grep { !/\A\s+\z/ } ($text =~
  771         5304  
55             m/(
56             (?:
57             [^\p{General_Category: Open_Punctuation}\p{General_Category: Close_Punctuation}]+?
58             | .*? \p{General_Category: Open_Punctuation} .*? \p{General_Category: Close_Punctuation} .*?
59             )
60             (?: \z | [\n\?\!。?!]+ )
61             )/gx);
62             }
63 773         1855 return shift @sentences;
64             }
65 3         34 }
66              
67             sub presuf_iterator {
68 3     3 1 4794 my ($input_iter, $opts) = @_;
69              
70 3         5 my %stats;
71 3   50     12 my $threshold = $opts->{threshold} || 9; # an arbitrary choice.
72 3   50     27 my $lengths = $opts->{lengths} || [2,3];
73              
74             my $phrase_iter = grep_iterator(
75             phrase_iterator( $input_iter ),
76 21     21   113 sub { /\A\p{Han}+\z/ }
77 3         21 );
78              
79 3         7 my (%extracted, @extracted);
80             return sub {
81 5 50   5   23 if (@extracted) {
82 0         0 return shift @extracted;
83             }
84              
85 5   100     15 while (!@extracted && defined(my $phrase = $phrase_iter->())) {
86 21         45 for my $len ( @$lengths ) {
87 42         87 my $re = '\p{Han}{' . $len . '}';
88 42 100 66     556 next unless length($phrase) >= $len * 2 && $phrase =~ /\A($re) .* ($re)\z/x;
89 36         5949 my ($prefix, $suffix) = ($1, $2);
90 36 50       133 $stats{prefix}{$prefix}++ unless $extracted{$prefix};
91 36 50       98 $stats{suffix}{$suffix}++ unless $extracted{$suffix};
92              
93 36         69 for my $x ($prefix, $suffix) {
94 72 100 66     393 if (! $extracted{$x}
      100        
      100        
      100        
95             && $stats{prefix}{$x}
96             && $stats{suffix}{$x}
97             && $stats{prefix}{$x} > $threshold
98             && $stats{suffix}{$x} > $threshold
99             ) {
100 2         5 $extracted{$x} = 1;
101 2         5 delete $stats{prefix}{$x};
102 2         3 delete $stats{suffix}{$x};
103              
104 2         6 push @extracted, $x;
105             }
106             }
107             }
108             }
109              
110 5 100       12 if (@extracted) {
111 2         9 return shift @extracted;
112             }
113              
114 3         8 return undef;
115 3         17 };
116             }
117              
118             sub extract_presuf {
119 1     1 1 6311 my ($input_iter, $opts) = @_;
120 1         5 return [ exhaust(presuf_iterator($input_iter, $opts)) ];
121             }
122              
123             sub word_iterator {
124 1     1 1 3 my ($input_iter) = @_;
125              
126 1         3 my $threshold = 5;
127 1         2 my (%lcontext, %rcontext, %word, @words);
128              
129             my $phrase_iter = grep_iterator(
130             phrase_iterator( $input_iter ),
131 9     9   50 sub { /\A\p{Han}+\z/ }
132 1         6 );
133              
134             return sub {
135 2 50   2   8 if (@words) {
136 0         0 return shift @words;
137             }
138              
139 2   100     10 while (!@words && defined( my $txt = $phrase_iter->() )) {
140 9         30 my @c = split("", $txt);
141              
142 9         33 for my $i (0..$#c) {
143 67 100       120 if ($i > 0) {
144 58         139 $lcontext{$c[$i]}{$c[$i-1]}++;
145 58         90 for my $n (2,3) {
146 116 100       200 if ($i >= $n) {
147 89         188 my $tok = join('', @c[ ($i-$n+1) .. $i] );
148 89 100       162 unless ($word{$tok}) {
149 88 50       168 if (length($tok) > 1) {
150 88         175 $lcontext{ $tok }{$c[$i - $n]}++;
151             }
152              
153 88 50 66     104 if ($threshold <= (keys %{$lcontext{$tok}}) && $threshold <= (keys %{$rcontext{$tok}})) {
  88         251  
  1         7  
154 0         0 $word{$tok} = 1;
155 0         0 push @words, $tok;
156             }
157             }
158             }
159             }
160             }
161 67 100       134 if ($i < $#c) {
162 58         149 $rcontext{$c[$i]}{$c[$i+1]}++;
163 58         91 for my $n (2,3) {
164 116 100       226 if ($i + $n <= $#c) {
165 89         217 my $tok = join('', @c[$i .. ($i+$n-1)]);
166 89 50       164 unless ($word{$tok}) {
167 89 50       171 if (length($tok) > 1) {
168 89         289 $rcontext{ $tok }{ $c[$i+$n] }++;
169             }
170              
171 89 100 66     117 if ($threshold <= (keys %{$lcontext{$tok}}) && $threshold <= (keys %{$rcontext{$tok}})) {
  89         422  
  1         5  
172 1         2 $word{$tok} = 1;
173 1         3 push @words, $tok;
174             }
175             }
176             }
177             }
178             }
179             }
180             }
181 2         10 return shift @words;
182             }
183 1         11 }
184              
185             sub extract_words {
186 1     1 1 141 return [ exhaust(word_iterator(@_)) ];
187             }
188              
189             sub tokenize_by_script {
190 1     1 1 98 my ($str) = @_;
191 1         3 my @tokens;
192 1         10 my @chars = grep { defined($_) } split "", $str;
  41         66  
193 1 50       6 return () unless @chars;
194              
195 1         3 my $t = shift(@chars);
196 1         8 my $s = charscript(ord($t));
197 1         15308 while(my $char = shift @chars) {
198 40         89 my $_s = charscript(ord($char));
199 40 100       2781 if ($_s eq $s) {
200 29         71 $t .= $char;
201             }
202             else {
203 11         20 push @tokens, $t;
204 11         18 $s = $_s;
205 11         43 $t = $char;
206             }
207             }
208 1         3 push @tokens, $t;
209 1         4 return grep { ! /\A\s*\z/u } @tokens;
  12         51  
210             }
211              
212             sub looks_like_simplified_chinese {
213 2     2 1 6587 my ($txt) = @_;
214 2         42 return $txt =~ /$RE_simplified_chinese_characters/o;
215             }
216              
217             1;
218              
219             __END__