"Unsuccessful: create table"エラーの解決

PlayFrameworkを用いて開発していたところ,db=mem環境では動いていたのに,MySQLに移行したところ,動かなくなってしまった.

調べてみると,Entityクラスを元にMySQLのテーブル自動生成時にエラーがでており,一部のテーブルが生成されていなかった.

元となったEntityクラス

package models;

import javax.persistence.Entity;
import javax.persistence.Lob;
import play.db.jpa.Model;

/**
 * @author Syo.Takasaki
 */
@Entity
public class TestCondition extends Model {
    
    @Lob
    public String condition;
    
    public Integer priority;
    
    @Lob
    public String comment;
}

出力されたエラー

20:44:43,596 ERROR ~ Unsuccessful: create table TestCondition (id bigint not null auto_increment, comment longtext, condition longtext, priority integer, primary key (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8
20:44:43,597 ERROR ~ You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'condition longtext, priority integer, primary key (id)) ENGINE=InnoDB DEFAULT CH' at line 1

なお,MySQLのバージョンは5.1である.
出力されたcreate tableを直接叩いてみたが,同様のエラー.

解決(というか原因)

もしかして予約語使っていないか? と気づき,MySQLの仕様を調べ……る前にカラム名を変えてみた.
結果,conditionを使わなければ見事(?)にテーブルが生成された.

結論:conditionは使ってはダメです.

http://dev.mysql.com/doc/refman/5.1/ja/reserved-words.html予約語一覧にもしっかり書いてありました.

PlayFrameworkでOutOfMemory

 そんなにメモリ食うコードは直せよ,というのはおいておいて.
 環境にも寄ると思うが,通常,256MB以上のメモリは確保しないため,OutOfMemoryエラーが発生してしまう.
 ので,より多くのメモリを確保できるようにオプションを設定する.

 application.confに以下の行を追記する.

jvm.memory=-Xmx512M

結局ファイル書き出しでmecab&cabocha

 パフォーマンス低下が著しいし,TOKENの設定が難しいので,入力データも出力データも一旦ファイルシステムを使うことに.
 将来的にはtmpfsを使えばいいかな.

 オプション指定があるので,前回とは違い,mecabおよびcabocha専用.

CabochaTest.java

import java.io.*;
import org.jpn.syo.*;
/**
 *
 * @author SyoTakasaki
 */
public class CabochaTest {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) throws Exception {
        // INIT
        String file_text = args[0];
        String data_text = null;
        String data_mecab = null;
        String data_cabocha = null;

        // テキストファイルの読み込み
        data_text = read_file_as_text(file_text);
        data_text = data_text.replaceAll("。", "。\n");
        data_text = data_text.replaceAll(".", ".\n");

        // Mecab
        data_mecab = Execute.exec("mecab", data_text);

        // Cabocha
        data_cabocha = Execute.exec("cabocha", data_mecab);

        System.out.println(data_mecab);
    }

    private static String read_file_as_text(String file_path) throws FileNotFoundException, UnsupportedEncodingException, IOException{
        StringBuilder sb = new StringBuilder();
        BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file_path), "UTF-8"));
        String line;
        while((line = br.readLine()) != null){
            sb.append(line).append("\n");
        }
        br.close();
        return sb.toString();
    }

}

org/jpn/syo/Execute.java

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.jpn.syo;

import java.io.*;
/**
 *
 * @author SyoTakasaki
 */
public class Execute {

    private static final java.util.Random random = new java.util.Random();

    // cmdには"mecab"あるいは"cabocha"を受け付ける
    public static String exec(String cmd, String inputText) throws IOException, InterruptedException, Exception {
        String result = null;
        String file_name = null;
        String command = null;

        // ファイル名用ランダム文字列
        file_name = Long.toHexString(random.nextLong());
        file_name = new File(file_name).getAbsolutePath();

        // テキストファイルへの書き出し
        write_file_as_text(file_name + ".in", inputText);

        // Mecabの場合
        if(cmd.equals("mecab")){
            command = "/usr/bin/mecab " + file_name + ".in --output=" + file_name + ".out";
        }
        // Cabochaの場合
        if(cmd.equals("cabocha")){
            command = "/usr/bin/cabocha -f1 -I1 " + file_name + ".in --output=" + file_name + ".out";
        }
        if(command == null){
            Exception ex = new Exception("UnsupportedCommandException");
            throw ex;
        }

        // 実行
        Process p = new ProcessBuilder(command.split(" ")).start();
        p.waitFor();
        p.destroy();

        // 結果の読み込み
        result = read_file_as_text(file_name + ".out");

        // ファイル削除
        new File(file_name + ".in").delete();
        new File(file_name + ".out").delete();

        return result;
    }

