COM で分かち書き IWordBreaker for C++

WindowsのIndex Serverが持っている Windows形態素解析分かち書きを呼び出して使おうって感じで作ってみた。

Windows には、 IWordBreaker って COMコンポーネントがあって、これが形態素解析をやっているらしい。
これを C# から呼び出すのは既に実装されている方が何名かいらっしゃいます。
http://d.hatena.ne.jp/veveve/20090317/1237298291
http://a-tak.com/xoops/modules/wordpress/2004/10/11/393
http://ameblo.jp/norixp/entry-10017071359.html#main

ですが、C++ から呼び出す実装はあまりなかったので作ってみました。

ソースリポジトリ

svn checkout http://rti-source-spool.googlecode.com/svn/trunk/XLWordBreaker/ rti-source-spool-read-only

ダウンロードはこちらからどうぞ。
http://rti-source-spool.googlecode.com/files/XLWordBreaker.zip

__stdcall _com_util::ConvertStringToBSTR(char const *) へのリンクエラーが表示される場合は、以下のlib をリンクに追加してください。

comsuppw.lib
大苦戦

予想以上に大変な奴だった。。。
COMだし、IRegExp とかそのほかである程度やったから大丈夫だろうとなめてました。。。

何より IWordBreaker の Interface が公開されていない!!
OLE Viewer で見ても表示されない。
定義がさっぱりわかんない。

結局、ネットを散々探し回った末に、ネットに2つだけ C++ のサンプルが落ちていた。
http://the-lazy-programmer.com/blog/?p=14
http://topic.csdn.net/t/20050127/10/3757244.html

最初のサイトは Referer を見ているのか、アクセスすると即トップページに飛ばされるけど、サンプルソースがダウンロードできる非常に重要なサイト。

Q:IWordBreaker のインターフェースはどこにあるのか?
A:
Windows SDK についてきます。 #include ってやるとロードできます。
なぜか OLEViewer では Interfaceを見れません。
このindexsrv.h を include すると、 IWordBreaker が定義されます。

Q:どうやって実体化するのか?
A:
2つ方法があります。
一つはGUIDによる指定です。

ただし、これはOSごとによってことなります。
こんな感じです。

#pragma pack(push, 8)

struct __declspec(uuid("80A3E9B0-A246-11D3-BB8C-0090272FA362"))   CLSID_IWordBreaker_Win7;
struct __declspec(uuid("E1E8F15E-8BEC-45DF-83BF-50FF84D0CAB5"))   CLSID_IWordBreaker_WinVista;
struct __declspec(uuid("BE41F4E6-9EAD-498f-A473-F3CA66F9BE8B"))   CLSID_IWordBreaker_WinXP;

#pragma pack(pop)

もう一つは名前による指定です。
"NLG.Japanese Wordbreaker" を CLSIDFromString で CLSID に変換します。
とりあえずこんな感じで動きました。

	HRESULT hr;

	CLSID clsidWordBreaker;
	hr = CLSIDFromString(L"NLG.Japanese Wordbreaker",&clsidWordBreaker);
	if (FAILED(hr))
	{
		_com_issue_error(hr);
	}
//	hr = WordBreaker.CoCreateInstance(__uuidof(CLSID_IWordBreaker_WinXP));
	hr = WordBreaker.CoCreateInstance(clsidWordBreaker);
	if (FAILED(hr))
	{
		_com_issue_error(hr);
	}

	//初期化
	BOOL pfLicense = TRUE;
	hr = WordBreaker->Init(true, 2000, &pfLicense);
	if (FAILED(hr))
	{
		_com_issue_error(hr);
	}

the-lazy-programmer.comさんはレジストリを見ていますが、別にコレでもいいような気がします。

Q:どうやって呼び出すのか?
A:
IWordBreaker の BreakText メソッドを呼び出すには、TEXT_SOURCEとIWordSinkの2つの壁があります。

TEXT_SOURCE は、変換するテキストを指定する構造体です。
面倒なのは、コールバックする関数を指定しなければいけないところ。
私はこんな感じでインナークラスで逃げました。

