Igo: JavaScriptで形態素解析

Igo: GoogleAppEngineで形態素解析サーバで用意したサーバ(※追加修正あり。後述)を使って形態素解析を行うJavaScriptを書いてみた。

制限

結構制限が多い。

  • 対応がUTF-8のみ
    • レスポンスのJSONに含まれる文字列内のASCII以外の文字を16進数表記(\uXXXX)にエスケープすればEUC-JPやShift_JISでも大丈夫だった(2010/10/20)
  • JSONPを使ってサーバと通信しているため、一回のリクエストテキストの最大長が制限される
    • 具体的に何文字まで可能かは使用ブラウザとGoogleAppEngineの制限に左右される(数百文字なら大丈夫?)
  • (詳しくは知らないけど)解析サーバがGoogleAppEngineの使用制限を越えたら当然使えなくなる

形態素解析JavaScript

形態素解析を行うJavaScript関数群。
やっていることは、ほとんどigo-morp.appspot.comに対してリクエストを投げているだけ。
http://igo-morp.appspot.com/igo.jsに配置。

// ファイル: http://igo-morp.appspot.com/igo.js

function igo_common(method, text, callback_name) {
    var enc_text = encodeURIComponent(text);
    var query = "text="+enc_text+"&callback="+callback_name;
    var url = "http://igo-morp.appspot.com/"+method+"_jsonp?"+query;
    
    var elem = document.createElement('div'); 
    elem.innerHTML = "<script type='text/javascript' src='"+url+"'><\/script>";
    document.getElementById("igo_jsonp_block").appendChild(elem);
}

// 形態素解析を行う。
// - text: 対象テキスト 
// - callback_name: コールバック関数の名前
//
// コールバック関数は形態素解析終了後に呼び出される一引数関数
// - callback(parse_result)
// -- parse_result: [[形態素名,素性文字列]]
function igo_parse(text, callback_name) {
    igo_common("parse", text, callback_name);
}

// 分かち書きを行う。
// - text: 対象テキスト 
// - callback_name: コールバック関数の名前
//
// コールバック関数は分かち書き終了後に呼び出される一引数関数
// - callback(wakati_result)
// -- parse_result: [形態素名]
function igo_wakati(text, callback_name) {
    igo_common("wakati", text, callback_name);
}

document.write("<div id='igo_jsonp_block'></div>");

サンプル

形態素解析を行うサンプルHTML。
http://igo-morp.appspot.com/js-morp-sample.html

<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <script type="text/javascript" src="http://igo-morp.appspot.com/igo.js"></script>

    <!-- callback function -->
    <script type="text/javascript" language="javascript">
      <!--
        function parse_callback(rlt) {
        var result =document.getElementById("result");
        var text="";
        for(var i=0; i < rlt.length; i++)
          text += rlt[i][0] + "\t" + rlt[i][1] + "\n";
        result.innerHTML = text;
      }

      function parse() {
        var text=document.getElementById("text").value;
        igo_parse(text, "parse_callback");
        return false;
      }
      -->
    </script>
    
    <title>形態素解析サンプル</title>
  </head>
  <body>
    <!-- input text -->
    <form onsubmit="return parse()">
      <textarea id="text" cols="30" rows="10"></textarea>
      <br />
      <input type="submit"  value="analyze" >
    </form>
    <br />
    
    <!-- result -->
    <pre id="result"></pre>
  </body>
</html>

形態素解析サーバ変更点

JSONPに対応するために以下の二つのAPI(?)を追加。

追加したファイル。
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>
  
  <!-- ここから下が追加分 -->
  <servlet>
    <servlet-name>wakati_jsonp</servlet-name>
    <servlet-class>WakatiJSONPServlet</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>wakati_jsonp</servlet-name>
    <url-pattern>/wakati_jsonp</url-pattern>
  </servlet-mapping>

  <servlet>
    <servlet-name>parse_jsonp</servlet-name>
    <servlet-class>ParseJSONPServlet</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>parse_jsonp</servlet-name>
    <url-pattern>/parse_jsonp</url-pattern>
  </servlet-mapping>
</web-app>

ParseJSONPServer.java:

import java.io.IOException;
import javax.servlet.http.*;
import net.reduls.igo.Morpheme;
import net.arnx.jsonic.JSON;
import java.util.ArrayList;

public class ParseJSONPServlet extends HttpServlet {
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        String text = req.getParameter("text");
        String callback = req.getParameter("callback");

        if(text==null)
            text = "";

        if(callback==null)
            callback="callback";
        
        ArrayList<ArrayList<String>> morps = new ArrayList<ArrayList<String>>();
        resp.setContentType("Content-type: text/plain; charset=UTF-8");
        for(Morpheme m : Igo.parse(text)) {
            ArrayList<String> morp = new ArrayList<String>(2);
            morp.add(m.surface);
            morp.add(m.feature);
            morps.add(morp);
        }
        resp.getWriter().println(callback+"("+JSON.encode(morps)+");");
    }
    
    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        doGet(req,resp);
    }
}

WakatiJSONPServer.java:

import java.io.IOException;
import javax.servlet.http.*;
import net.arnx.jsonic.JSON;

public class WakatiJSONPServlet extends HttpServlet {
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        String text = req.getParameter("text");
        String callback = req.getParameter("callback");
        if(text==null)
            text = "";

        if(callback==null)
            callback="callback";
        
        resp.setContentType("Content-type: text/plain; charset=UTF-8");
        resp.getWriter().println(callback+"("+JSON.encode(Igo.wakati(text))+");");
    }
    
    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        doGet(req,resp);
    }
}