    private static String read_file_as_text(String file_path) throws FileNotFoundException, UnsupportedEncodingException, IOException{
        StringBuilder sb = new StringBuilder();
        BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file_path), "UTF-8"));
        String line;
        while((line = br.readLine()) != null){
            sb.append(line).append("\n");
        }
        br.close();
        return sb.toString();
    }

    private static void write_file_as_text(String file_path, String text) throws UnsupportedEncodingException, IOException {
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file_path), "UTF-8"));
        bw.write(text);
        bw.flush();
        bw.close();
    }
}

Mecab | Cabocha -I1 -f1 from Java

 先日,ProcessBuilderを使ったMecabの呼び出しを書いたが,テキストの規模が大きくなるとフリーズする(プロセスがどっかへ逝く)ことが判明した.
 Mecabより先にCabochaの方が限界を迎える.


 原因は,JavaのBufferedStream(Reader|Writer)のバッファサイズの限界.バッファサイズを大きくしても解決出来なかった.
 STDOUTとSTDERRをスレッドで読み込んで……STDINもスレッドで書き出して……とやってみたが,STDINが途中で止まってしまう.(1文STDINに出力する度にSTDOUTに解析結果が出力されることを期待したのだが)
 ので,適当なデータサイズで区切って呼び出すようにしてみた.ついでに外部コマンド呼び出しをパッケージ化.


 3万行のサンプルデータでの実行を確認.仮想マシン上で45秒.catとパイプを使って実行すると20秒なので,50%以上がオーバーヘッドになってしまった.


 TOKEN_SIZEを150にしたところ,手持ちのデータではダメだったので,安全策を取って100に設定.
 これは,テキストデータの100行分を1セットにしてMecabやCabochaを呼び出すことを意味する.
 本当は,MecabとCabochaの限界は違うから別の値を設定するべきだし,バイト数で考えるべきだが,今日はここまで.

CabochaTest.java

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import org.jpn.syo.*;
/**
 *
 * @author SyoTakasaki
 */
public class CabochaTest {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) throws Exception {
        // INIT
        String file_text = args[0];
        String data_text = null;
        String data_mecab = null;
        String data_cabocha = null;

        // テキストファイルの読み込み
        data_text = read_file_as_text(file_text);
        data_text = data_text.replaceAll("。", "。\n");
        data_text = data_text.replaceAll(".", ".\n");

        // Mecab
        data_mecab = Execute.exec("/usr/bin/mecab", data_text);

        // Cabocha
        data_cabocha = Execute.exec("/usr/bin/cabocha -f1 -I1", data_mecab);

        System.out.println(data_cabocha);

    }

    private static String read_file_as_text(String file_path) throws FileNotFoundException, UnsupportedEncodingException, IOException{
        StringBuilder sb = new StringBuilder();
        BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file_path), "UTF-8"));
        String line;
        while((line = br.readLine()) != null){
            sb.append(line).append("\n");
        }
        br.close();
        return sb.toString();
    }
}

org/jpn/syo/Execute.java

package org.jpn.syo;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 *
 * @author SyoTakasaki
 */
public class Execute {

    private static final int TOKEN_SIZE = 100;