//形態素解析中に何度かコールバックされる関数を定義するためのインナークラス
class pfnFillTextBufferTmp
{
public:
	//何か知らんけど、これを返すようにするらしい。
	static long __stdcall pfnFillTextBuffer(TEXT_SOURCE * pTextSource)
	{
		// return WBREAK_E_END_OF_TEXT
		return 0x80041780;
	}
};
TEXT_SOURCE textsource;
//形態素解析中に何度かコールバックされる関数!
textsource.pfnFillTextBuffer =  pfnFillTextBufferTmp::pfnFillTextBuffer;
//変換する文字列 もちろん WCHAR* で
textsource.awcBuffer = _bstr_t(inString.c_str());
//形態素解析を開始する地点(WCHARなので1 だと2バイト進む)
textsource.iCur = 0;
//形態素解析を終了する地点(WCHARなので1 だと2バイト進む)
textsource.iEnd = lstrlenW(textsource.awcBuffer);	//WなワイドAPIでやるよ

IWordSink は、形態素解析が行われるたびに呼び出されるコールバックを定義します。
アレ? IRegExp とかは変換結果をまとめてくれたのにこれはコールバックなのか、、、って思いますよね。
何でこんなアホな実装にしたんでしょうか。。。
C# とかの実装を見ると、 IWordSink を .toString() で文字列化して即終了って感じですが、C++にはそんな便利なものはないので、まじめに実装します。

COMのインターフェースの再定義とかをやったことがなかったので大苦戦でした。
終わったあとから見れば、ただの純粋仮想クラス(virtual)なんですけど、、、

IWordSink でオーバーライドしたいメソッドは pushWord です。
pushWord メソッドが形態素解析ごとに呼ばれます。
この中で形態素解析された文字列をリストに追加していきます。

では、 IWordSinkのpushWordだけを実体化すればいいかというとそういうわけではありません。
こいつが使っている全メソッドを定義してあげる必要があります。 IUnknown までさかのぼって全部。

とりあえず私はこうやりました。

//何かコールバックされるらしい。だからオーバーライドする。
//何でコールバックにしたのか。IRegExpみたいに結果を配列に入れて返せばいいのに。
//実装者はアホだなw
class pfnWordSink : public XLNeetIUnknown<IWordSink>
{
public:
	//形態素解析されるたびに飛んでいます。
	void pushWord(ULONG cwc, WCHAR const *pwcInBuf, ULONG cwcSrcLen, ULONG cwcSrcPos)
	{
		//skip
			//ここが形態素解析が進むにつれて呼び出される。
			//結果を自前のリスト等に保存する.
		//skip
	}

	virtual HRESULT STDMETHODCALLTYPE PutWord(ULONG cwc, WCHAR const *pwcInBuf, ULONG cwcSrcLen, ULONG cwcSrcPos)
	{
		this->pushWord(cwc,pwcInBuf,cwcSrcLen,cwcSrcPos);
		return 0;
	}
	virtual HRESULT STDMETHODCALLTYPE PutAltWord(ULONG cwc, WCHAR const *pwcInBuf, ULONG cwcSrcLen, ULONG cwcSrcPos)
	{
		//これの意味がよく分からない。
		return 0;
	}
	virtual HRESULT STDMETHODCALLTYPE StartAltPhrase( void) 
	{
		return 0;
	}

	virtual HRESULT STDMETHODCALLTYPE EndAltPhrase( void)
	{
		return 0;
	}

	virtual HRESULT STDMETHODCALLTYPE PutBreak( WORDREP_BREAK_TYPE )
	{
	   return 0;
	}

	//インナーだし全部publicで。

	//たまに形態素解析の結果がやってこないワードがあるみたいなので補正する.
	//変換する文字列
	WCHAR const *  poolawcBuffer;
	//戦闘から何個目(バイトにあらず)まで変換したか。
	ULONG          lastPos;

	//これに形態素解析の結果を代入する.
	XLStringList * poolStringList;
};


