File Coverage

blib/lib/Acme/Pinoko.pm
Criterion Covered Total %
statement 40 202 19.8
branch 4 80 5.0
condition 0 96 0.0
subroutine 12 17 70.5
pod 2 3 66.6
total 58 398 14.5


line stmt bran cond sub pod time code
1             package Acme::Pinoko;
2              
3 4     4   28567 use 5.008_008;
  4         16  
  4         175  
4 4     4   21 use strict;
  4         8  
  4         137  
5 4     4   22 use warnings;
  4         10  
  4         120  
6 4     4   4562 use utf8;
  4         44  
  4         19  
7              
8 4     4   135 use Carp ();
  4         7  
  4         64  
9 4     4   5026 use Encode ();
  4         94787  
  4         112  
10 4     4   12328 use Module::Load ();
  4         8837  
  4         105  
11 4     4   6459 use Data::Recursive::Encode ();
  4         6186  
  4         103  
12 4     4   7408 use Lingua::JA::Regular::Unicode ();
  4         102831  
  4         166  
13 4     4   7158 use Lingua::JA::Halfwidth::Katakana;
  4         1093  
  4         6058  
14              
15             our $VERSION = '0.02';
16              
17             # KyTea でデフォルトじゃないモデルを使う場合は変更が必要な場合もある
18             our $KYTEA_POSTAG_NUM = 0;
19             our $KYTEA_PRONTAG_NUM = 1;
20              
21             my @PARSERS = qw/Text::MeCab Text::KyTea/;
22              
23             my %HIRAGANA_INVALID_POS;
24             @HIRAGANA_INVALID_POS{qw/助詞 語尾 副詞 動詞 助動詞 形容詞 形状詞 連体詞 接頭詞 接頭辞 代名詞/} = ();
25              
26             my %TERMINATOR_CHAR;
27             @TERMINATOR_CHAR{ split(//, "。。..  \n\t…‥!!") } = ();
28              
29             sub _options
30             {
31             return {
32 2     2   9 parser => 'Text::MeCab',
33             parser_config => undef,
34             };
35             }
36              
37             sub new
38             {
39 2     2 1 19295 my $class = shift;
40 2 50       12 my %args = (ref $_[0] eq 'HASH' ? %{$_[0]} : @_);
  0         0  
41              
42 2         9 my $options = $class->_options;
43              
44 2         7 for my $key (keys %args)
45             {
46 2 100       6 if ( ! exists $options->{$key} ) { Carp::croak "Unknown option: '$key'"; }
  1         155  
47 1         4 else { $options->{$key} = $args{$key}; }
48             }
49              
50 1 50       4 Carp::croak "Invalid parser: '$options->{parser}'" if ! grep { $options->{parser} eq $_ } @PARSERS;
  2         237  
51              
52 0           Module::Load::load($options->{parser});
53              
54 0           my $self = bless $options, $class;
55              
56 0           $self->_load_parser;
57              
58 0           return $self;
59             }
60              
61             sub say
62             {
63 0     0 1   my ($self, $text) = @_;
64              
65 0 0         return unless defined $text;
66 0           return $self->_to_pinoko( $self->_parse(\$text) );
67             }
68              
69             sub _load_parser
70             {
71 0     0     my ($self) = @_;
72              
73 0           $self->{parser_name} = delete $self->{parser};
74              
75 0 0         if ($self->{parser_name} eq 'Text::MeCab')
76             {
77 0           my $mecab;
78              
79 0 0         if ( ! $self->{paser_config} ) { $mecab = Text::MeCab->new; }
  0            
80 0           else { $mecab = Text::MeCab->new($self->{parser_config}); }
81              
82 0           $self->{parser} = $mecab;
83 0           $self->{encoder} = Encode::find_encoding(Text::MeCab::ENCODING());
84             }
85             else # Text::KyTea
86             {
87 0           my $kytea;
88              
89 0 0         if ( ! $self->{parser_config} ) { $kytea = Text::KyTea->new({ tagmax => 1 }); }
  0            
90 0           else { $kytea = Text::KyTea->new($self->{parser_config}); }
91              
92 0           $self->{parser} = $kytea;
93             }
94              
95 0           return;
96             }
97              
98             sub _parse
99             {
100 0     0     my ($self, $text_ref) = @_;
101              
102 0           my (@surfaces, @poses, @prons);
103              
104 0 0         if ($self->{parser_name} eq 'Text::MeCab')
105             {
106 0           my $encoder = $self->{encoder};
107              
108 0           for my $text ( split(/(\s+)/, $$text_ref) )
109             {
110 0 0         if ($text =~ /\s/)
111             {
112 0           push(@surfaces, $text);
113 0           push(@poses, '記号');
114 0           push(@prons, 'UNK');
115 0           next;
116             }
117              
118 0           my $encoded_text = $encoder->encode($text);
119              
120 0           for (my $node = $self->{parser}->parse($encoded_text); $node; $node = $node->next)
121             {
122 0 0 0       next if $node->stat == 2 || $node->stat == 3;
123              
124 0           my $surface = $encoder->decode($node->surface);
125 0           push(@surfaces, $surface);
126              
127 0           my ($pos, $pron) = (split(/,/, $encoder->decode($node->feature), 9))[0,7];
128              
129 0 0 0       if ( (! defined $pron) || $pron eq '*' )
130             {
131 0 0         if ($surface =~ /^\p{InKatakana}+$/) { $pron = Lingua::JA::Regular::Unicode::katakana2hiragana($surface); }
  0            
132 0           else { $pron = 'UNK'; }
133             }
134 0           else { $pron = Lingua::JA::Regular::Unicode::katakana2hiragana($pron); }
135              
136 0           push(@poses, $pos);
137 0           push(@prons, $pron);
138             }
139             }
140             }
141             else # Text::KyTea
142             {
143 0           my $results = $self->{parser}->parse($$text_ref);
144              
145 0           $results = Data::Recursive::Encode->decode_utf8($results);
146              
147 0           for my $result (@{$results})
  0            
148             {
149 0           push(@surfaces, $result->{surface});
150 0           push(@poses, $result->{tags}[$KYTEA_POSTAG_NUM][0]{feature});
151 0           push(@prons, $result->{tags}[$KYTEA_PRONTAG_NUM][0]{feature});
152             }
153             }
154              
155 0           return (\@surfaces, \@poses, \@prons);
156             }
157              
158             sub _to_pinoko
159             {
160 0     0     my ($self, $surfaces_ref, $poses_ref, $prons_ref) = @_;
161              
162 0           my $ret = '';
163              
164 0           for my $i (0 .. $#{$prons_ref})
  0            
165             {
166 0           my $surf = $surfaces_ref->[$i];
167              
168 0 0 0       if (
    0 0        
      0        
      0        
169             $poses_ref->[$i] eq '記号'
170             || $poses_ref->[$i] eq '補助記号'
171             || ( $prons_ref->[$i] eq 'UNK' && $surf =~ /[^\p{InHalfwidthKatakana}]/ )
172             || $surf =~ /^[a-zA-Za-zA-Z0-90-9]+$/
173             )
174             {
175 0           $ret .= $surf;
176             }
177             elsif ($surf =~ /[^\p{InHiragana}]/)
178             {
179 0 0 0       if (
      0        
      0        
      0        
      0        
180             $surf eq '手術'
181             || $surf eq '笑'
182             || $surf eq 'シーウーノ'
183             || $surf eq 'アラマンチュ'
184             || $surf eq 'シーウーノアラマンチュ'
185             || $surf =~ /^アッチョンブリケー*/
186             )
187             {
188 0           $ret .= $surf;
189             }
190             else
191             {
192             # e.g. 「アめりカ合衆国の州」の場合
193             # @surfaces の中身は以下の通り
194             # [0]: アめりカ
195             # [1]: 合衆国
196             # [2]: の
197             # [3]: 州
198 0           my @surfaces = grep { length } split(/([0-90-9]*[\p{Han}ケヶ]+[0-90-9]*|[^\p{Han}]+)/, $surf);
  0            
199              
200 0           my (@kanji_prons, $regexp);
201              
202 0           for my $surface (@surfaces)
203             {
204 0 0         if ($surface =~ /[0-90-9]*[\p{Han}ケヶ]/) { $regexp .= "(.+)"; }
  0            
205             else
206             {
207 0 0         if ($self->{parser_name} eq 'Text::MeCab')
208             {
209 0           $regexp .= Lingua::JA::Regular::Unicode::katakana2hiragana($surface);
210             }
211             else # Text::KyTea
212             {
213 0 0         if ($surface =~ /(?:ず|づ)/)
214             {
215 0           my $pron = Lingua::JA::Regular::Unicode::katakana2hiragana($surface);
216 0           my $du = $pron; $du =~ tr/ず/づ/;
  0            
217 0           my $zu = $pron; $zu =~ tr/づ/ず/;
  0            
218              
219 0           $regexp .= "(?:$du|$zu)";
220             }
221             else
222             {
223 0 0         if ($surface =~ /[あ-おぁ-ぉア-オァ-ォ]{1}/)
224             {
225 0           $regexp .= "[" . Lingua::JA::Regular::Unicode::katakana2hiragana($surface) . "|ー]";
226             }
227 0           else { $regexp .= Lingua::JA::Regular::Unicode::katakana2hiragana($surface); }
228             }
229             }
230             }
231             }
232              
233 0 0         if ($regexp =~ /\(\.\+\)/)
234             {
235 0           $regexp =~ tr/\x{005F}\x{3000}\x{3095}/\x{FF3F}\x{FF3F}\x{304B}/; # 「_ ゕ」-> 「__か」
236 0           @kanji_prons = $prons_ref->[$i] =~ /$regexp/;
237             }
238              
239 0           for my $surface (@surfaces)
240             {
241 0 0         if ($surface =~ /\p{Han}/)
242             {
243 0           my $pron = shift @kanji_prons;
244 0           my $pinoko_pron = $self->pinoko($pron);
245              
246 0 0 0       if ( (! defined $pinoko_pron) || $pron eq $pinoko_pron ) { $ret .= $surface; }
  0            
247 0           else { $ret .= $pron; }
248             }
249             else
250             {
251 0 0         if ($surface =~ /[^\p{InHalfwidthKatakana}]/)
252             {
253 0 0         if ($surface =~ /^\p{InKatakana}+$/)
254             {
255 0           my $pron = Lingua::JA::Regular::Unicode::katakana2hiragana($surface);
256 0           $ret .= Lingua::JA::Regular::Unicode::hiragana2katakana($self->pinoko($pron));
257             }
258 0           else { $ret .= $surface; }
259             }
260             else # 半角カタカナのみ
261             {
262             # 半角文字を kataka2hiragana すると濁点等が分離してしまうので
263             # 一旦全角にしてから kataka2hiragana する
264 0           my $pron = Lingua::JA::Regular::Unicode::katakana_h2z($surface);
265 0           $pron = Lingua::JA::Regular::Unicode::katakana2hiragana($pron);
266 0           $ret .= Lingua::JA::Regular::Unicode::katakana_z2h(
267             Lingua::JA::Regular::Unicode::hiragana2katakana($self->pinoko($pron))
268             );
269             }
270             }
271             }
272             }
273             }
274             else # 平仮名のみ
275             {
276 0           my $pos = $poses_ref->[$i];
277 0           my $pron = $prons_ref->[$i];
278              
279 0 0 0       if ($pos eq '助詞' || $pos eq '語尾' || $pos eq '助動詞')
      0        
280             {
281 0           my $next_pos = $poses_ref->[$i + 1];
282 0 0         $next_pos = '' unless defined $next_pos;
283              
284 0           my $next_surface = $surfaces_ref->[$i + 1];
285 0 0         $next_surface = '' unless defined $next_surface;
286              
287 0           my $next_next_surface = $surfaces_ref->[$i + 2];
288 0 0         $next_next_surface = '' unless defined $next_next_surface;
289              
290 0 0 0       if (
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
291             exists $HIRAGANA_INVALID_POS{$next_pos}
292             || $next_surface eq '?'
293             || $next_surface eq '?'
294             || (
295             ($next_pos eq '名詞' || $next_pos eq '記号' || $next_pos eq '補助記号')
296             && ! exists $TERMINATOR_CHAR{$next_surface}
297             && $next_surface ne '・・'
298             && ! ($next_surface eq '・' && $next_next_surface eq '・')
299             && ! ($next_surface eq '・' && $next_next_surface eq '・')
300             && $next_surface !~ /^w+$/
301             )
302             )
303             {
304 0           $ret .= $pron;
305             }
306             else
307             {
308 0 0 0       if ($pron eq 'わ')
    0 0        
    0          
    0          
309             {
310 0 0         if ( int( rand(2) ) == 0 ) { $ret .= 'わのよ'; }
  0            
311 0           else { $ret .= 'わのね'; }
312             }
313             elsif ($pron eq 'の')
314             {
315 0 0 0       if ( $i != 0 && $poses_ref->[$i - 1] eq '名詞' && ($next_surface eq '' || $next_surface =~ /\s/) )
      0        
      0        
316             {
317 0           $ret .= 'の';
318             }
319 0           else { $ret .= 'のよさ'; }
320             }
321             elsif ($pron eq 'うよ')
322             {
323 0           $ret .= 'うよのさ';
324             }
325             elsif ( $i != 0 && ($pron eq 'よ' || $pron eq 'ね') )
326             {
327 0           my $prev_surface = $surfaces_ref->[$i - 1];
328              
329 0 0         if ($pron eq 'よ')
330             {
331 0 0 0       if ($prev_surface eq 'わ' || $prev_surface eq 'だ')
    0          
332 0           { $ret .= 'のよ'; }
333 0           elsif ($prev_surface ne 'の') { $ret .= 'よのさ'; }
334 0           else { $ret .= 'よさ'; }
335             }
336             else # ね
337             {
338 0 0 0       if ($prev_surface eq 'わ' || $prev_surface eq 'よ') { $ret .= 'のね'; }
  0            
339 0           else { $ret .= 'ね'; }
340             }
341             }
342 0           else { $ret .= $pron; }
343             }
344             }
345 0           else { $ret .= $pron; }
346             }
347             }
348              
349 0           return $self->pinoko($ret);
350             }
351              
352             sub pinoko
353             {
354 0     0 0   local $_ = $_[1];
355              
356 0 0         return unless defined $_;
357              
358 0           s/奥さん/おくたん/g;
359 0           s/手術/シウツ/g;
360 0           s/しゅじゅつ/しうつ/g;
361 0           s/憂鬱/ユーツ/g;
362 0           s/抜群/バチグン/g;
363 0           s/ウソツキ/ウソチュキ/g; # MeCab専用
364 0           s/あくせさり/あくちぇちゃい/g;
365 0           s/す/ちゅ/g;
366 0           s/づ/じゅ/g;
367 0           s/じ(?=め)/じゅ/g;
368 0           s/ず(?!ー)/じゅ/g;
369 0           s/っつ/っちゅ/g;
370 0           s/けど/けよ/g;
371 0           s/あのね/あんね/g;
372 0           s/こども/こよも/g;
373 0           s/なんだ/なんや/g;
374 0           s/それで/そいれ/g;
375 0           s/そりゃ[あー]/そやァ/g;
376 0           s/うそつき/うそちゅき/g;
377 0           s/(? ちゃ
378 0           s/(?
379 0           s/し(?!う|ち)/ち/g; # しう, しち でなければ し -> ち
380 0           s/れでぃー?/れれい/g;
381 0           s/きゃんでぃー?/きゃんれー/g;
382 0           s/り(?!ゃ|ゅ|ょ)/い/g;
383 0           s/(?
384 0           tr/でらるろ/れやゆよ/;
385 0           s/ど(?!よ)/ろ/g;
386 0           s/だ(?!のよ|ゆ|が)/ら/g;
387              
388 0           $_;
389             }
390              
391             1;
392              
393             __END__