    public static String exec(String cmd, String inputText) throws IOException, InterruptedException {
        StringBuilder stdout = new StringBuilder();
        StringBuilder stderr = new StringBuilder();

        // command
        List<String> command = new ArrayList<String>();
        command.addAll(Arrays.asList(cmd.split(" ")));

        // input text
        ArrayList<String> data = new ArrayList<String>();
        data.addAll(Arrays.asList(inputText.split("\n")));

        // mecab or cabochaの場合,適度なサイズに区切る
        ArrayList<String> data2 = null;
        if (command.get(0).endsWith("mecab") || command.get(0).endsWith("cabocha")) {
            data2 = new ArrayList<String>();
            boolean isCabocha = false;
            boolean isMecab = false;
            
            if (command.get(0).endsWith("cabocha")) {
                isCabocha = true;
            }
            if (command.get(0).endsWith("mecab")) {
                isMecab = true;
            }

            int token_size = 0;
            StringBuilder tmp = new StringBuilder();
            for (String line : data) {
                if ((isCabocha && line.equals("EOS")) || isMecab) {
                    tmp.append(line).append("\n");
                    if (token_size > TOKEN_SIZE) {
                        data2.add(tmp.toString());
                        tmp = new StringBuilder();
                        token_size = 0;
                    } else {
                        token_size++;
                    }
                } else {
                    tmp.append(line).append("\n");
                }
            }
            data2.add(tmp.toString());
        } else {
            data2 = data;
        }

        for (String d2 : data2){
           
            Process p = new ProcessBuilder(command).start();

            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(p.getOutputStream()));
            bw.write(d2);
            bw.flush();
            bw.close();

            BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
            String line;
            while ((line = br.readLine()) != null) {
                stdout.append(line).append("\n");
            }

            p.waitFor();
            p.destroy();
        }
        return stdout.toString();
    }
}

実行

$ java CabochaTest sample.txt

JavaでXMLを簡単に解析する方法

JAXB(Java Architecture for XML Binding)を使う方法を,Yahoo校正支援APIを題材に解説します.

どこら辺が「簡単」なのか?

各種API系のサービスで配布されている,XMLスキーマファイル(xsd)から,Javaクラスファイル群を自動生成することで,
自前のコードはほんの一握り(テンプレートにできそうなぐらい,一部のみです)になり,かつ自前でParseすることを考えたら,手間も品質も雲泥の差です.

正規表現で無理やり,とか怪しすぎるし,SAXやDOMを使えば意のままにコーディングできるけど拡張大変だし.
というわけで,JAXBに丸投げします.

前提環境

0. JAXB用のクラスファイルの自動生成

まず,http://jlp.yahooapis.jp/KouseiService/V1/kousei.xsd をダウンロードします.次に,Terminalで,以下のコマンドを入力

$ xjc kousei.xsd

すると,以下のディレクトリ,ファイルが生成されます.

./yahoo
`-- jp
    `-- jlp
        `-- kouseiservice
            |-- ObjectFactory.java
            |-- ResultSet.java
            |-- WordType.java
            `-- package-info.java

私は,この後に,kouseiserviceと同じところにkousei.xsdを移動しました.

1. サクッとテストプログラム作成

Kousei.javaを作成

import java.net.URL;
import java.net.URLEncoder;
import javax.xml.XMLConstants;
import javax.xml.bind.*;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import org.xml.sax.SAXException;
import yahoo.jp.jlp.kouseiservice.*;
/**
 *
 * @author Syo Takasaki
 */
public class Kousei {
    private static String appid  = "dummy";
    private static String appuri = "http://jlp.yahooapis.jp/KouseiService/V1/kousei";
    private static String path_xsd    = "yahoo/jp/jlp/kouseiservice/kousei.xsd";
    private static URL schemaURL;
    private static Schema schema;
    private static String path_context = "yahoo.jp.jlp.kouseiservice";
    private static JAXBContext context;
    private static Unmarshaller unmarshaller;

    public static void main(String[] args) throws Exception{
        Kousei exe = new Kousei();
        ResultSet results = exe.getResults("遙か彼方に小形飛行機が見える.");
        System.out.println("SENTENCE:遙か彼方に小形飛行機が見える.");
        for(WordType data : results.getResult()){
            System.out.print(data.getStartPos() + "\t");
            System.out.print(data.getLength() + "\t");
            System.out.print(data.getSurface() + "\t");
            System.out.print(data.getShitekiInfo() + "\t");
            System.out.println(data.getShitekiWord());
        }
        results = exe.getResults("モルジブ大学行く.大学行く.");
        System.out.println("SENTENCE:モルジブ大学行く.大学行く.");
        for(WordType data : results.getResult()){
            System.out.print(data.getStartPos() + "\t");
            System.out.print(data.getLength() + "\t");
            System.out.print(data.getSurface() + "\t");
            System.out.print(data.getShitekiInfo() + "\t");
            System.out.println(data.getShitekiWord());
        }
    }