//IWordSinkを継承して作った独自クラスを実体化、、つーかスタック上に確保。
pfnWordSink		wordSink;
//これに結果を入れてもらう.
wordSink.poolStringList = outStringList;
wordSink.poolawcBuffer = textsource.awcBuffer;
wordSink.lastPos = 0;


このなかで XLNeetIUnknown って言うのがあると思うんですが、 IUnknown を実体化するのが面倒だったからテンプレートにして外に追い出しました。
実体はこんな感じです。
何もしないニートな IUnknown ができます。

#pragma once

//何も仕事をしない IUnknown.
template <class _T> class XLNeetIUnknown : public _T
{
	virtual HRESULT STDMETHODCALLTYPE QueryInterface(const struct _GUID &,void **)
	{
		//クラス作成? マンドクセー
		return E_FAIL;
	}
	virtual ULONG STDMETHODCALLTYPE AddRef( void)
	{
		//追加? とりあえず 1返しとけばいいんでしょ。
		//次呼ばれたら本気出す。
		return 1;
	}

	virtual ULONG STDMETHODCALLTYPE Release( void)
	{
		//開放なんてしないんだよババァ
		return 0;
	}
};

さて、これでようやく、 TEXT_SOURCEとIWordSinkがそろいました。
これで、念願の BreakText メソッドを呼び出して形態素解析ができます。

//形態素解析開始
HRESULT hr;
hr = WordBreaker->BreakText(&textsource,&wordSink,NULL);
if (FAILED(hr))
{
	_com_issue_error(hr);
}

Q:たまに形態素解析されたデータのデータ抜けがおきない?

はい、たまに形態素解析されたテキストがロストしますwww
OSのバージョン等によるのかもしれませんが、Windows XPで開発しているとデータの抜けがよくおきます。
毎回発生するのは、 「です。」 「なのは、」などの。や、で末尾が終わる形態素解析をしたときです。
そのデータはどっかに行ってしまうようです。
なんというアホ仕様。。。

仕方ないので自分で補正してます。
前回どこまでやったかを記録していて、データ抜けが起きた場合自動的に補正するようにしてます。
また、「です。」 「なのは、」のように。や、がワードにくっついてしまうため、これらを引っぺがして保存しています。

です。 → です | 。
なのは、 → なのは | 、

補正するルーチンはこんな感じです。

//形態素解析されるたびに飛んでいます。
void pushWord(ULONG cwc, WCHAR const *pwcInBuf, ULONG cwcSrcLen, ULONG cwcSrcPos)
{
	USES_CONVERSION;
	const char * p ;

	//何かまれに変換漏れが起きるような。。。
	if (this->lastPos != cwcSrcPos)
	{
		//変換漏れを起こしたワードがあったらしい。
		//変換漏れが発生した場合、 。と、は区切られないので強引に区切る。
		std::wstring wstrFull = std::wstring(poolawcBuffer , this->lastPos , cwcSrcPos - this->lastPos);
		std::wstring wstrFullMinusOne = std::wstring(poolawcBuffer , this->lastPos , cwcSrcPos - this->lastPos - 1);
		std::wstring wstrLast = wstrFull.substr(wstrFullMinusOne.length() , 1);
		if ( wstrFullMinusOne.length() >= 2 && (wstrLast[0] == 0x3001 || wstrLast[0] == 0x3002) )
		{
			//。と、は区切る
			p = W2A(wstrFullMinusOne.c_str());
			poolStringList->push_back( p );

			p = W2A(wstrLast.c_str());
			poolStringList->push_back( p );
		}
		else
		{
			p = W2A(wstrFull.c_str());
			poolStringList->push_back( p );
		}
		this->lastPos = cwcSrcPos;
	}

	std::wstring wstrFull = std::wstring(poolawcBuffer , cwcSrcPos , cwcSrcLen);
	p = W2A(wstrFull.c_str());
	poolStringList->push_back( p );

	//ここまで変換した。
	this->lastPos = cwcSrcPos + cwcSrcLen;
}

Q:なんか文章末尾の。が消えるんだけど。
A:
仕様ですw
文章末尾の。が消えます。
仕方ないので、文章末尾が消えたら自動的に補正するようにしていますw。
#手のかかる子だなぁ。。。

