#title(日本語の文章を単語に分割する(分かち書きをする)) #navi(.NETプログラミング研究) #contents *日本語の文章を単語に分割する(分かち書きをする) [#i0c6db54] 文章を単語単位に分割したいと思う時があります。英語ならば単語がスペースで区切られているため比較的簡単にできそうですが、日本語ではどうやったらいいのか見当もつきません。 調べてみると、文章を単語で分割して記述することを「分かち書き」と言うそうです。そして、分かち書きを行うには、「形態素解析」を行えばよいようです。形態素解析とは、文章を意味のある最小の言葉(形態素)に分割し、それらの品詞を判別することです。 -[[わかち書き - Wikipedia>http://ja.wikipedia.org/wiki/%E5%88%86%E3%81%8B%E3%81%A1%E6%9B%B8%E3%81%8D]] -[[形態素解析 - Wikipedia>http://ja.wikipedia.org/wiki/%E5%BD%A2%E6%85%8B%E7%B4%A0%E8%A7%A3%E6%9E%90]] こんなに難しそうなことは素人の手には負えませんので、簡単にできそうな方法を探してみました。まず形態素解析を行う方法ですが、これは次の2つの方法しか見つかりませんでした。 +形態素解析ツールをインストールして使う +Webサービスを使う 形態素解析ツールとして有名なものには、[[KAKASHI>http://kakasi.namazu.org/]]、[[ChaSen(茶筌)>http://chasen-legacy.sourceforge.jp/]]、[[MeCab(和布蕪)>http://mecab.sourceforge.net/]]などがあります。この内MeCabについては、その機能を簡単に呼び出すことができる.NETのライブラリがいくつか公開されています。私が調べた限りでは、次のようなものがありました。 -[[MeCabSharp - Meteor Factory>http://mf3.dotpp.net/?Software%2FMeCabSharp]](v1.2, for MeCab 0.97) -[[MeCab.NET プロジェクト日本語トップページ - SourceForge.JP>http://sourceforge.jp/projects/mecabdotnet/]](バージョン 0.0.0.3) -[[mecab/cabocha用C#ラッパーライブラリ mutterofstar : Vector>http://www.vector.co.jp/soft/winnt/prog/se446647.html]](バージョン 0.1) Webサービスによる方法というのは、Yahoo! Japanの[[日本語形態素解析Webサービス>http://developer.yahoo.co.jp/webapi/jlp/ma/v1/parse.html]]のことです。これ以外に形態素解析を行うWebサービスがあるかは、分かりません。 これらの方法は、外部のアプリケーション、外部のWebサービスを使わなければならないという大きな足かせがあります。しかし、それ以外の方法は見つかりませんでした。 次に分かち書きだけを行う方法ですが、以下のような方法が見つかりました。 +公開されている分かち書きを行うコード([[TinySegmenter>http://chasen.org/~taku/software/TinySegmenter/]])を参考にする +IWordBreakerを使う ここではこの2つの方法を順番に紹介します。なお、形態素解析については、次回以降に紹介します。 **TinySegmenterを移植する [#sfbafd90] JavaScriptだけで日本語分かち書きを行う、[[TinySegmenter>http://chasen.org/~taku/software/TinySegmenter/]]というソフトウェアがあります。非常にコンパクトで、辞書を使用しません。ライセンスは、修正BSDライセンスです。 TinySegmenterは様々な言語に移植されていますが、C#やVB.NETへの移植版は見つかりませんでした。TinySegmenterのコードを見たところ、JavaScriptの知識がそれほどでもない私でもなんとか理解できそうな位シンプルでしたので、C#とVB.NETに移植してみました。かなりの行数のコードになってしまいましたのでここには載せませんが、以下の場所で公開します。使い方なども、そちらで説明しています。 -[[TinySegmenter.NET : 分かち書きを行うC#のクラス - DoboWiki>http://wiki.dobon.net/index.php?free%2FTinySegmenter.NET]] -[[TinySegmenter VB.NET : 分かち書きを行うVB.NETのクラス - DoboWiki>http://wiki.dobon.net/index.php?free%2FTinySegmenter%20VB.NET]] なお、他の言語への移植版には、以下のようなものがありました。 -Perl: [[Text-TinySegmenter>http://search.cpan.org/dist/Text-TinySegmenter/]] -C++: [[tinysegmenter-cpp>http://code.google.com/p/tinysegmenter-cpp/]] -Ruby: [[TinySegmenterをRubyに移植してみた[Ruby]>http://d.hatena.ne.jp/zegenvs/20080212/p1]] -Ruby: [[TinySegmenterをRubyに移植 - llameradaの日記>http://d.hatena.ne.jp/llamerada/20080224/1203818061]] -Python: [[TinySegmenterをPythonで書いてみた>http://www.programming-magic.com/20080726203844/]] -Python: [[TinySegmenter in Python>http://lilyx.net/pages/tinysegmenterp.html]] -PHP: [[PHP版TinySegmenter作ってみた>http://www.programming-magic.com/20080816010106/]] -Java: [[TinySegmenter.java>http://code.google.com/p/cmecab-java/source/browse/trunk/src/net/moraleboost/tinysegmenter/TinySegmenter.java]] -VBA: [[Excelで自然言語処理: VBAでTinySegmenterしてみる>http://pub.ne.jp/arihagne/?entry_id=2768818]] -Objective-C: [[TinySegmenterをiPhone(Objective-C)に移植してみました>http://blog.bornneet.com/Entry/276/]] -RegexKitLite: [[TinySegmenter.mをRegexKitLiteに対応させてみた>http://blog.bornneet.com/Entry/277/]] -xyzzy lisp: [[tiny-segmenter - xyzzy Lisp だけで実装されたコンパクトな分かち書きソフトウェア>http://miyamuko.s56.xrea.com/xyzzy/tiny-segmenter.html]] **IWordBreakerを使用する [#c00e18e4] WindowsのIndex Serviceで使用されている[[IWordBreaker>http://msdn.microsoft.com/en-us/library/ms691079.aspx]]を使用すれば、分かち書きができます。これは、Windows 2000以降でサポートされているようです。 IWordBreakerで分かち書きをする方法はちょっと難しいですが、Andrew Cenciniさんのブログ「[[Part 1: Testing Full-Text Wordbreakers>http://web.archive.org/web/20040426011720/http://sqljunkies.com/WebLog/acencini/articles/595.aspx]]」(元のページが消滅しているため、Internet Archiveへのリンク)で説明されています。また、以下のページでも詳しく説明されています。ほとんどのページは、「Testing Full-Text Wordbreakers」で紹介されているコードが基になっています。 -[[Sql005 フルテキスト検索機能のワードブレーカを検証するツール>http://sqljp.com/yoshihirokawabata/archive/2004/06/16/2749.aspx]] -[[C#で分かち書き>http://a-tak.com/xoops2/modules/wordpress/2004/10/11/c%E3%81%A7%E5%88%86%E3%81%8B%E3%81%A1%E6%9B%B8%E3%81%8D/]] -[[C#からIndex Serviceを使って”分かち書き(わかちがき)”する>http://ameblo.jp/norixp/entry-10017071359.html]] -[[WordBreakerで形態素解析>http://d.hatena.ne.jp/veveve/20090317/1237298291]] -[[IWordBreaker とファイル検索>http://d.hatena.ne.jp/NyaRuRu/20101031/p1]] すでにこれだけの記事がありますので、ここで紹介する必要もないとは思いますが、元サイトが消えているということもありますので、「Testing Full-Text Wordbreakers」に掲載されているコードを以下に紹介させていただきます(ちょっとだけ書き換えていますが、ほぼ同じです)。また、VB.NETに書き換えたコードも示します(コード変換には「[[Snippet Converter for .NET 4.0>http://codeconverter.sharpdevelop.net/SnippetConverter.aspx]]」を使用しました)。 まずは、WordBreaker.csです。 #code(csharp){{ //=============================================================== // WordBreaker.cs //=============================================================== using System; using System.Runtime.InteropServices; namespace StemText { //=============================================================== // Wordbreaker stuff //=============================================================== [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; } // used to fill the buffer for TEXT_SOURCE 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); } //HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ContentIndex\Language //の Japanese_Default の WBreakerClass の値を調べてGuidを書き換える [ComImport] //Windows 2000 //[Guid("80A3E9B0-A246-11D3-BB8C-0090272FA362")] //Windows XP //[Guid("BE41F4E6-9EAD-498f-A473-F3CA66F9BE8B")] //Windows Vista, 7 [Guid("E1E8F15E-8BEC-45DF-83BF-50FF84D0CAB5")] public class CWordBreaker { } } }} #code(vbnet){{ '=============================================================== ' WordBreaker.vb '=============================================================== Imports System.Runtime.InteropServices Namespace StemText '=============================================================== ' Wordbreaker stuff '=============================================================== <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 ' used to fill the buffer for TEXT_SOURCE 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 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ContentIndex\Language 'の Japanese_Default の WBreakerClass の値を調べてGuidを書き換える 'Windows 2000 '<Guid("80A3E9B0-A246-11D3-BB8C-0090272FA362")> 'Windows XP '<Guid("BE41F4E6-9EAD-498f-A473-F3CA66F9BE8B")> 'Windows Vista, 7 <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別の値みたいなものを書いておきましたが、これが本当に合っているかは保証できません。 次に分かち書きを実行する部分です。 #code(csharp){{ //============================================================== // Main.cs //============================================================== using System; using System.Runtime.InteropServices; namespace StemText { //============================================================== // Main class //============================================================== class MainClass { static uint pfnFillTextBuffer(ref TEXT_SOURCE pTextSource) { // return WBREAK_E_END_OF_TEXT 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); //string tokStr = args[0]; 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(); } } } }} #code(vbnet){{ '============================================================== ' Main.vb '============================================================== Imports System.Runtime.InteropServices Namespace StemText '============================================================== ' Main Module '============================================================== Module MainModule Function pfnFillTextBuffer(ByRef pTextSource As TEXT_SOURCE) As UInteger ' return WBREAK_E_END_OF_TEXT 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 tokStr As String = args(0) 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 }} このコードを実行すると、以下のように出力されます。 #pre{{ PutWord buffer: 今日 PutWord buffer: は PutWord buffer: いい PutWord buffer: 天気 PutWord buffer: です PutWord buffer: ね PutBreak : EOP }} **TinySegmenterとIWordBreakerの結果の比較 [#jd797a68] TinySegmenterとIWordBreakerの結果の比較が、「[[COM で分かち書き IWordBreaker for C++>http://d.hatena.ne.jp/rti7743/20100314/1268550369]]」などで紹介されています。これらによると、IWordBreakerはTinySegmenterと比較して、より大きな塊で分ける傾向があるようです。また、IWordBreakerでは記号がなくなってしまうようです。 **予告 [#tf9a6b05] 次回は、形態素解析について紹介する予定です。 **コメント [#e4ba3995] #comment //これより下は編集しないでください #pageinfo([[:Category/.NET]] [[:Category/ASP.NET]],2010-11-24 (水) 01:05:56,DOBON!,2010-11-24 (水) 01:05:56,DOBON!) |