    public Kousei() throws SAXException, JAXBException{
        SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        schemaURL = this.getClass().getClassLoader().getResource(path_xsd);
        schema = factory.newSchema(schemaURL);
        context = JAXBContext.newInstance(path_context);
        unmarshaller = context.createUnmarshaller();
        unmarshaller.setSchema(schema);
    }

    public ResultSet getResults(String sentence){
        ResultSet results = null;
        try{
            URL _url = new URL(appuri + "?appid=" + appid + "&sentence=" + URLEncoder.encode(sentence, "UTF-8"));
            results = (ResultSet)unmarshaller.unmarshal(_url);
        }catch (Exception e){
            System.err.println("ERROR:" + e);
        }
        return results;
    }
}

2. 実行

$ javac Kousei.java
$ java Kousei
SENTENCE:遙か彼方に小形飛行機が見える.
0        2        遙か        表外漢字あり        ●か        
2        2        彼方        用字        彼方(かなた)、かなた        
5        5        小形飛行機        誤変換        小型飛行機        
SENTENCE:モルジブ大学行く.大学行く.
0        4        モルジブ        外国地名        モルディブ        
9        4        大学行く        助詞不足の可能性あり   

後書きと懺悔

きっかけは,Play framework上でGroovyがうまく使えなかったから(テンプレートエンジンはGroovyだけど,それ以外のところで使いたかった).

「GroovyならサクッとXMLパースもできるんだけど……Pure Javaか」……と意気消沈したところで,調べてみたわけ.

で,エイヤと作ったはいいものの,エラーハンドリングは適当過ぎるし,いくつかのサイトを参考にしたからコーディングスタイルもごちゃごちゃだし.
そもそも引数でテスト文字列与えられないってどういうことよ,自分orz

でも動いたから満足してしまった.path_xsdとpath_contextが良く分からず,いろんな指定の組み合わせをがんばって,一番良さげな書き方に落ち着いたところが一番満足.

くれぐれも「このまま」は使わないように.

なお,生成は以下のURLの1番目を参考にしてください.
marshallerを追加するだけでいけると思います.

Play frameworkでmecab apiを自炊する

Play frameworkのインストールは,zipを解凍するだけなので省略.
環境はUbuntu Desktop 10.04.MecabUTF-8阪.
Mecabのインストールは,http://d.hatena.ne.jp/Syo-Takasaki/20090528/1243484754 を参照してください.

apiアプリの生成

$ ./play new api

api/app/controllers/Mecab.javaの作成

$ which mecab
/usr/bin/mecab
package controllers;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import play.mvc.*;

public class Mecab extends Controller {

    public static void mecab(String sentence) throws IOException {
        StringBuilder sb = new StringBuilder();

        Process process = Runtime.getRuntime().exec("/usr/bin/mecab");
        OutputStream os = process.getOutputStream();
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
        bw.write(sentence + "\n");
        bw.flush();
        bw.close();

        InputStream is = process.getInputStream();
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        String line;
        while((line = br.readLine()) != null){
            sb.append(line).append("\n");
        }

        process.destroy();

        String mecab_result = sb.toString();
        render(mecab_result);
    }

}

app/views/Mecab/mecab.htmlの作成

${mecab_result}

実行!

$ ./play run api

ブラウザから,以下にアクセスしてみる.

http://127.0.0.1:9000/Mecab/mecab?sentence=形態素解析してくれぃ.

Citrix XenServer上でプライベートネットワーク構築

背景(読まなくてOK)

 i7 12GBメモリのマシンにCitrix XenServerを入れ,「これで仮想マシン使い放題!」と思ったのもつかの間.
 そのまま仮想マシンを作っても,ブリッジ接続にしかならず,IPが枯渇してしまう.IPv4の枯渇も問題だけど,うちのネットワークは既に*.254がDHCPから割り振られてしまう状態.絶賛枯渇中です.

 で,XenCenterでNetworkタブを開いてみたら,「Add Network...」のボタンを発見.これはプライベートネットワークを構築すれば,これ以上IP消費せずにいけるんじゃなかろうか? と思い立ったわけです.