//よく最後が忘れ去られるのでつけてあげる。
if (wordSink.lastPos < textsource.iEnd)
{
	USES_CONVERSION;
	std::wstring wstrFull = std::wstring(wordSink.poolawcBuffer , wordSink.lastPos , textsource.iEnd - wordSink.lastPos);
	const char * p = W2A(wstrFull.c_str());
	outStringList->push_back( p );
}

Q:形態素解析結果について
A
こんな感じです。
結構大きなくくりで分けてくれるようです。

元の文    科学の力ではどうしようもできない、魑魅魍魎などの奇怪な輩に立ち向かう胡散臭い男。
WordBreaker 科学の | 力では | どう | しようもできない | 、 | 魑魅 | 魍魎などの | 奇怪な | 輩に | 立ち向かう | 胡散臭い | 男 | 。
TinySegMenter 科学 | の | 力 | で | は | どう | しよ | う | も | でき | ない | 、 | 魑魅 | 魍魎 | など | の | 奇怪 | な | 輩 | に | 立ち向かう | 胡散 | 臭い | 男 | 。
Mecab     科学 | の | 力 | で | は | どう | しよう | も | でき | ない | 、 | 魑魅魍魎 | など | の | 奇怪 | な | 輩 | に | 立ち向う | 胡散臭い | 男 | 。
ついでだから主要なソースコードをまるコピペするよ!
////////////////////////////////////////////////////
///XLNeetIUnknown.h
////////////////////////////////////////////////////
#pragma once

//何も仕事をしない IUnknown.
template <class _T> class XLNeetIUnknown : public _T
{
	virtual HRESULT STDMETHODCALLTYPE QueryInterface(const struct _GUID &,void **)
	{
		//クラス作成? マンドクセー
		return E_FAIL;
	}
	virtual ULONG STDMETHODCALLTYPE AddRef( void)
	{
		//追加? とりあえず 1返しとけばいいんでしょ。
		//次呼ばれたら本気出す。
		return 1;
	}

	virtual ULONG STDMETHODCALLTYPE Release( void)
	{
		//開放なんてしないんだよババァ
		return 0;
	}
};

//////////////////////////////////////////////////
//XLWordBreaker.h
//////////////////////////////////////////////////
#if !defined(AFX_XLWORDBREAKER_H__2FA97595_1CAE_4251_9992_3A7DAF6871DB__INCLUDED_)
#define AFX_XLWORDBREAKER_H__2FA97595_1CAE_4251_9992_3A7DAF6871DB__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

#include "comm.h"
#include "XLException.h"
#include <atlbase.h>
#include <comdef.h>
#include <indexsrv.h>

class XLWordBreaker  
{
public:
	XLWordBreaker();
	virtual ~XLWordBreaker();

	bool Init();
	bool BreakText(const std::string & inString, XLStringList * outStringList);
	static void test();
private:

	CComPtr<IWordBreaker>	WordBreaker;
};

#endif // !defined(AFX_XLWORDBREAKER_H__2FA97595_1CAE_4251_9992_3A7DAF6871DB__INCLUDED_)


//////////////////////////////////////////////////
//XLWordBreaker.cpp
//////////////////////////////////////////////////
#include "XLWordBreaker.h"
#include "XLNeetIUnknown.h"

/*
#pragma pack(push, 8)

struct __declspec(uuid("80A3E9B0-A246-11D3-BB8C-0090272FA362"))   CLSID_WordBreaker_Win7;
struct __declspec(uuid("E1E8F15E-8BEC-45DF-83BF-50FF84D0CAB5"))   CLSID_IWordBreaker_WinVista;
struct __declspec(uuid("BE41F4E6-9EAD-498f-A473-F3CA66F9BE8B"))   CLSID_IWordBreaker_WinXP;

#pragma pack(pop)

*/

