日本語の文章を単語に分割する(分かち書きをする) †
文章を単語単位に分割したいと思う時があります。英語ならば単語がスペースで区切られているため比較的簡単にできそうですが、日本語ではどうやったらいいのか見当もつきません。
調べてみると、文章を単語で分割して記述することを「分かち書き」と言うそうです。そして、分かち書きを行うには、「形態素解析」を行えばよいようです。形態素解析とは、文章を意味のある最小の言葉(形態素)に分割し、それらの品詞を判別することです。
こんなに難しそうなことは素人の手には負えませんので、簡単にできそうな方法を探してみました。まず形態素解析を行う方法ですが、これは次の2つの方法しか見つかりませんでした。
- 形態素解析ツールをインストールして使う
- Webサービスを使う
形態素解析ツールとして有名なものには、KAKASHI、ChaSen(茶筌)、MeCab(和布蕪)などがあります。この内MeCabについては、その機能を簡単に呼び出すことができる.NETのライブラリがいくつか公開されています。私が調べた限りでは、次のようなものがありました。
Webサービスによる方法というのは、Yahoo! Japanの日本語形態素解析Webサービスのことです。これ以外に形態素解析を行うWebサービスがあるかは、分かりません。
これらの方法は、外部のアプリケーション、外部のWebサービスを使わなければならないという大きな足かせがあります。しかし、それ以外の方法は見つかりませんでした。
次に分かち書きだけを行う方法ですが、以下のような方法が見つかりました。
- 公開されている分かち書きを行うコード(TinySegmenter)を参考にする
- IWordBreakerを使う
ここではこの2つの方法を順番に紹介します。なお、形態素解析については、次回以降に紹介します。
TinySegmenterを移植する †
JavaScriptだけで日本語分かち書きを行う、TinySegmenterというソフトウェアがあります。非常にコンパクトで、辞書を使用しません。ライセンスは、修正BSDライセンスです。
TinySegmenterは様々な言語に移植されていますが、C#やVB.NETへの移植版は見つかりませんでした。TinySegmenterのコードを見たところ、JavaScriptの知識がそれほどでもない私でもなんとか理解できそうな位シンプルでしたので、C#とVB.NETに移植してみました。かなりの行数のコードになってしまいましたのでここには載せませんが、以下の場所で公開します。使い方なども、そちらで説明しています。
なお、他の言語への移植版には、以下のようなものがありました。
IWordBreakerを使用する †
WindowsのIndex Serviceで使用されているIWordBreakerを使用すれば、分かち書きができます。これは、Windows 2000以降でサポートされているようです。
IWordBreakerで分かち書きをする方法はちょっと難しいですが、Andrew Cenciniさんのブログ「Part 1: Testing Full-Text Wordbreakers」(元のページが消滅しているため、Internet Archiveへのリンク)で説明されています。また、以下のページでも詳しく説明されています。ほとんどのページは、「Testing Full-Text Wordbreakers」で紹介されているコードが基になっています。
すでにこれだけの記事がありますので、ここで紹介する必要もないとは思いますが、元サイトが消えているということもありますので、「Testing Full-Text Wordbreakers」に掲載されているコードを以下に紹介させていただきます(ちょっとだけ書き換えていますが、ほぼ同じです)。また、VB.NETに書き換えたコードも示します(コード変換には「Snippet Converter for .NET 4.0」を使用しました)。
まずは、WordBreaker.csです。
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
| | using System;
using System.Runtime.InteropServices;
namespace StemText
{
[Flags]
public enum WORDREP_BREAK_TYPE
{
WORDREP_BREAK_EOW = 0,
WORDREP_BREAK_EOS = 1,
WORDREP_BREAK_EOP = 2,
WORDREP_BREAK_EOC = 3
}
[ComImport]
[Guid("CC907054-C058-101A-B554-08002B33B0E6")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IWordSink
{
void PutWord([MarshalAs(UnmanagedType.U4)] int cwc,
[MarshalAs(UnmanagedType.LPWStr)] string pwcInBuf,
[MarshalAs(UnmanagedType.U4)] int cwcSrcLen,
[MarshalAs(UnmanagedType.U4)] int cwcSrcPos);
void PutAltWord([MarshalAs(UnmanagedType.U4)] int cwc,
[MarshalAs(UnmanagedType.LPWStr)] string pwcInBuf,
[MarshalAs(UnmanagedType.U4)] int cwcSrcLen,
[MarshalAs(UnmanagedType.U4)] int cwcSrcPos);
void StartAltPhrase();
void EndAltPhrase();
void PutBreak(WORDREP_BREAK_TYPE breakType);
}
[ComImport]
[Guid("CC906FF0-C058-101A-B554-08002B33B0E6")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IPhraseSink
{
void PutSmallPhrase([MarshalAs(UnmanagedType.LPWStr)] string pwcNoun,
[MarshalAs(UnmanagedType.U4)] int cwcNoun,
[MarshalAs(UnmanagedType.LPWStr)] string pwcModifier,
[MarshalAs(UnmanagedType.U4)] int cwcModifier,
[MarshalAs(UnmanagedType.U4)] int ulAttachmentType);
void PutPhrase([MarshalAs(UnmanagedType.LPWStr)] string pwcPhrase,
[MarshalAs(UnmanagedType.U4)] int cwcPhrase);
}
public class CWordSink : IWordSink
{
public void PutWord(int cwc, string pwcInBuf, int cwcSrcLen, int cwcSrcPos)
{
Console.WriteLine("PutWord buffer: " + pwcInBuf.Substring(0, cwc));
}
public void PutAltWord(int cwc, string pwcInBuf, int cwcSrcLen, int cwcSrcPos)
{
Console.WriteLine("PutAltWord buffer: " + pwcInBuf.Substring(0, cwc));
}
public void StartAltPhrase()
{
Console.WriteLine("StartAltPhrase");
}
public void EndAltPhrase()
{
Console.WriteLine("EndAltPhrase");
}
public void PutBreak(StemText.WORDREP_BREAK_TYPE breakType)
{
string strBreak;
switch (breakType)
{
case WORDREP_BREAK_TYPE.WORDREP_BREAK_EOC:
strBreak = "EOC";
break;
case WORDREP_BREAK_TYPE.WORDREP_BREAK_EOP:
strBreak = "EOP";
break;
case WORDREP_BREAK_TYPE.WORDREP_BREAK_EOS:
strBreak = "EOS";
break;
case WORDREP_BREAK_TYPE.WORDREP_BREAK_EOW:
strBreak = "EOW";
break;
default:
strBreak = "ERROR";
break;
}
Console.WriteLine("PutBreak : " + strBreak);
}
}
public class CPhraseSink : IPhraseSink
{
public void PutSmallPhrase(string pwcNoun, int cwcNoun, string pwcModifier,
int cwcModifier, int ulAttachmentType)
{
Console.WriteLine("PutSmallPhrase: " + pwcNoun.Substring(0, cwcNoun)
+ " , " + pwcModifier.Substring(0, cwcModifier));
}
public void PutPhrase(string pwcPhrase, int cwcPhrase)
{
Console.WriteLine("PutPhrase: " + pwcPhrase.Substring(0, cwcPhrase));
}
}
[StructLayout(LayoutKind.Sequential)]
public struct TEXT_SOURCE
{
[MarshalAs(UnmanagedType.FunctionPtr)]
public delFillTextBuffer pfnFillTextBuffer;
[MarshalAs(UnmanagedType.LPWStr)]
public string awcBuffer;
[MarshalAs(UnmanagedType.U4)]
public int iEnd;
[MarshalAs(UnmanagedType.U4)]
public int iCur;
}
public delegate uint delFillTextBuffer(
[MarshalAs(UnmanagedType.Struct)] ref TEXT_SOURCE pTextSource);
[ComImport]
[Guid("D53552C8-77E3-101A-B552-08002B33B0E6")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IWordBreaker
{
void Init([MarshalAs(UnmanagedType.Bool)] bool fQuery,
[MarshalAs(UnmanagedType.U4)] int maxTokenSize,
[MarshalAs(UnmanagedType.Bool)] out bool pfLicense);
void BreakText([MarshalAs(UnmanagedType.Struct)] ref TEXT_SOURCE pTextSource,
[MarshalAs(UnmanagedType.Interface)] IWordSink pWordSink,
[MarshalAs(UnmanagedType.Interface)] IPhraseSink pPhraseSink);
void GetLicenseToUse([MarshalAs(UnmanagedType.LPWStr)] out string ppwcsLicense);
}
[ComImport]
[Guid("E1E8F15E-8BEC-45DF-83BF-50FF84D0CAB5")]
public class CWordBreaker
{
}
}
|
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
| | Imports System.Runtime.InteropServices
Namespace StemText
<Flags()> _
Public Enum WORDREP_BREAK_TYPE
WORDREP_BREAK_EOW = 0
WORDREP_BREAK_EOS = 1
WORDREP_BREAK_EOP = 2
WORDREP_BREAK_EOC = 3
End Enum
<ComImport()> _
<Guid("CC907054-C058-101A-B554-08002B33B0E6")> _
<InterfaceType(ComInterfaceType.InterfaceIsIUnknown)> _
Public Interface IWordSink
Sub PutWord(<MarshalAs(UnmanagedType.U4)> ByVal cwc As Integer, _
<MarshalAs(UnmanagedType.LPWStr)> ByVal pwcInBuf As String, _
<MarshalAs(UnmanagedType.U4)> ByVal cwcSrcLen As Integer, _
<MarshalAs(UnmanagedType.U4)> ByVal cwcSrcPos As Integer)
Sub PutAltWord(<MarshalAs(UnmanagedType.U4)> ByVal cwc As Integer, _
<MarshalAs(UnmanagedType.LPWStr)> ByVal pwcInBuf As String, _
<MarshalAs(UnmanagedType.U4)> ByVal cwcSrcLen As Integer, _
<MarshalAs(UnmanagedType.U4)> ByVal cwcSrcPos As Integer)
Sub StartAltPhrase()
Sub EndAltPhrase()
Sub PutBreak(ByVal breakType As WORDREP_BREAK_TYPE)
End Interface
<ComImport()> _
<Guid("CC906FF0-C058-101A-B554-08002B33B0E6")> _
<InterfaceType(ComInterfaceType.InterfaceIsIUnknown)> _
Public Interface IPhraseSink
Sub PutSmallPhrase(<MarshalAs(UnmanagedType.LPWStr)> ByVal pwcNoun As String, _
<MarshalAs(UnmanagedType.U4)> ByVal cwcNoun As Integer, _
<MarshalAs(UnmanagedType.LPWStr)> ByVal pwcModifier As String, _
<MarshalAs(UnmanagedType.U4)> ByVal cwcModifier As Integer, _
<MarshalAs(UnmanagedType.U4)> ByVal ulAttachmentType As Integer)
Sub PutPhrase(<MarshalAs(UnmanagedType.LPWStr)> ByVal pwcPhrase As String, _
<MarshalAs(UnmanagedType.U4)> ByVal cwcPhrase As Integer)
End Interface
Public Class CWordSink
Implements IWordSink
Public Sub PutWord(ByVal cwc As Integer, _
ByVal pwcInBuf As String, _
ByVal cwcSrcLen As Integer, _
ByVal cwcSrcPos As Integer) _
Implements IWordSink.PutWord
Console.WriteLine("PutWord buffer: " & pwcInBuf.Substring(0, cwc))
End Sub
Public Sub PutAltWord(ByVal cwc As Integer, _
ByVal pwcInBuf As String, _
ByVal cwcSrcLen As Integer, _
ByVal cwcSrcPos As Integer) _
Implements IWordSink.PutAltWord
Console.WriteLine("PutAltWord buffer: " & pwcInBuf.Substring(0, cwc))
End Sub
Public Sub StartAltPhrase() Implements IWordSink.StartAltPhrase
Console.WriteLine("StartAltPhrase")
End Sub
Public Sub EndAltPhrase() Implements IWordSink.EndAltPhrase
Console.WriteLine("EndAltPhrase")
End Sub
Public Sub PutBreak(ByVal breakType As StemText.WORDREP_BREAK_TYPE) _
Implements IWordSink.PutBreak
Dim strBreak As String
Select Case breakType
Case WORDREP_BREAK_TYPE.WORDREP_BREAK_EOC
strBreak = "EOC"
Exit Select
Case WORDREP_BREAK_TYPE.WORDREP_BREAK_EOP
strBreak = "EOP"
Exit Select
Case WORDREP_BREAK_TYPE.WORDREP_BREAK_EOS
strBreak = "EOS"
Exit Select
Case WORDREP_BREAK_TYPE.WORDREP_BREAK_EOW
strBreak = "EOW"
Exit Select
Case Else
strBreak = "ERROR"
Exit Select
End Select
Console.WriteLine("PutBreak : " & strBreak)
End Sub
End Class
Public Class CPhraseSink
Implements IPhraseSink
Public Sub PutSmallPhrase(ByVal pwcNoun As String, _
ByVal cwcNoun As Integer, _
ByVal pwcModifier As String, _
ByVal cwcModifier As Integer, _
ByVal ulAttachmentType As Integer) _
Implements IPhraseSink.PutSmallPhrase
Console.WriteLine("PutSmallPhrase: " & pwcNoun.Substring(0, cwcNoun) & _
" , " & pwcModifier.Substring(0, cwcModifier))
End Sub
Public Sub PutPhrase(ByVal pwcPhrase As String, _
ByVal cwcPhrase As Integer) _
Implements IPhraseSink.PutPhrase
Console.WriteLine("PutPhrase: " & pwcPhrase.Substring(0, cwcPhrase))
End Sub
End Class
<StructLayout(LayoutKind.Sequential)> _
Public Structure TEXT_SOURCE
<MarshalAs(UnmanagedType.FunctionPtr)> _
Public pfnFillTextBuffer As delFillTextBuffer
<MarshalAs(UnmanagedType.LPWStr)> _
Public awcBuffer As String
<MarshalAs(UnmanagedType.U4)> _
Public iEnd As Integer
<MarshalAs(UnmanagedType.U4)> _
Public iCur As Integer
End Structure
Public Delegate Function delFillTextBuffer( _
<MarshalAs(UnmanagedType.Struct)> ByRef pTextSource As TEXT_SOURCE) As UInteger
<ComImport()> _
<Guid("D53552C8-77E3-101A-B552-08002B33B0E6")> _
<InterfaceType(ComInterfaceType.InterfaceIsIUnknown)> _
Public Interface IWordBreaker
Sub Init(<MarshalAs(UnmanagedType.Bool)> ByVal fQuery As Boolean, _
<MarshalAs(UnmanagedType.U4)> ByVal maxTokenSize As Integer, _
<MarshalAs(UnmanagedType.Bool)> ByRef pfLicense As Boolean)
Sub BreakText(<MarshalAs(UnmanagedType.Struct)> ByRef pTextSource As TEXT_SOURCE, _
<MarshalAs(UnmanagedType.[Interface])> ByVal pWordSink As IWordSink, _
<MarshalAs(UnmanagedType.[Interface])> ByVal pPhraseSink As IPhraseSink)
Sub GetLicenseToUse(<MarshalAs(UnmanagedType.LPWStr)> ByVal ppwcsLicense As String)
End Interface
<ComImport()> _
<Guid("E1E8F15E-8BEC-45DF-83BF-50FF84D0CAB5")> _
Public Class CWordBreaker
End Class
End Namespace
|
最後のCWordBreakerクラスの部分ですが、GuidがOSによって変わるようです。コメントに書いたように、レジストリの「HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ContentIndex\Language\Japanese_Default」の「WBreakerClass」の値を調べて書き換えてください。コメントにOS別の値みたいなものを書いておきましたが、これが本当に合っているかは保証できません。
次に分かち書きを実行する部分です。
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
| | using System;
using System.Runtime.InteropServices;
namespace StemText
{
class MainClass
{
static uint pfnFillTextBuffer(ref TEXT_SOURCE pTextSource)
{
return 0x80041780;
}
[STAThread]
static void Main(string[] args)
{
string tokStr = "今日はいい天気ですね。";
try
{
CWordBreaker wb = new CWordBreaker();
IWordBreaker iwb = (IWordBreaker)wb;
CWordSink cws = new CWordSink();
IWordSink iws = (IWordSink)cws;
CPhraseSink cps = new CPhraseSink();
IPhraseSink ips = (IPhraseSink)cps;
bool pfLicense = true;
iwb.Init(true, 1000, out pfLicense);
TEXT_SOURCE pTextSource = new TEXT_SOURCE();
pTextSource.pfnFillTextBuffer =
new delFillTextBuffer(pfnFillTextBuffer);
pTextSource.awcBuffer = tokStr;
pTextSource.iCur = 0;
pTextSource.iEnd = tokStr.Length;
iwb.BreakText(ref pTextSource, iws, ips);
}
catch (System.Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.ReadLine();
}
}
}
|
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
| | Imports System.Runtime.InteropServices
Namespace StemText
Module MainModule
Function pfnFillTextBuffer(ByRef pTextSource As TEXT_SOURCE) As UInteger
Return &H80041780UI
End Function
<STAThread()> _
Sub Main(ByVal args As String())
Dim tokStr As String = "今日はいい天気ですね。"
Try
Dim wb As New CWordBreaker()
Dim iwb As IWordBreaker = DirectCast(wb, IWordBreaker)
Dim cws As New CWordSink()
Dim iws As IWordSink = DirectCast(cws, IWordSink)
Dim cps As New CPhraseSink()
Dim ips As IPhraseSink = DirectCast(cps, IPhraseSink)
Dim pfLicense As Boolean = True
iwb.Init(True, 1000, pfLicense)
Dim pTextSource As New TEXT_SOURCE()
pTextSource.pfnFillTextBuffer = _
New delFillTextBuffer(AddressOf pfnFillTextBuffer)
pTextSource.awcBuffer = tokStr
pTextSource.iCur = 0
pTextSource.iEnd = tokStr.Length
iwb.BreakText(pTextSource, iws, ips)
Catch ex As System.Exception
Console.WriteLine(ex.Message)
End Try
Console.ReadLine()
End Sub
End Module
End Namespace
|
このコードを実行すると、以下のように出力されます。
PutWord buffer: 今日
PutWord buffer: は
PutWord buffer: いい
PutWord buffer: 天気
PutWord buffer: です
PutWord buffer: ね
PutBreak : EOP
TinySegmenterとIWordBreakerの結果の比較 †
TinySegmenterとIWordBreakerの結果の比較が、「COM で分かち書き IWordBreaker for C++」などで紹介されています。これらによると、IWordBreakerはTinySegmenterと比較して、より大きな塊で分ける傾向があるようです。また、IWordBreakerでは記号がなくなってしまうようです。
予告 †
次回は、形態素解析について紹介する予定です。
コメント †