Igo: GoogleAppEngineで形態素解析サーバ
IgoをGoogleAppEngine上で動かしてみた。
URL
URLと仕様。
- トップ: http://igo-morp.appspot.com/
- 形態素解析: http://igo-morp.appspot.com/parse
- 分かち書き: http://igo-morp.appspot.com/wakati
形態素解析と分かち書きの結果の書式はmecabコマンドのそれと同様。
$ curl -d text='すもももももももものうち' http://igo-morp.appspot.com/parse すもも 名詞,一般,*,*,*,*,すもも,スモモ,スモモ も 助詞,係助詞,*,*,*,*,も,モ,モ もも 名詞,一般,*,*,*,*,もも,モモ,モモ も 助詞,係助詞,*,*,*,*,も,モ,モ もも 名詞,一般,*,*,*,*,もも,モモ,モモ の 助詞,連体化,*,*,*,*,の,ノ,ノ うち 名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ EOS
動かすまでの手順などは後で再現可能なように後日まとめる。
GAEを一度試してみたかった*2だけで、特に何かに利用する予定はなし。
追記(2010/10/08):
少し修正。
もともとはGAEのアップロードサイズ制限に引っかかっていたファイル(辞書データ)を圧縮しておき、クラスロード時に解凍して読み込むようにしていた。
ただそれだとロードの度に凄く時間が掛かる(20〜30秒程度)ので、圧縮ではなく、単純にファイルを分割して保存/読み込みするように変更。
これでロードに掛かる時間は4〜5秒程度に短縮された模様。
追記2(2010/10/09): GAE上で動かすためのpatchとサーブレートのソース等
Igo(0.4.2)をGoogle App Engine上で動かすためには以下の修正が必要だった。
- ファイルサイズ制限対応
- 10MBを越えるファイルは分割して保存/読み込みを行うように修正*3
- MappedByteBufferを非使用に
- GAEでは使えないようなのでMappedByteBufferで行っていたファイル読み込み部分を、DataInputStreamで置換
- バイトオーダーをビッグエンディアンに統一
これらの修正を踏まえて、以下のpatchファイルを作成。
igo-0.4.2-gae.patch
diff -crN igo-0.4.2-src/src/net/reduls/igo/dictionary/WordDic.java igo-0.4.2-gae/src/net/reduls/igo/dictionary/WordDic.java *** igo-0.4.2-src/src/net/reduls/igo/dictionary/WordDic.java 2010-03-29 09:25:40.000000000 +0900 --- igo-0.4.2-gae/src/net/reduls/igo/dictionary/WordDic.java 2010-10-07 12:58:27.000000000 +0900 *************** *** 17,23 **** public WordDic(String dataDir) throws IOException { trie = new Searcher(dataDir+"/word2id"); ! data = FileMappedInputStream.getString(dataDir+"/word.dat"); indices = FileMappedInputStream.getIntArray(dataDir+"/word.ary.idx"); { --- 17,25 ---- public WordDic(String dataDir) throws IOException { trie = new Searcher(dataDir+"/word2id"); ! data = FileMappedInputStream.getString(dataDir+"/word.dat.1")+ ! FileMappedInputStream.getString(dataDir+"/word.dat.2")+ ! FileMappedInputStream.getString(dataDir+"/word.dat.3"); indices = FileMappedInputStream.getIntArray(dataDir+"/word.ary.idx"); { diff -crN igo-0.4.2-src/src/net/reduls/igo/dictionary/build/WordDic.java igo-0.4.2-gae/src/net/reduls/igo/dictionary/build/WordDic.java *** igo-0.4.2-src/src/net/reduls/igo/dictionary/build/WordDic.java 2010-03-29 18:35:02.000000000 +0900 --- igo-0.4.2-gae/src/net/reduls/igo/dictionary/build/WordDic.java 2010-10-07 13:18:09.000000000 +0900 *************** *** 131,143 **** } // 単語データを出力 ! final FileMappedOutputStream fmosDat = ! new FileMappedOutputStream(outputDir+"/word.dat", wdat.length()*2); ! try { ! fmosDat.putString(wdat.toString()); ! } finally { ! fmosDat.close(); ! } // 単語情報の配列へのインデックスを保存する { --- 131,147 ---- } // 単語データを出力 ! final int[] wdatLens = {wdat.length()/3, wdat.length()/3, wdat.length()/3+wdat.length()%3}; ! final int[] wdatBegs = {0, wdat.length()/3, wdat.length()/3*2}; ! for(int i=0; i < 3; i++) { ! final FileMappedOutputStream fmosDat = ! new FileMappedOutputStream(outputDir+"/word.dat."+(i+1), wdatLens[i]*2); ! try { ! fmosDat.putString(wdat.substring(wdatBegs[i],wdatBegs[i]+wdatLens[i]).toString()); ! } finally { ! fmosDat.close(); ! } ! } // 単語情報の配列へのインデックスを保存する { diff -crN igo-0.4.2-src/src/net/reduls/igo/util/FileMappedInputStream.java igo-0.4.2-gae/src/net/reduls/igo/util/FileMappedInputStream.java *** igo-0.4.2-src/src/net/reduls/igo/util/FileMappedInputStream.java 2010-03-29 18:35:02.000000000 +0900 --- igo-0.4.2-gae/src/net/reduls/igo/util/FileMappedInputStream.java 2010-10-09 18:57:47.662956449 +0900 *************** *** 6,35 **** import java.nio.ByteOrder; import java.nio.channels.FileChannel; /** * ファイルにマッピングされた入力ストリーム<br /> * net.reduls.igo以下のパッケージではファイルからバイナリデータを取得する場合、必ずこのクラスが使用される */ public final class FileMappedInputStream { ! private final FileChannel cnl; ! private int cur=0; ! /** * 入力ストリームを作成する * * @param filepath マッピングするファイルのパス */ public FileMappedInputStream(String filepath) throws IOException { ! cnl = new FileInputStream(filepath).getChannel(); } public int getInt() throws IOException { ! return map(4).getInt(); } public int[] getIntArray(int elementCount) throws IOException { final int[] ary = new int[elementCount]; ! map(elementCount*4).asIntBuffer().get(ary); return ary; } --- 6,50 ---- import java.nio.ByteOrder; import java.nio.channels.FileChannel; + import java.io.File; + import java.io.DataInputStream; + import java.io.BufferedInputStream; + import java.io.EOFException; + import java.util.zip.GZIPInputStream; + /** * ファイルにマッピングされた入力ストリーム<br /> * net.reduls.igo以下のパッケージではファイルからバイナリデータを取得する場合、必ずこのクラスが使用される */ public final class FileMappedInputStream { ! private final DataInputStream dis; ! private final int fileSize; /** * 入力ストリームを作成する * * @param filepath マッピングするファイルのパス */ public FileMappedInputStream(String filepath) throws IOException { ! dis = new DataInputStream(new BufferedInputStream(new FileInputStream(filepath))); ! fileSize = (int)new File(filepath).length(); } public int getInt() throws IOException { ! return dis.readInt(); ! } ! ! public short getShort() throws IOException { ! return dis.readShort(); ! } ! ! public char getChar() throws IOException { ! return dis.readChar(); } public int[] getIntArray(int elementCount) throws IOException { final int[] ary = new int[elementCount]; ! for(int i=0; i < elementCount; i++) ! ary[i] = getInt(); return ary; } *************** *** 44,84 **** public short[] getShortArray(int elementCount) throws IOException { final short[] ary = new short[elementCount]; ! map(elementCount*2).asShortBuffer().get(ary); return ary; } public char[] getCharArray(int elementCount) throws IOException { final char[] ary = new char[elementCount]; ! map(elementCount*2).asCharBuffer().get(ary); return ary; } public String getString(int elementCount) throws IOException { ! return map(elementCount*2).asCharBuffer().toString(); } public static String getString(String filepath) throws IOException { final FileMappedInputStream fmis = new FileMappedInputStream(filepath); try { ! return fmis.getString(fmis.size()/2); } finally { fmis.close(); } } public int size() throws IOException { ! return (int)cnl.size(); } public void close() { try { ! cnl.close(); } catch (IOException e) {} } ! ! private ByteBuffer map(int size) throws IOException { ! cur += size; ! return cnl.map(FileChannel.MapMode.READ_ONLY, cur-size, size).order(ByteOrder.nativeOrder()); ! } ! } \ ファイル末尾に改行がありません --- 59,104 ---- public short[] getShortArray(int elementCount) throws IOException { final short[] ary = new short[elementCount]; ! for(int i=0; i < elementCount; i++) ! ary[i] = getShort(); return ary; } public char[] getCharArray(int elementCount) throws IOException { final char[] ary = new char[elementCount]; ! for(int i=0; i < elementCount; i++) ! ary[i] = getChar(); return ary; } public String getString(int elementCount) throws IOException { ! StringBuilder sb = new StringBuilder(elementCount); ! for(int i=0; i < elementCount; i++) ! sb.append(dis.readChar()); ! return sb.toString(); } public static String getString(String filepath) throws IOException { final FileMappedInputStream fmis = new FileMappedInputStream(filepath); try { ! StringBuilder sb = new StringBuilder(fmis.size()); ! try { ! for(;;) ! sb.append(fmis.getChar()); ! } catch (EOFException e) {} ! return sb.toString(); } finally { fmis.close(); } } public int size() throws IOException { ! return fileSize; } public void close() { try { ! dis.close(); } catch (IOException e) {} } ! } diff -crN igo-0.4.2-src/src/net/reduls/igo/util/FileMappedOutputStream.java igo-0.4.2-gae/src/net/reduls/igo/util/FileMappedOutputStream.java *** igo-0.4.2-src/src/net/reduls/igo/util/FileMappedOutputStream.java 2010-03-29 18:35:02.000000000 +0900 --- igo-0.4.2-gae/src/net/reduls/igo/util/FileMappedOutputStream.java 2010-10-06 17:22:58.000000000 +0900 *************** *** 26,32 **** final FileChannel cnl = new RandomAccessFile(filepath,"rw").getChannel(); try { mbb = cnl.map(FileChannel.MapMode.READ_WRITE, 0, size); ! mbb.order(ByteOrder.nativeOrder()); } finally { cnl.close(); } --- 26,32 ---- final FileChannel cnl = new RandomAccessFile(filepath,"rw").getChannel(); try { mbb = cnl.map(FileChannel.MapMode.READ_WRITE, 0, size); ! mbb.order(ByteOrder.BIG_ENDIAN); } finally { cnl.close(); }
パッチを当てた後も使用方法は通常版と変わらない。
※ ただし、バイナリ辞書の互換性はなくなる
# パッチを当てる手順 $ tar zxvf igo-0.4.2-src.tar.gz $ patch -p0 < igo-0.4.2-gae.patch
// ファイル名: Igo.java // - クラスロード時にTaggerをロードしてstaticに持つ import java.util.List; import net.reduls.igo.Tagger; import net.reduls.igo.Morpheme; import javax.jdo.annotations.IdGeneratorStrategy; import javax.jdo.annotations.IdentityType; import javax.jdo.annotations.PersistenceCapable; import javax.jdo.annotations.Persistent; import javax.jdo.annotations.PrimaryKey; @PersistenceCapable(identityType = IdentityType.APPLICATION) public class Igo { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Long id; private static Tagger tagger; static { try { // 辞書ロード: 辞書のパスは固定 tagger = new Tagger("ipadic/"); } catch (Exception e) {} } public static List<String> wakati(String text) { return tagger.wakati(text); } public static List<Morpheme> parse(String text) { return tagger.parse(text); } }
// ファイル名: ParseServlet.java // - http://igo-morp.appspot.com/parseリクエスト時に使用されるクラス import java.io.IOException; import javax.servlet.http.*; import net.reduls.igo.Morpheme; public class ParseServlet extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { String text = req.getParameter("text"); if(text==null) text = ""; resp.setContentType("text/plain; charset=UTF-8"); for(Morpheme m : Igo.parse(text)) resp.getWriter().println(m.surface+"\t"+m.feature); resp.getWriter().println("EOS"); } public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { doGet(req,resp); } }
// ファイル名: ParseServlet.java // - http://igo-morp.appspot.com/wakatiリクエスト時に使用されるクラス import java.io.IOException; import javax.servlet.http.*; public class WakatiServlet extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { String text = req.getParameter("text"); if(text==null) text = ""; boolean first = true; resp.setContentType("text/plain; charset=UTF-8"); for(String s : Igo.wakati(text)) { if(!first) resp.getWriter().print(" "); first = false; resp.getWriter().print(s); } resp.getWriter().println(""); } public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { doGet(req,resp); } }
JSP。
<!-- ファイル名: igo.jsp --> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.util.List" %> <%@ page import="javax.jdo.PersistenceManager" %> <html> <head> <Meta Http-equiv="content-type" Content="text/html; charset=UTF-8"> <title>Igo: 形態素解析</title> </head> <body> <a href="http://sourceforge.jp/projects/igo/">Igo</a>を用いた形態素解析を行います。<br /> テキストを入力して「形態素解析」ボタンを押してください。<br /> <form method="POST" action="/parse"> <textarea name="text" cols="50" rows="10"></textarea> <br /> <input type="submit" value="形態素解析" /> </form> </body> </html>
web.xml。
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd"> <web-app xmlns="http://java.sun.com/xml/ns/javaee" version="2.5"> <servlet> <servlet-name>wakati</servlet-name> <servlet-class>WakatiServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>wakati</servlet-name> <url-pattern>/wakati</url-pattern> </servlet-mapping> <servlet> <servlet-name>parse</servlet-name> <servlet-class>ParseServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>parse</servlet-name> <url-pattern>/parse</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>igo.jsp</welcome-file> </welcome-file-list> </web-app>