XLWordBreaker::XLWordBreaker()
{
	HRESULT hr;

	CLSID clsidWordBreaker;
	hr = CLSIDFromString(L"NLG.Japanese Wordbreaker",&clsidWordBreaker);
	if (FAILED(hr))
	{
		_com_issue_error(hr);
	}
//	hr = WordBreaker.CoCreateInstance(__uuidof(CLSID_IWordBreaker_WinXP));
	hr = WordBreaker.CoCreateInstance(clsidWordBreaker);
	if (FAILED(hr))
	{
		_com_issue_error(hr);
	}

	//初期化
	BOOL pfLicense = TRUE;
	hr = WordBreaker->Init(true, 2000, &pfLicense);
	if (FAILED(hr))
	{
		_com_issue_error(hr);
	}
}

XLWordBreaker::~XLWordBreaker()
{
}


bool XLWordBreaker::BreakText(const std::string & inString , XLStringList * outStringList)
{
	//形態素解析中に何度かコールバックされる関数を定義するためのインナークラス
	class pfnFillTextBufferTmp
	{
	public:
		//何か知らんけど、これを返すようにするらしい。
		static long __stdcall pfnFillTextBuffer(TEXT_SOURCE * pTextSource)
		{
			// return WBREAK_E_END_OF_TEXT
			return 0x80041780;
		}
	};
	TEXT_SOURCE textsource;
	//形態素解析中に何度かコールバックされる関数!
	textsource.pfnFillTextBuffer =  pfnFillTextBufferTmp::pfnFillTextBuffer;
	//変換する文字列 もちろん WCHAR* で
	textsource.awcBuffer = _bstr_t(inString.c_str());
	//形態素解析を開始する地点(WCHARなので1 だと2バイト進む)
    textsource.iCur = 0;
	//形態素解析を終了する地点(WCHARなので1 だと2バイト進む)
    textsource.iEnd = lstrlenW(textsource.awcBuffer);	//WなワイドAPIでやるよ


	//何かコールバックされるらしい。だからオーバーライドする。
	//何でコールバックにしたのか。IRegExpみたいに結果を配列に入れて返せばいいのに。
	//実装者はアホだなw
	class pfnWordSink : public XLNeetIUnknown<IWordSink>
	{
	public:
		//形態素解析されるたびに飛んでいます。
		void pushWord(ULONG cwc, WCHAR const *pwcInBuf, ULONG cwcSrcLen, ULONG cwcSrcPos)
		{
			USES_CONVERSION;
			const char * p ;

			//何かまれに変換漏れが起きるような。。。
			if (this->lastPos != cwcSrcPos)
			{
				//変換漏れを起こしたワードがあったらしい。
				//変換漏れが発生した場合、 。と、は区切られないので強引に区切る。
				std::wstring wstrFull = std::wstring(poolawcBuffer , this->lastPos , cwcSrcPos - this->lastPos);
				std::wstring wstrFullMinusOne = std::wstring(poolawcBuffer , this->lastPos , cwcSrcPos - this->lastPos - 1);
				std::wstring wstrLast = wstrFull.substr(wstrFullMinusOne.length() , 1);
				if ( wstrFullMinusOne.length() >= 2 && (wstrLast[0] == 0x3001 || wstrLast[0] == 0x3002) )
				{
					//。と、は区切る
					p = W2A(wstrFullMinusOne.c_str());
					poolStringList->push_back( p );

					p = W2A(wstrLast.c_str());
					poolStringList->push_back( p );
				}
				else
				{
					p = W2A(wstrFull.c_str());
					poolStringList->push_back( p );
				}
				this->lastPos = cwcSrcPos;
			}

			std::wstring wstrFull = std::wstring(poolawcBuffer , cwcSrcPos , cwcSrcLen);
			p = W2A(wstrFull.c_str());
			poolStringList->push_back( p );

			//ここまで変換した。
			this->lastPos = cwcSrcPos + cwcSrcLen;
		}

		virtual HRESULT STDMETHODCALLTYPE PutWord(ULONG cwc, WCHAR const *pwcInBuf, ULONG cwcSrcLen, ULONG cwcSrcPos)
		{
			this->pushWord(cwc,pwcInBuf,cwcSrcLen,cwcSrcPos);
			return 0;
		}
		virtual HRESULT STDMETHODCALLTYPE PutAltWord(ULONG cwc, WCHAR const *pwcInBuf, ULONG cwcSrcLen, ULONG cwcSrcPos)
		{
			//これの意味がよく分からない。
			return 0;
		}
		virtual HRESULT STDMETHODCALLTYPE StartAltPhrase( void) 
		{
			return 0;
		}

		virtual HRESULT STDMETHODCALLTYPE EndAltPhrase( void)
		{
			return 0;
		}

		virtual HRESULT STDMETHODCALLTYPE PutBreak( WORDREP_BREAK_TYPE )
		{
		   return 0;
		}

		//インナーだし全部publicで。

		//たまに形態素解析の結果がやってこないワードがあるみたいなので補正する.
		//変換する文字列
		WCHAR const *  poolawcBuffer;
		//戦闘から何個目(バイトにあらず)まで変換したか。
		ULONG          lastPos;

		//これに形態素解析の結果を代入する.
		XLStringList * poolStringList;
	};


	//IWordSinkを継承して作った独自クラスを実体化、、つーかスタック上に確保。
	pfnWordSink		wordSink;
	//これに結果を入れてもらう.
	wordSink.poolStringList = outStringList;
	wordSink.poolawcBuffer = textsource.awcBuffer;
	wordSink.lastPos = 0;

	//形態素解析開始
	HRESULT hr;
	hr = WordBreaker->BreakText(&textsource,&wordSink,NULL);
	if (FAILED(hr))
	{
		_com_issue_error(hr);
	}
	//よく最後が忘れ去られるのでつけてあげる。
	if (wordSink.lastPos < textsource.iEnd)
	{
		USES_CONVERSION;
		std::wstring wstrFull = std::wstring(wordSink.poolawcBuffer , wordSink.lastPos , textsource.iEnd - wordSink.lastPos);
		const char * p = W2A(wstrFull.c_str());
		outStringList->push_back( p );
	}

	return true;
}