Agenda

  1. プライベートネットワークの作成
  2. NATルータとなる仮想マシンの作成
  3. NATルータ上にDHCPサーバ構築
  4. NATルータの設定
  5. プライベートネットワーク上に仮想マシンを作成

 の5本立てです.

環境
  • 仮想化環境:Citrix XenServer 5.6.0
  • 仮想マシンのOS:Ubuntu Server 10.04.1
  • GlobalNetwork:192.168.1.0/24
  • PrivateNetwork:192.168.2.0/24
  • NATルータのネットワーク:Global -> eth0, Private -> eth1
  • プライベートネットワーク上のマシン:Private -> eth0

プライベートネットワークの作成

 XenServer上では,「Internal Network」.

  1. XenCenterを立ち上げる
  2. 左側のツリーから,物理サーバを選ぶ
  3. 右側上部のタブから「Network」を選ぶ
  4. 「Add Netowrk...」をクリック
  5. Internal Networkを作成する

 この時点ではIPレンジ等はまったく関係ない.
 XenServer内部に,新たなハブが出来上がったと考えると分かりやすいかも.

NATルータとなる仮想マシンの作成

 既に仮想マシンがある場合は,シャットダウンしてNetowrkタブを開いて,「Add Interface...」をクリックし,先程作ったInternal Networkを追加.
 新規に仮想マシンを作成する場合は,Networkの設定画面にて,「Network 0」はそのままに,新たに作成したInternal Networkも追加.
 MAC Addressは自動生成のままで.

 あとは普通に(OpenSSHは入れましたが)インストール.

NATルータ上にDHCPサーバ構築

ネットワーク設定

 /etc/network/interfacesを編集.編集後,"/etc/init.d/networking restart".

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto eth0
iface eth0 inet dhcp
# ここはstaticに設定して良いが,今回は重要でないのでDHCPのまま.

auto eth1
iface eth1 inet static
        address 192.168.2.1
        netmask 255.255.255.0
        gateway 192.168.1.1
DHCPサーバインストール
$ sudo apt-get install dhcp3-server

 この時点ではDHCPサーバの起動にエラーが出ますが,気にしない.

DHCPサーバ設定

 /etc/dhcp3/dhcpd.confに以下を追記.

subnet 192.168.2.0 netmask 255.255.255.0 {
  range 192.168.2.100 192.168.2.200;
  option routers 192.168.2.1;
}

NATルータの設定

 ufwを使う.
 https://help.ubuntu.com/10.04/serverguide/C/firewall.html#ip-masquerade-ufw
 を参考に(というかそのまま).

/etc/default/ufw を編集

 DROPをACCEPTに変更.

DEFAULT_FORWARD_POLICY="ACCEPT"
/etc/ufw/sysctl.conf を編集

 コメントアウトされているので,コメントイン.

net/ipv4/ip_forward=1
/etc/ufw/before.rules に追記

 追記というよりも,前追? 既にいろいろと書いてあるのだが,先頭の11文ぐらいのコメントの直後に以下を書く.COMMITまで忘れずに.

# nat Table rules
*nat
:POSTROUTING ACCEPT [0:0]

# Forward traffic from eth1 through eth0.
-A POSTROUTING -s 192.168.2.0/24 -o eth0 -j MASQUERADE

# don't delete the 'COMMIT' line or these nat table rules won't be processed
COMMIT
ufwを有効化

 sshを使っている人はその許可も忘れずに.

$ sudo ufw allow ssh
$ sudo ufw enable

プライベートネットワーク上に仮想マシンを作成

 仮想マシン作成ウィザード中の設定画面では,デフォルトである「Network 0」を削除し,Internal Networkを追加.
 DHCPでIPも取得するし(192.168.2.100),ネットにも繋がる.

 あとは,192.168.2.2〜99までの非DHCP IPを割り当て,NATルータマシン経由でSSHするなり,Webアプリなら,ApacheでReverseProxyを使うなりすれば,XenServerの外からでもアクセス出来る.