void XLWordBreaker::test()
{
	{
		//パーステスト
		XLWordBreaker wb;
		XLStringList slist;
		wb.BreakText("科学の力ではどうしようもできない、魑魅魍魎などの奇怪な輩に立ち向かう胡散臭い男。", &slist);
		XLStringList::iterator it = slist.begin();
		std::string r;

		ASSERT(slist.size() == 13);
		ASSERT((r = *it) == "科学の");	it++;
		ASSERT((r = *it) == "力では");	it++;
		ASSERT((r = *it) == "どう");	it++;
		ASSERT((r = *it) == "しようもできない");	it++;
		ASSERT((r = *it) == "、");	it++;
		ASSERT((r = *it) == "魑魅");	it++;
		ASSERT((r = *it) == "魍魎などの");	it++;
		ASSERT((r = *it) == "奇怪な");	it++;
		ASSERT((r = *it) == "輩に");	it++;
		ASSERT((r = *it) == "立ち向かう");	it++;
		ASSERT((r = *it) == "胡散臭い");	it++;
		ASSERT((r = *it) == "男");	it++;
		ASSERT((r = *it) == "。");	it++;
	}
}
   _
   \ヽ, ,、
     `''|/ノ
      .|
 _    |
 \`ヽ、|
   \, V
      `L,,_
      |ヽ、)
     .|
    /           ,、
    /        ヽYノ
   .|       r''ヽ、.|
   |        `ー-ヽ|ヮ
    |            `|
   ヽ,    ,r      .|
     ヽ,r'''ヽ!'-‐'''''ヽ、ノ
 ,,,..---r'",r, , 、`ヽ、 ヾ
 ヽ、__/ ./ハレハ i`ヽ、 `''r`ミ_
   .レ//r,,,、 レ'レハヾ,  L,,_ `ヽ、
    "レ, l;;;l   l;;;l`i.リレ' リ ̄~~
     ヽ、 ワ `"/-'`'`'    か
       `''''''''"        ∨
                ┼ヽ  -|r‐、. レ |
                 d⌒) ./| _ノ  __ノ