|
|
all blog entries fetched with curl - KazMuzik Blog
2009-05-28 09:31
1年前には、Nutch (0.9) を利用して、このブログのエントリをすべて fetch して、以降は、同様に差分だけを fetch して、これらの HTML をコンバートして、kazmuzik.net/lj を作成していました。しかし、各エントリのページだけならよいのですが、Tag のページや最新の 20エントリを含むページなども、その都度、fetch してコンバートしていたので、かなり無駄な作業をしていました。効率化する構想はずっとあったのですが、つい延ばし延ばしになっていました。今回は、web hosting service を利用し始めたので、本格的にブログを移行するため、本格的に手を入れることにしました。
まずは、各エントリページの fetch の部分を curl を用いることにしました。以前は Nutch を使っていたものの、depth=1 で、seeds のページだけを fetch していたので、crawler としては活用していませんでしたし、Hadoop ファイルシステムにデータがあったので、扱いが面倒になっていました。今回は curl によるシンプルな fetch として、各エントリごとに、ひとつのファイルとします。
その前に、自分のエントリの ID をすべて得ておく必要があります。これには、月ごとのページを利用することにします。例えば、今月のページは、http://kazuomik.livejournal.com/2009/05/ ですが、これから今月のエントリの ID をすべて得ることができます。このブログは 2006年から始めたので、それ以降の月のページをすべて curl で fetch します。
$ curl http://kazuomik.livejournal.com/[2006-2009]/[01-12]/ -o "#1-#2.html"
$ ls
2006-01.html 2006-11.html 2007-09.html 2008-07.html 2009-05.html
2006-02.html 2006-12.html 2007-10.html 2008-08.html 2009-06.html
2006-03.html 2007-01.html 2007-11.html 2008-09.html 2009-07.html
2006-04.html 2007-02.html 2007-12.html 2008-10.html 2009-08.html
2006-05.html 2007-03.html 2008-01.html 2008-11.html 2009-09.html
2006-06.html 2007-04.html 2008-02.html 2008-12.html 2009-10.html
2006-07.html 2007-05.html 2008-03.html 2009-01.html 2009-11.html
2006-08.html 2007-06.html 2008-04.html 2009-02.html 2009-12.html
2006-09.html 2007-07.html 2008-05.html 2009-03.html
2006-10.html 2007-08.html 2008-06.html 2009-04.html
$ |
一部、不要な月もありますが、データはないので気にする必要はありません。
次に、これらの HTML のパターンを解析して、ID を抽出します。
$ cat *.html | grep 'm: <a href="http://kazuomik.livejournal.com/' \
| sed -e 's/^.*livejournal\.com\///' -e 's/\.html\">.*$//' \
| sort -k1nr \
> id.txt
$ cat id.txt
341891
341649
341249
341145
340856
340482
340465
340137
339949
339476
...
2603
2409
2080
1960
1598
1332
1120
1008
722
495
$ wc -l id.txt
1335 id.txt
$ |
1,335 のエントリがありました。
あとは、各エントリのページをひとつずつ fetch します。
$ cat curl-all.sh
#!/bin/sh
for N in `cat id.txt`
do
curl http://kazuomik.livejournal.com/${N}.html -o ${N}.html
done
$ sh curl-all.sh
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 55323 100 55323 0 0 124k 0 --:--:-- --:--:-- --:--:-- 630k
...
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 52824 100 52824 0 0 78390 0 --:--:-- --:--:-- --:--:-- 142k
$ |
25分程度で、1,335 すべてのエントリが fetch できました。Tags: programming
|
|
Google SiteSearch Box - KazMuzik Blog
2009-05-28 07:07
2009-05-28
長い間、このエントリをアップデートしてきましたが、ブログの移行に際して、今後はアップデートはアップデートしないことにしました。Tags: american_life, caltrain, computer_technology, español, français, game, health, immigration, internet, japanese, jobs_in_america, music, music_and_computer, music_gear, music_technology, programming, rebate, recycle_and_donation, second_life, tax, test, useful_link, walking
|
|
simple script for Twitter API (#5) - KazMuzik Blog
2009-05-12 05:50
Twitter REST API では、1回の API call で、200個までの updates しかとれないので、今までは、2回 call して、手作業で、ひとつのファイルにまとめていました。そこで、簡単な shell script を書きました。
#!/bin/sh
name="kazmuzik"
date=`date +"%Y%m%d"`
tmpfile="tmp-${date}.xml"
xmlfile="${name}-${date}.xml"
url="http://twitter.com/statuses/user_timeline.xml"
curl "${url}?screen_name=${name}&count=200&page=1" > $tmpfile
n=`cat $tmpfile | wc -l`
n=`expr $n - 1`
head -$n $tmpfile > $xmlfile
rm -f $tmpfile
curl "${url}?screen_name=${name}&count=200&page=2" | tail +3 >> $xmlfile
|
かなり手抜きしたので、400個を超えたら、また修正する必要があります。しかし、その頃には別の方法を試していると思うので、今のところこのままにしておきます。Tags: programming
|
|
net.kazmuzik.twitter package : XML to HTML (#4) - KazMuzik Blog
2009-05-10 09:22
Twitter でのつぶやき(update)を、REST API Timeline Method を用いて、ひとつの XML ファイルにしましたが、ここでは、それを HTML に変換して、このブログに取り込みやすいようにします。本来ならば、JAXP などの XML ライブラリを用いるべきですが、ある事情で、最近は、シンプルな String クラスのみを用いて、parser をがりがり書くことが多いので、手っ取り早く、そちらで強引にやってしまいました。また、一部、手抜きをしているところもあります。
まずは、ひとつの status に対応するオブジェクトですが、とりあえず create_at (time) と text が最低限必要です。また、今後の拡張を考慮して、それに id (long) を加えておきました。
package net.kazmuzik.twitter;
import java.io.Serializable;
import java.util.Date;
public class TwitterStatus implements Serializable {
private Date time;
private long id;
private String text;
public TwitterStatus() {
}
public Date getTime() {
return time;
}
public long getId() {
return id;
}
public String getText() {
return text;
}
public void setTime(Date time) {
this.time = time;
}
public void setId(long id) {
this.id = id;
}
public void setText(String text) {
this.text = text;
}
} |
次は、XML ファイルを読み込んで、上記のオブジェクトを返す Reader (parser) です。
package net.kazmuzik.twitter;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Reader;
import java.util.Date;
public class TwitterXMLReader extends BufferedReader {
private static final boolean debug = false;
private static final String tag0 = "<created_at>";
private static final String tag1 = "<id>";
private static final String tag2 = "<text>";
private static final String tag3 = "</status>";
private static final int tag0len = tag0.length();
private static final int tag1len = tag1.length();
private static final int tag2len = tag2.length();
public TwitterXMLReader(Reader in) throws IOException {
super(in);
}
public TwitterXMLReader(InputStream in) throws IOException {
super(new InputStreamReader(in, "UTF-8"));
}
public TwitterStatus readStatus() throws IOException {
TwitterStatus status = new TwitterStatus();
int tag = 0;
while (true) {
String line = readLine();
if (line == null) {
return null;
}
if (debug) {
System.err.printf("%d: %s%n", tag, line.trim());
}
if (tag == 0) {
int n = line.indexOf(tag0);
if (n < 0) {
continue;
}
n += tag0len;
int m = line.indexOf('<', n);
if (m < 0) {
return null;
}
Date time = parseTime(line.substring(n,m));
status.setTime(time);
if (debug) {
System.err.printf("#: time=%TF %TT%n", time, time);
}
tag = 1;
continue;
}
else if (tag == 1) {
int n = line.indexOf(tag1);
if (n < 0) {
continue;
}
n += tag1len;
int m = line.indexOf('<', n);
if (m < 0) {
return null;
}
long id = Long.parseLong(line.substring(n,m));
status.setId(id);
if (debug) {
System.err.printf("#: id=%d%n", id);
}
tag = 2;
continue;
}
else if (tag == 2) {
int n = line.indexOf(tag2);
if (n < 0) {
continue;
}
n += tag2len;
int m = line.indexOf('<', n);
if (m < 0) {
return null;
}
String text = convertEntities(line.substring(n,m));
status.setText(text);
if (debug) {
System.err.printf("#: text=%s%n", text);
}
tag = 3;
continue;
}
else if (tag == 3) {
int n = line.indexOf(tag3);
if (n < 0) {
continue;
}
return status;
}
}
}
private static String convertEntities(String text) {
StringBuilder sb = new StringBuilder();
int n = 0;
int x = 0;
for (char c : text.toCharArray()) {
if (n == 0) {
if (c == '&') {
n = 1;
}
else {
sb.append(c);
}
continue;
}
else if (n == 1) {
if (c == '#') {
n = 2;
x = 0;
}
else {
sb.append('&');
sb.append(c);
n = 0;
}
continue;
}
else if (n == 2) {
if (c == ';') {
sb.append((char)x);
n = 0;
}
else if (c < '0' || c > '9') {
sb.append(c);
n = 0;
}
else {
x *= 10;
x += (c - '0');
}
continue;
}
}
return sb.toString();
}
private static Date parseTime(String s) {
return new Date(s);
}
} |
あとは、これを用いて、適当な HTML に変換してやります。
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.Date;
import net.kazmuzik.twitter.TwitterStatus;
import net.kazmuzik.twitter.TwitterXMLReader;
public class TwitterXMLConverter {
public static void main(String[] args) throws Exception {
InputStream is = System.in;
if (args.length > 0) {
is = new FileInputStream(args[0]);
}
TwitterXMLReader in = new TwitterXMLReader(is);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out, "UTF-8"));
out.println("<html><head>");
out.println("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />");
out.println("</head><body>");
out.println("<ul>");
Date prevTime = null;
while (true) {
TwitterStatus status = in.readStatus();
if (status == null) {
break;
}
Date time = status.getTime();
String text = addATag(status.getText());
if (prevTime != null) {
if (time.getDay() != prevTime.getDay()) {
out.println("</ul>");
out.println("<ul>");
}
}
out.printf("<li><i><font color=#0000cc>%TF %TT</font> "
+ "<a href=\"http://twitter.com/kazmuzik\">"
+ "<font color=#00cc00>#%d</font></a></i><br>",
time, time, status.getId());
out.printf("%s<br> </li>%n", text);
prevTime = time;
}
out.println("</ul>");
out.println("</body></html>");
out.flush();
out.close();
}
private static String addATag(String text) {
StringBuilder sb = new StringBuilder();
int n = 0;
while (true) {
int m = text.indexOf("http://", n);
if (m < 0) {
sb.append(text.substring(n));
break;
}
sb.append(text.substring(n,m));
n = text.indexOf(' ', m);
String url = null;
if (n < 0) {
url = text.substring(m);
}
else {
url = text.substring(m, n);
}
sb.append("<a href=\"");
sb.append(url);
sb.append("\">");
sb.append(url);
sb.append("</a>");
if (n < 0) {
break;
}
}
return sb.toString();
}
} |
これを実行して、HTML ファイルを作ります。
$ mkdir classes
$ javac -d classes *.java
$ java -classpath classes TwitterXMLConverter kazmuzik.xml > kazmuzik.html
$ |
これを、一日分ずつ、cut & paste で、ブログのエントリに貼付けていきました。5/8/2009 のサンプルです。とりあえず、5月分だけ、作成しました。
せっかくなので、復習もかねて、JAXP でも使えば良かったかな、と反省しています。
2009-05-11 update Header に UTF-8 を指定しておきました。Tags: programming
|
|
Twitter REST API (#3) - KazMuzik Blog
2009-05-10 08:44
4月は、このブログを更新する時間もありませんでしたが、その分、なるべく Twitter でつぶいていました。そこで、Twitter API を用いて、なるべく簡単にこのブログに取り込んでみることを考えてみました。
Twitter API は、HTTP ベースで、Search API と REST API があります。今回は、REST API Timeline method の statuses/user_timeline を用います。HTTP ベースなので、curl コマンドで簡単にテストできます。
$ curl 'http://twitter.com/statuses/user_timeline.xml?screen_name=kazmuzik&count=200' > kazmuzik.xml
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 414k 100 414k 0 0 603k 0 --:--:-- --:--:-- --:--:-- 809k
$ cat kazmuzik.xml
<?xml version="1.0" encoding="UTF-8"?>
<statuses type="array">
<status>
<created_at>Sun May 10 15:38:12 +0000 2009</created_at>
<id>1755243926</id>
<text>140文字で表現できる内容です\
が、英語だと日本語の半分以\
下になってしまいます。ある\
程度、中身がある場合は、日\
本語が圧倒的に有利です。こ\
れも、Unicodeが普及してきたおか\
げです。</text>
<source>web</source>
<truncated>false</truncated>
<in_reply_to_status_id></in_reply_to_status_id>
<in_reply_to_user_id></in_reply_to_user_id>
<favorited>false</favorited>
<in_reply_to_screen_name></in_reply_to_screen_name>
<user>
<id>26517941</id>
<name>Kaz Muzik</name>
...
<statuses_count>204</statuses_count>
<notifications></notifications>
<following></following>
</user>
</status>
</statuses>
$ |
1回の API call で取得できる上限は 200個です。現在のところ、すでにその上限を超えているので、2ページ目も取得して、concatenate してから、つなぎの 3行をマニュアルで削除して、ひとつの XML ファイルにしました。
$ curl 'http://twitter.com/statuses/user_timeline.xml?screen_name=kazmuzik&count=200&page=2' >> kazmuzik.xml
$ vi kazmuzik.xml
$ |
これで、自分の今までのつぶやきがすべてひとつの XML ファイルに収まりました。ただし、上記の例のように、このままでは、扱いに不便なので、次は、これを加工してみます。Tags: internet, programming
|
|
|
|
Pro Tools 8 Plug-Ins List - KazMuzik Blog
2009-01-08 21:47
1/2/2009 には、Pro Tools LE 8 (など)の Plug-in のリストを載せましたが、各 .dpm ディレクトリの下には、Contents フォルダがあり、そこに Info.plist という XML ファイルがあります。そこで、このファイルを簡単に parse して、追加の情報も加えてみました。
本来ならば、DOM か SAX などの JAXP API を使えばいいのですが、通勤中の Caltrain の中でさっと開発したため、API ドキュメントにアクセスできず、強引に plain text として処理し、必要な項目だけを抜き出すことにしました。また、value も string のものだけをサポートしています。
$ cat PList.java
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
public class PList implements Serializable {
private Map<String,String> dict;
public PList() {
dict = new HashMap<String,String>();
}
public void put(String key, String value) {
dict.put(key, value);
}
public String get(String key) {
return dict.get(key);
}
public Map<String,String> getDict() {
return dict;
}
}
$ cat PListReader.java
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.PrintWriter;
public class PListReader extends BufferedReader {
public PListReader(Reader in) throws IOException {
super(in);
}
public PListReader(InputStream is) throws IOException {
this(new InputStreamReader(is, "UTF-8"));
}
public PListReader(String filename) throws IOException {
this(new FileInputStream(filename));
}
public String readKey() throws IOException {
while (true) {
String line = readLine();
if (line == null) {
return null;
}
int n = line.indexOf("<key>");
if (n < 0) {
continue;
}
int m = line.indexOf("</key>", n+5);
if (m < 0) {
continue;
}
return line.substring(n+5, m);
}
}
public String readStringValue() throws IOException {
String line = readLine();
if (line == null) {
return null;
}
int n = line.indexOf("<string>");
if (n < 0) {
return null;
}
int m = line.indexOf("</string>", n+8);
if (m < 0) {
return null;
}
return line.substring(n+8, m);
}
public String[] readKeyAndStringValue() throws IOException {
while (true) {
String key = readKey();
if (key == null) {
break;
}
String value = readStringValue();
if (value == null) {
continue;
}
return new String[] { key, value };
}
return null;
}
public PList readPList() throws IOException {
PList plist = new PList();
while (true) {
String[] pair = readKeyAndStringValue();
if (pair == null) {
break;
}
String key = pair[0];
String value = pair[1];
plist.put(key, value);
}
return plist;
}
public void printPList(PrintWriter out, String separator) throws IOException {
while (true) {
String[] pair = readKeyAndStringValue();
if (pair == null) {
break;
}
String key = pair[0];
String value = pair[1];
out.printf("%s%s%s%n", key, separator, value);
}
}
public static void main(String[] args) throws IOException {
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out, "UTF-8"));
if (args.length > 0) {
for (String filename : args) {
plist(out, filename);
}
}
else {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in, "UTF-8"));
while (true) {
String filename = in.readLine();
if (filename == null) {
break;
}
plist(out, filename);
}
}
out.flush();
out.close();
}
public static void plist(PrintWriter out, String filename) throws IOException {
out.printf("[ %s ]%n", filename);
PListReader in = new PListReader(filename);
in.printPList(out, " = ");
in.close();
out.println();
// out.flush();
}
}
$ cat DpmInfo.java
import java.io.File;
import java.io.IOException;
public class DpmInfo {
private static final String pluginFolderName
= "/Library/Application Support/Digidesign/Plug-ins";
private String folderName;
private String fileName;
private PList plist;
public DpmInfo(String dpmName) throws IOException {
folderName = dpmName;
if (! folderName.startsWith("/")) {
folderName = pluginFolderName + "/" + folderName;
}
if (! folderName.endsWith(".dpm")) {
folderName = folderName + ".dpm";
}
int n = folderName.lastIndexOf('/');
fileName = folderName.substring(n+1);
String plistFilename = folderName + "/Contents/Info.plist";
PListReader in = new PListReader(plistFilename);
plist = in.readPList();
in.close();
}
public String getFolderName() {
return folderName;
}
public String getFileName() {
return fileName;
}
public String getBundleName() {
return plist.get("CFBundleName");
}
public String getBundleVersion() {
return plist.get("CFBundleVersion");
}
public String getBundleInfo() {
String bundleInfo = plist.get("CFBundleGetInfoString");
if (bundleInfo == null) {
bundleInfo = "";
}
return bundleInfo;
}
}
$ cat DpmLister.java
import java.io.File;
import java.io.IOException;
public class DpmLister {
private static final int fontSize = 1;
private static final String pluginFolderName
= "/Library/Application Support/Digidesign/Plug-ins";
public static void main(String[] args) throws IOException {
File folder = new File(pluginFolderName);
System.out.println("<table border=1>");
System.out.print("<tr>");
System.out.print("<td align=center><i>File</i></td>");
System.out.print("<td align=center><i>Name</i></td>");
System.out.print("<td align=center><i>Version</i></td>");
System.out.print("<td align=center><i>Bundle Info</i></td>");
System.out.println("</tr>");
for (String dpmName : folder.list()) {
if (! dpmName.endsWith(".dpm")) {
continue;
}
DpmInfo dpmInfo = new DpmInfo(dpmName);
printDpmInfo(dpmInfo);
}
System.out.println("</table>");
System.out.flush();
}
public static void printDpmInfo(DpmInfo dpmInfo) throws IOException {
String fileName = dpmInfo.getFileName();
String bundleName = dpmInfo.getBundleName();
String bundleVersion = dpmInfo.getBundleVersion();
String bundleInfo = dpmInfo.getBundleInfo();
if (bundleInfo == null) {
bundleInfo = "";
}
print(new String[] { fileName, bundleName, bundleVersion, bundleInfo });
}
private static void print(String[] ss) {
System.out.print("<tr>");
for (String s : ss) {
if (s == null || s.length() == 0) {
s = "∓nbsp;";
}
System.out.printf("<td align=left valign=top><font size=%d>%s</font></td>", fontSize, s);
}
System.out.println("</tr>");
}
}
$ javac *.java
$ java DpmLister > dpmlist.html
$ |
| File | Name | Version | Bundle Info |
| AIRChorus.dpm | AIRChorus | 1.0.0.8542 | 1.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| AIRDistortion.dpm | AIRDistortion | 1.0.0.8542 | 1.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| AIRDynamicDelay.dpm | AIRDynamicDelay | 1.0.0.8542 | 1.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| AIREnhancer.dpm | AIREnhancer | 1.0.0.8542 | 1.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| AIREnsemble.dpm | AIREnsemble | 1.0.0.8542 | 1.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| AIRFilterGate.dpm | AIRFilterGate | 1.0.0.8542 | 1.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| AIRFlanger.dpm | AIRFlanger | 1.0.0.8542 | 1.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| AIRFrequencyShifter.dpm | AIRFrequencyShifter | 1.0.0.8542 | 1.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| AIRFuzz-Wah.dpm | AIRFuzz-Wah | 1.0.0.8542 | 1.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| AIRKillEQ.dpm | AIRKillEQ | 1.0.0.8542 | 1.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| AIRLo-Fi.dpm | AIRLo-Fi | 1.0.0.8542 | 1.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| AIRMulti-Chorus.dpm | AIRMulti-Chorus | 1.0.0.8542 | 1.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| AIRMulti-Delay.dpm | AIRMulti-Delay | 1.0.0.8542 | 1.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| AIRNon-linearReverb.dpm | AIRNon-LinearReverb | 1.0.0.8542 | 1.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| AIRPhaser.dpm | AIRPhaser | 1.0.0.8542 | 1.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| AIRReverb.dpm | AIRReverb | 1.0.0.8542 | 1.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| AIRSpringReverb.dpm | AIRSpringReverb | 1.0.0.8542 | 1.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| AIRStereoWidth.dpm | AIRStereoWidth | 1.0.0.8542 | 1.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| AIRTalkbox.dpm | AIRTalkbox | 1.0.0.8542 | 1.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| AIRVintageFilter.dpm | AIRVintageFilter | 1.0.0.8542 | 1.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| Analog_Channel_LE.dpm | Analog_Channel_LE | 3.2x18 | 3.2x18, Copyright 1998-2007 McDowell Signal Processing, LLC |
| BF Essential Clip Remover.dpm | BF Essential Clip Remover | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| BF Essential Corr.dpm | BF Essential Corr | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| BF Essential Meter.dpm | BF Essential Meter | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| BF Essential Noise.dpm | BF Essential Noise | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| bombfactory BF-2A.dpm | bombfactory BF-2A | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| bombfactory BF-3A.dpm | bombfactory BF-3A | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| bombfactory BF76.dpm | bombfactory BF76 | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| Boom.dpm | Boom | 1.0.0.8542 | 1.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| Chorus.dpm | Chorus | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| Chrome_Tone_LE.dpm | Chrome_Tone_LE | 2.2x18 | 2.2x18, Copyright 1998-2007 McDowell Signal Processing, LLC |
| Click.dpm | Click | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| CompressorBank_LE.dpm | CompressorBank_LE | 4.2x18 | 4.2x18, Copyright 1998-2007 McDowell Signal Processing, LLC |
| Cosmonaut Voice.dpm | Cosmonaut Voice | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| D-Verb.dpm | D-Verb | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| DB-33.dpm | DB-33 | 1.0.0.8542 | 1.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| DigiReWire.dpm | DigiReWire | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| Dither.dpm | Dither | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| Dynamics III.dpm | Dynamics III | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| Elastic Audio.dpm | Elastic Audio | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| Eleven Free.dpm | Eleven Free | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| Eleven LE.dpm | Eleven LE | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| EQ III.dpm | EQ III | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| EZplayer.dpm | EZplayer | 7.3d0 | ? Toontrack AB 2006, Version 1.0.2 |
| Fairchild 660.dpm | Fairchild 660 | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| Fairchild 670.dpm | Fairchild 670 | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| FilterBank_LE.dpm | FilterBank_LE | 4.2x18 | 4.2x18, Copyright 1998-2007 McDowell Signal Processing, LLC |
| Flanger.dpm | Flanger | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| Hybrid.dpm | Hybrid | 1.5.2.7597 | 1.5.2.7597 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| Invert-Duplicate.dpm | Invert-Duplicate | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| JOEMEEK Compressor.dpm | JOEMEEK Compressor | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| JOEMEEK Meequalizer.dpm | JOEMEEK Meequalizer | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| LoFi.dpm | LoFi | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| Maxim.dpm | Maxim | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| MelodyneBridge.dpm | | 3.2.2.2 | |
| MiniGrand.dpm | MiniGrand | 1.0.0.8542 | 1.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| ML4000_LE.dpm | ML4000_LE | 1.1x18 | 1.1x18, Copyright 1998-2007 McDowell Signal Processing, LLC |
| Mod Delay II.dpm | Mod Delay II | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| moogerfooger AD.dpm | moogerfooger AD | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| moogerfooger LP.dpm | moogerfooger LP | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| moogerfooger Ph.dpm | moogerfooger Ph | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| moogerfooger RM.dpm | moogerfooger RM | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| Multi-Tap Delay.dpm | Multi-Tap Delay | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| Normalize-Gain.dpm | Normalize-Gain | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| Ping-Pong Delay.dpm | Ping-Pong Delay | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| Purple Audio MC77.dpm | Purple Audio MC77 | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| RectiFi.dpm | RectiFi | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| Reverse-DC Removal.dpm | Reverse-DC Removal | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| Revolver_LE.dpm | Revolver_LE | 1.2x18 | 1.2x18, Copyright 1998-2007 McDowell Signal Processing, LLC |
| SansAmp PSA-1.dpm | SansAmp PSA-1 | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| SciFi.dpm | SciFi | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| Signal Generator.dpm | Signal Generator | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| SignalTools.dpm | SignalTools | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| Smack! LE.dpm | Smack! LE | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| SoundReplacer.dpm | SoundReplacer | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| Structure.dpm | Structure LE | 1.0.2.7584 | 1.0.2.7584 Copyright 2008 Digidesign, A Division of Avid Technology, Inc. |
| Superior Drummer.dpm | Superior Drummer | 7.3d0 | ? Toontrack AB 2007, Version 2.0.1 |
| Synth_One_LE.dpm | Synth_One_LE | 4.2x18 | 4.2x18, Copyright 1998-2007 McDowell Signal Processing, LLC |
| Tel-Ray Delay.dpm | Tel-Ray Delay | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| Time Comp-Exp-Pitch Shift.dpm | Time Comp-Exp-Pitch Shift | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| Time Shift.dpm | Time Shift | 8.0.0f314 | 8.0.0, Copyright ? 2005 by Digidesign, a division of Avid. All rights reserved. |
| TimeAdjuster.dpm | TimeAdjuster | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| TL AutoPan.dpm | TL AutoPan | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| TL EveryPhase.dpm | TL EveryPhase | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| TL InTune.dpm | TL InTune | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| TL MasterMeter.dpm | TL MasterMeter | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| TL Metro.dpm | TL Metro | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| TL Space Native.dpm | TL Space Native | 8.0.0f314 | 8.0.0, Copyright 1991-2006 Digidesign, A Division of Avid Technology, Inc. |
| Trim.dpm | Trim | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| Vacuum.dpm | Vacuum | 1.0.0.8542 | 1.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
| VariFi.dpm | VariFi | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| Voce Chorus-Vibrato.dpm | Voce Chorus-Vibrato | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| Voce Spin.dpm | Voce Spin | 8.0.0f314 | 8.0.0, Copyright 1991-2008 Digidesign, A Division of Avid Technology, Inc. |
| WaveShell-DAE 6.0.6.dpm | WaveShell-DAE 6.0.6 | 6.0.6 | |
| XPand!.dpm | Xpand! | 2.0.0.8542 | 2.0.0.8542 Copyright 2005-2007 Digidesign, A Division of Avid Technology, Inc. |
Tags: music_technology, programming
|
|
Large Pyramid - LSL #18 - SL #29 - KazMuzik Blog
2008-12-07 18:01
正八面体は、思った以上に簡単だったので、一辺が 2倍の 17.32m のピラミッドを作ってみます。まず、ひとつの prim で作成できる最大の一辺 8.66m の正三角形を、4つ組み合わせて、一辺 17.32m の正三角形を作成します。
setRotation() {
llSetRot(llEuler2Rot(<0.0, 0.0, PI>));
}
rezTriangles() {
vector p = llGetPos();
llRezAtRoot("triangle", p + < 5.0, 0.0, 0.0>, <0.0, 0.0, 0.0>, ZERO_ROTATION, 0);
llRezAtRoot("triangle", p + <-2.5, 4.330127, 0.0>, <0.0, 0.0, 0.0>, ZERO_ROTATION, 0);
llRezAtRoot("triangle", p + <-2.5,-4.330127, 0.0>, <0.0, 0.0, 0.0>, ZERO_ROTATION, 0);
}
default {
state_entry() {
}
touch_start(integer total_number) {
llRequestPermissions(llGetOwner(), PERMISSION_CHANGE_LINKS);
}
run_time_permissions(integer perm) {
if (perm & PERMISSION_CHANGE_LINKS) {
setRotation();
rezTriangles();
}
}
object_rez(key id) {
llCreateLink(id, TRUE);
}
} |
一辺 8.66m の正三角形の prim に、このスクリプトと、同じ prim を "triangle" という名前で、contents に入れます。これを touch すると、大きな正三角形が出来ます。まずは、自分自身を 180°回転してから、それぞれの辺の方向 5m のところに、つまり、辺に対して線対称となるように、同じ正三角形を 3個、配置します。この大きな三角形を "large triangle" としておきます。
この "large triangle" と、8.66m の正方形の prim "square 8.66m" を、底辺の中心となる prim に入れて、次のスクリプトを実行して、touch すると、大きなピラミッドができます。
rezTriangle(vector pos, vector rot) {
llRezAtRoot("large triangle", pos, <0.0, 0.0, 0.0>, llEuler2Rot(rot), 0);
}
rezTriangles() {
vector p = llGetPos();
rezTriangle(p + <-5.773502, 0.0, 4.08248>, <0.0, PI-0.95532, 0.0>);
rezTriangle(p + < 5.773502, 0.0, 4.08248>, <0.0, 0.95532, 0.0>);
rezTriangle(p + <0.0,-5.773502, 4.08248>, <PI+0.95532, 0.0, PI_BY_TWO>);
rezTriangle(p + <0.0, 5.773502, 4.08248>, <PI-0.95532, 0.0,-PI_BY_TWO>);
}
rezSquares() {
rezSquaresX(llGetPos());
}
rezSquaresX(vector pos) {
rezSquaresY(pos + <-4.330127, 0.0, 0.0>);
rezSquaresY(pos + < 4.330127, 0.0, 0.0>);
}
rezSquaresY(vector pos) {
rezSquare(pos + <0.0, -4.330127, 0.0>);
rezSquare(pos + <0.0, 4.330127, 0.0>);
}
rezSquare(vector pos) {
llRezObject("square 8.66m", pos, <0.0, 0.0, 0.0>, ZERO_ROTATION, 0);
}
setInvisible() {
llSetScale(<0.01, 0.01, 0.01>);
llSetAlpha(0.0, ALL_SIDES);
}
default {
state_entry() {
}
touch_start(integer total_number) {
llRequestPermissions(llGetOwner(), PERMISSION_CHANGE_LINKS);
}
run_time_permissions(integer perm) {
if (perm & PERMISSION_CHANGE_LINKS) {
rezTriangles();
rezSquares();
setInvisible();
}
}
object_rez(key id) {
llCreateLink(id, TRUE);
}
} |
側面の大きな正三角形を作成したときに、root となる prim を 180°回転したせいか、rotation の vector は、調整する必要がありました。Tags: programming, second_life
|
|
Octahedron - LSL #17 - SL #28 - KazMuzik Blog
2008-12-07 08:59
12/5 には、pyramid を作成しましたが、そのスクリプトを応用して、正八面体(Regular Octahedron)を作ってみました。前回の pyramid は、すべての辺の長さが等しいので、四角錐の 4つの正三角形の側面を、下の方にも作り、底面の正方形を消すだけです。
rezTriangle(vector pos, vector rot) {
llRezAtRoot("triangle", pos, <0.0, 0.0, 0.0>, llEuler2Rot(rot), 0);
}
rezTriangles() {
vector p = llGetPos();
rezTriangle(p + <-2.88675, 0.0, 2.04124>, <0.0, -0.95532, 0.0>);
rezTriangle(p + < 2.88675, 0.0, 2.04124>, <0.0, PI+0.95532, 0.0>);
rezTriangle(p + <0.0,-2.88675, 2.04124>, < 0.95532, 0.0, PI_BY_TWO>);
rezTriangle(p + <0.0, 2.88675, 2.04124>, <-0.95532, 0.0, -PI_BY_TWO>);
rezTriangle(p + <-2.88675, 0.0, -2.04124>, <0.0, 0.95532, 0.0>);
rezTriangle(p + < 2.88675, 0.0, -2.04124>, <0.0, PI-0.95532, 0.0>);
rezTriangle(p + <0.0,-2.88675, -2.04124>, <-0.95532, 0.0, PI_BY_TWO>);
rezTriangle(p + <0.0, 2.88675, -2.04124>, < 0.95532, 0.0, -PI_BY_TWO>);
}
setInvisible() {
llSetScale(<0.01, 0.01, 0.01>);
llSetAlpha(0.0, ALL_SIDES);
}
default {
state_entry() {
}
touch_start(integer total_number) {
llRequestPermissions(llGetOwner(), PERMISSION_CHANGE_LINKS);
}
run_time_permissions(integer perm) {
if (perm & PERMISSION_CHANGE_LINKS) {
rezTriangles();
setInvisible();
}
}
object_rez(key id) {
llCreateLink(id, TRUE);
}
} |
Tags: programming, second_life
|
|
Pyramid - LSL #16 - SL #26 - KazMuzik Blog
2008-12-05 19:12
20m の大きな Cube (立方体)の作成に成功したので、今回は Pyramid にチャレンジします。まずは、各面がひとつの prim のものから始めます。
Prim の形状(building block type)には、今まで使用してきた Box の他にも、Cylinder (円柱), Sphere (球), Torus (ドーナツ形)などがあります。今回は、正三角形の面があるので、Prism (三角錐)を使ってみます。これで、X, Y軸方向の長さを 10m にセットしたところ、大きな正三角形がでてきました。しかし、一辺 10m の正方形よりは、ひとまわり小さく、一辺が 10m はないようです。そこで、タイルの texture を貼付けてみたところ、高さが 7.5m で、中心は、1/3 の、底辺から 2.5m のところにあることがわかりました。ちなみに、高さの方向は、X軸で、頂点が (5.0, 0), 底辺が x=-2.5 です。このため、底面の正方形の一辺の長さは、7.5 * 2 / sqrt(3) = 5 * sqrt(3) = 8.66025m となります。つまり、すべての辺が 8.66m の正四角錐を作ることになります。
まずは、一辺 8.66025m の正方形の prim を作成して、その中に、上記の正三角形の prim を入れます。あとは、この正三角形を 4回、適当に回転して、適切な位置に rez して link するスクリプトを入れて、実行するだけです。しかし、その position と rotation を求めるのが、簡単ではありません。まずは、スクリプトから、紹介します。
rezTriangles() {
vector p = llGetPos();
llRezAtRoot("triangle", p + <-2.88675, 0.0, 2.04124>, <0.0, 0.0, 0.0>, llEuler2Rot(<0.0, -0.95532, 0.0>), 0);
llRezAtRoot("triangle", p + < 2.88675, 0.0, 2.04124>, <0.0, 0.0, 0.0>, llEuler2Rot(<0.0, PI+0.95532, 0.0>), 0);
llRezAtRoot("triangle", p + <0.0,-2.88675, 2.04124>, <0.0, 0.0, 0.0>, llEuler2Rot(< 0.95532, 0.0, PI_BY_TWO>), 0);
llRezAtRoot("triangle", p + <0.0, 2.88675, 2.04124>, <0.0, 0.0, 0.0>, llEuler2Rot(<-0.95532, 0.0, -PI_BY_TWO>), 0);
}
default {
state_entry() {
}
touch_start(integer total_number) {
llRequestPermissions(llGetOwner(), PERMISSION_CHANGE_LINKS);
}
run_time_permissions(integer perm) {
if (perm & PERMISSION_CHANGE_LINKS) {
rezTriangles();
}
}
object_rez(key id) {
llCreateLink(id, TRUE);
}
} |
まず、最初の面ですが、X軸とZ軸を含む平面で考えると、正三角錐の高さの平方は、(7.5)^2 - (5 * sqrt(3) / 2)^2 = (5/2)^2 * 6 なので、高さは、(5/2)*sqrt(6) = 6.12372m となります。これと、中心が底辺から 1/3 のところにあることを利用して、上記スクリプトでの position が決まります。
また、rotation については、Y軸の上(北)から見て、負の方向に回転する必要があります。ここでは、python を利用してみました。
$ python
Python 2.5.1 (r251:54863, Apr 15 2008, 22:57:26)
>>> from math import sqrt
>>> from math import atan2
>>> 5*sqrt(3)
8.6602540378443855
>>> 5*sqrt(6)/2
6.1237243569579451
>>> atan2( sqrt(6), sqrt(3) )
0.9553166181245093
>>>
|
ひとつ決まれば、あとの 3つは、符号を変えたり、軸を変えたり、rotation に関しては、PI (180度)や PI_BY_TWO (90度) だけ調整することにより、試行錯誤でも得られます。

 Tags: programming, second_life
|
|
Fitting Room in the Sky - LSL #15 - SL #24 - KazMuzik Blog
2008-12-04 19:37
20m cube の応用として、上空に、個人専用の Fitting Room を作成してみます。
Prim を作成して、20m cube のオブジェクトと、次のスクリプトを入れます。
float height = 3776.0;
moveUp() {
vector p = llGetPos();
float h = p.z;
while (h + 10.0 < height) {
p += <0.0, 0.0, 10.0>;
h += 10.0;
llSetPos(p);
}
p.z = height;
llSetPos(p);
}
default {
state_entry() {
}
changed(integer param) {
if (param & CHANGED_LINK) {
moveUp();
string name = llGetInventoryName(INVENTORY_OBJECT, 0);
llRezObject(name, llGetPos(), <0.0, 0.0, 0.0>, ZERO_ROTATION, 0);
llSetTimerEvent(6.0);
}
}
timer() {
llDie();
}
} |
これにすわると、1行目に指定された高さまで移動して、そこで、20m cube を作成して、timer() イベントにより、自分自身を消します。これで、上空の 20m cube の中に、閉じ込められることになります。
Second Life 内に、自分の家がない場合、いろいろな服を買ったり、free で get してきても、自由に試すことが、なかなか出来ないかもしれません。しかし、このスクリプトが入れた prim があれば、sandbox へ行って、rez して、座るだけで、20m cube の自分専用の fitting room となります。
Sandbox では、使用後は、削除していくのがエチケットですが、この場合、上空から、自分自身(avator) が落下してしまうことになります。Tags: programming, second_life
|
|
20m Cube (#2 linked) - LSL #14 - SL #23 - KazMuzik Blog
2008-12-04 07:56
前回の続きで、20m cube を構成する 8つの parts を、link して、ひとつのオブジェクトとします。
まずは、8つのオブジェクトに、"cube 10m ---", "cube 10m ---+", ..., "cube 10m +++" と名付けて、take します。符号は、順に、X, Y, Z軸の、どちらの surface をもつかを示します。例えば、"---" は、西、南、下の 3つの面をもつことをあらわします。
次は、上記のオブジェクトを rez して link するスクリプトです。
rezCube(string object, vector pos) {
llRezObject(object, pos, <0.0, 0.0, 0.0>, ZERO_ROTATION, 0);
}
rezCubes() {
string name = "cube 10m ";
vector pos = llGetPos();
rezCubesX(name, pos);
}
rezCubesX(string name, vector pos) {
rezCubesY(name + "-", pos + <-5.0, 0.0, 0.0>);
rezCubesY(name + "+", pos + < 5.0, 0.0, 0.0>);
}
rezCubesY(string name, vector pos) {
rezCubesZ(name + "-", pos + <0.0, -5.0, 0.0>);
rezCubesZ(name + "+", pos + <0.0, 5.0, 0.0>);
}
rezCubesZ(string name, vector pos) {
rezCube(name + "-", pos + <0.0, 0.0, -5.0>);
rezCube(name + "+", pos + <0.0, 0.0, 5.0>);
}
setInvisible() {
llSetScale(<0.01, 0.01, 0.01>);
llSetAlpha(0.0, ALL_SIDES);
}
default {
state_entry() {
}
touch_start(integer n) {
key owner = llGetOwner();
integer i;
for (i = 0; i < n; i++) {
if (llDetectedKey(i) == owner) {
llRequestPermissions(owner, PERMISSION_CHANGE_LINKS);
}
}
}
run_time_permissions(integer perm) {
if (perm & PERMISSION_CHANGE_LINKS) {
rezCubes();
setInvisible();
}
}
object_rez(key id) {
llCreateLink(id, TRUE);
}
} |
このスクリプトと上記の 8つのオブジェクトを入れた prim を touch することにより、一辺 20m の cube が、ひとつのオブジェクトとして、作成できます。いったん、link されてしまえば、contents の内容は、すべて削除してもかまいません。これで、例えば、object の編集で、texture を一気に変更すること、などができるようになります。もちろん、rez や take も、1回の操作で、できます。
その後、30m cube の作成にもチャレンジしてみましたが、llRezObject() の制限にひっかかってきました。11/30 には、各軸で、20m が制限らしいと書きましたが、今日の結果では、root position の直線距離で、17m が限界のようです。例えば、一辺 10m の立方体を 2つ並べることを考えます。ひとつの面を共有するように並べた場合、中心間の距離は、10m で、これは大丈夫です。また、ひとつの辺を共有するように並べた場合は、10 x sqrt(2) = 14.14213m で、これも大丈夫です。ところが、ひとつの頂点だけを共有するように、X,Y,Z各軸に 10m ずつ平行移動するような場合は、10 x sqrt(3) = 17.32050m となり、17m を超えているせいか、rez に失敗してしまいます。Tags: programming, second_life
|
|
20m Cube - LSL #13 - SL #22 - KazMuzik Blog
2008-12-04 07:24
Flexible に cuboid の surface を作れる script を書いたので、これを利用して、一辺 20m の Cube (立方体)を作ってみます。
まず、surface 用のスクリプトでは、size = <10.0, 10.0, 10.0> としておきます。
次に、20m cube を、8つの 10m cube に分解して、それぞれの cube の 3つの surface を作るスクリプトです。
vector size = <10.0, 10.0, 10.0>;
integer rezParam;
rezRectangle(string surface, vector pos, integer param) {
llRezAtRoot(surface, pos, <0.0, 0.0, 0.0>, ZERO_ROTATION, param);
}
rezRectangles() {
string surface = llGetInventoryName(INVENTORY_OBJECT, 0);
vector pos = llGetPos();
if (rezParam & 1) {
rezRectangle(surface, pos + <-size.x/2, 0.0, 0.0>, 1);
}
if (rezParam & 2) {
rezRectangle(surface, pos + < size.x/2, 0.0, 0.0>, 1);
}
if (rezParam & 4) {
rezRectangle(surface, pos + < 0.0,-size.y/2, 0.0>, 2);
}
if (rezParam & 8) {
rezRectangle(surface, pos + < 0.0, size.y/2, 0.0>, 2);
}
if (rezParam & 16) {
rezRectangle(surface, pos + < 0.0, 0.0,-size.z/2>, 3);
}
if (rezParam & 32) {
rezRectangle(surface, pos + < 0.0, 0.0, size.z/2>, 3);
}
}
setInvisible() {
llSetScale(<0.01, 0.01, 0.01>);
llSetAlpha(0.0, ALL_SIDES);
}
default {
state_entry() {
}
touch_start(integer n) {
if (rezParam > 0) {
llRequestPermissions(llGetOwner(), PERMISSION_CHANGE_LINKS);
}
}
on_rez(integer param) {
rezParam = param;
}
run_time_permissions(integer perm) {
if (perm & PERMISSION_CHANGE_LINKS) {
rezRectangles();
setInvisible();
}
}
object_rez(key id) {
llCreateLink(id, TRUE);
}
} |
これは、次のスクリプトを含むオブジェクトから、rez されますが、そのときのパラメータから、どの surface をつくるか決定します。例えば、21 = 1 + 4 + 16 の場合、1 により、X軸(東西)の西側の面を作成します。同様に、4 は Y軸(南北)の南側の面、... となります。
Rez のときには、パラメータを保存するだけで、実際には touch されてから、surface(s) を rez して、link します。これは、全体では 24 = 3 x 8 = 4 x 6 個の 10m x 10m surface になり、llCreateLink() は 1秒間 sleep するため、一気にすべて rez してしまうと、event queue が overflow してしまうためです。これは、複数のオブジェクトを link するときの重要な注意点で、できるだけ、使用時ではなく、事前にlink しておくことが大切です。このため、スクリプトを作成時用と使用時用の 2つを準備することになり、少し煩雑になってしまいます。
次に、上記のスクリプトを含む 8つのオブジェクトを生成するスクリプトです。
rezCube(string object, vector pos, integer param) {
llRezObject(object, pos, <0.0, 0.0, 0.0>, ZERO_ROTATION, param);
}
rezCubes() {
string object = llGetInventoryName(INVENTORY_OBJECT, 0);
vector pos = llGetPos();
rezCubesX(object, pos, 0);
}
rezCubesX(string object, vector pos, integer param) {
rezCubesY(object, pos + <-5.0, 0.0, 0.0>, param+1);
rezCubesY(object, pos + < 5.0, 0.0, 0.0>, param+2);
}
rezCubesY(string object, vector pos, integer param) {
rezCubesZ(object, pos + <0.0, -5.0, 0.0>, param+4);
rezCubesZ(object, pos + <0.0, 5.0, 0.0>, param+8);
}
rezCubesZ(string object, vector pos, integer param) {
rezCube(object, pos + <0.0, 0.0, -5.0>, param+16);
rezCube(object, pos + <0.0, 0.0, 5.0>, param+32);
}
default {
state_entry() {
}
touch_start(integer n) {
key owner = llGetOwner();
integer i;
for (i = 0; i < n; i++) {
if (llDetectedKey(i) == owner) {
rezCubes();
}
}
}
} |
このスクリプトと、上記のスクリプトを含むオブジェクトをもつ prim を作成して、touch すると、それを中心に、立体的に 8つの prim が配置されるので、それぞれを順番に touch していくことにより、20m cube が作成されます。ただし、この時点では、8つの parts は link されていません。また、touch するときは、event queue が overflow しないように、あまり急ぎすぎては、いけません。
つづくTags: programming, second_life
|
|
Cuboid - LSL #12 - SL #21 - KazMuzik Blog
2008-12-03 22:19
先週は、大きな square (正方形)を作りましたが、今週は、平面から、立体に challenge を移しました。このために、Z軸も考慮する必要があるため、できるだけ、X, Y, Z 軸に対して、対称的に扱えるようにしました。
まずは、直方体 (cuboid) の面 (surface) を作るためのスクリプトです。簡単のため、各辺の長さは、1行目に vector として記述してあります。この prim を llRezObject() で rez すると、最後のパラメータが on_rez() イベントのパラメータとして渡ってくるので、これで、どの面を作成するのかを決定します。これが、1 の場合は、X軸に垂直な面を作るため、この例では、4m x 5m の長方形になります。2, 3 の場合は、同様に、Y軸、Z軸に垂直な長方形を作成します。また、普通に、inventory から、drag and drop すると、パラメータが 0 となるため、特に、大きさは変更されず、このため、オブジェクトの編集やデバッグも容易に行えます。
vector size = <3.0, 4.0, 5.0>;
setSize(integer param) {
vector v = size;
if (param == 1) {
v.x = 0.01;
}
else if (param == 2) {
v.y = 0.01;
}
else if (param == 3) {
v.z = 0.01;
}
else {
return;
}
llSetScale(v);
}
default {
state_entry() {
}
on_rez(integer param) {
if (param > 0) {
setSize(param);
}
}
} |
次に、これを利用して、cuboid の 6つの面を作成して、link するスクリプトです。中心になる prim にこのスクリプトと、上記のスクリプトを含む prim "rectangle surface" を入れて、touch すると、6つの面を作成して、link して、自分自身(中心の prim)は、可能な限り小さくなって、見えなくなります。
vector size = <3.0, 4.0, 5.0>;
key owner;
rezRectangle(vector pos, integer param) {
llRezAtRoot("rectangle surface", pos, <0.0, 0.0, 0.0>, ZERO_ROTATION, param);
}
rezRectangles() {
vector pos = llGetPos();
rezRectangle(pos + < size.x/2, 0.0, 0.0>, 1);
rezRectangle(pos + <-size.x/2, 0.0, 0.0>, 1);
rezRectangle(pos + < 0.0, size.y/2, 0.0>, 2);
rezRectangle(pos + < 0.0,-size.y/2, 0.0>, 2);
rezRectangle(pos + < 0.0, 0.0, size.z/2>, 3);
rezRectangle(pos + < 0.0, 0.0,-size.z/2>, 3);
}
setInvisible() {
llSetScale(<0.01, 0.01, 0.01>);
llSetAlpha(0.0, ALL_SIDES);
}
default {
state_entry() {
owner = llGetOwner();
}
touch_start(integer n) {
llRequestPermissions(owner, PERMISSION_CHANGE_LINKS);
}
run_time_permissions(integer perm) {
if (perm & PERMISSION_CHANGE_LINKS) {
rezRectangles();
setInvisible();
}
}
object_rez(key id) {
llCreateLink(id, TRUE);
}
} |
これで、X軸方向に 3m, Y軸方向に 4m, Z軸方向に 5m の直方体(の6面)が作成され、中心の小さな見えない prim に link されます。
1行目の vector の値を変更することにより、各辺を 10m までの範囲で、変更することができます。Tags: programming, second_life
|
|
Face Light (home-made, 自作) - LSL #11 - SL #19 - KazMuzik Blog
2008-12-02 05:36
Face Light を自作してみました。昨日の snapshot(s) などを見ると、顔が暗くなっています。妻のアドバイスによると、face light というアイテムがあり、それを装備することにより、顔が明るくなって、美しく見えるとのことです。といっても、free の face light を探すのも面倒なので、自分でスクリプトを書いて、実装することにしました。
基本的なアイデアは、prim をひとつ(あるいは複数)、顔の正面に置いて、Lightの設定により、光源として使うことです。ただし、光源の prim が見えてしまっては邪魔なので、見えないようにする工夫が必要です。また、最初から、見えないような prim を作ってしまうと、編集できなくなってしまうので、スクリプト内の on_rez() イベントで、見えなくすることにします。
まずは、光源の prim を作成します。Features タブに、Light の項目があるので、check して、必要ならばパラメータを調整します。それから、Contents に、次の、見えなくするスクリプトを入れます。基本的には、llSetAlpha() で 0.0 を設定することにより、透明にしていますが、念のため、サイズをできるだけ小さくしています。
setInvisible() {
llSetScale(<0.01, 0.01, 0.01>);
llSetAlpha(0.0, ALL_SIDES);
}
default {
state_entry() {
}
on_rez(integer param) {
setInvisible();
}
} | |
これに、"light" と名付けて、take します。
次に、Nose (など)に attach (装着)する prim を作成し、上記の "light" object を入れます。さらに次のスクリプトにより、touch_start() イベントを受け取ったら、"light" object を rez して、link することにします。
string light = "light";
key owner;
setLightName() {
if (llGetInventoryNumber(INVENTORY_OBJECT) > 0) {
light = llGetInventoryName(INVENTORY_OBJECT, 0);
}
}
rezLight(vector pos) {
llRezObject(light, pos, <0.0, 0.0, 0.0>, ZERO_ROTATION, 0);
}
rezLights() {
setLightName();
vector pos = llGetPos();
vector v = <0.10, 0.0, 0.0>;
vector p = pos + v;
rezLight(p);
}
default {
state_entry() {
owner = llGetOwner();
}
touch_start(integer n) {
integer i;
for (i = 0; i < n; i++) {
if (llDetectedKey(i) == owner) {
llRequestPermissions(owner, PERMISSION_CHANGE_LINKS);
}
}
}
run_time_permissions(integer perm) {
if (perm & PERMISSION_CHANGE_LINKS) {
rezLights();
}
}
object_rez(key id) {
llCreateLink(id, TRUE);
}
} | |
これで、touch すると、このオブジェクトには、見えない光源となるオブジェクトが link されることになります。
なお、この例では、attach される部所の 10cm のところに、光源のオブジェクトを 1個だけ、配置しています。必要ならば、光源の個数や場所を調整しておきます。
この時点で、Contents の中身は不要になるので、いったん削除しておきます。
さらに、attach() イベントを受け取ったら、自分自身を同様に見えなくする最初のものと似たスクリプトを入れます。
setInvisible() {
llSetScale(<0.01, 0.01, 0.01>);
llSetAlpha(0.0, ALL_SIDES);
}
default {
state_entry() {
}
attach(key id) {
setInvisible();
}
} | |
これを take して、自分の inventory に入れておきます。このオブジェクトを、例えば nose に attach すると、鼻の 10cm 先に、光源ができて、顔が明るくなるわけです。
上記のスクリプトや、object のパラメータを変更することにより、光源の数、距離、色などを調整することが可能です。ただし、あまり多くの光源を使うと、逆に陰ができたりして、調整がむずかしくなることがあるので、注意が必要です。
下記は、snapshots ですが、以前の snapshot と比べて、顔の色が明るくなっているのが、わかります。特に、左の snapshot は、夜ですが、明るく照らされているのがわかります。
Tags: programming, second_life
|
|
50m square floor #2 - LSL #10 - SL #17 - KazMuzik Blog
2008-11-30 11:28
11/28 には、object の rez や link には、10m の壁があるようだと書きましたが、いろいろ試してみたところ、どうも、それぞれの軸に対して 20m が限界のようです。ただし、llRezAtRoot() では、object の原点で決まるのに対して、link では、object の中心で決まるようです。Spec を調べたわけではなく、自分でいくつかスクリプトを書いて確かめてみただけなので、ひょっとして間違いがあるかもしれませんが。(*)
このため、正方形の大きな object を作成しようとした時には、今まで紹介したように、ひとつの object としては、30m まで、また、ひとつの script から複数の object を rez する場合には 50m というのが、限界のようです。ただし、後者に対しては、複数の入れ子になった object を用意して、 on_rez() イベントで、連鎖的に行う場合には、もっと大きなものが作れると思います。
なお、11/29 のタイルの 50m square floor は、厚さを 1m で作成しましたが、smoke texture に変更して、厚さも 0.01m に変更しようとしたところ、texture はすべてに適用されましたが、size (scale) は、root object にだけ適用されました。このため、表面がでこぼこになってしまったため、作り直しました。

まずは、右上に配置する 30m(東西, X軸) x 20m(南北, Y軸)の長方形の object を作成します。このために、まず、prim をひとつ作成して、size を 10m x 10m x 0.01m にし、Smoke Texture を貼付けます。これを、"square 10m" と名付けて、いったん inventory に取り込みます。これを、再び rez して、この中に、同じ "square 10m" を入れ、次のようなスクリプトを新規作成します。
string square10 = "square 10m";
key owner;
rezObjects() {
vector pos = llGetPos();
integer i;
for (i = -1; i < 2; i++) {
integer j;
for (j = 0; j < 2; j++) {
if (i != 0 || j != 0) {
llRezObject(square10, pos + <10.0 * (float)i, 10.0 * (float)j, 0.0>,
<0.0, 0.0, 0.0>, <0.0, 0.0, 0,0>, 0);
}
}
}
}
default {
state_entry() {
owner = llGetOwner();
}
touch_start(integer total_number) {
integer i;
for (i = 0; i < total_number; i++) {
if (llDetectedKey(i) == owner) {
llRequestPermissions(owner, PERMISSION_CHANGE_LINKS);
}
}
}
run_time_permissions(integer perm) {
if (perm & PERMISSION_CHANGE_LINKS) {
rezObjects();
}
}
object_rez(key id) {
llCreateLink(id, TRUE);
}
} |
これを実行して、touch すると、目的の 30m x 20m の長方形の object ができます。Contents にある "square 10m" とスクリプトは、用済みなので削除します。これを、"rectangle 30x20-1" と名付けて、inventory に取り込んでおきます。
同様にして、左上に配置される 20m x 30m の "rectangle 20x30-2" を、さらに、左下の "rectangle 30x20-3", 右下の "rectangle 20x30-4" を作成します。
次に、中心となる "square 10m" を rez して、この中に、上記の 4つの大きな長方形の objects を入れます。これに、次のようなスクリプトを新規作成します。
makeLargeSquare() {
vector pos = llGetPos();
llRezAtRoot("rectangle 30x20-1", pos + < 10.0, 10.0, 0.0>, <0.0, 0.0, 0.0>, <0.0, 0.0, 0,0>, 0);
llRezAtRoot("rectangle 20x30-2", pos + <-10.0, 10.0, 0.0>, <0.0, 0.0, 0.0>, <0.0, 0.0, 0,0>, 0);
llRezAtRoot("rectangle 30x20-3", pos + <-10.0, -10.0, 0.0>, <0.0, 0.0, 0.0>, <0.0, 0.0, 0,0>, 0);
llRezAtRoot("rectangle 20x30-4", pos + < 10.0, -10.0, 0.0>, <0.0, 0.0, 0.0>, <0.0, 0.0, 0,0>, 0);
}
default {
state_entry() {
}
on_rez(integer param) {
makeLargeSquare();
}
} |
これを、"square 10m to 50m on rez" と名付けて、inventory に取り込みます。後は、この 10m square の prim を rez すると、まわりに 4つの大きな長方形が rez されて、50m の正方形ができます。ただし、link は、されていないので、独立した 5つの objects として、扱われます。
また、普通に rez してしまうと、地上に 50m squre ができてしまいます。そこで、もう一工夫します。別の適当な大きさの prim を作成して、上記の "square 10m to 50m on rez" と次のスクリプトをいれます。
default {
state_entry() {
}
touch_start(integer n) {
if (llDetectedKey(0) == llGetOwner()) {
vector p = llGetPos();
llRezObject("square 10m to 50m on rez", p + <0.0, 0.0, -1.0>,
<0.0, 0.0, 0.0>, <0.0, 0.0, 0,0>, 0);
}
}
} |
これに、座ってから、object の edit window から、position の Z に 4096.0 以下の大きな値を入力します。これで、座ったまま、一気に上空に移動するので、そこで touch すると、上記の snapshot のように、足下を中心に、50m 四方の正方形の床ができます。
2008-12-04 update (*) -> 20m Cube (#2 linked) - LSL #14 - SL #23 .. 17m !?Tags: programming, second_life
|
|
30m square floor with fence (single large object) - LSL #9 - SL #16 - KazMuzik Blog
2008-11-30 01:08
土地を持っていないので、スクリプトを実行してみるには、sandbox で行う必要があります。ただし、地上は、ごちゃごちゃしていることが多いので、最近では、数千メートルの上空で作業しています。ただし、50m 四方の広い床があったとしても、間違って、落ちてしまう可能性もあり、Second Life 内のことなので、大事に至ることはありませんが、多少、面倒なことになります。このため、10m の立方体を作成したテクニックを応用して、30m 四方の床の回りに、高さ 10m のフェンスを作ってみました。今回も、smoke の(半)透明な texture を使ってみました。


これで落ちる心配をせずに、安心して作業することができるようになりました。
なお、Library にある Default Transparent Texture をセットしたところ、完全に透明になりました。どこまで床があるか全くわかりませんが、フェンスがあるので大丈夫です。ところが、この完全に透明なオブジェクトがマウスで選択できなくなりました。このため、削除もできず、自動的に消滅するまで、放置しておくことになってしまいました。Tags: programming, second_life
|
|
50m square floor - LSL #7 - SL #13 - KazMuzik Blog
2008-11-29 09:54
昨日は、30m 四方の square object を作成しましたが、さらに大きな床を作ろうとすると、けっこう大変です。まずは、snapshot からです。

50m 四方の square の床の上に立っています。小さい青いひとつのタイルの大きさは、2.5m 四方なので、一辺 20個で、50m というのがわかります。ほぼ中央に、赤い帽子をかぶった avator が立っていますが、これからも、かなり大きいことがわかります。
いろいろ試行錯誤してみましたが、link における 10m の壁を乗り越えることはできず、ひとつの object として作成することはできませんでした。まん中の 10m 四方の prim のまわりに、20m x 30m の object を 4つ、取り囲むように配置しています。( 1 + (2 * 3) * 4 = 25 = 5 * 5 )
このため、小さな prim に script を組み込み、touch することにより、上記の 5つの object により、50m 四方の床を自動的に作るようにしました。
2008-11-29 update 別のタイルの texture を使ってみました。斜め 45度から眺めていますが、中の正方形の対角線が 10m、つまり、一辺はピタゴラスの定理(三平方の定理)より、10/sqrt(2) = 5 * sqrt(2) = 7.071m です。
 Tags: programming, second_life
|
|
30m Square (large object) - LSL #6 - SL #11 - KazMuzik Blog
2008-11-28 22:08
昨日の写真では、10m x 10m x 1m の prim を使用しましたが、Second Life の制限により、10m 以上の prim を作成することはできません。しかし、複数の prim を link して、ひとつの object として扱うことができます。これを Linden Script で記述して、一辺が 30m の square を作成してみます。
まずは、prim をひとつ作成して、size を 10m x 10m x 1m とします。名前は、"square 10m" として、take して、自分の inventory に取り込みます。これを、rez して、この中に、同じ "square 10m" を入れ、次の script を作成します。
string square10 = "square 10m";
key owner;
integer count = 8;
list squares = [];
rezSquares() {
vector pos = llGetPos();
integer i;
for (i = -1; i < 2; i++) {
integer j;
for (j = -1; j < 2; j++) {
if (i != 0 || j!= 0) {
llRezAtRoot(square10, pos + <10.0 * (float)i, 10.0 * (float)j, 0.0>,
<0.0, 0.0, 0.0>, <0.0, 0.0, 0,0>, 0);
}
}
}
}
default {
state_entry() {
owner = llGetOwner();
}
touch_start(integer total_number) {
integer i = 0;
for ( ; i < total_number; i++) {
if (llDetectedKey(i) == owner) {
llRequestPermissions(owner, PERMISSION_CHANGE_LINKS);
}
}
}
run_time_permissions(integer perm) {
if (perm & PERMISSION_CHANGE_LINKS) {
rezSquares();
}
}
object_rez(key id) {
squares += id;
count --;
if (count == 0) {
integer i;
for (i = 0; i < 8; i++) {
llCreateLink(llList2Key(squares, i), TRUE);
}
}
}
} |
Root になる、もともとの prim の inventory にある 10m 四方の prim を、8回 rez して、自分のまわりに並べます。これだけでも、一応 30m 四方の square ができますが、移動したり、take などの操作では、別々の 9個の object として扱わなければならないので、面倒です。そこで、llCreateLink() で、周辺の prim を、中央の root prim に、link しています。Link を変更するためには、runtime permission が必要になるため、最初に、llRequestPermissions() で、owner に permission を要求しています。
なお、一定の距離以上(多分、10m 以上)離れていると、llRezAtRoot() や llRezObject(), また、llCreateLink() が失敗します。また、このときに、object の root position で計算しているのか、center position なのかで、状況が変わってきます。このため、上記のスクリプトでは、llRezAtRoot() を用いて、root position で object を rez したあとに、まとめて、link しています。小さな object を扱っている場合は、特に問題がない場合もありますが、このように大きな object を扱う場合は、注意が必要です。
昨日と同様に、30m square の object を 3776m の上空に浮かべて、記念写真をとりました。

 Tags: programming, second_life
|
|
HTTP Response Header and Servlet - KazMuzik Blog
2008-11-20 20:06
この 1年ぐらいは、back-end のプログラミングが多く、Servlet や JSP などには、ほとんど手を付けていませんでしたが、最近、LSL を front-end として、ファイルのダウンロードをサービスする Servlet を書いたので、メモしておきます。ブラウザで特定のファイル名で保存できるように、ダイアログが立ち上がるように、HTTP Response Header には、Content-Disposition を加えておきました。その他には、Content-Type と Content-Length です。
import java.io.*;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class KazMuzikServlet extends HttpServlet {
private static final String filename = "kazmuzik.mp3";
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws IOException, ServletException {
File file = new File(filename);
int len = (int)file.length();
res.setContentType("audio/mpeg");
res.setHeader("Content-Length", "" + len);
res.setHeader("Content-Disposition",
"attachment; filename=\"" + filename + "\"");
InputStream in = new BufferedInputStream(new FileInputStream(file));
OutputStream out = new BufferedOutputStream(res.getOutputStream());
byte[] b = new byte[len];
int n = 0;
while (true) {
int m = in.read(b, n, len-n);
out.write(b, n, m);
n += m;
if (n == len) {
break;
}
}
in.close();
out.flush();
out.close();
return;
}
public void doPost(HttpServletRequest req, HttpServletResponse res)
throws IOException, ServletException {
doGet(req, res);
}
} |
Tags: programming
|
|
Java I18n tips on Mac OS X Terminal.app - KazMuzik Blog
2008-11-19 23:36
最近では、個人のソフトウェアプロジェクトの開発環境も、Linux (Fedora) から、Mac OS X へ移行しつつあります。その中で、最近、Java の i18n (国際化) に関連する件で、少し悩んだことがあったので、ここでまとめておきます。
次のような、日本語のテキストを terminal に表示する簡単なプログラムを考えます。
public class Test {
private static final String text = "日本語のテスト";
public static void main(String[] args) throws Exception {
System.out.println(text);
}
} |
このように、Java のソースの中に、アスキー以外の文字を記述した場合は、どのような locale でも、ちゃんとコンパイルできるように、native2ascii コマンドで、Unicode のリテラルに変換しておくのが、推奨されています。しかし、開発環境の locale と、ソースコードの encoding が同じ場合は、Linux や Solaris での Sun の実装では、問題なく動作します。Mac OS X でも、下記のように、同様に動作するようにみえます。
$ echo $LANG
en_US.UTF-8
$ javac Test.java
$ java Test
日本語のテスト
$ |
ところが、Servlet の中で、上記のように、日本語(non-ASCII characters)を UTF-8 でハードコードしたところ、文字化けが発生してしまいました。その Servlet は、JDBC で Derby データベースも使用しているので、最初は、Tomcat や Derby などの設定が悪いのかと思い、いろいろ調査したり、デバッグしてみましたが、どうも問題はないようです。そこで、text.length() と文字列の長さ(character の数)をプリントしたところ、7 ではなく、21 となっていました。これは、UTF-8 でのバイト数に一致します。どうも、Mac OS X の javac が、ソースコードを UTF-8 ではなく、ISO-8859-1 と認識しているようです。そこで native2ascii を使用してみました。
$ mv Test.java Test.txt
$ native2ascii -encoding UTF-8 Test.txt > Test.java
$ grep final Test.java
private static final String text = "\u65e5\u672c\u8a9e\u306e\u30c6\u30b9\u30c8";
$ javac Test.java
$ java Test
???????
$ |
今度は、7 文字となりましたが、System.out への出力で、? に化けてしまいました。これも、先ほどの javac と同様、system の default encoding を正しく、認識していないためのようです。もともと、System.out は、java.io.PrintStream (byte stream) ですが、Sun の実装では細工がしてあり、PrintWriter のように扱えますが、Mac OS X では、ISO-8859-1 とみなしているようです。そこで、明示的に、UTF-8 を encoding に指定して、Writer (character stream) を作成してみました。
$ cat Test.java
public class Test {
private static final String text = "\u65e5\u672c\u8a9e\u306e\u30c6\u30b9\u30c8";
public static void main(String[] args) throws Exception {
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out, "UTF-8"));
out.println(text);
out.flush();
}
}
$ javac Test.java
$ java Test
日本語のテスト
$ |
今度は、Mac OS X の Terminal.app 上でも、正しく表示されるようになりました。
Mac OS X 上で、Java のプログラムを開発するときは、default の encoding に頼らずに、アスキー以外の文字をハードコードした場合は native2ascii を使用するとか、正しく encoding を指定して Reader や Writer を作成するなど、注意が必要です。Terminal.app 上で、日本語(など)が正しく表示されるから、といって安心してはいけません。Tags: programming
|
|
item seller object - LSL #5 - SL #8 - KazMuzik Blog
2008-11-17 12:18
前回のスクリプトを改良して、inventroy にあるオブジェクトを売るスクリプトにしました。
integer defaultPrice = 100;
integer price = defaultPrice;
string item;
string notecardName;
integer notecardSize;
key owner;
key notecardRequest;
setInfo() {
item = llGetInventoryName(INVENTORY_OBJECT, 0);
if (item == "") {
llInstantMessage(owner, "Please put an item to my inventory.");
}
else {
notecardName = llGetInventoryName(INVENTORY_NOTECARD, 0);
if (notecardName == "") {
price = defaultPrice;
llInstantMessage(owner, "The price is set to L$" + (string)defaultPrice
+ " (default) because there is no notecard in my inventory.");
setItemPrice();
}
else {
integer value = (integer)notecardName;
if (value > 0) {
price = value;
llInstantMessage(owner, "The price is set to L$" + (string)price
+ " from the notecard name.");
setItemPrice();
}
else {
notecardSize = -1;
notecardRequest = llGetNumberOfNotecardLines(notecardName);
// -> dataserver()
}
}
}
}
setItemPrice() {
llSetObjectName(item + " - item seller");
llSetPayPrice(price, [price, PAY_HIDE, PAY_HIDE, PAY_HIDE]);
llSetText(replaceName(item) + "\nL$" + (string)price, <1.0, 1.0, 1.0>, 1.0);
string texture = llGetInventoryName(INVENTORY_TEXTURE, 0);
if (texture != "") {
llSetTexture(texture, ALL_SIDES);
}
}
string replaceName(string s) {
integer n = 0;
while (n >= 0) {
n = llSubStringIndex(s, " - ");
if (n >= 0) {
s = llDeleteSubString(s, n, n+2);
s = llInsertString(s, n, "\n");
}
}
return s;
}
default {
state_entry() {
owner = llGetOwner();
llSetClickAction(CLICK_ACTION_PAY);
setInfo();
}
changed(integer change) {
if (change & CHANGED_INVENTORY) {
setInfo();
}
}
dataserver(key query, string data) {
if (query == notecardRequest) {
if (notecardSize < 0) {
notecardSize = (integer)data;
if (notecardSize <= 0) {
price = defaultPrice;
llInstantMessage(owner, "The price is set to L$" + (string)price
+ " (default) because the notecard \"" + notecardName + "\" is empty.");
setItemPrice();
}
else {
notecardRequest = llGetNotecardLine(notecardName, 0);
// -> dataserver()
}
}
else {
price = (integer)data;
if (price < 0) {
price = 0;
llInstantMessage(owner, "The price for \"" + item + "\" is set to L$" + (string)price
+ " because the price of notecard \"" + notecardName + "\" is negative.");
}
else {
llInstantMessage(owner, "The price for \"" + item + "\" is set to L$" + (string)price
+ " from the notecard \"" + notecardName + "\".");
}
setItemPrice();
}
}
}
money(key buyer, integer amount) {
if (amount >= price) {
llInstantMessage(owner, llKey2Name(buyer) + " paid you L$" + (string)amount + " for \"" + item + "\".");
llInstantMessage(buyer, "Thank you for your purchase of \"" + item + "\" with L$" + (string)amount + ".");
llGiveInventory(buyer, item);
}
else {
llInstantMessage(buyer, "You paid L$" + (string)amount + ", but it is less than the price L$"
+ (string)price + ", so your payment is considered as a donation. Thank you for your donation.");
llInstantMessage(owner, llKey2Name(buyer) + " paid you L$" + (string)amount + " as donation.");
}
}
} |
価格は、前回と同様、inventory にある notecard から取得しますが、メンテナンスを容易にするため、もし notecard の名前が数字の場合は、それを価格にします。また、売り物であるオブジェクトの名前を取得して、それと価格をあわせて、llSetText() で、表示するようになっています。さらに、inventory に texture があれば、それも llSetTexture() でセットします。
Avator が Pay を実行すると、money() イベントが発生しますが、そこでは、支払われた金額と価格を比較して、価格に満たない場合は、donation とみなすことにしました。価格以上の支払いがあった場合には、llGiveInventory() で、売り物であるオブジェクトを、avator に与えます。
なお、llSetPayPrice() で、最初のパラメータも PAY_HIDE にして、llSetPayPrice(PAY_HIDE, [price, PAY_HIDE, PAY_HIDE, PAY_HIDE]); とすると、指定した金額のボタンだけが現れます。Tags: programming, second_life
|
|
Pay and price from notecard - LSL #4 - SL #7 - KazMuzik Blog
2008-11-16 13:23
前回の inventory にある notecard を用いて、payment の price を dynamic に設定する方法を紹介します。
Notecard の最初の行に、price を書いて、それを inventory に含めると、その金額が payment のときに参照されます。もし、notecard がひとつもない場合には、ハードコードされた defaultPrice が利用されます。
integer defaultPrice = 100;
integer price = defaultPrice;
string notecardName;
integer notecardSize;
key owner;
key notecardRequest;
setPrice() {
notecardName = llGetInventoryName(INVENTORY_NOTECARD, 0);
if (notecardName == "") {
price = defaultPrice;
llInstantMessage(owner, "The price is set to L$" + (string)defaultPrice
+ " because there is no notecard in my inventory.");
llSetPayPrice(price, [price, PAY_HIDE, PAY_HIDE, PAY_HIDE]);
}
else {
notecardSize = -1;
notecardRequest = llGetNumberOfNotecardLines(notecardName);
// -> dataserver()
}
}
default {
state_entry() {
owner = llGetOwner();
llSetClickAction(CLICK_ACTION_PAY);
setPrice();
}
changed(integer change) {
if (change & CHANGED_INVENTORY) {
setPrice();
}
}
dataserver(key query, string data) {
if (query == notecardRequest) {
if (notecardSize < 0) {
notecardSize = (integer)data;
if (notecardSize <= 0) {
price = defaultPrice;
llInstantMessage(owner, "The price is set to L$" + (string)defaultPrice
+ " because the notecard \"" + notecardName + "\" is empty.");
llSetPayPrice(price, [price, PAY_HIDE, PAY_HIDE, PAY_HIDE]);
}
else {
notecardRequest = llGetNotecardLine(notecardName, 0);
// -> dataserver()
}
}
else {
price = (integer)data;
if (price < 0) {
price = 0;
llInstantMessage(owner, "The price is set to L$" + (string)price
+ " because the price of notecard \"" + notecardName + "\" is negative.");
}
else {
llInstantMessage(owner, "The price is set to L$" + (string)price
+ " from the notecard \"" + notecardName + "\".");
}
llSetPayPrice(price, [price, PAY_HIDE, PAY_HIDE, PAY_HIDE]);
}
}
}
money(key payer, integer amount) {
llInstantMessage(owner, llKey2Name(payer) + " paid you L$" + (string)amount + ".");
}
} |
注意点としては、イベントが発生するような関数を呼び出した後は、そのイベントから、速やかに抜けるということです。これは、別のイベントが発生するため、そちらに制御が移ってしまうためです。この例では、私が定義した setPrice() という関数も、その中で、dataserver() というイベントが発生する llGetNumberOfNotecardLines() という関数を呼び出しているので、setPrice() はイベントの最後になるように書かなければいけません。最初は、その後に、llSetPayPrice() を書いていましたが、条件によって実行されないため、上記のことに気付き、setPrice() の中に入れました。
下記の実行例では、inventory に notecard がない状態でスタートしたので、最初は L$100 でしたが、1行目に 10 とだけ書かれた "sample price" という notecard を、inventory に含めたときに、L$10 になりました。さらに、notecard を edit して、1 としたところ、L$1 になりました。その後、Sample Payer という avator が、Pay に設定されている左クリックで、L$1 の支払いをしました。
[13:01] Payment Example: The price is set to L$100 because there is no notecard in my inventory.
[13:02] Payment Example: The price is set to L$10 from the notecard "sample price".
[13:03] Payment Example: The price is set to L$1 from the notecard "sample price".
[13:04] Payment Example: Sample Payer paid you L$1.
|
なお、Pay は、円形のメニューにありますが、money() イベントを実装しないと、選択できません。同様に、touch_start() イベントがないと、Touch ができません。今回の例では、簡単のため、削除しておきました。Tags: programming, second_life
|
|
Notecard and Inventory - LSL #3 - Second Life #6 - KazMuzik Blog
2008-11-15 11:19
オブジェクトで走るスクリプトを複数、書いていると、ほとんど同じですが、いくつかのパラメータだけが違うようなオブジェクトを作成したいことがあります。このような場合、スクリプトにハードコードしないで、それらのパラメータを Notecard に書いて、それを Inventory として持たせることで、同一のスクリプトを使うことができます。
まず、自分(オブジェクト)の inventory にアクセスする方法ですが、llGetInventoryNumber() と llGetInventoryName() という関数で、すべての、あるいは notecard など特定のタイプの inventory の数と、名前を得ることができます。
また、notecard に記述されている内容にアクセスするには、llGetNumberOfNotecardLines() で行数を得て、llGetNotecardLine() で、それぞれの行のテキストを得ることができます。ただし、これらは直接、結果を返すのではなく、dataserver() というイベントによって、受け取る必要があります。
これらの関数を応用したスクリプトで、Touch されたら、inventory にある最初の notecard の内容を、instant message で owner に送ります。
string notecardName;
integer notecardSize;
integer notecardRead;
list notecardLines;
key owner;
key notecardRequest;
setNotecard() {
notecardName = llGetInventoryName(INVENTORY_NOTECARD, 0);
}
printNotecardName() {
llInstantMessage(owner, "notecardName=\"" + notecardName + "\"");
}
printNotecardLines() {
string s = "";
s += "\n---BEGIN OF NOTECARD ---";
integer i = 0;
for ( ; i < notecardSize; i++) {
s += ("\n" + llList2String(notecardLines, i));
}
s += "\n--- END OF NOTECARD ---";
llInstantMessage(owner, s);
}
default {
state_entry() {
setNotecard();
owner = llGetOwner();
llSetTouchText("Read Notecard");
printNotecardName();
}
changed(integer change) {
if (change & CHANGED_INVENTORY) {
setNotecard();
printNotecardName();
}
}
touch_start(integer total_number) {
integer i = 0;
for ( ; i < total_number ; i++) {
key avator = llDetectedKey(i);
if (avator == owner) {
if (notecardName == "") {
llInstantMessage(owner, "no notecard in my inventory");
}
else {
notecardSize = -1;
notecardRequest = llGetNumberOfNotecardLines(notecardName);
}
}
}
}
dataserver(key query, string data) {
if (query == notecardRequest) {
if (notecardSize < 0) {
notecardSize = (integer)data;
if (notecardSize <= 0) {
llInstantMessage(owner, "The notecard \"" + notecardName + "\" is empty.");
}
else {
llInstantMessage(owner, (string)notecardSize + " lines"
+ " in the notecard \"" + notecardName + "\".");
notecardRead = 0;
notecardLines = [];
notecardRequest = llGetNotecardLine(notecardName, 0);
}
}
else {
notecardRead ++;
notecardLines += data;
if (notecardRead < notecardSize) {
notecardRequest = llGetNotecardLine(notecardName, notecardRead);
}
else {
printNotecardLines();
}
}
}
}
} |
Notecard の内容を保持するのに、list 型を使ってみました。また、途中で、inventory に変更があり、例えば、最初は notecard がなくても、途中で、追加された場合でも対応できるように、changed() イベントを実装してあります。また、status の外に、user defined の関数を定義しておきました。
[10:10] Notecard example: notecardName="sample text"
[10:11] Notecard example: 3 lines in the notecard "sample text".
[10:11] Notecard example:
---BEGIN OF NOTECARD ---
This is the first line.
This is the second line.
これは、3行目です。
--- END OF NOTECARD ---
|
この例のように、ASCII 以外の、例えば日本語の文字もきちんとサポートされています。Tags: programming, second_life
|
|
HTTP and JSP - Lenden Script Language (LSL) #2 - Second Life #5 - KazMuzik Blog
2008-11-13 01:16
11/11 には、簡単な Linden Script を紹介しましたが、iMac++ に Tomcat をセットアップしたので、HTTP で通信してみました。LSL Portal によると、llHTTPRequest() という関数に、URL を渡して呼び出すと、http_response() というイベントにより、HTTP Response を受け取れるようです。また、X-SecondLife-Object-Key などのヘッダーも送られるので、Servlet では、これを利用することができます。
それでは、Linden Script でのプログラムからです。
key httpRequestId;
key owner;
default {
state_entry() {
llSay(0, "Hello, Avatar!");
}
touch_start(integer total_number) {
key obj = llGetKey();
owner = llGetOwner();
key creator = llGetCreator();
string objName = llKey2Name(obj);
string ownerName = llKey2Name(owner);
string creatorName = llKey2Name(creator);
integer i = 0;
for ( ; i < total_number; i++) {
key avator = llDetectedKey(i);
if (avator != owner && avator != creator) {
llInstantMessage(avator, "Sorry, you cannot touch me.");
}
else {
llInstantMessage(avator,
"I am " + objName + " (" + (string)obj + ").");
llInstantMessage(avator,
"I am owned by " + ownerName + " (" + (string)owner + ").");
llInstantMessage(avator,
"I was created by " + creatorName + " (" + (string)creator + ").");
llInstantMessage(avator,
"You are " + llDetectedName(i) + " (" + (string)avator + ").");
httpRequestId = llHTTPRequest("http://kazmuzik.net:8080/sltest/test.jsp", [], "");
}
}
}
http_response(key requestId, integer status, list metadata, string body) {
if (requestId == httpRequestId) {
llInstantMessage(owner, llStringTrim(body, STRING_TRIM));
}
}
} |
ローカルで、前回と同様の情報を instant message で送った後、私の自宅にある iMac++ に HTTP Request を送っています。
この HTTP Request を受け付けて、処理する簡単な JSP です。
<%
String objectKey = request.getHeader("X-SecondLife-Object-Key");
String objectName = request.getHeader("X-SecondLife-Object-Name");
String ownerKey = request.getHeader("X-SecondLife-Owner-Key");
String ownerName = request.getHeader("X-SecondLife-Owner-Name");
java.util.Formatter f = new java.util.Formatter();
f.format("%s (%s) touched by %s (%s)%n", objectName, objectKey, ownerName, ownerKey);
%>
<%= f.toString() %>
|
これを Tomcat に deploy してやります。
$ cd /opt/tomcat/webapps
$ mkdir sltest
$ cd sltest
$ vi test.jsp (上記のファイル)
$
|
JSP なので、これだけです。
実際に、Second Life で、上記の Linden Script が走っているオブジェクト(HTTP sample object) をクリックするか、右クリックの円メニューから Touch を選択すると、次のようなメッセージを受け取ります。
[01:23] HTTP sample object: I am HTTP sample object (12345678-90ab-cdef-1234-567890abcdef).
[01:23] HTTP sample object: I am owned by Kaz Muzik (abcdef12-3456-7890-abcd-ef1234567890).
[01:23] HTTP sample object: I was created by Kaz Muzik (abcdef12-3456-7890-abcd-ef1234567890).
[01:23] HTTP sample object: You are Kaz Muzik (abcdef12-3456-7890-abcd-ef1234567890).
[01:23] HTTP sample object: HTTP sample object (12345678-90ab-cdef-1234-567890abcdef) touched \
by Kaz Muzik (abcdef12-3456-7890-abcd-ef1234567890)
|
最後のラインが、実際に、JSP が作成したものです。Tags: programming, second_life
|
|
Linden Script Language (LSL) - Second Life #4 - KazMuzik Blog
2008-11-11 20:11
Second Life の Linden Script Language (LSL) で簡単なスクリプトを書いてみました。Java や C言語で、event driven なコードを書いた方は、すぐにわかると思います。
まずは、オブジェクトを作りますが、これはどこでもできるわけではなく、sandbox と呼ばれる場所へ行く必要があります。そこで、mouse cursor を地面のところへ移動して、右クリックすると、丸いメニューが現れるので、Create を選択すると、立方体のオブジェクトができます。さらに、それを選択して、同様に、Edit... を選択すると、オブジェクト を edit する window がポップアッップしてきます。まん中あたりにタブがあるので、Content をクリックすると、その下の部分が変わるので、New Script... をクリックすると、新しいスクリプトが Contents に追加されます。それをダブルクリックするか、右ボタンで Open... を選択すると、スクリプト編集用の window がポップアップしてきます。
Second Life では、object はすべて UUID で表現される key を持っています。また、名前(name, type は string) もありますが、これは重複する可能性があるので、key で扱うのがいいようです。また、avator も object の一種なので、key (UUID) をもっています。まずは、touch されると、自分(object) と、その owner, それに、touch した avator の key と name をプリントする簡単なスクリプトを書いてみました。
default {
state_entry() {
llSay(0, "Hello, Avatar!");
}
touch_start(integer total_number) {
llSay(0, "Touched.");
key k = llGetKey();
llSay(0, "My key is " + (string)k + ".");
llSay(0, "My name is " + llKey2Name(k) + ".");
k = llGetOwner();
llSay(0, "My owner's key is " + (string)k + ".");
llSay(0, "My owner's name is " + llKey2Name(k) + ".");
integer i = 0;
for ( ; i < total_number; i++) {
k = llDetectedKey(i);
llInstantMessage(k, "Your key is " + (string)k + ".");
llInstantMessage(k, "Your name is " + llDetectedName(i) + ".");
}
}
} |
一番外側の括弧の default は state で、次のレベルの括弧の state_entry() や touch_start() が event です。この中に、実行するスクリプトを書いていきます。llSay() など、"ll" で始まるのが pre-defined された function で、llSay() は、特定の channel にメッセージを表示し、llInstantMessage() は、特定の user に、メッセージを送ります。
スクリプトを書いたら、右下の Save ボタンを押すと、コンパイルが実行され、エラーがなければ、save されて実行されます。オブジェクトをクリックして、メニューから Touch を選ぶと、
Object: Touched.
Object: My key is 12345678-90ab-cdef-1234-567890abcdef.
Object: My name is Object.
Object: My owner's key is abcdef12-3456-7890-abcd-ef1234567890.
Object: My owner's name is Kaz Muzik.
Object: Your key is abcdef12-3456-7890-abcd-ef1234567890.
Object: Your name is Kaz Muzik.
|
などと表示されます。
また、オブジェクトのメニューから、Take を選択すると、地面から、自分の inventory に移動されます。Tags: programming, second_life
|
|
Ruby Raven - Java Build with Rake and Gems - KazMuzik Blog
2008-08-24 17:22
8/20/2008 には、Apache Maven を紹介しましたが、今日は、Ruby をベースに、Gems と Rake を利用した Raven を使ってみました。今回も、Fedora 上で行いました。
# yum install ruby
# yum install rubygems
$ rpm -q ruby
ruby-1.8.6.230-4.fc8
$ rpm -q rubygems
rubygems-0.9.4-1.fc8
# gem install rake
# gem install raven
#
$ ls -F /usr/lib/ruby/gems/1.8/gems/raven-1.2.4
bin/ lib/ LICENSE README.rdoc
$ rake -V
rake, version 0.8.1
$ raven import
$ ls -F ~/.raven
cache/ doc/ gems/ m2_central.mvnidx m2_ibiblio.mvnidx specifications/
$
$ cd ~/kazmuzik-pagerank
$ cat rakefile
require 'raven'
require 'rake/clean'
CLEAN.include('target')
task 'default' => 'kazmuzik-pagerank.jar'
dependency 'compile_deps' do |t|
t.deps << ['commons-math']
end
javac 'compile' => 'compile_deps' do |t|
t.build_path << "src/main/java"
end
jar 'kazmuzik-pagerank.jar' => 'compile'
gem_wrap_inst 'gem' => 'kazmuzik-pagerank.jar' do |t|
t.version = '1.0'
end
$ rake kazmuzik-pagerank.jar --trace
(in /home/kazmuzik/kazmuzik-pagerank)
** Invoke kazmuzik-pagerank.jar (first_time)
** Invoke compile (first_time)
** Invoke compile_deps (first_time)
rake aborted!
wrong number of arguments (1 for 0)
...
$ |
yum で、ruby と rubygems をインストールしてから、gems で rake と raven をインストールしました。raven import コマンドで、Maven の repository を取り込むことができます。しかし、rakefile を書いて、rake コマンドを実行するとエラーになってしまいました。
# gem update --system
# yum install ruby-devel
# gem install rcov
$ rake
(in /home/kazmuzik/kazmuzik-pagerank)
rake aborted!
Platform is not a module
/usr/lib/ruby/gems/1.8/gems/raven-1.2.4/lib/raven/gem_init.rb:22
...
$ |
いろいろ試行錯誤してみましたが、Fedora ではどうもうまくいかないようです。
そこで、JRuby とバンドルされているものをダウンロードしてみました。
$ wget http://rubyforge-files.ruby-forum.com/raven/raven-1.2.4-jruby-1.0.1.zip
$ cd /opt
$ unzip ~/raven-1.2.4-jruby-1.0.1.zip
$ ln -s raven-1.2.4-jruby-1.0.1 raven
$ PATH=/opt/raven/bin:$PATH
$ cd ~/kazmuzik-pagerank
$ rake clean
(in /home/kazmuzik/kazmuzik-pagerank)
rm -r target
$ rake
(in /home/kazmuzik/kazmuzik-pagerank)
Using local gem commons-math-commons-math (1.2) to satisfy dependency commons-math
Building path src/main/java
javac -classpath "/home/kazmuzik/.raven/gems/commons-math-commons-math-1.2-java/ext/commons-math-1.2.jar\
:target/classes" -sourcepath "src/main/java" -d target/classes src/main/java/net/kazmuzik/pagerank/*.java
Built jar file kazmuzik-pagerank.jar.
$ ls -F target
classes/ kazmuzik-pagerank.jar
$ |
今度は、一発で、うまくいきました。Tags: programming
|
|
Apache Maven #1 - KazMuzik Blog
2008-08-20 23:45
Build tool は、ずっと ant を使っていますが、maven も使う必要がでてきたため、簡単に紹介しておきます。今回は、試しに、Fedora 8 (64-bit) マシンにセットアップして、PageRank のプログラムに適用してみます。まずは、手っ取り早く、yum で、maven2 パッケージをインストールします。
% sudo yum -y install maven2
...
$ mvn -v
/usr/lib/jvm/java
Maven version: 2.0.4
$ export JAVA_HOME=/usr/java/jdk
$ mvn -v
/usr/java/jdk
Maven version: 2.0.4
$ |
Maven の最新版は 4/10/2008 にリリースされた 2.0.9 ですが、Fedora 8 の repository では 2.0.4 のようです。また、依存関係を利用して、たくさんの JPackage の RPMs もインストールされました。
それでは、PageRank のプロジェクトの skelton を作成してみます。
$ mvn archetype:create -DgroupId=net.kazmuzik.pagerank -DartifactId=kazmuzik-pagerank
...
[INFO] artifact org.apache.maven.plugins:maven-archetype-plugin: checking for updates from central
Downloading: http://repo1.maven.org/maven2/org/apache/maven/plugins/maven-archetype-plugin/2.0-alpha-3/\
maven-archetype-plugin-2.0-alpha-3.pom
4K downloaded
[WARNING] Skipping jpp repository file:///usr/share/maven2/repository in vanilla mode
...
$ ls -l /usr/share/maven2/repository
lrwxrwxrwx 1 root root 15 2008-08-20 23:45 JPP -> /usr/share/java
$ |
kazmuzik-pagerank 以下にファイルは無事作成されましたが、JPP repository の /usr/share/maven2/repository に関する警告がたくさん出ました。そのディレクトリをみると、jar ファイルがたくさんある /usr/share/java ディレクトリへのシンボリックリンクである JPP がひとつだけありました。この後も、同様の警告がよく出ますが、特に問題は内容です。
8/11, 8/14, 8/16 にソースコードを載せた PageRanker.java, PageRankExample1.java, PageRankExample2.java の3つのファイルを、src/main/java/net/kazmuzik/pagerank ディレクトリにコピーして、先頭に、"package net.kazmuzik.pagerank;" とパッケージの宣言を加えました。
また、commons-math への依存性や、Java のバージョンを pom.xml に加えてやりました。
$ cat pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>net.kazmuzik.pagerank</groupId>
<artifactId>kazmuzik-pagerank</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>kazmuzik-pagerank</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-math</groupId>
<artifactId>commons-math</artifactId>
<version>1.2</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
$ mvn compile
...
$ mvn package
...
$ ls -F target
classes/ kazmuzik-pagerank-1.0-SNAPSHOT.jar surefire-reports/ test-classes/
$ ls ~/.m2/repository/commons-math/commons-math/1.2
commons-math-1.2.jar commons-math-1.2.pom
commons-math-1.2.jar.md5 commons-math-1.2.pom.md5
$ |
これで、コンパイル、jar ファイルの作成ができました。なお、依存関係にある commons-math-1.2.jar は、ホームディレクトリの .m2 ディレクトリ以下に、ダウンロードされました。
$ java -classpath target/kazmuzik-pagerank-1.0-SNAPSHOT.jar:\
/home/kazmuzik/.m2/repository/commons-math/commons-math/1.2/commons-math-1.2.jar \
net.kazmuzik.pagerank.PageRankExample1
A 0.0303
B 0.3854
C 0.3438
D 0.0392
E 0.0811
F 0.0392
g 0.0162
h 0.0162
i 0.0162
j 0.0162
k 0.0162
$ |
なお、TECHSCORE に、Apache Maven の日本語の解説があったので、参考にしました。Tags: programming
|
|
Apache Commons Math linear package - PageRank #3 - KazMuzik Blog
2008-08-16 20:41
Java の matrix package として、NIST の Jama を紹介しましたが、Apache Commons にも、Math の linear パッケージに、行列のクラスがあります。
今回は、このうち RealMatrix を使って、PageRank のプログラムを書き直してみます。
import org.apache.commons.math.linear.MatrixUtils;
import org.apache.commons.math.linear.RealMatrix;
public class PageRanker {
...
public double[] solve() {
int len = pages.length;
double[][] a = new double[len][len];
for (int j = 0; j < len; j++) {
int n = 0;
for (int i = 0; i < len; i++) {
n += links[i][j];
}
double v = -1. * dampingFactor / (double)(n==0?(len-1):n);
for (int i = 0; i < len; i++) {
a[i][j] = (i==j)?1.:((double)(n==0?1:links[i][j])*v);
}
}
if (dampingFactor == 1.) {
for (int j = 0; j < len; j++) {
a[0][j] = 1.;
}
}
RealMatrix A = MatrixUtils.createRealMatrix(a);
double[] b = new double[len];
if (dampingFactor == 1.) {
b[0] = 1.;
for (int i = 1; i < len; i++) {
b[i] = 0.;
}
}
else {
double v = (1. - dampingFactor) / (double)len;
for (int i = 0; i < len; i++) {
b[i] = v;
}
}
RealMatrix B = MatrixUtils.createRowRealMatrix(b).transpose();
RealMatrix X = A.solve(B);
double[] pageRanks = new double[len];
for (int i = 0; i < len; i++) {
pageRanks[i] = X.getEntry(i, 0);
}
return pageRanks;
}
} |
基本的には、Jama のときと、ほとんど変わりません。また、行列を扱っているのは、上記の solve() メソッドの中だけなので、PageRankExample1.java などは、そのままです。
$ javac -classpath commons-math-1.2.jar PageRanker.java
$ java -classpath .:commons-math-1.2.jar PageRankExample1
A 0.0303
B 0.3854
C 0.3438
D 0.0392
E 0.0811
F 0.0392
g 0.0162
h 0.0162
i 0.0162
j 0.0162
k 0.0162
$ java -classpath .:commons-math-1.2.jar PageRankExample2
ID=1 0.3035
ID=2 0.1661
ID=3 0.1406
ID=4 0.1054
ID=5 0.1789
ID=6 0.0447
ID=7 0.0607
$ |
もちろん、実行結果も同じです。Tags: programming
|
|
Jama and PageRank #2 - KazMuzik Blog
2008-08-14 22:14
前回(8/11)には、Wikipedia の PageRank のページにある最初の例を使ってみましたが、今回は、ページの中程にある図の例で実行してみます。
public class PageRankExample2 extends PageRanker {
public PageRankExample2() {
super( new String[] {"ID=1", "ID=2", "ID=3", "ID=4",
"ID=5", "ID=6", "ID=7"} );
addLinks();
setDampingFactor(1.0);
}
private void addLinks() {
addLink("ID=1", "ID=2");
addLink("ID=1", "ID=3");
addLink("ID=1", "ID=5");
addLink("ID=1", "ID=6");
addLink("ID=2", "ID=1");
addLink("ID=2", "ID=3");
addLink("ID=2", "ID=4");
addLink("ID=3", "ID=1");
addLink("ID=3", "ID=4");
addLink("ID=3", "ID=5");
addLink("ID=4", "ID=1");
addLink("ID=4", "ID=5");
addLink("ID=5", "ID=1");
addLink("ID=5", "ID=4");
addLink("ID=5", "ID=6");
addLink("ID=5", "ID=7");
addLink("ID=6", "ID=5");
addLink("ID=7", "ID=1");
}
public static void main(String[] args) throws Exception {
PageRanker pr = new PageRankExample2();
double[] ranks = pr.solve();
print(pr.getPages(), ranks);
}
private static void print(String[] pages, double[] ranks) {
int i = 0;
for (String page : pages) {
print(page, ranks[i++]);
}
}
private static void print(String page, double rank) {
System.out.printf("%s\t%.4f%n", page, rank);
}
} |
$ java -classpath .:Jama-1.0.2.jar PageRankExample2
1.000 -1.000 -0.500 -0.000 -0.250 -0.500 -0.000
-0.200 1.000 -0.500 -0.333 -0.000 -0.000 -0.000
-0.200 -0.000 1.000 -0.333 -0.250 -0.000 -0.000
-0.200 -0.000 -0.000 1.000 -0.250 -0.000 -0.000
-0.200 -0.000 -0.000 -0.333 1.000 -0.500 -1.000
-0.000 -0.000 -0.000 -0.000 -0.250 1.000 -0.000
-0.200 -0.000 -0.000 -0.000 -0.000 -0.000 1.000
Exception in thread "main" java.lang.RuntimeException: Matrix is singular.
at Jama.LUDecomposition.solve(LUDecomposition.java:282)
at Jama.Matrix.solve(Matrix.java:815)
at PageRanker.solve(PageRanker.java:59)
at PageRankExample2.main(PageRankExample2.java:33)
$ |
Damping Factor を 1.0 にして実行してみましたが、Exception が発生してしまいました。これは、B の要素がすべて 0 になり、連立方程式が一次独立でなくなったためです。例えば、ひとつ解があれば、すべてをある定数倍しても、解になります。今回の場合は、すべてのページの PageRank の合計が 1.0 という条件があるので、Damping Factor が 1.0 の場合、最初の行を、これに置き換えてみます。
public class PageRanker {
...
public double[] solve() {
int len = pages.length;
Matrix A = new Matrix(len,len);
for (int j = 0; j < len; j++) {
int n = 0;
for (int i = 0; i < len; i++) {
n += links[i][j];
}
double v = -1. * dampingFactor / (double)(n==0?(len-1):n);
for (int i = 0; i < len; i++) {
A.set(i, j, (i==j)?1.:((double)(n==0?1:links[i][j])*v));
}
}
if (dampingFactor == 1.) {
for (int j = 0; j < len; j++) {
A.set(0, j, 1.);
}
}
A.print(5,3);
Matrix B = new Matrix(len,1);
if (dampingFactor == 1.) {
B.set(0, 0, 1.);
for (int i = 1; i < len; i++) {
B.set(i, 0, 0.);
}
}
else {
double v = (1. - dampingFactor) / (double)len;
for (int i = 0; i < len; i++) {
B.set(i, 0, v);
}
}
Matrix X = A.solve(B);
double[] pageRanks = new double[len];
for (int i = 0; i < len; i++) {
pageRanks[i] = X.get(i, 0);
}
return pageRanks;
}
} |
$ java -classpath \
.:Jama-1.0.2.jar \
PageRankExample2
ID=1 0.3035
ID=2 0.1661
ID=3 0.1406
ID=4 0.1054
ID=5 0.1789
ID=6 0.0447
ID=7 0.0607
$ |
|

|
これで、図にある通りの解になりました。Tags: programming
|
|
Jama and PageRank - KazMuzik Blog
2008-08-11 22:48
7/29/2008 には、鶴亀算を解くのに、NIST の Jama (A Java Matrix Package) を使いましたが、せっかくなので、別の用途で使ってみます。今回は、Google の Page Rank を、Wikipedia のページにある例で、計算してみます。
ページの中程にある式で、右辺の第1項のベクトルを B, 第2項の行列に d をかけた行列を A とすると、 となるので、N元連立一次方程式となります。
これを、Jama パッケージを用いて、Java で実装してみます。
import java.util.HashMap;
import java.util.Map;
import Jama.Matrix;
public class PageRanker {
private String[] pages;
private int[][] links;
private Map<String,Integer> pageMap;
private double dampingFactor = 0.85;
public PageRanker(String[] pages) {
this.pages = pages;
links = new int[pages.length][pages.length];
pageMap = new HashMap<String,Integer>();
int i = 0;
for (String page : pages) {
pageMap.put(page, i++);
}
}
public String[] getPages() {
return pages;
}
public void addLink(String pageTo, String pageFrom) {
int i = pageMap.get(pageTo);
int j = pageMap.get(pageFrom);
links[i][j] += 1;
}
public void setDampingFactor(double d) {
dampingFactor = d;
}
public double[] solve() {
int len = pages.length;
Matrix A = new Matrix(len,len);
for (int j = 0; j < len; j++) {
int n = 0;
for (int i = 0; i < len; i++) {
n += links[i][j];
}
double v = -1. * dampingFactor / (double)(n==0?(len-1):n);
for (int i = 0; i < len; i++) {
A.set(i, j, (i==j)?1.:((double)(n==0?1:links[i][j])*v));
}
}
A.print(5,3);
Matrix B = new Matrix(len,1);
double v = (1. - dampingFactor) / (double)len;
for (int i = 0; i < len; i++) {
B.set(i, 0, v);
}
Matrix X = A.solve(B);
double[] pageRanks = new double[len];
for (int i = 0; i < len; i++) {
pageRanks[i] = X.get(i, 0);
}
return pageRanks;
}
} |
addLink() では、配列にリンクの数を加えていくだけで、solve() の中で、Matrix オブジェクトをつくり、解を求めます。
それでは、最初の例で実行してみます。
public class PageRankExample1 extends PageRanker {
public PageRankExample1() {
super( new String[] {"A", "B", "C", "D", "E", "F",
"g", "h", "i", "j", "k"} );
addLinks();
}
private void addLinks() {
addLink("A", "D");
addLink("B", "C");
addLink("B", "D");
addLink("B", "E");
addLink("B", "F");
addLink("B", "g");
addLink("B", "h");
addLink("B", "i");
addLink("C", "B");
addLink("D", "E");
addLink("E", "F");
addLink("E", "g");
addLink("E", "h");
addLink("E", "i");
addLink("E", "j");
addLink("E", "k");
addLink("F", "E");
}
public static void main(String[] args) throws Exception {
PageRanker pr = new PageRankExample1();
double[] ranks = pr.solve();
print(pr.getPages(), ranks);
}
private static void print(String[] pages, double[] ranks) {
int i = 0;
for (String page : pages) {
print(page, ranks[i++]);
}
}
private static void print(String page, double rank) {
System.out.printf("%s\t%.4f%n", page, rank);
}
} |
$ javac -classpath Jama-1.0.2.jar PageRanker.java PageRankExample1.java
$ java -classpath .:Jama-1.0.2.jar PageRankExample1
1.000 -0.000 -0.000 -0.425 -0.000 -0.000 -0.000 -0.000 -0.000 -0.000 -0.000
-0.085 1.000 -0.850 -0.425 -0.283 -0.425 -0.425 -0.425 -0.425 -0.000 -0.000
-0.085 -0.850 1.000 -0.000 -0.000 -0.000 -0.000 -0.000 -0.000 -0.000 -0.000
-0.085 -0.000 -0.000 1.000 -0.283 -0.000 -0.000 -0.000 -0.000 -0.000 -0.000
-0.085 -0.000 -0.000 -0.000 1.000 -0.425 -0.425 -0.425 -0.425 -0.850 -0.850
-0.085 -0.000 -0.000 -0.000 -0.283 1.000 -0.000 -0.000 -0.000 -0.000 -0.000
-0.085 -0.000 -0.000 -0.000 -0.000 -0.000 1.000 -0.000 -0.000 -0.000 -0.000
-0.085 -0.000 -0.000 -0.000 -0.000 -0.000 -0.000 1.000 -0.000 -0.000 -0.000
-0.085 -0.000 -0.000 -0.000 -0.000 -0.000 -0.000 -0.000 1.000 -0.000 -0.000
-0.085 -0.000 -0.000 -0.000 -0.000 -0.000 -0.000 -0.000 -0.000 1.000 -0.000
-0.085 -0.000 -0.000 -0.000 -0.000 -0.000 -0.000 -0.000 -0.000 -0.000 1.000
A 0.0303
B 0.3854
C 0.3438
D 0.0392
E 0.0811
F 0.0392
g 0.0162
h 0.0162
i 0.0162
j 0.0162
k 0.0162
$ |
ちゃんと、Wikipedia のページの例にある数字になりました。
2008-08-14 update -> 例 2Tags: programming
|
|
鶴亀算 & Jama - KazMuzik Blog
2008-07-29 05:15
鶴(crane)と亀(tortoise)があわせて 12匹います。また、足の数は、合計で 34本です。さて、鶴と亀は、それぞれ何匹ずついるでしょうか?
鶴の数を x, 亀の数を y とすると、合計で 12匹なので、
また、足の数の合計が 34本なので、
2 * x + 4 * y = 34 ... (2)
|
という2元1次連立方程式が得られます。
(1), (2) を行列で表現すると、次のようになります。
1 1 x 12
( ) * ( ) = ( ) ... (3)
2 4 y 34
|
NIST (National Institute of Standards and Technology) の Jama (A Java Matrix Package) を使って、解いてみます。
$ cat CraneAndTortoise.java
import Jama.Matrix;
public class CraneAndTortoise {
public static void main(String[] args) throws Exception {
int n = Integer.parseInt(args[0]);
int m = Integer.parseInt(args[1]);
Matrix A = new Matrix( new double[][] {{1.,1.},{2.,4.}} );
print(A);
Matrix B = new Matrix( new double[][] {{(double)n},{(double)m}} );
print(B);
Matrix X = A.solve(B);
print(X);
}
private static void print(Matrix M) {
M.print(2,0);
}
}
$ wget http://math.nist.gov/javanumerics/jama/Jama-1.0.2.jar
$ javac -classpath Jama-1.0.2.jar CraneAndTortoise.java
$ java -classpath .:Jama-1.0.2.jar CraneAndTortoise 12 34
1 1
2 4
12
34
7
5
$ |
鶴(crane)が 7羽、亀(tortoise)が 5匹と、解が求まりました。Tags: programming
|
|
opencsv - KazMuzik Blog
2008-04-06 18:53
Java のプログラムで、データを、CSV (comma-separated values) 形式でファイルから読み込んだり、ファイルに書いたりすることは、そんなに頻繁ではなくても、たまに必要になることがあります。だいたいは、その場で、in.readLine().split(","); のようなコードを書いて、すますことが多いのですが、データに','(comma) が含まれていたり、'"'(quote) で囲まれていたときにデータの '"' をエスケープしたり、またデータが複数行にわたっていた場合の処理などがあると、汎用的な parser を書くのはけっこうめんどうになってきます。
そこで、ちょこっと探していたところ、opencsv というのがありました。
import java.io.InputStreamReader;
import java.util.Arrays;
import au.com.bytecode.opencsv.CSVReader;
public class CSVReaderTest {
public static void main(String[] args) throws Exception {
CSVReader in = new CSVReader(new InputStreamReader(System.in, "UTF-8"));
while (true) {
String[] values = in.readNext();
if (values == null) {
break;
}
System.out.println( Arrays.toString(values) );
}
in.close();
}
} |
このような感じで、手軽に使えます。
また、SQL の ResultSet を、CSV に dump したり、CSV ファイルの、ヘッダー情報を利用して、JavaBean を作成することも、簡単にできるようです。Tags: programming
|
|
|
|
sampo nutch project #4 - FilterReader(s) - KazMuzik Blog
2008-03-16 18:46
今日は、LiveJournal.com の HTML を、KazMuzik.net 用に変換するために開発した、java.io.FilterReader の subclass(es) を紹介します。
以前は、HTML を変換するクラスを、ひとつにまとめて書いていましたが、いろんな機能を追加していくと、だんだん parse が入り組んできて、複雑になり、debug が困難になってくるため、今回は、機能ごとに単純な FilterReader を実装して、それらを chain で組み合わせて、使うことにしました。Unix コマンドを、pipe でつなぐような感じです。実際、grep や sed で十分な機能もありましたが、Java から使う必要があったため、実装してしまいました。
これらは、net.java.sampo.util.io パッケージとしました。まずは、FileFilterReader クラスですが、これは、最初に入力(ファイルなど)をすべて読み込んで、filter の処理をした character array を作成してしまう FilterReader 用の super class で、abstract になっています。実際のクラスのひとつが、TextReplacerFilterReader で、前回、紹介した TextRepalcer ユーティリティクラスを用いて、指定した文字列で挟まれた部分を、別の文字列で置き換える FilterReader です。Constructor には、複数の TextReplacer オブジェクトを与えることができます。
上記は、最初に入力 stream をすべて読み込みますが、LineFilterReader は、一行ずつ読み込んで、filter 処理をするための abstract superclass です。例えば、空行をスキップするとか、のような処理に適しています。中で、readLine() を使うために、BufferedReader を使っているところが、いまいちですが、今後の課題としておきます。
実際の使用例は、LiveJournal.com から KazMuzik.net への conversion を sample project として、ぼちぼち紹介していく予定です。Tags: programming
|
|
sampo nutch project #3 - TextReplacer and RemoveAllTagsCommand - KazMuzik Blog
2008-03-15 13:56
LiveJournal.com から、ブログを移行する際、URL や、Ads を置き換えたり、不要な部分を削除したり、しています。いったん、Nutch segment にstore された content を変換していますが、エントリだけで 900近くなり、その他に、Tag のページなどを含めると、1,000近いページになります。このため、手作業では不可能なため、主に、Java でプログラムを書いて、変換しています。最初は手っ取り早く、ハードコードして、すませていたのですが、汎用的なクラスは、整理して、sampo project に入れていくことにしました。
今回は、TextReplacer です。これは、指定した文字列で挟まれた部分を、別の文字列で置き換える utility class です。例えば、new TextReplacer("<script", "</script>", "<!-- SCRIPT REMOVED -->").replace(text) とすると、text から script タグが削除されて、コメントが挿入されます。また、コンストラクタの 2番目のパラメータを null, あるいは省力すると、1番目のパラメータで指定された文字列が置き換えられます。標準では、String クラスの toLowerCase() を使い、大文字、小文字を区別しないで、マッチさせていますが、最後の boolean フラグを false にすることにより、大文字、小文字を区別することができます。
RemoveAllTagsCommand は、new TextReplacer("<", ">", "", false) を用いて、ファイルから、HTML や XML のタグをすべて取り除く Command クラスです。Tags: programming
|
|
sampo nutch project #2 - URLFileMap - KazMuzik.net Project #7 - KazMuzik Blog
2008-03-11 03:13
Nutch segment に store されている LiveJournal.com のページを parse するプログラム を改良してきましたが、だいぶん落ち着いてきました。まだ、手っ取り早く、このブログ用にハードコードしている部分もありますが、その部分は、徐々に、外部の properties や configuration ファイルに移しながら、忘れないうちに、sampo の CVS Repository に commit していこうと考えています。
まずは、ContentExtractor ですが、新規に開発した URLFileMap を利用するようにしました。これで、dynamic に生成されたページのパターンの URL も、指定したファイル名に対応させることができるようになりました。
$ cat lj.sh
#!/bin/sh
L=$SAMPO_HOME/lib
CP=$L/sampo-nutch.jar:$L/hadoop-0.12.2-core.jar:$L/nutch-0.9.jar\
:$L/commons-logging-1.1.1.jar:$L/log4j-1.2.15.jar:$L/commons-io-1.3.2.jar
CN=net.java.sampo.nutch.util.ContentsExtractor
S=`ls -d kazmuzik-segment-$1/20* | tail -1`
U="http://kazuomik.livejournal.com/"
D=$2
java -classpath $CP $CN $S $U $D
$ sh lj.sh 20080309 lj
$ |
まだ、URLFileMap を有効利用していませんが、上記の ContentExtractor は byte[] をそのままファイルに保存するだけなので、character based の parser を紹介するときに、使用例なども update したいと思います。Tags: programming
|
|
sampo nutch project - Kaz Muzik Blog Backup Project #32 - KazMuzik Blog
2008-03-04 19:38
昨日(3/3)は、11/17, 2/17 と同様の手順で、Nutch の segment に、このブログの backup をとりました。今日は、それから、derby の database を作成しないで、直接、ファイルを作成してみました。このため、LiveJournalEntryExtractor.java を元に、Nutch segment から、すべての content をそのまま file に書き出す ContentsExtractor クラス を作成して、java.net の sampo プロジェクトに、sampo-nutch subproject として、commit しました。
$ cvs -d :pserver:username@cvs.dev.java.net:/cvs login
$ cvs -d :pserver:username@cvs.dev.java.net:/cvs checkout sampo
$ cd sampo/nutch
$ export JAVA_HOME=/usr/java/jdk1.6.0_04
$ export SAMPO_HOME=/var/sampo
$ export NUTCH_HOME=/usr/local/nutch-0.9
$ export PATH=$SAMPO_HOME/bin:$JAVA_HOME/bin:$PATH
$ ant init
$ ant -DSAMPO_HOME=$SAMPO_HOME install
$ cd $NUTCH_HOME
$ mkdir lj
$ vi lj.sh
$ cat lj.sh
#!/bin/sh
L=$SAMPO_HOME/lib
CP=$L/sampo-nutch.jar:$L/hadoop-0.12.2-core.jar:$L/nutch-0.9.jar\
:$L/commons-logging-1.1.1.jar:$L/log4j-1.2.15.jar:$L/commons-io-1.3.2.jar
CN=net.java.sampo.nutch.util.ContentsExtractor
S=`ls -d $1/20* | tail -1`
D=$2
java -classpath $CP $CN $S $D
$ sh lj.sh kazmuzik-segment-20080303 lj
$ |
これで、lj ディレクトに、Nutch segment にある content(s) が、(HTML)ファイルとして、保存されました。Tags: programming
|
|
sampo language project #7 - KazMuzik Blog
2008-02-29 19:39
前回の language module を使うことにより、sampo プロジェクトのコードとは無関係に、libtextcat で利用することができるようになりました。しかし、私も別のプロジェクトで使ってみたのですが、まだまだ改善すべき点は、多々あります。
まずは、サポートする言語の数が多いほど、誤認識する可能性が高くなります。特に、article 数の少ない言語は、メインページがあまり充実していないため、英語も含まれていたりして、このため、英語のテキストを classify すると、これらの言語が [en.UTF-8][en.ISO-8859-15]の他に、混ざってくることがあります。特に、Herero (hz) のメインページは、この言語の wikipedia は close された、ということが英語で記述されています。このようなページから作った language module は、役に立たないばかりか、誤認識の原因となるので、削除しなければいけません。
このため、WikipediaLanguage に、selectLanguages() という static method を実装して、article 数と、言語コードの長さによって、filter できるようにしました。例えば、WikipediaLanguage.selectLanguages(10000, 2) により、10,000 以上の article をもち、Alpha-2 の ISO 639-1 のコードをもつ 66言語だけを得ることができます。これを、Chain から使えるように、SelectWikipediaLanguagesCommand を用意して、ExtractAllWikipediaTextsCommand を変更しました。
$ cvs -d :pserver:username@cvs.dev.java.net:/cvs login
$ cvs -d :pserver:username@cvs.dev.java.net:/cvs checkout sampo
$ cd sampo/language
$ export JAVA_HOME=/usr/java/jdk1.6.0_04
$ export SAMPO_HOME=/var/sampo
$ export PATH=$SAMPO_HOME/bin:$JAVA_HOME/bin:$PATH
$ ant init
$ ant compile
$ ant -DSAMPO_HOME=$SAMPO_HOME install
$ cd $SAMPO_HOME
$ rm -rf wikipedia-mainpage-text LM
$ wikipedia-mainpage-text.sh 10000 2
$ create-local-encoding-texts.sh
$ wikipedia-mainpage-createfp.sh
$ cd wikipedia-mainpage-text
$ test-jni-libtextcat.sh en.*.txt ja.*.txt
en.ISO-8859-15.txt [en.ISO-8859-15]
en.UTF-8.txt [en.ISO-8859-15][en.UTF-8]
ja.EUC-JP.txt [ja.EUC-JP]
ja.ISO-2022-JP.txt [ja.ISO-2022-JP]
ja.Shift_JIS.txt [ja.Shift_JIS]
ja.UTF-8.txt [ja.UTF-8]
ja.EUC-JP.txt [ja.EUC-JP]
ja.ISO-2022-JP.txt [ja.ISO-2022-JP]
ja.Shift_JIS.txt [ja.Shift_JIS]
ja.UTF-8.txt [ja.UTF-8]
$ |
今回のツールを使用しなくても、直接 config file を edit してもいいのですが、実際のアプリケーションに最適な language module を作成するために、いろいろな config file で試したかったため、実装してみました。Tags: programming
|
|
sampo TextCategorizer with libtextcat #2 - KazMuzik Blog
2008-02-27 22:08
昨日の 1.1 では、native method を介して、long の handle を渡していましたが、今日の 1.2 では、C言語による実装の中で、直接 handle をアクセスするように変更しました。これにより、native method の interface が簡単になりました。しかし、これらは private なので、public な interface には変更はありません。
また、昨日の実行例では、textcat_Classify() が、empty string "" を返したり、実行の度に、結果が変わったりと安定していませんでした。これは、Wikipedia のメインページから抜き出したテキストが数千バイトあり、これをすべて textcat_Classify() に渡していたためで、最初の 1024 bytes だけにしたところ、testtextcat と同様の結果が得られるようになりました。判別する文字列は、長ければ良い、ということでもないようです。
$ test-jni-libtextcat.sh ja.*.txt ja.*.txt en.*.txt zh.*.txt
ja.EUC-JP.txt [ja.EUC-JP]
ja.ISO-2022-JP.txt [ja.ISO-2022-JP]
ja.Shift_JIS.txt [ja.Shift_JIS]
ja.UTF-8.txt [ja.UTF-8]
ja.EUC-JP.txt [ja.EUC-JP]
ja.ISO-2022-JP.txt [ja.ISO-2022-JP]
ja.Shift_JIS.txt [ja.Shift_JIS]
ja.UTF-8.txt [ja.UTF-8]
en.ISO-8859-15.txt [en.ISO-8859-15]
en.UTF-8.txt [en.ISO-8859-15][en.UTF-8]
zh.Big5.txt [zh.Big5]
zh.GB18030.txt [zh.GB18030]
zh.GB2312.txt [zh.GB2312]
zh.UTF-8.txt [zh.UTF-8]
$ |
Tags: programming
|
|
sampo TextCategorizer with libtextcat via JNI - KazMuzik Blog
2008-02-26 21:35
前回までで、Wikipedia のメインページから、libtextcat 用の language module(s) を作成しましたが、libtextcat 自体は、C言語から利用するライブラリなので、このままでは Java からは利用できません。今日は、これを JNI (Java Native Interface) を用いて、Java から呼び出してみます。
その前に、libtextcat の開発環境を用意する必要があります。yum で libtextcat package を install すると、実行環境だけなので、header file(s) が含まれていません。
# yum install libtextcat-devel
$ rpm -q libtextcat-devel
libtextcat-devel-2.2-4.fc8
libtextcat-devel-2.2-4.fc8
$ rpm -ql libtextcat-devel
/usr/bin/createfp
/usr/include/libtextcat
/usr/include/libtextcat/common.h
/usr/include/libtextcat/constants.h
/usr/include/libtextcat/fingerprint.h
/usr/include/libtextcat/textcat.h
/usr/include/libtextcat/utf8misc.h
/usr/lib64/libtextcat.so
/usr/bin/createfp
/usr/include/libtextcat
...
/usr/lib/libtextcat.so
$ |
libtextcat のページからは、JTextCat 0.1 へのリンクがあり、ダウンロードもできます。これも、JNI を利用していますが、JTextCat.java では、class method として、classify() が native として宣言されているだけです。JTextCat.c の実装で、libtextcat の関数を続けて 3つ呼び出していますが、これだと、毎回 textcat_Init() でハンドラを取得して、textcat_Classify() で文字列を分類した後に、textcat_Done() でリソースを開放しているので、何回でも classify() を実行したい場合には、textcat_Init() と textcat_Done() のオーバーヘッドがあります。
そこで、sampo プロジェクトの一環として、自作することにしました。まずは、TextCategorizer.java を書きました。Constructor から native method の libTextCatInit() を呼び、native で textcat_Init() を呼びハンドラを取得します。同様に、独立した libTextCatClassify() libTextCatDone() native method(s) を用意しています。これから、javac コマンドで class file を作成します。Class file からは、javah コマンドで、C言語の header file を作成します。これを元に、C言語の関数を実装します。基本的には、native method に対応した libtextcat の関数を呼び出すだけですが、パラメータの渡し方など、JNI 特有の決まり事があります。あとは、コンパイルして、dynamic library を作成します。一応、サンプルのスクリプトも用意しておきました。
$ cvs -d :pserver:username@cvs.dev.java.net:/cvs login
$ cvs -d :pserver:username@cvs.dev.java.net:/cvs checkout sampo
$ cd sampo/language
$ export JAVA_HOME=/usr/java/jdk1.6.0_04
$ export SAMPO_HOME=/var/sampo
$ export PATH=$SAMPO_HOME/bin:$JAVA_HOME/bin:$PATH
$ ant init
$ ant compile
$ sh bin/javah.sh
$ sh bin/jnicc.sh
$ ant -DSAMPO_HOME=$SAMPO_HOME install
$ test-jni-libtextcat.sh ja.*.txt en.*.txt zh.*.txt
ja.EUC-JP.txt
ja.ISO-2022-JP.txt [ja.ISO-2022-JP]
ja.Shift_JIS.txt [ja.Shift_JIS]
ja.UTF-8.txt [ja.UTF-8]
en.ISO-8859-15.txt [en.ISO-8859-15]
en.UTF-8.txt [en.UTF-8]
zh.Big5.txt
zh.GB18030.txt
zh.GB2312.txt [zh.GB2312]
zh.UTF-8.txt [zh.UTF-8]
$ |
libtextcat_Init() が返すハンドラは、ポインタ(void *)ですが、TextCategorizer オブジェクトの中では、long (64-bit integer) に保存しました。最初は、int (32-bit) に保存していたのですが、Fedora 8 の 32-bit 版(i386)では、うまく動いていたのに、64-bit 版(x86_64) では、エラーが発生してしてしまい、デバッグに手間取ってしまいました。
2008-02-27 update 上記の実行例には、バグがありました。 -> sampo TextCategorizer with libtextcat #2Tags: programming
|
|
sampo java.net project and libTextCat #2 - KazMuzik Blog
2008-02-25 08:01
前回は、UTF-8 だけをサポートしましたが、今回は、Shift_JIS や EUC-JP, また ISO-8859-1 などの character encoding もサポートしてみます。このために、追加する言語とエンコーディングの組み合わせを与えるコンフィグレーションファイルを用意します。
en.ISO-8859-15
es.ISO-8859-15
de.ISO-8859-15
fr.ISO-8859-15
it.ISO-8859-15
po.ISO-8859-15
cs.ISO-8859-2
sl.ISO-8859-2
bg.ISO-8859-5
ru.ISO-8859-5
ar.ISO-8859-6
el.ISO-8859-7
he.ISO-8859-8
ja.EUC-JP
ko.EUC-KR
ja.ISO-2022-JP
ko.ISO-2022-KR
ja.Shift_JIS
zh.GB18030
zh.GB2312
zh.Big5
ru.KOI8-R
uk.KOI8-R
|
今回は、オリジナルの libTextCat のリストや、IANA (Internet Assigned Numbers Authority) に登録されている character set のリスト、また、Java でサポートされているエンコーディングのリストを参考に、上記の最小限にしておきました。
また、wikipedia-mainpage-createfp.sh では、言語コードが、アルファベット 2 あるいは 3文字のものだけをサポートするようにしました。
$ create-local-encoding-texts.sh
$ wikipedia-mainpage-createfp.sh
$ cd $SAMPO_HOME/wikipedia-mainpage-text
$ testtextcat.sh en.*.txt
en.ISO-8859-15.txt
TextCat 2.2 (out of place)
Result == [en.ISO-8859-15]
That took 5 ms.
en.UTF-8.txt
TextCat 2.2 (out of place)
Result == [en.ISO-8859-15][en.UTF-8]
That took 5 ms.
en.ISO-8859-15.txt
TextCat 2.2 (out of place)
Result == [en.ISO-8859-15]
That took 5 ms.
en.UTF-8.txt
TextCat 2.2 (out of place)
Result == [en.ISO-8859-15][en.UTF-8]
That took 5 ms.
$ testtextcat.sh ja.*.txt
ja.EUC-JP.txt
TextCat 2.2 (out of place)
Result == [ja.EUC-JP]
That took 3 ms.
ja.ISO-2022-JP.txt
TextCat 2.2 (out of place)
Result == [ja.ISO-2022-JP]
That took 4 ms.
ja.Shift_JIS.txt
TextCat 2.2 (out of place)
Result == [ja.Shift_JIS]
That took 4 ms.
ja.UTF-8.txt
TextCat 2.2 (out of place)
Result == [ja.UTF-8]
That took 3 ms.
$ testtextcat.sh zh*.txt
zh.Big5.txt
TextCat 2.2 (out of place)
Result == [zh.Big5]
That took 4 ms.
zh-classical.UTF-8.txt
TextCat 2.2 (out of place)
Result == [zh.UTF-8]
That took 4 ms.
zh.GB18030.txt
TextCat 2.2 (out of place)
Result == [zh.GB18030]
That took 4 ms.
zh.GB2312.txt
TextCat 2.2 (out of place)
Result == [zh.GB2312]
That took 4 ms.
zh-min-nan.UTF-8.txt
TextCat 2.2 (out of place)
Result == [hak.UTF-8]
That took 6 ms.
zh.UTF-8.txt
TextCat 2.2 (out of place)
Result == [zh.UTF-8]
That took 4 ms.
zh-yue.UTF-8.txt
TextCat 2.2 (out of place)
Result == [zh.UTF-8]
That took 4 ms.
$ |
今回も、fingerprint を作成したテキストでテストしているため、ほとんど正しく認識されています。特に、日本語(ja)に関しては、EUC-JP, ISO-2022-JP, Shift_JIS, UTF-8 のすべてでうまくいっています。英語(en)の UTF-8 のファイルでは、UTF-8 か ISO-8859-15 か判定できませんでしたが、これはオリジナルの HTML に、ほんのいくつか   が含まれていたためで、完全な US-ASCII ではありませんでしたが、誤差の範囲と言えますし、実質的に大きな問題になることは、ないと思います。
中国語(Chinese, zh)に関しては、今回は、あまり真面目に取り扱っていません。表記上は、中国本土で主に使われている Simplified Chinese と、台湾や香港などで使われている Traditional Chinese があり、システムやアプリケーションによっては、国コードも含めて、それぞれ zh_CN, zh_TW と扱う場合もありますが、厳密に言えば、これは言語の分類ではありませんし、ISO 639 や Wikipedia でも、このアプローチはとっていないようです。これについては、とりあえず、後ほどの課題として残しておきます。Tags: computer_technology, programming
|
|
sampo java.net project and libTextCat - KazMuzik Blog
2008-02-24 21:24
sampo project に、Wikipedia 関連の script(s) を加え、build.xml に install ターゲットを加えました。
$ cvs -d :pserver:username@cvs.dev.java.net:/cvs login
$ cvs -d :pserver:username@cvs.dev.java.net:/cvs checkout sampo
$ cd sampo/language
$ export JAVA_HOME=/usr/java/jdk1.6.0_04
$ export SAMPO_HOME=/var/sampo
$ export PATH=$SAMPO_HOME/bin:$JAVA_HOME/bin:$PATH
$ ant -DSAMPO_HOME=$SAMPO_HOME install
$ wikipedia-mainpage-html.sh
$ wikipedia-mainpage-text.sh
$ |
これで、/var/sampo ($SAMPO_HOME) の bin に script ファイルが、lib に jar ファイルがコピーされ、2つの script(s) の実行により、/var/sampo/wikipedia-mainpage-html に 256言語のメインページが ja.html などの名前でダウンロードされ、/var/sampo/wikipedia-mainpage-text に、そこから抽出されたテキストが ja.UTF-8.txt などの名前で保存されます。
この 256言語のテキストファイルと、約1年前の 3/3/2007 に紹介した libTextCat と組み合わせて、言語の判別をしてみます。
# yum install libtextcat
$ rpm -q libtextcat
libtextcat-2.2-4.fc8
libtextcat-2.2-4.fc8
$ rpm -ql libtextcat
/usr/lib64/libtextcat.so.0
/usr/lib64/libtextcat.so.0.0.0
/usr/share/doc/libtextcat-2.2
/usr/share/doc/libtextcat-2.2/ChangeLog
/usr/share/doc/libtextcat-2.2/LICENSE
/usr/share/doc/libtextcat-2.2/README
/usr/share/doc/libtextcat-2.2/TODO
/usr/share/libtextcat
/usr/share/libtextcat/afrikaans.lm
...
/usr/share/libtextcat/finnish.lm
/usr/share/libtextcat/fpdb.conf
/usr/share/libtextcat/french.lm
...
$ cat /usr/share/libtextcat/fpdb.conf
...
italian.lm it--utf8
japanese.lm ja--utf8
korean.lm ko--utf8
...
$ |
Fedora 8 では、yum で libtextcat-2.2-4.fc8 がインストールできました。しかし、インストールされるのは、ライブラリと、サンプルの language module (N-Gram の fingerprint)だけです。/usr/share/libtextcat/fpdb.conf がコンフィグレーションファイルで、UTF-8 で 68言語サポートされています。testtextcat と createfp スクリプトがないので、3/3/3007 のように、オリジナルのサイトから、準備します。
$ wget http://software.wise-guys.nl/download/libtextcat-2.2.tar.gz
$ tar zxvpf libtextcat-2.2.tar.gz
$ cd libtextcat-2.2
$ ./configure --prefix=/usr
$ make
$ cd src
$ cp testtextcat createfp $SAMPO_HOME/bin
$ cd /usr/share/libtextcat
$ cat $SAMPO_HOME/wikipedia-mainpage-text/en.UTF-8.txt | testtextcat fpdb.conf
TextCat 2.2 (out of place)
Result == [en--utf8]
That took 2 ms
$ cat $SAMPO_HOME/wikipedia-mainpage-text/ja.UTF-8.txt | testtextcat fpdb.conf
TextCat 2.2 (out of place)
Result == [mr--utf8][am--utf8][yi--utf8]
That took 2 ms.
$ |
英語は正しく認識されましたが、日本語は見事に失敗しました。
次は、Wikikpedia のメインページから、language modul を作成して、テストしてみます。
$ wikipedia-mainpage-createfp.sh
$ cd $SAMPO_HOME/wikipedia-mainpage-text
$ testtextcat.sh *.txt
aa.UTF-8.txt
TextCat 2.2 (out of place)
Result == [aa.UTF-8]
That took 3 ms.
ab.UTF-8.txt
TextCat 2.2 (out of place)
Result == [ab.UTF-8]
That took 3 ms.
af.UTF-8.txt
TextCat 2.2 (out of place)
Result == [af.UTF-8]
That took 4 ms.
...
ja.UTF-8.txt
TextCat 2.2 (out of place)
Result == [ja.UTF-8]
That took 3 ms.
...
zu.UTF-8.txt
TextCat 2.2 (out of place)
Result == [zu.UTF-8]
That took 2 ms.
$ |
Fingerprint を作成したテキストファイルを判別したため、当然ですが、今度はほとんどがただしく認識されました。Tags: computer_technology, programming
|
|
Wikipedia Languages - KazMuzik Blog
2008-02-23 22:29
2/22 には、ISO 639 について紹介し、639-3 には 7700 言語が登録されていると書きましたが、実際にはどのくらい active に使われているのかと思い、多くの言語で記事が投稿されている Wikipedia を調べてみました。Wikimedia の Meta-Wiki には、List of Wikipedias のページがあり、256 言語がリストされていました。1番はもちろん英語(English, en)で、2,240,735 の記事があります。以下、ドイツ語(Deutsch, de)、フランス語(Français, fr)、ポーランド語(Polski, pl)と続き、日本語(Japanese, ja)は、5位で、469,919 articles あります。言語コードは、可能な限り、ISO 639-1 のアルファベット2文字を使っていますが、34位には、ISO 638-2 で new のネパール・バサ語(Nepal Bhasa, नेपाल भाषा)が現れています。
java.net の "sampo" project の language package には、WikipediaLanguage.java をはじめ、各言語のメインページを parse して、その言語のテキストを抜き出すクラスや、ツールを commit しておきました。tool のクラスは、後ほど、便利なように、Apache Commons Chain の Command として実装してあります。Tags: computer_technology, programming
|
|
|
|
"sampo" project at java.net - KazMuzik Blog
2008-02-21 19:48
2/19 には、久しぶりに、programming tag のエントリに、ソースコードを載せましたが、実際、そこまで読んでくれている人は少ない(というよりも、まったくいないかも ?)と思います。このため、去年からいろいろ考えていたのですが、Thanksgiving の連休あたりに、java.net のプロジェクトにして、そこの CVS Repository を利用することにしました。プロジェクト名を付けなければいけなかったので、当時、毎週末、がんばっていた walking (散歩) にちなんで、sampo と名付けて申請しました。プロジェクト自体は、すぐに approve されたのですが、他のプロジェクトで忙しかったのもあり、3ヶ月放ったらかしに、なっていました。
今日は、最近書いていた GNIS や Census 2000 の基本的なクラスを、CVS Repository に add して commit しておきました。最初は、src の下に、すぐに、net/java/sampo などと package に対応した path で directory を作っていたのですが、他のプロジェクトも参考にして、src の下には、プログラミング言語を示す、java というディレクトリを作成し、その下に、net/java/sampo/... としました。しかし、ディレクトリを add するときに間違って、src/net/java/sampo/... というのを作ってしまったため、ファイルは remove したのですが、ディレクトリは残ってしまいました。CVS の場合、ディレクトリの add は、同時に commit もされ、また、後から remove はできないので、格好悪いのですが、どうしようもありません。一応、build.xml の init ターゲットで、余分はディレクトリは削除するようにしておきました。
今後は、なるべくこのブログには、ソースコードを載せずに、sampo.dev.java.net を参照するようにします。なお、上記の usgov の他に、language もありますが、これは、別途、紹介します。Tags: programming
|
|
FIPS55 State - KazMuzik Blog
2008-02-19 23:54
2/15 に、U.S. Government 発行のデータの parser について、書きましたが、まずは、State のクラスを載せます。50州と、District of Columbia, それに、Puerto Rico や Guam などを含めても 60 なので、FIPS PUB 55-3 のデータを static に持つことにしました。
package net.java.sampo.usgov.fips;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
public class State implements Serializable, Comparable<State> {
private int seq;
private String fipsCode;
private String alphaCode;
private String name;
private State(int seq, String fipsCode, String alphaCode, String name) {
this.seq = seq;
this.fipsCode = fipsCode;
this.alphaCode = alphaCode;
this.name = name;
}
public int getSequenceNumber() {
return seq;
}
public String getFipsCode() {
return fipsCode;
}
public String getAlphaCode() {
return alphaCode;
}
public String getName() {
return name;
}
public int compareTo(State other) {
return seq - other.seq;
}
// http://www.itl.nist.gov/fipspubs/fip55-3.htm
public static final State AL = new State( 1, "01", "AL", "Alabama");
public static final State AK = new State( 2, "02", "AK", "Alaska");
public static final State AZ = new State( 3, "04", "AZ", "Arizona");
public static final State AR = new State( 4, "05", "AR", "Arkansas");
public static final State CA = new State( 5, "06", "CA", "California");
public static final State CO = new State( 6, "08", "CO", "Colorado");
public static final State CT = new State( 7, "09", "CT", "Connecticut"); // ?
public static final State DE = new State( 8, "10", "DE", "Delaware");
public static final State DC = new State( 9, "11", "DC", "District of Columbia");
public static final State FL = new State(10, "12", "FL", "Florida");
public static final State GA = new State(11, "13", "GA", "Georgia");
public static final State HI = new State(12, "15", "HI", "Hawaii");
public static final State ID = new State(13, "16", "ID", "Idaho");
public static final State IL = new State(14, "17", "IL", "Illinois");
public static final State IN = new State(15, "18", "IN", "Indiana");
public static final State IA = new State(16, "19", "IA", "Iowa");
public static final State KS = new State(17, "20", "KS", "Kansas");
public static final State KY = new State(18, "21", "KY", "Kentucky");
public static final State LA = new State(19, "22", "LA", "Louisiana");
public static final State ME = new State(20, "23", "ME", "Maine");
public static final State MD = new State(21, "24", "MD", "Maryland");
public static final State MA = new State(22, "25", "MA", "Massachusetts");
public static final State MI = new State(23, "26", "MI", "Michigan");
public static final State MN = new State(24, "27", "MN", "Minnesota");
public static final State MS = new State(25, "28", "MS", "Mississippi");
public static final State MO = new State(26, "29", "MO", "Missouri");
public static final State MT = new State(27, "30", "MT", "Montana");
public static final State NE = new State(28, "31", "NE", "Nebraska");
public static final State NV = new State(29, "32", "NV", "Nevada");
public static final State NH = new State(30, "33", "NH", "New Hampshire");
public static final State NJ = new State(31, "34", "NJ", "New Jersey");
public static final State NM = new State(32, "35", "NM", "New Mexico");
public static final State NY = new State(33, "36", "NY", "New York");
public static final State NC = new State(34, "37", "NC", "North Carolina");
public static final State ND = new State(35, "38", "ND", "North Dakota");
public static final State OH = new State(36, "39", "OH", "Ohio");
public static final State OK = new State(37, "40", "OK", "Oklahoma");
public static final State OR = new State(38, "41", "OR", "Oregon");
public static final State PA = new State(39, "42", "PA", "Pennsylvania");
public static final State RI = new State(40, "44", "RI", "Rhode Island");
public static final State SC = new State(41, "45", "SC", "South Carolina");
public static final State SD = new State(42, "46", "SD", "South Dakota");
public static final State TN = new State(43, "47", "TN", "Tennessee");
public static final State TX = new State(44, "48", "TX", "Texas");
public static final State UT = new State(45, "49", "UT", "Utah");
public static final State VT = new State(46, "50", "VT", "Vermont");
public static final State VA = new State(47, "51", "VA", "Virginia");
public static final State WA = new State(48, "53", "WA", "Washington");
public static final State WV = new State(49, "54", "WV", "West Virginia");
public static final State WI = new State(50, "55", "WI", "Wisconsin");
public static final State WY = new State(51, "56", "WY", "Wyoming");
//
public static final State AS = new State(52, "60", "AS", "American Samoa");
public static final State GU = new State(54, "66", "GU", "Guam");
public static final State MP = new State(56, "69", "MP", "Northern Mariana Islands");
public static final State PR = new State(58, "72", "PR", "Puerto Rico");
public static final State UM = new State(59, "74", "UM", "U.S. Minor Outlying Islands");
public static final State VI = new State(60, "78", "VI", "Virgin Islands of the U.S.");
//
public static final State FM = new State(53, "64", "FM", "Federated States of Micronesia");
public static final State MH = new State(55, "68", "MH", "Marshall Islands");
public static final State PW = new State(57, "70", "PW", "Palau");
private static final State[] allStates = new State[] {
AL, AK, AZ, AR, CA, CO, CT, DE, DC, FL,
GA, HI, ID, IL, IN, IA, KS, KY, LA, ME,
MD, MA, MI, MN, MS, MO, MT, NE, NV, NH,
NJ, NM, NY, NC, ND, OH, OK, OR, PA, RI,
SC, SD, TN, TX, UT, VT, VA, WA, WV, WI,
WY, AS, FM, GU, MH, MP, PW, PR, UM, VI
};
public static State[] getAllStates() {
return allStates;
}
private static State[] fipsStates = new State[79];
private static Map<String,State> alphaMap = new HashMap<String,State>();
static {
for (State state : allStates) {
fipsStates[getIntFipsCode(state.getFipsCode())] = state;
alphaMap.put(state.getAlphaCode(), state);
}
}
private static int getIntFipsCode(String fipsCode) {
if (fipsCode == null || fipsCode.length() != 2) {
return -1;
}
int m = fipsCode.charAt(0) - '0';
if (m < 0 || m > 9) {
return -1;
}
int n = fipsCode.charAt(1) - '0';
if (n < 0 || n > 9) {
return -1;
}
return m * 10 + n;
}
public static State getStateByFipsCode(String fipsCode) {
int n = getIntFipsCode(fipsCode);
if (n < 1 || n >= fipsStates.length) {
return null;
}
return fipsStates[n];
}
public static State getStateByAlphaCode(String alphaCode) {
return alphaMap.get(alphaCode);
}
} |
2008-02-21 update -> "sampo" project at java.net -> net.java.sampo.usgov.fips.State.javaTags: programming
|
|
Spring Framework JdbcTemplate - USCIS Processing Times #7 - KazMuzik Blog
2007-12-18 21:14
前回、予告したように、今回からデータベースを用いることにします。12/12 に紹介した Spring Framework から、今回は、JdbcTemplate を用います。
前回の ProcessingTime オブジェクトに対応するテーブルを作成します。
package net.java.sampo.immigration.processingTime.db;
import javax.sql.DataSource;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.apache.commons.chain.Command;
import org.apache.commons.chain.Context;
import org.apache.commons.chain.impl.ContextBase;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class CreateTableCommand implements Command {
private static Log log = LogFactory.getLog(CreateTableCommand.class);
private static final String sql
= "create table PROCESSINGTIME ("
+ "FACILITY varchar(32),"
+ "POSTEDDATE timestamp,"
+ "LINENO integer,"
+ "FORM varchar(8),"
+ "TITLE varchar(128),"
+ "CLASSIFICATION varchar(128),"
+ "TIMEFRAME varchar(32),"
+ "PROCESSINGDATE timestamp,"
+ "DAYS integer,"
+ "primary key(FACILITY, POSTEDDATE, LINENO))";
private JdbcTemplate jdbcTemplate;
public CreateTableCommand() {
}
public void setDataSource(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
public void execute() {
execute(new ContextBase());
}
public boolean execute(Context context) {
try {
jdbcTemplate.execute(sql);
}
catch (DataAccessException e) {
log.error(e);
return true;
}
return false;
}
public static void main(String[] args) throws Exception {
Resource resource = new ClassPathResource("processingTime.xml");
XmlBeanFactory factory = new XmlBeanFactory(resource);
log.info("getting command bean ..");
CreateTableCommand command
= (CreateTableCommand)factory.getBean("createTableCommand");
log.info("executing ...");
command.execute();
}
} |
今までと同様、Command クラスとして、実装しています。Spring Framework の IoC により、Bean の property として、DataSource を与えて、execute() メソッドで、SQL を実行しています。
次は、Spring のリソースファイルです。
<?xml version="1.0" encoding="utf-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
">
<bean id="createTableCommand"
class="net.java.sampo.immigration.processingTime.db.CreateTableCommand">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="dataSource" destroy-method="close"
class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName"
value="org.apache.derby.jdbc.EmbeddedDriver" />
<property name="url"
value="jdbc:derby:processingTime.db;create=true" />
</bean>
</beans>
|
DataSource では、Apache Commons DBCP (DataBase Connection Pool) を利用しました。データベースは、いつもの Apache Derby です。なお、commons-dbcp は、Commons Pool を使っているので、実行時には、commons-pool の JAR ファイルも必要になります。Tags: immigration, programming
|
|
USCIS Processing Times Tracking Project #6 - Processing Time & ParserUtils - KazMuzik Blog
2007-12-17 22:19
12/7 に、USCIS の Processing Times について書いてから、posted date は 11/14 のままで、更新はされていません。12/8 に、ProcessingTime クラスと、parser を紹介しましたが、今日は、今後の計画も含めて、アップデートしておきます。
まずは、ProcessingTime ですが、データベースのテーブルの 1行と対応させるため、Facility の名前、Posted Date, (そのページでの) 行番号を追加しておきます。また、元の timeframe は、せっかくデータベースに入れても扱いにくいので、Date 型の Processing Date と、int の timeframe (日数) を返す method も加えておきました。12/10 の snapshot の画像で、サンプルを見ることができます。
package net.java.sampo.immigration.processingTime;
import java.io.Serializable;
import java.util.Date;
import java.util.Formatter;
import org.apache.commons.lang.time.DateUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import static net.java.sampo.immigration.processingTime.ParserUtils.parseDate;
public class ProcessingTime implements Serializable {
private static final Log log = LogFactory.getLog(ProcessingTime.class);
private String facility;
private Date date;
private int lineNo;
private String form;
private String title; // name
private String classification; // basis
private String timeframe;
public ProcessingTime(String facility, Date date, int lineNo,
String form, String name, String timeframe) {
this(facility, date, lineNo, form, name, null, timeframe);
}
public ProcessingTime(String facility, Date date, int lineNo,
String form, String title, String classification, String timeframe) {
this.facility = facility;
this.date = date;
this.lineNo = lineNo;
this.form = form;
this.title = title;
this.classification = classification;
this.timeframe = timeframe;
}
public String getFacility() {
return facility;
}
public Date getDate() {
return date;
}
public int getLineNO() {
return lineNo;
}
public String getForm() {
return form;
}
public String getTitle() {
return title;
}
public String getName() {
return title;
}
public String getClassification() {
return classification;
}
public String getTimeframe() {
return timeframe;
}
public String toString() {
return toString(false);
}
public String toString(boolean additionalInfo) {
Formatter f = new Formatter();
f.format("[%s|%tF|%d|%s|%s|", facility, date, lineNo, form, title);
if (classification != null) {
f.format("%s|", classification);
}
f.format("%s", timeframe);
if (additionalInfo) {
f.format("|%tF|%d", getProcessingDate(), getTimeframeInDay());
}
f.format("]");
return f.toString();
}
public Date getProcessingDate() {
Date postedDate = date;
if (timeframe == null || timeframe.length() == 0) {
return null;
}
String s = timeframe.toLowerCase();
char c = s.charAt(0);
if (c >= 'a' && c <= 'z') {
return parseDate(s);
}
int i = s.indexOf(' ');
int n = 0 - Integer.parseInt(s.substring(0,i));
s = s.substring(i+1);
if (s.startsWith("day")) {
return DateUtils.addDays(postedDate, n);
}
else if (s.startsWith("week")) {
return DateUtils.addWeeks(postedDate, n);
}
else if (s.startsWith("month")) {
return DateUtils.addMonths(postedDate, n);
}
else if (s.startsWith("year")) {
return DateUtils.addYears(postedDate, n);
}
else if (s.startsWith("semana")) {
return DateUtils.addWeeks(postedDate, n);
}
else if (s.startsWith("mese")) {
return DateUtils.addMonths(postedDate, n);
}
else {
log.warn("invalid time frame \"" + timeframe + "\"");
return null;
}
}
public int getTimeframeInDay() {
Date postedDate = date;
Date processingDate = getProcessingDate();
if (processingDate == null) {
return -1;
}
return (int)((postedDate.getTime() - processingDate.getTime()) / 86400000L);
// 86400000 = 24 x 60 x 60 x 1000
}
} |
次は、ParserUtils ですが、parser 関係の method(s) を static として、まとめました。また、USCIS の Processing Times の web での公開は、4年前の 2003年11月から始まっていますが、当時のデータも保存してあったので、それを利用するために、昔のデータも parse できるよう、変更を加えてました。
package net.java.sampo.immigration.processingTime;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Formatter;
import java.util.List;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class ParserUtils {
private static Log log = LogFactory.getLog(ParserUtils.class);
public static String toTrimedString(Reader in) throws IOException {
StringBuilder sb = new StringBuilder();
for (String line : (List<String>)IOUtils.readLines(in)) {
sb.append(line.trim());
}
return sb.toString();
}
public static String cutContent(byte[] bytes) throws IOException {
Reader in = new InputStreamReader(new ByteArrayInputStream(bytes), "US-ASCII");
return cutContent( toTrimedString(in) );
}
public static String cutContent(File file) throws IOException {
Reader in = new InputStreamReader(new FileInputStream(file), "US-ASCII");
return cutContent(in);
}
public static String cutContent(Reader in) throws IOException {
return cutContent( toTrimedString(in) );
}
public static String cutContent(String html) {
String sl = html.toLowerCase();
int n = sl.indexOf("posted ");
n = sl.indexOf("posted ", n+7);
int m1 = sl.lastIndexOf("<b><big>", n);
if (m1 < 0) { // NBC 2003-11-14
m1 = n;
}
m1 = sl.lastIndexOf(">", m1);
int m2 = sl.indexOf("<", n);
StringBuilder sb = new StringBuilder();
sb.append("<p align=\"center\">");
sb.append(html.substring(m1+1,m2));
sb.append("</p>");
m1 = sl.indexOf("<table", m2);
m2 = sl.indexOf("</table>", m1);
sb.append(html.substring(m1,m2+8));
return sb.toString();
}
public static String parseFacilityName(String content) {
String s = content.toLowerCase();
int n = s.indexOf("<big>");
if (n < 0) {
if (log.isWarnEnabled()) {
log.warn("\"<big> \" tag not found.");
}
return "National Benefits Center"; // NBC 2003-11-14
}
int m = s.indexOf("</", n+5); // 2003-11-14
if (m < 0) {
if (log.isWarnEnabled()) {
log.warn("\"</big> \" tag not found.");
}
return null;
}
String name = content.substring(n+5, m).trim();
// 2003-11-14
if (s.substring(0,n).indexOf("service center") >= 0
&& name.toLowerCase().indexOf("service center") < 0) {
name = name + " Service Center";
}
return name;
}
public static Date parsePostedDate(String content) {
int n = content.toLowerCase().indexOf("posted ");
if (n < 0) {
if (log.isWarnEnabled()) {
log.warn("\"Posted \" not found.");
}
return null;
}
return parseDate(content.substring(n+7));
}
public static ProcessingTime[] parseProcessingTimes(String content) {
String facility = parseFacilityName(content);
Date date = parsePostedDate(content);
int lineNo = 0;
//
String s = content.toLowerCase();
int n = s.indexOf("form");
List<ProcessingTime> list = new ArrayList<ProcessingTime>();
while (true) {
n = s.indexOf("<tr", n);
if (n < 0) {
break;
}
int m = s.indexOf("<tr", n+4); // CSC 2003-11-14 </tr> missing !!
String line = null;
if (m < 0) {
line = content.substring(n);
}
else {
line = content.substring(n, m);
}
list.add( parseLine(facility, date, lineNo, line) );
lineNo ++;
if (m < 0) {
break;
}
n = m;
}
ProcessingTime[] array = new ProcessingTime[ list.size() ];
list.toArray(array);
return array;
}
private static ProcessingTime parseLine(String facility, Date date, int lineNo, String s) {
if (log.isDebugEnabled()) {
log.debug(s);
}
String sl = s.toLowerCase();
int m = sl.indexOf("<b>");
int n = sl.indexOf("<", m+3); // 2003-11-14
List<String> list = new ArrayList<String>(4);
list.add(s.substring(m+3, n).trim()); // form
if (log.isDebugEnabled()) {
log.debug(list);
}
while (true) {
n = sl.indexOf("</td", n+5); // 2005-10-19 District Office
if (n < 0) {
break;
}
m = sl.lastIndexOf(">", n);
list.add( s.substring(m+1, n).trim() );
if (log.isDebugEnabled()) {
log.debug(list);
}
}
ProcessingTime processingTime = null;
if (list.size() == 3) {
processingTime = new ProcessingTime(facility, date, lineNo,
list.get(0), list.get(1), list.get(2));
}
else if (list.size() >= 4) {
processingTime = new ProcessingTime(facility, date, lineNo,
list.get(0), list.get(1), list.get(2), list.get(3));
}
else {
if (log.isWarnEnabled()) {
log.warn("invalid field number = " + list.size() + " for " + s);
}
}
return processingTime;
}
public static Date parseDate(String s) {
int n = s.indexOf(' ');
int m = s.indexOf(',', n+1);
int dd = Integer.parseInt(s.substring(n+1,m));
int yyyy = Integer.parseInt(s.substring(m+2,m+6));
int mm = parseMonth(s.substring(0,n));
if (mm < 1 || mm > 12) {
return null;
}
Calendar cal = Calendar.getInstance();
cal.set(yyyy, mm-1, dd, 12, 0, 0);
return cal.getTime();
}
private static final String[] months = {
"jan", "feb", "mar", "apr", "may", "jun",
"jul", "aug", "sep", "oct", "nov", "dec"
};
private static int parseMonth(String s) {
s = s.toLowerCase();
for (int i = 0; i < months.length; i++) {
if (s.startsWith(months[i])) {
return i+1;
}
}
return -1;
}
} |
Tags: immigration, programming
|
|
Spring Framework and Apache Commons Chain #2 - USCIS Processing Times #7 - KazMuzik Blog
2007-12-16 07:16
12/12 に、Spring Framework の IoC を用いて、Command を使用する方法を紹介しましたが、これを Chain に拡張して、さらに便利にします。12/11 に、5つの Command(s) からなる Chain を紹介したので、これを例に使います。
まずは、XML のリソースです。
<?xml version="1.0" encoding="utf-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
">
<bean id="chain" class="net.java.sampo.immigration.processingTime.command.spring.CommandChain">
<property name="commands">
<list>
<ref bean="fetchCommand" />
<ref bean="cutContentCommand" />
<ref bean="parseContentCommand" />
<ref bean="setFilenameCommand" />
<ref bean="writeBytesCommand" />
</list>
</property>
<property name="contextMap">
<map>
<entry key="url"
value="https://egov.uscis.gov/cris/jsps/officeProcesstimes.jsp?selectedOffice=70" />
<entry key="outPath" value="/tmp/pt" />
</map>
</property>
</bean>
<bean id="fetchCommand"
class="net.java.sampo.immigration.processingTime.command.FetchCommand" />
<bean id="cutContentCommand"
class="net.java.sampo.immigration.processingTime.command.CutContentCommand" />
<bean id="parseContentCommand"
class="net.java.sampo.immigration.processingTime.command.ParseContentCommand" />
<bean id="setFilenameCommand"
class="net.java.sampo.immigration.processingTime.command.SetFilenameCommand" />
<bean id="writeBytesCommand"
class="net.java.sampo.immigration.processingTime.command.WriteBytesCommand" />
</beans>
|
"chain" bean では、List で Command の bean(s) を、Map で Context の entry を定義してあります。
次に、"chain" bean の CommandChian クラスです。main() で、上記のリソースを読み込み、実行しています。
package net.java.sampo.immigration.processingTime.command.spring;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.apache.commons.chain.Command;
import org.apache.commons.chain.Context;
import org.apache.commons.chain.impl.ChainBase;
import org.apache.commons.chain.impl.ContextBase;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class CommandChain extends ChainBase implements InitializingBean {
private static Log log = LogFactory.getLog(CommandChain.class);
private List<Command> commands;
private Map<String,String> contextMap;
private Context context;
public CommandChain() {
context = null;
}
public void setCommands(List<Command> commands) {
this.commands = commands;
}
public List<Command> getCommands() {
return commands;
}
public void setContextMap(Map<String,String> contextMap) {
this.contextMap = contextMap;
}
public Map<String,String> getContextMap() {
return contextMap;
}
public Context getContext() {
return context;
}
public void afterPropertiesSet() {
// adding commands
for (Command command : commands) {
addCommand(command);
}
// setting context
context = new ContextBase();
for (String key : contextMap.keySet()) {
String value = contextMap.get(key);
context.put(key, value);
}
}
public void execute() throws Exception {
super.execute(context);
}
public static void main(String[] args) throws Exception {
Resource resource = new ClassPathResource("chain.xml");
XmlBeanFactory factory = new XmlBeanFactory(resource);
log.info("getting chain ..");
CommandChain chain = (CommandChain)factory.getBean("chain");
Context context = chain.getContext();
log.info("context=" + context);
log.info("executing ...");
chain.execute();
//
context.remove("bytes");
context.remove("content");
context.remove("processingTimes");
log.info("context=" + context);
}
} |
InitializingBean で定義されている afterPropertiesSet() メソッドを実装して、properties がセットされた後に、必要な処理を自動的に行うようにしています。このため、main() では、"chain" bean を get した後は、execute() メソッドを呼ぶだけです。
$ cat log4j.properties
log4j.rootCategory=WARN,A1
log4j.category.net.java.sampo=INFO
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p %c{1} - %m%n
$ java -classpath .:lib/commons-logging-1.1.1.jar:lib/log4j-1.2.15.jar:lib/commons-chain-1.1.jar\
:lib/commons-io-1.3.2.jar:lib/spring-2.5.jar \
net.java.sampo.immigration.processingTime.command.spring.CommandChain
2007-12-16 07:39:34,508 [main] INFO CommandChain - getting chain ..
2007-12-16 07:39:34,569 [main] INFO CommandChain \
- context={outPath=/tmp/pt, url=https://egov.uscis.gov/cris/jsps/officeProcesstimes.jsp?selectedOffice=70}
2007-12-16 07:39:34,569 [main] INFO CommandChain - executing ...
2007-12-16 07:39:36,012 [main] INFO CommandChain \
- context={outPath=/tmp/pt, facility=San Jose CA, posted=Wed Nov 14 12:00:00 PST 2007,\
url=https://egov.uscis.gov/cris/jsps/officeProcesstimes.jsp?selectedOffice=70,\
outFile=/tmp/pt/San_Jose_CA-20071114.html}
$ |
CommandChain 自体は、今回の USCIS Processing Times の処理と独立しているので、リソースファイルで、Chain で実行する Command の List と、Context の Map を記述すれば、コンパイルせずに、任意の command chain と context を使って実行することができます。Tags: immigration, programming
|
|
Spring Framework IoC - USCIS Processing Times #6 - KazMuzik Blog
2007-12-12 22:19
#4 の FetchAllCommand を、Spring Framework の Inversion of Control (IoC) を利用して、呼び出してみます。
まずは、簡単な main メソッドを持つ CommandExecutor クラスです。
package net.java.sampo.immigration.processingTime.command.spring;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.apache.commons.chain.Command;
import org.apache.commons.chain.Context;
public class CommandExecutor {
public static void main(String[] args) throws Exception {
Resource resource = new ClassPathResource("command.xml");
XmlBeanFactory factory = new XmlBeanFactory(resource);
Command command = (Command)factory.getBean("command");
Context context = (Context)factory.getBean("context");
command.execute(context);
}
}
|
下記の XML で記述された Bean(s) のリソースファイルを元に Factory を用意して、Command と Context の bean(s) を get して、execute しています。
次は、上記の CommandExecutor から利用される Context クラスです。
package net.java.sampo.immigration.processingTime.command.spring;
import java.util.HashMap;
import org.apache.commons.chain.Context;
public class CommandContext extends HashMap implements Context {
public CommandContext() {
super();
}
public String getOutPath() {
return (String)get("outPath");
}
public void setOutPath(String outPath) {
put("outPath", outPath);
}
}
|
"outPath" プロパティを用意しているだけです。最初は org.apache.commons.chain.impl.ContextBase を extend して作成しましたが、Spring の factory が bean を作成するときに、Refrection の関係で exception が発生してしまいました。このため、HashMap を extend して、Context インターフェースを implement するように変更しました。
最後に、Command と Context bean(s) を定義する、リソースファイルです。
<?xml version="1.0" encoding="utf-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
">
<bean id="command" class="net.java.sampo.immigration.processingTime.command.FetchAllCommand" />
<bean id="context" class="net.java.sampo.immigration.processingTime.command.spring.CommandContext">
<property name="outPath" value="/tmp/pt" />
</bean>
</beans>
|
これで、CommandExecutor を実行すれば、Spring Framework の IoC を利用して、Command と Context の bean が作成され、その後は、同じように FetchAllCommand が execute されます。CommandExecutor は、特に、FetchAllCommand には依存していないので、CommandContext クラスが適切な properties を持っていれば、XML のリソースファイルを変更するだけで、任意の Command クラスを利用することができます。
Spring Framework は、この IoC を core technology として、その他に、Middle Tier の技術として、Data Access に関する framework、それに MVC をベースにした独自の Web framework (Spring Web MVC) があります。これらについては、後に紹介することになると思います。Tags: programming
|
|
USCIS Processing Times #5 - Command(s) and Chain - KazMuzik Blog
2007-12-11 22:40
前回(12/10, #4)のエントリの補足です。FetchAllCommand で、本質的なところを抜粋します。
Chain command = new ChainBase();
command.addCommand(new FetchCommand());
command.addCommand(new CutContentCommand());
command.addCommand(new ParseContentCommand());
command.addCommand(new SetFilenameCommand());
command.addCommand(new WriteBytesCommand());
for (String facility : urlMap.getAllFacilityNames()) {
String url = urlMap.getUrl(facility);
Context eachContext = new ContextBase();
eachContext.put("url", url);
eachContext.put("outPath", outPath);
command.execute(eachContext);
} |
Chain に 5つの simple な Command(s) を add して、ループの中で、各 URL に対して、"url" と出力先ディレクトリの "outPath" を Context にセットして、Chain の Command 群を execute しています。それぞれの Command を見ていきます。
まずは、FetchCommand ですが、URL を与えて、前回の EgovFetcher により、byte 配列を得て、"bytes" という名前で、Context にセットしています。
public class FetchCommand implements Command {
private static Log log = LogFactory.getLog(FetchCommand.class);
public boolean execute(Context context) {
String url = (String)context.get("url");
if (url == null) {
log.error("url not set in context.");
return true;
}
byte[] bytes = null;
try {
bytes = EgovFetcher.getInstance().fetch(url);
}
catch (IOException e) {
log.error(e);
return true;
}
if (bytes == null) {
return true;
}
context.put("bytes", bytes);
return false;
}
} |
次の CutContentCommand は、FetchCommand で得られた byte 配列を、文字列として parse して、コンテンツとして本質的なところだけを切り出して、"content" という名前で、Content にセットしています。実質的な作業は、ParserUtils で行っています。
public class CutContentCommand implements Command {
private static Log log = LogFactory.getLog(CutContentCommand.class);
public boolean execute(Context context) {
byte[] bytes = (byte[])context.get("bytes");
if (bytes == null) {
log.error("bytes not set in context.");
return true;
}
String content = null;
try {
content = ParserUtils.cutContent(bytes);
}
catch (Exception e) {
log.error(e);
}
if (content == null) {
return true;
}
context.put("content", content);
return false;
}
} |
次の ParseContentCommand では、"content" にセットされている文字列を parse して、facility name, posted date, それに、詳細の processing time(s) の配列を抽出して、Context にセットします。これも、実質的な作業は ParserUtils のメソッドの中です。
public class ParseContentCommand implements Command {
private static Log log = LogFactory.getLog(ParseContentCommand.class);
public boolean execute(Context context) {
String content = (String)context.get("content");
if (content == null) {
log.error("content not set in context.");
return true;
}
String facility = ParserUtils.parseFacilityName(content);
context.put("facility", facility);
Date date = ParserUtils.parsePostedDate(content);
context.put("posted", date);
ProcessingTime[] times = ParserUtils.parseProcessingTimes(content);
context.put("processingTimes", times);
return false;
}
} |
次の SetFilenameCommand では、最初に "outPath" で与えられた出力ディレクトリ名と、前の ParseContentCommand でセットされた "facility" と "posted" の情報から、出力ファイル名を、"outFile" という名前でセットしています。例えば,outPath="/tmp", facility="San Jose CA", posted=2007-11-14 の場合は、outFile="/tmp/San_Jose_CA-20071114.html" となります。
public class SetFilenameCommand implements Command {
private static Log log = LogFactory.getLog(SetFilenameCommand.class);
public boolean execute(Context context) {
String path = (String)context.get("outPath");
if (path == null) {
log.error("outPath not set in context.");
}
String facility = (String)context.get("facility");
Date date = (Date)context.get("posted");
if (facility == null || date == null) {
if (new ParseContentCommand().execute(context)) {
log.error("facility, posted, or context not set in context.");
return true;
}
facility = (String)context.get("facility");
date = (Date)context.get("posted");
}
facility = facility.replaceAll("[\\s]+", "_");
Formatter f = new Formatter();
f.format("%s/%s-%tY%tm%td.html", path, facility, date, date, date);
String filename = f.toString();
context.put("outFile", filename);
return false;
}
} |
最後の WriteBytesCommand は、FetchCommand でセットされた byte 列を、前の SetFilenameCommand でセットされたファイル名のファイルに、byte stream で書き出します。
public class WriteBytesCommand implements Command {
private static Log log = LogFactory.getLog(WriteBytesCommand.class);
public boolean execute(Context context) {
byte[] bytes = (byte[])context.get("bytes");
if (bytes == null) {
log.error("bytes not set in context.");
return true;
}
String filename = (String)context.get("outFile");
if (filename == null) {
log.error("outFile not set in context.");
return true;
}
try {
FileUtils.writeByteArrayToFile(new File(filename), bytes);
}
catch (IOException e) {
log.error(e);
}
return false;
}
} |
2007-12-17 update ParserUtils のソースを、載せました。Tags: programming
|
|
USCIS Processing Times #4 - fetcher - KazMuzik Blog
2007-12-10 21:55
USCIS のサイトは、予定より早く、昨晩には立ち上がっていました。まずは、Processing Times のページをすべて fetch して、ローカルに保存しておくことにします。
まずは、特定の URL を fetch してきて、byte 配列に格納して返すクラスです。
package net.java.sampo.immigration.processingTime;
import java.io.InputStream;
import java.io.IOException;
import java.net.URL;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class EgovFetcher {
private static final long minimumFetchInterval = 1000; // 1 sec
private static Log log = LogFactory.getLog(EgovFetcher.class);
private static EgovFetcher fetcher = null;
public static EgovFetcher getInstance() {
if (fetcher == null) {
fetcher = new EgovFetcher();
}
return fetcher;
}
private long lastFetchedTime;
public EgovFetcher() {
lastFetchedTime = -1L;
}
synchronized public byte[] fetch(String url) throws IOException {
if (url == null) {
log.error("url is null.");
return null;
}
long now = System.currentTimeMillis();
long t = minimumFetchInterval - (now - lastFetchedTime);
if (t > 0L) {
if (log.isInfoEnabled()) {
log.info("sleeping " + t + "msecs ..");
}
try {
Thread.sleep(t);
}
catch (InterruptedException e) {
}
now = System.currentTimeMillis();
}
lastFetchedTime = now;
byte[] bytes = null;
InputStream in = new URL(url).openStream();
bytes = IOUtils.toByteArray(in);
return bytes;
}
} |
アクセスが集中しないように、fetch は、1秒間に 1回に制限してあります。また、このためと、使いやすさを考慮して、singleton になっています。
次は、Processing Times のトップページを parse して、facility (Field Office や Service Center)のリストと、それぞれの URL の Collection (List と Map) オブジェクトを準備するクラスです。
package net.java.sampo.immigration.processingTime;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class FacilityUrlMap extends HashMap<String, String> {
private static final String ptimesUrl = " https://egov.uscis.gov/cris/jsps/ptimes.jsp";
private static final String officeUrl
= "https://egov.uscis.gov/cris/jsps/officeProcesstimes.jsp?selectedOffice=";
private static final String centerUrl
= "https://egov.uscis.gov/cris/jsps/Processtimes.jsp?SeviceCenter=";
private static final String nbcUrl = "https://egov.uscis.gov/cris/jsps/NBCprocesstimes.jsp";
private static Log log = LogFactory.getLog(FacilityUrlMap.class);
private static FacilityUrlMap map;
public static FacilityUrlMap getInstance() {
if (map == null) {
map = new FacilityUrlMap();
}
return map;
}
private String[] facilityNames;
public FacilityUrlMap() {
super();
try {
init();
}
catch (IOException e) {
log.fatal(e);
}
}
public String getUrl(String facility) {
return get(facility);
}
public String[] getAllFacilityNames() {
return facilityNames;
}
private void init() throws IOException {
byte[] bytes = EgovFetcher.getInstance().fetch(ptimesUrl);
BufferedReader in
= new BufferedReader(new InputStreamReader(
new ByteArrayInputStream(bytes), "ISO-8859-1"));
parse(in);
}
private void parse(BufferedReader in) throws IOException {
List<String> list = new ArrayList<String>();
int mode = 0;
while (true) {
String line = in.readLine();
if (log.isDebugEnabled()) {
log.debug("" + mode + " : " + line);
}
if (line == null) {
break;
}
String s = line.toLowerCase();
if (mode == 0) {
if (s.indexOf("<select name=\"selectedoffice\">") >= 0) {
mode = 1;
}
else if (s.indexOf("<select name=\"sevicecenter\">") >= 0) { // Sevice : "r" missing !!
mode = 2;
}
continue;
}
if (s.indexOf("</select>") >= 0) {
if (mode == 2) {
break;
}
mode = 0;
continue;
}
//
int n = 0;
while (true) {
int m = s.indexOf("<option value=\"", n);
if (m < 0) {
break;
}
n = s.indexOf('"', m+15);
String id = line.substring(m+15,n).trim();
m = s.indexOf('<', n+2);
String name = line.substring(n+2, m).trim();
log.info(id + " : " + name);
if (mode == 1) {
int intId = Integer.parseInt(id);
put(name, officeUrl + intId);
list.add(name);
}
else if (mode == 2) {
put(name, centerUrl + id);
list.add(name);
}
n = m+8;
}
}
String name = "National Benefits Center";
put(name, nbcUrl);
list.add(name);
//
int n = list.size();
facilityNames = new String[n];
list.toArray(facilityNames);
}
} |
これも singleton です。
以上を利用して、すべての Processing Times のページを fetch して、ファイルに保存する Command クラスです。
package net.java.sampo.immigration.processingTime.command;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import org.apache.commons.chain.Chain;
import org.apache.commons.chain.Command;
import org.apache.commons.chain.Context;
import org.apache.commons.chain.impl.ChainBase;
import org.apache.commons.chain.impl.ContextBase;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import net.java.sampo.immigration.processingTime.FacilityUrlMap;
public class FetchAllCommand implements Command {
private static Log log = LogFactory.getLog(FetchAllCommand.class);
public boolean execute(Context context) {
FacilityUrlMap urlMap = FacilityUrlMap.getInstance();
if (urlMap == null) {
log.fatal("facility to url map is null.");
return true;
}
String outPath = (String)context.get("outPath");
if (outPath == null) {
log.error("outPath not set in context.");
return true;
}
File outDir = new File(outPath);
if (! outDir.exists()) {
log.warn("outPath not exist. creating..");
if (! outDir.mkdir()) {
log.warn("outPath failed to create.");
return true;
}
}
Chain command = new ChainBase();
command.addCommand(new FetchCommand());
command.addCommand(new CutContentCommand());
command.addCommand(new ParseContentCommand());
command.addCommand(new SetFilenameCommand());
command.addCommand(new WriteBytesCommand());
for (String facility : urlMap.getAllFacilityNames()) {
String url = urlMap.getUrl(facility);
Context eachContext = new ContextBase();
eachContext.put("url", url);
eachContext.put("outPath", outPath);
if (log.isInfoEnabled()) {
log.info(facility + " : " + url + " to " + outPath);
}
try {
command.execute(eachContext);
}
catch (Exception e) {
log.error(e);
}
}
return false;
}
public static void main(String[] args) throws Exception {
String path = "orig";
if (args.length > 0) {
path = args[0];
}
Context context = new ContextBase();
context.put("outPath", path);
Chain command = new ChainBase();
command.addCommand(new FetchAllCommand());
command.execute(context);
}
} |
いくつかの簡単な Command オブジェクトを Chain で使っていますが、これは後に紹介します。(2007-12-11 -> USCIS Processing Times #5 - Command(s) and Chain)
実行して、保存したファイルを加工したページのサンプルを載せておきます。今日(12/10)の時点では、Posted Date は、まだ 11/14 のままでした。

 Tags: immigration, programming
|
|
USCIS Processing Times #3 - custom Atom feed with ROME #2 - KazMuzik Blog
2007-12-09 03:02
USCIS のサイトでは、RSS フィードを提供していないので、12/6 に紹介した ROME を使って、自分で Atom フィードを作ってみます。
package net.java.sampo.immigration.processingTime;
import java.io.File;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Formatter;
import java.util.List;
import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.sun.syndication.feed.synd.SyndContent;
import com.sun.syndication.feed.synd.SyndContentImpl;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndEntryImpl;
import com.sun.syndication.feed.synd.SyndFeedImpl;
import com.sun.syndication.feed.synd.SyndLink;
import com.sun.syndication.feed.synd.SyndLinkImpl;
import com.sun.syndication.feed.synd.SyndPerson;
import com.sun.syndication.feed.synd.SyndPersonImpl;
import com.sun.syndication.io.FeedException;
import com.sun.syndication.io.SyndFeedOutput;
import static net.java.sampo.immigration.processingTime.ProcessingTimesPageParser.parsePostedDate;
public class ProcessingTimeFeed extends SyndFeedImpl {
private static final String urlPrefix = "http://localhost/immigration/";
private static Log log = LogFactory.getLog(ProcessingTimeFeed.class);
private ProcessingFacility facility;
public ProcessingTimeFeed(ProcessingFacility facility) {
this.facility = facility;
setFeed();
}
private void setFeed() {
String title = facility.getName() + " Processing Times";
String url = urlPrefix + facility.getStringId() + ".xml";
String urlAlt = urlPrefix + facility.getStringId() + ".html";
//
setFeedType("atom_1.0");
setTitle(title);
setDescription(title);
setPublishedDate(new Date());
//
List<SyndLink> links = new ArrayList<SyndLink>();
SyndLink link = new SyndLinkImpl();
link.setHref(urlAlt);
link.setRel("alternate");
link.setType("text/html");
links.add(link); // 0
//
link = new SyndLinkImpl();
link.setHref(url);
link.setRel("self");
link.setType("text/xml");
links.add(link); // 1
//
link = new SyndLinkImpl();
link.setHref(url);
link.setRel("service.feed");
link.setType("application/x.atom+xml");
link.setTitle(title);
links.add(link); // 2
setLinks(links);
//
List<SyndPerson> authors = new ArrayList<SyndPerson>();
SyndPerson author = new SyndPersonImpl();
author.setName("Kaz Muzik");
authors.add(author);
setAuthors(authors);
}
public void add(String contentText) {
Date date = parsePostedDate(contentText);
add(date, contentText);
}
public void add(Date date, String contentText) {
Formatter f = new Formatter();
f.format("%s%s-%tY%tm%td.html", urlPrefix, facility.getStringId(), date, date, date);
String url = f.toString();
f = new Formatter();
f.format("%tF @ %s", date, facility.getName());
String title = f.toString();
//
SyndEntry entry = new SyndEntryImpl();
entry.setPublishedDate(date);
entry.setTitle(title);
//
List<SyndContent> contents = new ArrayList<SyndContent>();
SyndContent content = new SyndContentImpl();
content.setValue(contentText);
content.setType("html");
contents.add(content);
entry.setContents(contents);
//
List<SyndLink> links = new ArrayList<SyndLink>();
SyndLink link = new SyndLinkImpl();
link.setHref(url);
link.setRel("alternate");
link.setType("text/html");
links.add(link);
entry.setLinks(links);
//
List<SyndPerson> authors = new ArrayList<SyndPerson>();
SyndPerson author = new SyndPersonImpl();
author.setName("Kaz Muzik");
authors.add(author);
entry.setAuthors(authors);
//
List<SyndEntry> entryList = (List<SyndEntry>)getEntries();
if (entryList == null) {
entryList = new ArrayList<SyndEntry>();
setEntries(entryList);
}
entryList.add(entry);
}
public String getXML() {
try {
return new SyndFeedOutput().outputString(this);
}
catch (FeedException e) {
log.error(e);
return null;
}
}
public static void main(String[] args) throws Exception {
ProcessingFacility facility = new FieldOffice(70, "San Jose CA");
ProcessingTimeFeed feed = new ProcessingTimeFeed(facility);
String content = FileUtils.readFileToString(new File("contents/070-20071114.html"));
feed.add(content);
System.out.println(feed.getXML());
}
} |
基本的には、SyndFeed オブジェクトに、基本的な情報をセットして、日付ごとに SyndEntry オブジェクトを追加した後に、Atom 1.0 のフォーマットで、XML を作成しています。今のところ、2007-11-14 の entry しかありませんが、次のようなフィードになります。
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/">
<title>San Jose CA Processing Times</title>
<link rel="alternate" href="http://localhost/immigration/070.html" />
<link rel="self" href="http://localhost/immigration/070.xml" />
<link rel="service.feed" href="http://localhost/immigration/070.xml" />
<author>
<name>Kaz Muzik</name>
</author>
<subtitle>San Jose CA Processing Times</subtitle>
<updated>2007-12-09T03:07:41Z</updated>
<dc:date>2007-12-09T03:07:41Z</dc:date>
<entry>
<title>2007-11-14 @ San Jose CA</title>
<link rel="alternate" type="text/html"
href="http://localhost/immigration/070-20071114.html" />
<author>
<name>Kaz Muzik</name>
</author>
<updated>2007-11-14T20:00:00Z</updated>
<published>2007-11-14T20:00:00Z</published>
<content type="html"><table border="0" cellpadding="0" cellspacing="0"
width="100%"><tbody><tr><td align="center">District
Office Processing Dates for <b><big>San Jose
CA</big></b> Posted November 14,
2007</td></tr></tbody></table><table
style="font-size: 10pt;" border="1" bordercolor="#045290" cellpadding="2"
cellspacing="0" width="100%"><tbody><tr
bgcolor="#000000"><td width="45"><font
color="#ffd200">Form</font></td><td
align="center"><font color="#ffd200">Form
Name</font></td><td align="center"><font
color="#ffd200">Processing
Timeframe:</font></td></tr><tr
bgcolor="#ffff99"><td><b>I-131</b></td><td
align="left">Application for Travel Documents</td><td
align="center">3 Months</td></tr><tr
bgcolor="#99ff99"><td><b>I-485</b></td><td
align="left">Application to Register Permanent Residence or Adjust
Status</td><td align="center">6 Months</td></tr><tr
bgcolor="#ffff99"><td><b>I-600</b></td><td
align="left">Petition to Classify Orphan as an Immediate
Relative</td><td align="center">July 05,
2007</td></tr><tr
bgcolor="#99ff99"><td><b>I-600A</b></td><td
align="left">Application for Advance Processing of Orphan
Petition</td><td align="center">July 05,
2007</td></tr><tr
bgcolor="#ffff99"><td><b>I-765</b></td><td
align="left">Application for Employment Authorization</td><td
align="center">11 Weeks</td></tr><tr
bgcolor="#99ff99"><td><b>N-400</b></td><td
align="left">Application for Naturalization</td><td
align="center">March 08, 2007</td></tr><tr
bgcolor="#ffff99"><td><b>N-600</b></td><td
align="left">Application for Certification of Citizenship</td><td
align="center">October 12,
2006</td></tr></tbody></table></content>
<dc:date>2007-11-14T20:00:00Z</dc:date>
</entry>
</feed> |
これを、Fedora 8 に、/var/www/html/immigration ディレクトリを作り、"070.xml" と名前をつけて保存します。これで、標準の設定で、Apache を起動すると、http://localhost/immigration/070.xml とアクセスできるので、試してみます。



後は、昨日の parser を使って、fetcher を cron か daemon で走らせといて、アップデートがあれば、新たなフィードを作成して、上記の Apache の Document Path に置けば、自分のデスクトップの Firefox から、上の2番目の絵のところで、アップデートを確認することができます。
2007-12-09 update Snapshot をとりながら、ソースを多少変更したので、一部、同期がとれていないところがあります。 また、いくつか、本質的でないクラスを載せていませんでした。Tags: programming
|
|
USCIS Processing Times - Java parser - KazMuzik Blog
2007-12-09 00:58
USCIS の Processing Times ですが、San Jose だけではなく、参考のため、Los Angeles と San Francisco も載せておきます。
| District Office Processing Dates for Los Angeles CA Posted November 14, 2007 |
| Form | Form Name | Processing Timeframe: | | I-131 | Application for Travel Documents | 3 Months | | I-485 | Application to Register Permanent Residence or Adjust Status | 6 Months | | I-600 | Petition to Classify Orphan as an Immediate Relative | July 28, 2007 | | I-600A | Application for Advance Processing of Orphan Petition | July 28, 2007 | | I-765 | Application for Employment Authorization | 11 Weeks | | N-400 | Application for Naturalization | 7 Months | | N-600 | Application for Certification of Citizenship | May 24, 2007 |
| District Office Processing Dates for San Francisco CA Posted November 14, 2007 |
| Form | Form Name | Processing Timeframe: | | I-131 | Application for Travel Documents | 3 Months | | I-485 | Application to Register Permanent Residence or Adjust Status | 6 Months | | I-600 | Petition to Classify Orphan as an Immediate Relative | June 07, 2007 | | I-600A | Application for Advance Processing of Orphan Petition | June 07, 2007 | | I-765 | Application for Employment Authorization | 11 Weeks | | N-400 | Application for Naturalization | 7 Months | | N-600 | Application for Certification of Citizenship | July 06, 2007 |
N-400 に関しては、San Jose は、"March 08, 2007" と処理している日付になっていますが、Los Angeles と San Francisco は、"7 Months" と期間になっています。昔は、すべて、日付による表示でしたが、いつからか、期間による表示も混在するようになったようです。
今日、USCIS の Processing Times のページをアクセスすると、次にようなメッセージが表示されました。
The Case Status Online, Processing Times, Office Locator, and Change of Address Online Systems \
will be experiencing a scheduled outage from 9:30 PM EST
on Friday, December 7 to 8:00 AM EST on Monday, December 10 due to scheduled maintenance. |
週末に、maintenance のためとはいえ、このような mission critical (!?)) なサービスを、丸 2日半(58.5時間)にわたって out of service にするとは、さすが、USCIS です。
Processing Time(s) を parse するクラスを書きましたが、本格的な fetch は、週明け、maintenance が完了してからになります。なお、上記の table(s) は、昨日、保存しておいたものから、テストを兼ねて作成したものです。
package net.java.sampo.immigration.processingTime;
import java.io.Serializable;
public class ProcessingTime implements Serializable {
private String form;
private String title; // name
private String classification; // basis
private String timeframe;
public ProcessingTime(String form, String name, String timeframe) {
this(form, name, null, timeframe);
}
public ProcessingTime(String form, String title, String classification, String timeframe) {
this.form = form;
this.title = title;
this.classification = classification;
this.timeframe = timeframe;
}
public String getForm() {
return form;
}
public String getTitle() {
return title;
}
public String getName() {
return title;
}
public String getClassification() {
return title;
}
public String getTimeframe() {
return timeframe;
}
public String toString() {
return "PT[" + form + "|" + title + "|"
+ ((classification==null)?"":(classification+"|"))
+ timeframe + "]";
}
} |
package net.java.sampo.immigration.processingTime;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Formatter;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import static net.java.sampo.immigration.processingTime.ProcessingTimeUtils.parseDate;
public class ProcessingTimesPageParser extends BufferedReader {
private static Log log = LogFactory.getLog(ProcessingTimesPageParser.class);
private String facilityName;
private Date date;
private ProcessingTime[] processingTimes;
private String content;
public ProcessingTimesPageParser(Reader in) {
super(in);
}
public ProcessingTime[] getProcessingTimes() {
return processingTimes;
}
public String getFacilityName() {
return facilityName;
}
public Date getPostedDate() {
return date;
}
public String getContent() {
return content;
}
public void parseAll() throws IOException {
content = parseContent();
if (log.isDebugEnabled()) {
log.debug(content);
}
facilityName = parseFacility(content);
date = parsePostedDate(content);
processingTimes = parseTable(content);
}
private String parseContent() throws IOException {
int postedCount = 0;
int endTableCount = 0;
StringBuilder sb = new StringBuilder();
String tableTag = null;
while (true) {
String line = readLine();
if (line == null) {
break;
}
line = line.trim();
String lineLowerCase = line.toLowerCase();
if (lineLowerCase.indexOf("<table") >= 0 && postedCount < 2) {
tableTag = line;
}
if (lineLowerCase.indexOf("<tr") >= 0 && postedCount < 2) {
sb = new StringBuilder();
sb.append(tableTag);
if (line.indexOf("<tbody") < 0) {
sb.append("<tbody>");
}
}
sb.append(line);
if (lineLowerCase.indexOf("posted ") >= 0) {
postedCount ++;
}
if (lineLowerCase.indexOf("</table>") >= 0 && postedCount >= 2) {
endTableCount ++;
if (endTableCount >= 2) {
break;
}
}
}
return sb.toString();
}
private String parseFacility(String content) {
String contentLowerCase = content.toLowerCase();
int n = contentLowerCase.indexOf("<big>");
if (n < 0) {
if (log.isWarnEnabled()) {
log.warn("\"<big> \" tag not found.");
}
return null;
}
int m = contentLowerCase.indexOf("</big>", n+5);
if (m < 0) {
if (log.isWarnEnabled()) {
log.warn("\"</big> \" tag not found.");
}
return null;
}
return content.substring(n+5, m);
}
private Date parsePostedDate(String content) {
int n = content.toLowerCase().indexOf("posted ");
if (n < 0) {
if (log.isWarnEnabled()) {
log.warn("\"Posted \" not found.");
}
return null;
}
return parseDate(content.substring(n+7));
}
private ProcessingTime[] parseTable(String content) {
String contentLowerCase = content.toLowerCase();
int n = contentLowerCase.indexOf("processing timeframe");
List<ProcessingTime> list = new ArrayList<ProcessingTime>();
while (true) {
n = contentLowerCase.indexOf("<tr", n);
if (n < 0) {
break;
}
int m = contentLowerCase.indexOf("</tr>", n+4);
list.add( parseLine( content.substring(n, m+5) ) );
n = m;
}
ProcessingTime[] array = new ProcessingTime[ list.size() ];
list.toArray(array);
return array;
}
private ProcessingTime parseLine(String s) {
if (log.isDebugEnabled()) {
log.debug(s);
}
String sl = s.toLowerCase();
int m = sl.indexOf("<b>");
int n = sl.indexOf("</b>", m+3);
List<String> list = new ArrayList<String>(4);
list.add(s.substring(m+3, n).trim()); // form
if (log.isDebugEnabled()) {
log.debug(list);
}
while (true) {
n = sl.indexOf("</td>", n+5);
if (n < 0) {
break;
}
m = sl.lastIndexOf(">", n);
list.add( s.substring(m+1, n).trim() );
if (log.isDebugEnabled()) {
log.debug(list);
}
}
ProcessingTime processingTime = null;
if (list.size() == 3) {
processingTime = new ProcessingTime(list.get(0), list.get(1), list.get(2));
}
else if (list.size() == 4) {
processingTime = new ProcessingTime(list.get(0), list.get(1), list.get(2), list.get(3));
}
else {
if (log.isWarnEnabled()) {
log.warn("invalid field number = " + list.size());
}
}
return processingTime;
}
public static void main(String[] args) throws Exception {
ProcessingTimesPageParser parser
= new ProcessingTimesPageParser(new InputStreamReader(System.in, "ISO-8859-1"));
parser.parseAll();
System.out.printf("%s : %tF%n", parser.getFacilityName(), parser.getPostedDate());
for (ProcessingTime pt : parser.getProcessingTimes()) {
System.out.println(pt.toString());
}
System.out.println(parser.getContent());
}
} |
package net.java.sampo.immigration.processingTime;
import java.util.Calendar;
import java.util.Date;
import java.util.Formatter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class ProcessingTimeUtils {
private static Log log = LogFactory.getLog(ProcessingTimeUtils.class);
public static Date parseDate(String s) {
int n = s.indexOf(' ');
int m = s.indexOf(',', n+1);
int dd = Integer.parseInt(s.substring(n+1,m));
int yyyy = Integer.parseInt(s.substring(m+2,m+6));
int mm = parseMonth(s.substring(0,n));
if (mm < 1 || mm > 12) {
return null;
}
Calendar cal = Calendar.getInstance();
cal.set(yyyy, mm-1, dd);
return cal.getTime();
}
private static final String[] months = {
"jan", "feb", "mar", "apr", "may", "jun",
"jul", "aug", "sep", "oct", "nov", "dec"
};
private static int parseMonth(String s) {
s = s.toLowerCase();
for (int i = 0; i < months.length; i++) {
if (s.startsWith(months[i])) {
return i+1;
}
}
return -1;
}
} |
一部、不自然なコードもありますが、これは、Field Office (District Office) のページだけではなく、Service Center のページも parse できるようにしたためです。
2007-12-17 update -> USCIS Processing Times Tracking Project #6 - Processing Time & ParserUtils このエントリのコードは、12/17/2007 に、大幅にアップデートしました。Tags: immigration, programming
|
|
英辞郎 format checker #2 - KazMuzik Blog
2007-12-07 13:30
12/5 に、Apache Commons Chain を紹介して、英辞郎 format checker プログラムを書きましたが、未完成だったので、enhance して、Ver.109 の最新データもチェックしてみます。
まずは、Java のソースコードです。基本的な流れは変わっていないのですが、ほとんど書き直しに近いので、すべて載せます。今回は、ファイルから一行読むところや、カウントアップして進行状況をログに書き出すところも、Command として実装して、Chain に含めました。
package net.java.sampo.eijiro;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.chain.Chain;
import org.apache.commons.chain.Command;
import org.apache.commons.chain.Context;
import org.apache.commons.chain.impl.ChainBase;
import org.apache.commons.chain.impl.ContextBase;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class EijiroFormatChecker {
private static Log log = LogFactory.getLog(EijiroFormatChecker.class);
private Context context;
private Command checkLineCommand;
public EijiroFormatChecker() throws IOException {
context = new ContextBase();
context.put("errors", new ArrayList<String>());
checkLineCommand = new CheckLineCommand();
}
public void close() throws IOException {
context = null;
}
public List<String> getErrorLines() {
return (List<String>)context.get("errors");
}
public void checkFile() throws IOException {
checkFile(108);
}
public void checkFile(int ver) throws IOException {
checkFile("EIJI-" + ver + ".TXT", "Shift-JIS");
}
public void checkFile(String filename, String encoding) throws IOException {
BufferedReader in
= new BufferedReader(new InputStreamReader(
new FileInputStream(filename), encoding));
context.put("reader", in);
context.put("count", 0);
if (log.isInfoEnabled()) {
log.info("Started checking \"" + filename + "\" ...");
}
while (true) {
try {
checkLineCommand.execute(context);
}
catch (Exception e) {
log.error(e.getMessage(), e);
}
if (context.get("line") == null) {
break;
}
}
if (log.isInfoEnabled()) {
log.info("" + context.get("count") + " lines processed, completed.");
}
if (log.isWarnEnabled()) {
log.warn("" + getErrorLines().size() + " errors found, "
+ context.get("count") + " lines read.");
}
in.close();
}
private static class CheckLineCommand extends ChainBase {
public CheckLineCommand() {
super();
addCommand(new ReadLineCommand());
addCommand(new CountUpCommand());
addCommand(new SeparateLineCommand());
addCommand(new CheckParenthesisChain());
}
}
private static class CountUpCommand implements Command {
public CountUpCommand() {
}
public boolean execute(Context context) {
Integer countObj = (Integer)context.get("count");
if (countObj == null) {
log.error("counter is not set.");
return true;
}
int count = countObj.intValue() + 1;
context.put("count", new Integer(count));
if (count % 100000 == 0) {
if (log.isInfoEnabled()) {
log.info("" + count + " lines processed.");
}
}
return false;
}
}
private static class ReadLineCommand implements Command {
public ReadLineCommand() {
}
public boolean execute(Context context) throws IOException {
BufferedReader in = (BufferedReader)context.get("reader");
String line = in.readLine();
context.put("line", line);
return (line == null)?true:false;
}
}
private static class SeparateLineCommand implements Command {
public SeparateLineCommand() {
}
public boolean execute(Context context) {
String line = (String)context.get("line");
if (line == null) {
log.error("line is null.");
return true;
}
int len = line.length();
if (len < 2) {
log.warn("line length is less than 2.");
return true;
}
List<String> fieldList = new ArrayList<String>();
int n = 1;
boolean ex = false;
for (int i = 1; i < len; i++) {
char c = line.charAt(i);
boolean match = false;
if (c == ':') {
if (len > i + 2 && line.charAt(i-1) == ' ' && line.charAt(i+1) == ' ') {
fieldList.add( line.substring(n,i-1) );
n = i + 2;
i++;
}
continue;
}
else if (c == '■') {
if (len <= i + 2) {
log.warn("invalid example mark in \"" + line + "\"");
return true;
}
if (line.charAt(i+1) != '・') {
log.warn("invalid example mark in \"" + line + "\"");
return true;
}
fieldList.add( line.substring(n,i) );
n = i + 2;
i++;
ex = true;
continue;
}
else if (c == '◆') {
fieldList.add( line.substring(n,i) );
n = i + 1;
ex = true;
continue;
}
else if (c == '、' && (! ex)) {
fieldList.add( line.substring(n,i) );
n = i + 1;
continue;
}
}
fieldList.add( line.substring(n) );
context.put("fields", fieldList);
return false;
}
}
private static class CheckParenthesisChain extends ChainBase {
public CheckParenthesisChain() {
super();
addCommand(new CheckParenthesisCommand('{', '}')); // 品詞ラベル
addCommand(new CheckParenthesisCommand('\u3010', '\u3011'));
// 【】 特殊フィールド, スピーチラベル(?)
addCommand(new CheckParenthesisCommand('\uff5b', '\uff5d')); // {} 読み仮名
addCommand(new CheckParenthesisCommand('\u300a', '\u300b')); // 《》 分野ラベル
addCommand(new CheckParenthesisCommand('\u3008', '\u3009')); // 〈〉 分野ラベル
addCommand(new CheckParenthesisCommand('<', '>'));
addCommand(new CheckParenthesisCommand('(', ')'));
addCommand(new CheckParenthesisCommand('[', ']'));
}
}
private static class CheckParenthesisCommand implements Command {
private char startChar;
private char endChar;
public CheckParenthesisCommand(char startChar, char endChar) {
this.startChar = startChar;
this.endChar = endChar;
}
public boolean execute(Context context) {
String line = (String)context.get("line");
if (line == null) {
log.error("line is null.");
return true;
}
List<String> fields = (List<String>)context.get("fields");
if (fields == null || fields.size() < 1) {
log.error("fields is null or empty in \"" + line + "\"");
return true;
}
int level = 0;
for (String s : fields) {
int len = s.length();
for (int i = 0; i < len; i++) {
char c = s.charAt(i);
if (c == startChar) {
level ++;
}
else if (c == endChar) {
if (level == 0) {
((List<String>)context.get("errors")).add(line);
if (log.isWarnEnabled()) {
log.warn("Unmatched \"" + endChar + "\" in \"" + s + "\"");
}
return true;
}
level --;
}
}
if (level > 0) {
((List<String>)context.get("errors")).add(line);
if (log.isWarnEnabled()) {
log.warn("Unmatched \"" + startChar + "\" in \"" + s + "\"");
}
return true;
}
}
return false;
}
}
public static void main(String[] args) throws Exception {
EijiroFormatChecker checker = new EijiroFormatChecker();
if (args.length < 1) {
checker.checkFile();
}
else if (args.length == 1) {
int ver = Integer.parseInt(args[0]);
checker.checkFile(ver);
}
else {
checker.checkFile(args[0], args[1]);
}
System.out.println();
for (String line : checker.getErrorLines()) {
System.out.println(line);
}
checker.close();
}
} |
ログの出力レベルを WARN にして、実行してみます。
$ cat check.sh
CP=lib/eijiro-lucene.jar:lib/commons-logging-1.1.1.jar:lib/log4j-1.2.15.jar:lib/commons-chain-1.1.jar
java -classpath .:$CP net.java.sampo.eijiro.EijiroFormatChecker $*
$ cat log4j.properties
log4j.rootCategory=WARN,A1
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p %c - %m%n
$ sh check.sh 108
2007-12-07 11:34:30,659 [main] WARN net.java.sampo.eijiro.EijiroFormatChecker \
- Unmatched "}" in "責任{せきにん}ある行動こうどう}を取る"
2007-12-07 11:34:31,071 [main] WARN net.java.sampo.eijiro.EijiroFormatChecker \
- Unmatched "】" in "【分節】】a・pla・net・ic"
2007-12-07 11:34:31,565 [main] WARN net.java.sampo.eijiro.EijiroFormatChecker ...
...
2007-12-07 11:34:38,792 [main] WARN net.java.sampo.eijiro.EijiroFormatChecker \
- Unmatched "(" in "超人的{ちょうじん てき}な(=superhuman"
2007-12-07 11:34:39,139 [main] WARN net.java.sampo.eijiro.EijiroFormatChecker \
- Unmatched "{" in "無線{むせん}トランシーバ[送受信機{そうじゅしんき]"
2007-12-07 11:34:39,272 [main] WARN net.java.sampo.eijiro.EijiroFormatChecker \
- 19 errors found, 1886646 lines read.
■act responsibly : 責任{せきにん}ある行動こうどう}を取る、責任を持って行動{する、自分の行動に責任を持つ
■aplanetic : 【発音】ae`plэne'tik、【分節】】a・pla・net・ic
■bow {{3-自他動-1} : 〔物・体などを〕弓形{ゆみがた}に曲げる
■disagreeable : 【レベル】7、【@】ディスアグリーアブル、デスアグリアブル、【変化】《複》disagreeables、\
【、【分節】dis・a・gree・a・ble
■Eisenhower {人名} : アイゼンハワー◆ドワイト・アイゼンハワー(
■face-to-face talk : 直接{ちょくせつ}会談{かいだん}[交渉うこうしょう}]、ひざ詰め談判{づめ だんぱん}、\
差し向かいの話し合い
■factor affecting : 〜に影響{えいきょう}する要因{よういん[因子{いんし}]
■history of treatment : 治療(既往)歴(ちりょう(きおう)れき}
■myelogenic : 【発音】】ma`iэlαdзe'nik、【@】マイアラジェニク
■operating temperature : 作動{さどう}[運転{うんてん}・使用{しよう・実用{じつよう}]温度{おんど}◆【略】OT
■pharmaceutical products : 医薬品{いやくひん、}、化学薬品{かがく やくひん}
■pulp fiction : 大衆文学{たいしゅう、ぶんがく}、大衆小説{たいしゅう、しょうせつ}、安っぽい小説{しょうせつ}、\
三文小説{さんもん しょうせつ}◆pulp(ざら紙)は安い。
■pulp novel : 大衆文学{たいしゅう、ぶんがく}、大衆小説{たいしゅう、しょうせつ}、安っぽい小説{しょうせつ}、\
三文小説{さんもん しょうせつ}◆pulp(ざら紙)は安い。
■release ~ into the bloodstream : 〜を血流{けつりゅう}[血液{けつえき]中に放出{ほうしゅつ}する
■scrump {自他動-2} : 米俗〉セックスする
■solution heat treatment : 固溶{こよう[溶体{ようたい}]化熱処理{か ねつしょり}◆【略】SHT
■soman {名} : 化》ソマン◆化学名は Pinacolyl methylphosphonofluoridate で神経ガスの一種◆【略】GD
■unhuman {形-2} : 超人的{ちょうじん てき}な(=superhuman
■wireless transceiver : 無線{むせん}トランシーバ[送受信機{そうじゅしんき
$ |
1,886,646 行読み込み、19 のバグが見つかりました。11/7 に報告した 6個以外にも、13個見つかりました。
それでは、最新版の Ver.109 をチェックしてみます。
$ sh check.sh 109
2007-12-07 12:25:59,984 [main] WARN net.java.sampo.eijiro.EijiroFormatChecker \
- Unmatched "】" in "【分節】】a・pla・net・ic"
2007-12-07 12:26:00,555 [main] WARN net.java.sampo.eijiro.EijiroFormatChecker \
- Unmatched "{" in "bow {{3-自他動-1}"
2007-12-07 12:26:01,991 [main] WARN net.java.sampo.eijiro.EijiroFormatChecker \
- Unmatched "(" in "ドワイト・アイゼンハワー("
2007-12-07 12:26:02,002 [main] WARN net.java.sampo.eijiro.EijiroFormatChecker \
- Unmatched "}" in "電気{でんき}出力{しゅつりょく}[動力{どうりょく}}"
2007-12-07 12:26:02,008 [main] WARN net.java.sampo.eijiro.EijiroFormatChecker \
- Unmatched "}" in "電気{でんき}出力{しゅつりょく}[動力{どうりょく}}"
2007-12-07 12:26:04,711 [main] WARN net.java.sampo.eijiro.EijiroFormatChecker \
- Unmatched "】" in "【発音】】ma`iэlαdзe'nik"
2007-12-07 12:26:06,788 [main] WARN net.java.sampo.eijiro.EijiroFormatChecker \
- Unmatched "》" in "化》ソマン"
2007-12-07 12:26:07,717 [main] WARN net.java.sampo.eijiro.EijiroFormatChecker
- Unmatched "(" in "超人的{ちょうじん てき}な(=superhuman"
2007-12-07 12:26:08,194 [main] WARN net.java.sampo.eijiro.EijiroFormatChecker \
- 8 errors found, 1899349 lines read.
■aplanetic : 【発音】ae`plэne'tik、【分節】】a・pla・net・ic
■bow {{3-自他動-1} : 〔物・体などを〕弓形{ゆみがた}に曲げる
■Eisenhower {人名} : アイゼンハワー◆ドワイト・アイゼンハワー(
■electric power : 電力{でんりょく}、電気{でんき}出力{しゅつりょく}[動力{どうりょく}}◆【略】EP
■electrical power : 電力{でんりょく}、電気{でんき}出力{しゅつりょく}[動力{どうりょく}}◆【略】EP
■myelogenic : 【発音】】ma`iэlαdзe'nik、【@】マイアラジェニク
■soman {名} : 化》ソマン◆化学名は Pinacolyl methylphosphonofluoridate で神経ガスの一種◆【略】GD
■unhuman {形-2} : 超人的{ちょうじん てき}な(=superhuman
$ |
全体では、1,899,349 行と、Ver,108 から、12,703 行増えていますが、エラーは 8個と減っています。13個修正され、新たに 2個加わったため、19 - 13 + 2 = 8 となっています。これらのバグは、前回同様、EDP へ報告しておきました。
2007-12-09 update (Buffered)Reader は、ReadLineCommand の constructor に渡すため、CheckLineComand の constructor に渡していましたが、Context オブジェクトにセットして渡すことにしました。これにより、CheckLineCommand (Chain) オブジェクトは、Reader オブジェクトと独立になりました。修正部分は、青になっています。Tags: programming
|
|
ROME - Kaz Muzik Blog AtomFeed #2 - KazMuzik Blog
2007-12-06 06:40
昨日、Firefox の Live Bookmarks のところで紹介したように、オレンジ色の Feed アイコンをクリックすると、2番目の画像のようなページが表示されますが、これは Atom Feed なので、View > Page Source で、ソースを表示させると、下記のような XML になっています。
<?xml version="1.0" encoding="utf-8"?>
<!-- If you are running a bot please visit this policy page outlining rules you must respect. http://www.livejournal.com/bots/ -->
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:lj="http://www.livejournal.com">
<id>urn:lj:livejournal.com:atom1:kazuomik</id>
<title>Kaz Muzik Blog</title>
<subtitle>Kaz Muzik</subtitle>
<author>
<name>Kaz Muzik</name>
</author>
<link rel="alternate" type="text/html" href="http://kazuomik.livejournal.com/"/>
<link rel="self" type="text/xml" href="http://kazuomik.livejournal.com/data/atom"/>
<updated>2007-12-06T04:06:17Z</updated>
<lj:journal username="kazuomik" type="personal"/>
<link rel="service.feed" type="application/x.atom+xml" href="http://kazuomik.livejournal.com/data/atom" title="Kaz Muzik Blog"/>
<entry>
<id>urn:lj:livejournal.com:atom1:kazuomik:165898</id>
<link rel="alternate" type="text/html" href="http://kazuomik.livejournal.com/165898.html"/>
<title>Kaz Muzik Blog Atom Feed and Firefox Live Bookmarks</title>
<published>2007-12-06T03:40:36Z</published>
<updated>2007-12-06T04:06:17Z</updated>
<category term="computer technology"/>
<content type="html">Firefox で、<a href="http://kazuomik.livejournal.com/">このブログのトップページ</a>へ行くと、</content>
</entry>
<entry>
<id>urn:lj:livejournal.com:atom1:kazuomik:165872</id>
<link rel="alternate" type="text/html" href="http://kazuomik.livejournal.com/165872.html"/>
<title>英辞郎 format checker - Apache Commons Chain</title>
<published>2007-12-05T16:21:41Z</published>
<updated>2007-12-05T16:23:12Z</updated>
<category term="programming"/>
<content type="html"><a href="/149594.html">11/7 のエントリで、英辞郎 Ver.108 のデータのバグを報告</a>しましたが、...</content>
</entry>
<entry>
...
</entry>
</feed>
|

|
今回は、これを、Java から扱ってみます。Rome という RSS や Atom feeds を扱う Java のライブラリがあるので、これを使ってみます。Rome の最新のリリースは、0.9 ですが、これは core な部分だけのライブラリです。今回は fetch したいので、subproject の Rome Fetcher も使うことにします。また、JDOM に依存しているので、準備しておきます。
import java.net.URL;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.fetcher.FeedFetcher;
import com.sun.syndication.fetcher.impl.HttpURLFeedFetcher;
public class KazMuzikBlogFeedFetcher {
private static final String kazMuzikBlogFeedUrl = "http://kazuomik.livejournal.com/data/atom";
public static void main(String[] args) throws Exception {
FeedFetcher fetcher = new HttpURLFeedFetcher();
SyndFeed feed = fetcher.retrieveFeed(new URL(kazMuzikBlogFeedUrl));
System.out.println(feed.toString());
}
} |
$ wget https://rome.dev.java.net/dist/rome-fetcher-0.9.zip
$ unzip rome-0.9.zip
Archive: rome-0.9.zip
creating: rome-0.9/
...
inflating: rome-0.9/rome-0.9.jar
$ wget https://rome.dev.java.net/dist/rome-fetcher-0.9.zip
$ unzip rome-fetcher-0.9.zip
$ wget http://www.jdom.org/dist/binary/jdom-1.1.zip
$ unzip jdom-1.1.zip
$ mkdir lib
$ cp rome-0.9/rome-0.9.jar lib
$ cp rome-fetcher-0.9/rome-fetcher-0.9.jar lib
$ cp jdom-1.1/build/jdom.jar lib/jdom-1.1.jar
$ javac -classpath lib/rome-0.9.jar:lib/rome-fetcher-0.9.jar KazMuzikBlogFeedFetcher.java
$ java -classpath .:lib/rome-0.9.jar:lib/rome-fetcher-0.9.jar:lib/jdom-1.1.jar KazMuzikBlogFeedFetcher \
| sed -e '/^SyndFeedImpl.*=null$/d' -e '/^SyndFeedImpl.*=\[\]$/d'
SyndFeedImpl.link=http://kazuomik.livejournal.com/
SyndFeedImpl.foreignMarkup[0]=[Element: <lj:journal [Namespace: http://www.livejournal.com]/>]
SyndFeedImpl.interface=interface com.sun.syndication.feed.synd.SyndFeed
SyndFeedImpl.descriptionEx.value=Kaz Muzik
SyndFeedImpl.descriptionEx.interface=interface com.sun.syndication.feed.synd.SyndContent
SyndFeedImpl.supportedFeedTypes[0]=rss_0.91N
SyndFeedImpl.supportedFeedTypes[1]=rss_0.93
SyndFeedImpl.supportedFeedTypes[2]=rss_0.92
SyndFeedImpl.supportedFeedTypes[3]=rss_1.0
SyndFeedImpl.supportedFeedTypes[4]=rss_0.94
SyndFeedImpl.supportedFeedTypes[5]=rss_2.0
SyndFeedImpl.supportedFeedTypes[6]=rss_0.91U
SyndFeedImpl.supportedFeedTypes[7]=rss_0.9
SyndFeedImpl.supportedFeedTypes[8]=atom_1.0
SyndFeedImpl.supportedFeedTypes[9]=atom_0.3
SyndFeedImpl.uri=urn:lj:livejournal.com:atom1:kazuomik
SyndFeedImpl.titleEx.value=Kaz Muzik Blog
SyndFeedImpl.titleEx.interface=interface com.sun.syndication.feed.synd.SyndContent
SyndFeedImpl.authors[0].name=Kaz Muzik
SyndFeedImpl.title=Kaz Muzik Blog
SyndFeedImpl.feedType=atom_1.0
SyndFeedImpl.description=Kaz Muzik
SyndFeedImpl.entries[0].contents[0].value=Firefox で、<a href="http://kazuomik.livejournal.com/">このブログのトップページ</a>へ行くと、...
SyndFeedImpl.entries[0].contents[0].interface=interface com.sun.syndication.feed.synd.SyndContent
SyndFeedImpl.entries[0].contents[0].type=html
SyndFeedImpl.entries[0].updatedDate=Wed Dec 05 20:06:17 PST 2007
SyndFeedImpl.entries[0].link=http://kazuomik.livejournal.com/165898.html
SyndFeedImpl.entries[0].links[0].hreflang=http://kazuomik.livejournal.com/165898.html
SyndFeedImpl.entries[0].links[0].length=0
SyndFeedImpl.entries[0].links[0].rel=alternate
SyndFeedImpl.entries[0].links[0].type=text/html
SyndFeedImpl.entries[0].links[0].href=http://kazuomik.livejournal.com/165898.html
SyndFeedImpl.entries[0].interface=interface com.sun.syndication.feed.synd.SyndEntry
SyndFeedImpl.entries[0].uri=urn:lj:livejournal.com:atom1:kazuomik:165898
SyndFeedImpl.entries[0].titleEx.value=Kaz Muzik Blog Atom Feed and Firefox Live Bookmarks
SyndFeedImpl.entries[0].titleEx.interface=interface com.sun.syndication.feed.synd.SyndContent
SyndFeedImpl.entries[0].author=
SyndFeedImpl.entries[0].title=Kaz Muzik Blog Atom Feed and Firefox Live Bookmarks
SyndFeedImpl.entries[0].categories[0].name=computer technology
SyndFeedImpl.entries[0].publishedDate=Wed Dec 05 19:40:36 PST 2007
SyndFeedImpl.entries[0].modules[0].date=Wed Dec 05 19:40:36 PST 2007
SyndFeedImpl.entries[0].modules[0].dates[0]=Wed Dec 05 19:40:36 PST 2007
SyndFeedImpl.entries[0].modules[0].interface=interface com.sun.syndication.feed.module.DCModule
SyndFeedImpl.entries[0].modules[0].uri=http://purl.org/dc/elements/1.1/
SyndFeedImpl.entries[1].contents[0].value=<a href="/149594.html">11/7 のエントリで、英辞郎 Ver.108 のデータのバグを報告</a>しましたが、...
SyndFeedImpl.entries[1].contents[0].interface=interface com.sun.syndication.feed.synd.SyndContent
SyndFeedImpl.entries[1].contents[0].type=html
SyndFeedImpl.entries[1].updatedDate=Wed Dec 05 08:23:12 PST 2007
SyndFeedImpl.entries[1].link=http://kazuomik.livejournal.com/165872.html
...
SyndFeedImpl.entries[24].modules[0].uri=http://purl.org/dc/elements/1.1/
SyndFeedImpl.publishedDate=Wed Dec 05 20:06:17 PST 2007
SyndFeedImpl.modules[0].date=Wed Dec 05 20:06:17 PST 2007
SyndFeedImpl.modules[0].dates[0]=Wed Dec 05 20:06:17 PST 2007
SyndFeedImpl.modules[0].interface=interface com.sun.syndication.feed.module.DCModule
SyndFeedImpl.modules[0].uri=http://purl.org/dc/elements/1.1/
$ |
Tags: programming
|
|
英辞郎 format checker - Apache Commons Chain - KazMuzik Blog
2007-12-05 07:44
11/7 のエントリで、英辞郎 Ver.108 のデータのバグを報告しましたが、Ver.109 もリリースされたので、Lucene Index を更新する前に、データのチェックプログラムを作成しました。
昨日は、Apache Log4j と Apache Commons Logging を紹介しましたが、Apache Commons には、いろいろ便利なパッケージがあるので、積極的に使っていこうと思います。今日は、Commons Chain を紹介します。これは、Chains of Responsibility (design) pattern のための API で、Command オブジェクトの execute() メソッドに Context オブジェクトを渡して、chain で実行していくものです。詳細は、いい Cookbook があるので、それを参考にされるのがいいと思います。
今回は、一行の英辞郎データをチェックするところに、commons-chain を使ってみました。
package net.java.sampo.eijiro;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.chain.Chain;
import org.apache.commons.chain.Command;
import org.apache.commons.chain.Context;
import org.apache.commons.chain.impl.ChainBase;
import org.apache.commons.chain.impl.ContextBase;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class EijiroFormatChecker {
private static final String eijiroFile = "EIJI-108.TXT";
private static final String eijiroFileEncoding = "Shift-JIS";
private static Log log = LogFactory.getLog(EijiroFormatChecker.class);
private Context context;
private Command checkLineCommand;
public EijiroFormatChecker() throws IOException {
context = new ContextBase();
context.put("errors", new ArrayList<String>());
checkLineCommand = new CheckLineCommand();
}
public void close() throws IOException {
context = null;
}
public List<String> getErrorLines() {
return (List<String>)context.get("errors");
}
public void checkFile() throws IOException {
checkFile(eijiroFile, eijiroFileEncoding);
}
public void checkFile(String filename, String encoding) throws IOException {
BufferedReader in
= new BufferedReader(new InputStreamReader(
new FileInputStream(filename), encoding));
int lineCount = 0;
int errorCount = 0;
if (log.isInfoEnabled()) {
log.info("Started checking \"" + filename + "\" ...");
}
while (true) {
String line = in.readLine();
if (line == null) {
break;
}
context.put("line", line);
try {
checkLineCommand.execute(context);
}
catch (Exception e) {
log.error(e.getMessage(), e);
}
lineCount ++;
if (lineCount % 100000 == 0) {
if (log.isInfoEnabled()) {
log.info("" + lineCount + " lines processed.");
}
}
}
if (log.isInfoEnabled()) {
log.info("" + lineCount + " lines processed, completed.");
log.info("" + getErrorLines().size() + " errors found.");
}
in.close();
}
private static class CheckLineCommand extends ChainBase {
public CheckLineCommand() {
super();
addCommand(new CheckSeparatorCommand(" : "));
addCommand(new CheckParenthesisChain());
}
}
private static class CheckSeparatorCommand implements Command {
private String separator;
public CheckSeparatorCommand(String separator) {
this.separator = separator;
}
public boolean execute(Context context) {
String line = (String)context.get("line");
if (line == null) {
log.error("line is null.");
return true;
}
int n = line.indexOf(separator);
if (n < 0) {
((List<String>)context.get("errors")).add(line);
if (log.isWarnEnabled()) {
log.warn("No separator \"" + separator+ "\" between English and Japanese - \""
+ line + "\"");
}
return false;
}
return false;
}
}
private static class CheckParenthesisChain extends ChainBase {
public CheckParenthesisChain() {
super();
addCommand(new CheckParenthesisCommand('{', '}')); // 品詞ラベル
addCommand(new CheckParenthesisCommand('\u3010', '\u3011'));
// 【】 特殊フィールド, スピーチラベル(?)
addCommand(new CheckParenthesisCommand('\uff5b', '\uff5d')); // {} 読み仮名
addCommand(new CheckParenthesisCommand('\u300a', '\u300b')); // 《》 分野ラベル
addCommand(new CheckParenthesisCommand('\u3008', '\u3009')); // 〈〉 分野ラベル
}
}
private static class CheckParenthesisCommand implements Command {
private char startChar;
private char endChar;
public CheckParenthesisCommand(char startChar, char endChar) {
this.startChar = startChar;
this.endChar = endChar;
}
public boolean execute(Context context) {
String line = (String)context.get("line");
if (line == null) {
log.error("line is null.");
return true;
}
int n = 0;
while (true) {
n = line.indexOf(startChar, n);
if (n < 0) {
break;
}
int m = line.indexOf(endChar, n+1);
if (m < 0) {
((List<String>)context.get("errors")).add(line);
if (log.isWarnEnabled()) {
log.warn("Unmatched \"" + startChar + "\" in \"" + line + "\"");
}
break;
}
if (m < 0 || m == line.length() - 1) {
break;
}
n = m + 1;
}
return false;
}
}
public static void main(String[] args) throws Exception {
EijiroFormatChecker checker = new EijiroFormatChecker();
checker.checkFile();
System.out.println();
for (String line : checker.getErrorLines()) {
System.out.println(line);
}
checker.close();
}
} |
Command や Chain オブジェクトの execute() メソッドは、boolean の値しか返さないので、入出力のデータはすべて、Context オブジェクトに格納してやることになります。
それでは、実行してみます。
$ cat check.sh
#!/bin/sh
CP=.:lib/eijiro-lucene.jar:lib/lucene-core-2.2.0.jar:lib/lucene-ja.jar:lib/sen.jar\
:lib/commons-logging-1.1.1.jar:lib/log4j-1.2.15.jar:lib/commons-chain-1.1.jar
java -Dsen.home=/usr/java/sen -classpath $CP net.java.sampo.eijiro.EijiroFormatChecker
$ sh check.sh
2007-12-05 07:21:38,982 [main] INFO net.java.sampo.eijiro.EijiroFormatChecker - Started checking "EIJI-108.TXT" ...
2007-12-05 07:21:39,202 [main] WARN net.java.sampo.eijiro.EijiroFormatChecker \
- Unmatched "{" in "■act responsibly : 責任{せきにん}ある行動こうどう}を取る、責任を持って行動{する、自分の行動に責任を持つ"
2007-12-05 07:21:39,430 [main] INFO net.java.sampo.eijiro.EijiroFormatChecker - 100000 lines processed.
2007-12-05 07:21:39,693 [main] INFO net.java.sampo.eijiro.EijiroFormatChecker - 200000 lines processed.
2007-12-05 07:21:39,949 [main] INFO net.java.sampo.eijiro.EijiroFormatChecker - 300000 lines processed.
2007-12-05 07:21:40,211 [main] INFO net.java.sampo.eijiro.EijiroFormatChecker - 400000 lines processed.
2007-12-05 07:21:40,470 [main] INFO net.java.sampo.eijiro.EijiroFormatChecker - 500000 lines processed.
2007-12-05 07:21:40,732 [main] INFO net.java.sampo.eijiro.EijiroFormatChecker - 600000 lines processed.
2007-12-05 07:21:40,996 [main] INFO net.java.sampo.eijiro.EijiroFormatChecker - 700000 lines processed.
2007-12-05 07:21:41,246 [main] INFO net.java.sampo.eijiro.EijiroFormatChecker - 800000 lines processed.
2007-12-05 07:21:41,511 [main] INFO net.java.sampo.eijiro.EijiroFormatChecker - 900000 lines processed.
2007-12-05 07:21:41,755 [main] INFO net.java.sampo.eijiro.EijiroFormatChecker - 1000000 lines processed.
2007-12-05 07:21:41,998 [main] INFO net.java.sampo.eijiro.EijiroFormatChecker - 1100000 lines processed.
2007-12-05 07:21:42,244 [main] INFO net.java.sampo.eijiro.EijiroFormatChecker - 1200000 lines processed.
2007-12-05 07:21:42,488 [main] INFO net.java.sampo.eijiro.EijiroFormatChecker - 1300000 lines processed.
2007-12-05 07:21:42,740 [main] INFO net.java.sampo.eijiro.EijiroFormatChecker - 1400000 lines processed.
2007-12-05 07:21:42,989 [main] INFO net.java.sampo.eijiro.EijiroFormatChecker - 1500000 lines processed.
2007-12-05 07:21:43,231 [main] INFO net.java.sampo.eijiro.EijiroFormatChecker - 1600000 lines processed.
2007-12-05 07:21:43,481 [main] INFO net.java.sampo.eijiro.EijiroFormatChecker - 1700000 lines processed.
2007-12-05 07:21:43,727 [main] INFO net.java.sampo.eijiro.EijiroFormatChecker - 1800000 lines processed.
2007-12-05 07:21:43,862 [main] WARN net.java.sampo.eijiro.EijiroFormatChecker \
- Unmatched "{" in "■wireless transceiver : 無線{むせん}トランシーバ[送受信機{そうじゅしんき]"
2007-12-05 07:21:43,934 [main] INFO net.java.sampo.eijiro.EijiroFormatChecker - 1886646 lines processed, completed.
2007-12-05 07:21:43,934 [main] INFO net.java.sampo.eijiro.EijiroFormatChecker - 2 errors found.
■act responsibly : 責任{せきにん}ある行動こうどう}を取る、責任を持って行動{する、自分の行動に責任を持つ
■wireless transceiver : 無線{むせん}トランシーバ[送受信機{そうじゅしんき]
$ |
Ver.108 のデータを使いましたが、11/7 のリストにあるバグのうちの 2つしか見つけることができませんでした。今回のは、ひとつの Command オブジェクトが、一行を通して、1つの対応している括弧をチェックしているので、前回のチェックより甘くなっているためです。
2007-12-07 update -> 英辞郎 format checker #2Tags: programming
|
|
Log4j (and commons-logging) - KazMuzik Blog
2007-12-04 17:46
英辞郎 Lucene Project で、もう一度、「天国への階段」を検索してみます。
$ cat index.sh
#!/bin/sh
CP=lib/eijiro-lucene.jar:lib/lucene-core-2.2.0.jar:lib/lucene-ja.jar:lib/sen.jar:lib/commons-logging.jar
java -Dsen.home=/usr/java/sen -classpath $CP EijiroSimpleIndex $*
$ sh index.sh
> 天国への階段
Dec 4, 2007 4:41:48 PM net.java.sen.Dictionary
INFO: token file = /usr/java/sen/dic/token.sen
Dec 4, 2007 4:41:48 PM net.java.sen.Dictionary
INFO: time to load posInfo file = 8[ms]
Dec 4, 2007 4:41:48 PM net.java.sen.Dictionary
INFO: double array trie dictionary = /usr/java/sen/dic/da.sen
Dec 4, 2007 4:41:48 PM net.java.sen.util.DoubleArrayTrie load
INFO: loading double array trie dict = /usr/java/sen/dic/da.sen
Dec 4, 2007 4:41:49 PM net.java.sen.util.DoubleArrayTrie load
INFO: loaded time = 0.141[ms]
Dec 4, 2007 4:41:49 PM net.java.sen.Dictionary
INFO: pos info file = /usr/java/sen/dic/posInfo.sen
Dec 4, 2007 4:41:49 PM net.java.sen.Dictionary
INFO: time to load pos info file = 0[ms]
Dec 4, 2007 4:41:49 PM net.java.sen.Tokenizer loadConnectCost
INFO: connection file = /usr/java/sen/dic/matrix.sen
Dec 4, 2007 4:41:49 PM net.java.sen.Tokenizer loadConnectCost
INFO: time to load connect cost file = 72[ms]
query=ja:"天国 へ の 階段"
1.000000[id=1858538] ■stairway to heaven : 〈楽曲〉天国への階段◆イギリスのロックグループ、レッド・ツェッペリンの代表曲。...
>
$ |
今回は、ログメッセージも載せました。CLASSPATH とログを見るとわかるように、Sen は Apache の Commons Logging API を使って、ログを書いています。
Java の Logging API としては、JDK にも 1.4 から、標準で、java.util.logging パッケージが入ってきましたが、世の中では Apache の Log4j が広く使われているようです。commons-logging からも、Log4j が使えるので、configuration ファイルを用意して、CLASSPATH に追加してみます。
$ cat commons-logging.properties
org.apache.commons.logging.Log=org.apache.commons.logging.impl.Log4JLogger
$ cat log4j.properties
log4j.rootCategory=INFO,A1
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p %c - %m%n
$ cat index.sh
#!/bin/sh
CP=lib/eijiro-lucene.jar:lib/lucene-core-2.2.0.jar:lib/lucene-ja.jar:lib/sen.jar:lib/commons-logging.jar
CP=.:$CP:lib/log4j-1.2.15.jar
java -Dsen.home=/usr/java/sen -classpath $CP EijiroSimpleIndex $*
$ sh index.sh
> 天国への階段
2007-12-04 17:23:31,136 [main] INFO net.java.sen.Dictionary - token file = /usr/java/sen/dic/token.sen
2007-12-04 17:23:31,155 [main] INFO net.java.sen.Dictionary - time to load posInfo file = 17[ms]
2007-12-04 17:23:31,155 [main] INFO net.java.sen.Dictionary \
- double array trie dictionary = /usr/java/sen/dic/da.sen
2007-12-04 17:23:31,156 [main] INFO net.java.sen.util.DoubleArrayTrie \
- loading double array trie dict = /usr/java/sen/dic/da.sen
2007-12-04 17:23:31,465 [main] INFO net.java.sen.util.DoubleArrayTrie \
- loaded time = 0.309[ms]
2007-12-04 17:23:31,466 [main] INFO net.java.sen.Dictionary - pos info file = /usr/java/sen/dic/posInfo.sen
2007-12-04 17:23:31,467 [main] INFO net.java.sen.Dictionary - time to load pos info file = 1[ms]
2007-12-04 17:23:31,468 [main] INFO net.java.sen.Tokenizer - connection file = /usr/java/sen/dic/matrix.sen
2007-12-04 17:23:31,617 [main] INFO net.java.sen.Tokenizer - time to load connect cost file = 148[ms]
query=ja:"天国 へ の 階段"
1.000000[id=1858538] ■stairway to heaven : 〈楽曲〉天国への階段◆イギリスのロックグループ、レッド・ツェッペリンの代表曲。...
>
$ |
ログが Log4j の PatternLayout で指定したフォーマットになりました。log4j.properties の INFO となっている出力レベルを WARN以上、例えば ERROR などとすれば、上記のログメッセージは出力されません。
Log4j では、階層的な Category という概念があり、ログの出力をコントロールすることができます。ここでは、net.java.sen の Dictionary と Tokenizer については、INFO レベルで出力し、net.java.sen.util.DoubleArrayTrie については、WARN レベルとします。
$ rm commons-logging.properties
$ cat log4j.properties
log4j.rootCategory=ERROR,A1
log4j.category.net.java.sen=INFO
log4j.category.net.java.sen.util=WARN
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p %c - %m%n
$ sh index.sh
> 天国への階段
2007-12-04 17:36:57,312 [main] INFO net.java.sen.Dictionary - token file = /usr/java/sen/dic/token.sen
2007-12-04 17:36:57,331 [main] INFO net.java.sen.Dictionary - time to load posInfo file = 17[ms]
2007-12-04 17:36:57,332 [main] INFO net.java.sen.Dictionary \
- double array trie dictionary = /usr/java/sen/dic/da.sen
2007-12-04 17:36:57,640 [main] INFO net.java.sen.Dictionary - pos info file = /usr/java/sen/dic/posInfo.sen
2007-12-04 17:36:57,642 [main] INFO net.java.sen.Dictionary - time to load pos info file = 2[ms]
2007-12-04 17:36:57,643 [main] INFO net.java.sen.Tokenizer - connection file = /usr/java/sen/dic/matrix.sen
2007-12-04 17:36:57,791 [main] INFO net.java.sen.Tokenizer - time to load connect cost file = 148[ms]
query=ja:"天国 へ の 階段"
1.000000[id=1858538] ■stairway to heaven : 〈楽曲〉天国への階段◆イギリスのロックグループ、レッド・ツェッペリンの代表曲。...
>
$ |
最初に commons-logging の configuration ファイルを削除していますが、これがなくても、環境が適当に設定されていれば、できるだけ Log4j を使うようになっているようです。
Log4j には、Category の他にも、Appender や Layout という interface があり、ログの出力を細かくコントロールできるようになっています。
今までのサンプルコードでは、安直に、System.err などに print していましたが、今後は Log4j を使っていこうと考えています。Tags: programming
|
|
画面解像度の計算 #2 - KazMuzik Blog
2007-11-27 11:21
11/23 のエントリ "Acer AL2216Wbd - Black Friday Sale #4" での画面解像度の計算ですが、補足しておきたいと思います。まずは、python で関数を定義して、まとめておきます。
$ python
>>> from math import sqrt
>>> def d(w,h):
... return sqrt(w*w+h*h)
...
>>> def r(w,h,l):
... return d(w,h) / l
...
>>> d(1680,1050)
1981.1360377318867
>>> r(1680,1050,22)
90.051638078722121
>>>
$
|
ここで、画面のドット数を、幅を W = 1680, 高さを H = 1050 とし、ピタゴラスの定理より、対角線のドット数 D を計算すると、W^2 + H^2 = D^2 より、D = 1981.136 となったわけですが、実際に、対角線に 1981(.136...) 個のドットが並んでいるわけではありません。
しかし、画面の実際の幅と高さが、ドット数に比例している場合には、対角線の長さ(インチ)を L とすると、幅は、L x W / D (inch) となり、解像度は W / (L x W / D) = D / L となり、上記の例では 90.05 dpi と一致します。これは高さでも同じことになります。Tags: programming
|
|
天国への階段 - 英辞郎 + Lucene #9 - KazMuzik Blog
2007-11-25 02:16
英辞郎 + Lucene Project で、"stairway to heaven" の検索結果が、いつも 「(パンストの)伝線」ばかりでは格好悪いので、Lucene index に、「天国への階段」を加えることにします。
import net.java.sampo.lucene.eijiro.index.EijiroLuceneIndex;
import static net.java.sampo.lucene.eijiro.index.EijiroLuceneIndex.EijiroSearchResult;
import static net.java.sampo.lucene.eijiro.index.EijiroLuceneIndex.parse;
import org.apache.lucene.document.Document;
public class StairwayToHeaven {
private static final String stairwayToHeaven
= "天国への階段";
private static final String lineStairwayToHeaven
= "■stairway to heaven : "
+ "〈楽曲〉天国への階段"
+ "◆イギリスのロックグループ、レッド・ツェッペリンの代表曲。"
+ "ジミー・ページとロバート・プラントによって作詞作曲された。"
+ "1971年に発表され、以後ロック史上最高の名曲の一つとして広く愛聴されている。"
+ "レコードでの演奏時間は約8分。";
// http://ja.wikipedia.org/wiki/
// %E5%A4%A9%E5%9B%BD%E3%81%B8%E3%81%AE%E9%9A%8E%E6%AE%B5_%28%E6%A5%BD%E6%9B%B2%29
public static void main(String[] args) throws Exception {
EijiroLuceneIndex index = new EijiroLuceneIndex("eijiro_simple_index3");
index.openSearcher();
EijiroSearchResult[] results = index.search("stairway AND heaven");
boolean exists = false;
for (EijiroSearchResult result : results) {
String line = result.getLine();
if (line.indexOf(stairwayToHeaven) >= 0) {
exists = true;
}
}
index.closeSearcher();
if (exists) {
System.err.println( "\"Stairway to Heaven\" already exists." );
}
else {
System.err.println( "Adding \"Stairway to Heaven\"..." );
index.openWriter();
Document doc = parse(lineStairwayToHeaven);
index.addDocument(doc);
index.closeWriter(false);
}
}
}
|
実は、EijiroLuceneIndex クラスにも手を入れていますが、これについては次回(以降)に説明します。基本的には、今回のクラスから使うためのメソッドを、切り出して、public にしただけです。
$ java -Dsen.home=/usr/java/sen \
-classpath .:lib/lucene-core-2.2.0.jar:lib/lucene-ja.jar:lib/sen.jar:lib/commons-logging.jar \
StairwayToHeaven
Adding "Stairway to Heaven" ...
$ sh index.sh 5
> stairway AND heaven
query=(+en:stairway +en:heaven)
1.000000[id=1576357] ■stairway to heaven : 〈豪俗〉(パンストの)伝線◆【同】run ; ladder
1.000000[id=1858538] ■stairway to heaven : 〈楽曲〉天国への階段◆イギリスのロックグループ、レッド・ツェッペリンの代表曲。\
ジミー・ページとロバート・プラントによって作詞作曲された。1971年に発表され、以後ロック史上最高の名曲の一つとして広く愛聴されている。\
レコードでの演奏時間は約8分。
> 天国への階段
query=ja:"天国 へ の 階段"
1.000000[id=1858538] ■stairway to heaven : 〈楽曲〉天国への階段◆イギリスのロックグループ、レッド・ツェッペリンの代表曲。\
ジミー・ページとロバート・プラントによって作詞作曲された。1971年に発表され、以後ロック史上最高の名曲の一つとして広く愛聴されている。\
レコードでの演奏時間は約8分。
>
$ |
いい感じになりました。Tags: programming
|
|
Kaz Muzik Blog Backup Project #29 - KazMuzik Blog
2007-11-17 23:25
7/4/2007 (#25) 以来、4ヶ月以上、backup をサボっていたため、今日、Nutch と Derby を使った backup をとりました。ほとんど、手順は同じですが、最初に Nutch の crawldb に inject するための URL リストは、#27 の monthly summary page(s) から作成しました。
$ cd ~/kazmuzikblog
$ mkdir /usr/local/nutch-0.9/kazmuzik-url-dir
$ java -classpath classes LiveJournalMonthlyManager \
| cut -f 2 \
| sed -e 's/^/http\:\/\/kazuomik.livejournal.com\//' -e 's/$/.html/' \
> /usr/local/nutch-0.9/kazmuzik-url-dir/20071116.txt
$ cd /usr/local/nutch-0.9
$ bin/nutch inject kazmuzik-crawldb kazmuzik-url-dir
...
$ mkdir kazmuzik-segments
$ bin/nutch generate kazmuzik-crawldb kazmuzik-segments
...
Generator: segment: kazmuzik-segments/20071117161617
...
$ bin/nutch fetch kazmuzik-segments/20071117161617
...
fetching http://kazuomik.livejournal.com/141077.html
fetching http://kazuomik.livejournal.com/88261.html
fetching http://kazuomik.livejournal.com/151961.html
...
fetching http://kazuomik.livejournal.com/36988.html
fetching http://kazuomik.livejournal.com/79224.html
fetching http://kazuomik.livejournal.com/81466.html
Fetcher: done
$ bin/nutch readseg -list -dir kazmuzik-segments
NAME GENERATED FETCHER START FETCHER END FETCHED PARSED
20071117161617 609 2007-11-17T16:16:46 2007-11-17T17:11:29 609 594
$ touch kazmuzik-segments/20071117161617/fetcher.done
$ bin/nutch updatedb kazmuzik-crawldb -dir kazmuzik-segments -noAdditions
...
$ bin/nutch readdb kazmuzik-crawldb -stats
CrawlDb statistics start: kazmuzik-crawldb
Statistics for CrawlDb: kazmuzik-crawldb
TOTAL urls: 609
retry 0: 609
min score: 1.001
avg score: 1.015
max score: 1.114
status 1 (db_unfetched): 15
status 2 (db_fetched): 594
CrawlDb statistics: done
$ bin/nutch generate kazmuzik-crawldb kazmuzik-segments
...
Generator: segment: kazmuzik-segments/20071117184651
...
$ bin/nutch fetch kazmuzik-segments/20071117184651
...
fetching http://kazuomik.livejournal.com/152532.html
fetching http://kazuomik.livejournal.com/148363.html
fetching http://kazuomik.livejournal.com/34221.html
Fetcher: done
$ bin/nutch readseg -list -dir kazmuzik-segments
NAME GENERATED FETCHER START FETCHER END FETCHED PARSED
20071117161617 609 2007-11-17T16:16:46 2007-11-17T17:11:29 609 594
20071117184651 15 2007-11-17T18:47:24 2007-11-17T18:48:40 15 15
$ bin/nutch updatedb kazmuzik-crawldb -dir kazmuzik-segments -noAdditions
...
$ bin/nutch readdb kazmuzik-crawldb -stats
CrawlDb statistics start: kazmuzik-crawldb
Statistics for CrawlDb: kazmuzik-crawldb
TOTAL urls: 609
retry 0: 609
min score: 1.002
avg score: 1.031
max score: 1.228
status 2 (db_fetched): 609
CrawlDb statistics: done
$ bin/nutch mergesegs kazmuzik-segments2 -dir kazmuzik-segments
...
$ bin/nutch readseg -list -dir kazmuzik-segments2
NAME GENERATED FETCHER START FETCHER END FETCHED PARSED
20071117185404 609 2007-11-17T16:16:46 2007-11-17T18:48:40 609 609
$ cd ~/kazmuzikblog
$ java -classpath classes:/usr/java/jdk/db/lib/derby.jar:/usr/local/nutch-0.9/nutch-0.9.jar:\
/usr/local/nutch-0.9/lib/hadoop-0.12.2-core.jar:/usr/local/nutch-0.9/lib/commons-logging-1.0.4.jar:\
/usr/local/nutch-0.9/lib/log4j-1.2.13.jar \
LiveJournalEntryDatabaseInitializer /usr/local/nutch-0.9/kazmuzik-segments2/20071117185404
...
$ |
3つのエントリで、LiveJournalEntryParser が Exception を throw してしまいました。152816 は、11/10 の書いたこのプロジェクトの #26 ですが、mood, location, tag(s) を parse するときに、そこのソースコードで "<" をエスケープしていなかったため、そこで引っ掛かってしまいました。128843 と 129431 は、Subject を書いていませんでしたが、parser がサポートしていませんでした。今回は、エントリに Subject を追加するという workaround で回避しておきました。この 3つの URL リストを作成して、freegen という Nutch のサブコマンドで、直接 Nutch segment を generate して、再開しました。
$ cd /usr/local/nutch-0.9
$ mkdir kazmuzik-url-dir2
$ cat > kazmuzik-url-dir2/20071116b.txt
http://kazuomik.livejournal.com/128843.html
http://kazuomik.livejournal.com/129431.html
http://kazuomik.livejournal.com/152816.html
^D
$ bin/nutch freegen kazmuzik-url-dir2 kazmuzik-segments2
...
$ bin/nutch fetch kazmuzik-segments2/20071117221341
...
$ bin/nutch mergesegs kazmuzik-segments3 -dir kazmuzik-segments2
...
$ bin/nutch readseg -list -dir kazmuzik-segments3
NAME GENERATED FETCHER START FETCHER END FETCHED PARSED
20071117221515 609 2007-11-17T16:16:46 2007-11-17T22:14:35 609 609
$ java -classpath classes:/usr/java/jdk/db/lib/derby.jar:/usr/local/nutch-0.9/nutch-0.9.jar:\
/usr/local/nutch-0.9/lib/hadoop-0.12.2-core.jar:/usr/local/nutch-0.9/lib/commons-logging-1.0.4.jar:\
/usr/local/nutch-0.9/lib/log4j-1.2.13.jar \
LiveJournalEntryDatabaseInitializer /usr/local/nutch-0.9/kazmuzik-segments3/20071117221515
$ java -classpath classes:/usr/java/derby/lib/derby.jar LiveJournalHtmlCreator
$ |
Tags: programming
|
|
英辞朗 + Lucene #8 - KazMuzik Blog
2007-11-12 00:35
前回(11/8, #6)のインデックスを用いて、英語と日本語でいくつか検索してみました。
$ cat index.sh
#!/bin/sh
CP=lib/eijiro-lucene.jar:lib/lucene-core-2.2.0.jar:lib/lucene-ja.jar:lib/sen.jar:lib/commons-logging.jar
java -Dsen.home=/usr/java/sen -classpath $CP EijiroSimpleIndex $*
$ sh index.sh 3
> stairway to heaven
1.000000 ■stairway to heaven : 〈豪俗〉(パンストの)伝線◆【同】run ; ladder
0.464318 ■stairway {名} : 階段{かいだん}、階段室{かいだんしつ}\
■・Tom runs up and down the stairway to get exercise.\
トムは運動のために階段を走って上り下りする。
0.464318 ■stairway : 【レベル】9、【@】ステアウェー、ステアウェイ、【変化】《複》stairways、【分節】stair・way
> 天国への階段
(no result)
> 天国 階段
1.000000 ■blessed land : 天国{てんごく}
1.000000 ■divine Kingdom : 天国{てんごく}
1.000000 ■happy land : 天国{てんごく}
>
$ |
"天国への階段" では、no result になってしまいました。英語の "stairway to heaven" では、"stairway" だけの単語でもヒットしています。実際、日本語でも、"天国 階段" と間にスペースを入れて検索すると、"天国" だけにもヒットします。JapaneseAnalyzer は "天国への階段" をきちんと tokenize できるはずなので、QueryParser が parse して作った Query オブジェクトに違いがあるようです。
EijiroSimpleIndex クラスの search() メソッドの中で、Query オブジェクトをプリントしてみます。
$ sh index.sh 1
> stairway to heaven
query=(en:stairway en:heaven)
1.000000 ■stairway to heaven : 〈豪俗〉(パンストの)伝線◆【同】run ; ladder
> stairway OR heaven
query=(en:stairway en:heaven)
1.000000 ■stairway to heaven : 〈豪俗〉(パンストの)伝線◆【同】run ; ladder
> stairway AND heaven
query=(+en:stairway +en:heaven)
1.000000 ■stairway to heaven : 〈豪俗〉(パンストの)伝線◆【同】run ; ladder
> "stairway to heaven"
query=en:"stairway heaven"
1.000000 ■stairway to heaven : 〈豪俗〉(パンストの)伝線◆【同】run ; ladder
> 天国への階段
query=ja:"天国 へ の 階段"
(no result)
> 天国 階段
query=(ja:天国 ja:階段)
1.000000 ■blessed land : 天国{てんごく}
> 天国 OR 階段
query=(ja:天国 ja:階段)
1.000000 ■blessed land : 天国{てんごく}
> 天国 AND 階段
query=(+ja:天国 +ja:階段)
(no result)
> 天国への
query=ja:"天国 へ の"
1.000000 ■Al Sirat : 宗教上の正道、天国への橋
> パンストの伝線
query=ja:"パン スト の 伝 線"
1.000000 ■ladder {名-3} : 〈英〉〔パンストの〕伝線{でんせん}◆【同】〈米〉run
> パンスト伝線
query=ja:"パン スト 伝 線"
(no result)
> パンスト 伝線
query=(ja:"パン スト" ja:"伝 線")
1.000000 ■ladder {名-3} : 〈英〉〔パンストの〕伝線{でんせん}◆【同】〈米〉run
> パンスト AND 伝線
query=(+ja:"パン スト" +ja:"伝 線")
1.000000 ■ladder {名-3} : 〈英〉〔パンストの〕伝線{でんせん}◆【同】〈米〉run
>
$ |
QueryParser が、"天国への階段" を parse すると、ja:"天国 へ の 階段" となり、日本語フィールドに、"天国", "へ", "の", "階段" と、その順に出てこないとマッチしません。一方、英語の "stairway to heaven" は、(en:stiarway en:heaven) となり、"stairway" または "heaven" があればマッチします。これは、英語の場合、スペースがあり、また "to" が Stop Word になっているためです。QueryParser の仕様を考えれば、当たり前の結果ですが、日本語で検索文字の入力を考えた場合、わざわざスペースを入れなくても、英語と同様に、OR での検索になってほしい場合があります。また、"の" や "へ" などの助詞は、Stop Word として扱った方がいい場合もあります。このあたりは、カスタマイズする必要がありそうです。Tags: programming
|
|
Kaz Muzik Blog Backup Project #28 - Kaz Muzik Blog Google Search Project #6 - KazMuzik Blog
2007-11-11 11:38
前回(#26) のソースコードをコンパイルして、実行します。
$ ant
$ cat monthly.sh
#!/bin/sh
java -classpath classes LiveJournalMonthlyManager \
| cut -f 2 \
> tmp1.txt
paste -d '#' tmp1.txt tmp1.txt \
| sed -e 's/^/\<a href\=\"\//' -e 's/\#/\.html\"\>/' -e 's/$/\<\/a\>/' \
> tmp2.txt
$ sh monthyly.sh
$ cat tmp2.txt
<a href="/2603.html">2603</a>
<a href="/2080.html">2080</a>
<a href="/2409.html">2409</a>
...
<a href="/151961.html">151961</a>
<a href="/145303.html">145303</a>
<a href="/152249.html">152249</a>
$ wc -l tmp2.txt
594 tmp2.txt
$ |
これを、Google Search From のエントリに貼り付けました。エントリ数は、600 近くになっていました。Tags: programming
|
|
Kaz Muzik Blog Backup Project #27 - LiveJournalMonthlyParser & LivrJournalMonthlyManager - KazMuzik Blog
2007-11-11 10:47
以前の方法では、このブログのバックアップがとれなくなったため、新しい方法が必要になりました。ヒントは Google Search にありました。うざいと思っていた calendar の月の summary page を、逆に利用してしまう方法です。このブログは、去年の 9月から始めたので、それからの月ごとのページをすべて fetch してくれば、エントリの内容まではわかりませんが、ID やタイトル、時刻の一覧表は作成できます。以前の方法では、エントリの数が多くなると、例え可能だったとしても、"/?skip=NNN" で何回アクセスすればいいかわかりませんでしたが、このアイデアによる方法では、月の数だけアクセスすれば、必ず、すべての ID が取得できます。今月だったら、(2007 - 2006) x 12 + (11 - 9) + 1 = 15回です。
まずは、月の summary page を parse するクラスです。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.Calendar;
import java.util.Date;
public class LiveJournalMonthlyParser extends BufferedReader {
private int yyyy;
private int mm;
private int dd;
public LiveJournalMonthlyParser(Reader in) throws IOException {
super(in);
}
public void close() throws IOException {
super.close();
}
public LiveJournalEntry parse() throws IOException {
int mode = 0;
while (true) {
String line = readLine();
if (line == null) {
break;
}
if (mode == 1) {
int n = line.indexOf(".livejournal.com/");
yyyy = Integer.parseInt( line.substring(n+17, n+21) );
mm = Integer.parseInt( line.substring(n+22, n+24) );
dd = Integer.parseInt( line.substring(n+25, n+27) );
mode = 0;
continue;
}
if (mode == 2) {
int n = line.indexOf(':');
int _hh = Integer.parseInt( line.substring(n-2,n) );
int _mm = Integer.parseInt( line.substring(n+1,n+3) );
if (line.charAt(n+4) == 'p') {
_hh += 12;
}
Calendar cal = Calendar.getInstance();
cal.set(yyyy, mm-1, dd, _hh, _mm);
Date date = cal.getTime();
n = line.indexOf(".livejournal.com/");
int m = line.indexOf('.', n+17);
int id = Integer.parseInt( line.substring(n+17,m) );
n = line.indexOf("</a>", m+7);
String title = line.substring(m+7,n);
LiveJournalEntry entry = new LiveJournalEntry(id);
entry.setTitle(title);
entry.setDate(date);
return entry;
}
if (line.indexOf("class=\"entryDate\"") >= 0) {
mode = 1;
continue;
}
if (line.indexOf("class=\"entry\"") >= 0) {
mode = 2;
continue;
}
}
return null;
}
} |
次は、これを利用して、月ごとに entry を管理するクラスです。
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.Formatter;
import java.util.List;
public class LiveJournalMonthlyManager {
public LiveJournalMonthlyManager() {
}
public List getEntries(int yyyy, int mm)
throws IOException {
String urlString = getURLString(yyyy, mm);
URL url = new URL( getURLString(yyyy, mm) );
InputStream is = null;
try {
is = url.openStream();
}
catch (Exception e) {
System.err.printf("! %s : %s%n", urlString, e.getMessage());
return null;
}
if (is == null) {
return null;
}
LiveJournalMonthlyParser parser
= new LiveJournalMonthlyParser(new InputStreamReader(is, "UTF-8"));
List list = new ArrayList();
while (true) {
LiveJournalEntry entry = parser.parse();
if (entry == null) {
break;
}
list.add(entry);
}
parser.close();
return list;
}
private static String getURLString(int yyyy, int mm) {
Formatter f = new Formatter();
f.format("http://kazuomik.livejournal.com/%04d/%02d/", yyyy, mm);
return f.toString();
}
public static void main(String[] args) throws Exception {
LiveJournalMonthlyManager manager = new LiveJournalMonthlyManager();
int yyyy = 2006;
int mm = 9;
while (true) {
List list = manager.getEntries(yyyy, mm);
if (list == null || list.size() < 1) {
break;
}
for (LiveJournalEntry entry : list) {
Date time = entry.getDate();
System.out.printf("%tF %tR\t%d\t%s%n",
time, time, entry.getId(), entry.getTitle());
}
if (++mm > 12) {
yyyy++;
mm -= 12;
}
}
}
} |
main() メソッドで、すべてのエントリの時刻、ID、タイトルをプリントします。次(#28)は、これを実行します。Tags: programming
|
|
Kaz Muzik Blog Backup Project #26 - KazMuzik Blog
2007-11-10 20:34
先ほどの Kaz Muzik Blog Google Search Project (#4) のために、しばらくサボっていた Kaz Muzik Blog Backup Project を再開させることにしました。
まずは、6/3 (#22) に書いたように、-init オプションをつけて LiveJournalFetchPreparator を実行しましたが、"/?skip=80" のページで Exception が発生して abort してしまいました。これは、4/30 (#11) の LiveJournalEntryParser にバグがあったためです。9/19 のエントリには、Tags, Current Location, Current Mood のすべてが付いていますが、HTML では、この 3つの情報がすべて、この順番に 1行に含まれています。ところが、この行を parse する順番が間違っていたため、Location に Current Mood: ... の余計な文字列が含まれて、長くなってしい、Derby データベースにストアするときに、エラーが発生してしまいました。まずは、この bug fix からです。
public class LiveJournalEntryParser extends BufferedReader {
...
public LiveJournalEntry parse() throws IOException {
...
String location = null;
m = line.indexOf("<strong>Current Mood:</strong> ");
if (m >= 0) {
mood = removeTags( line.substring(m+31) );
line = line.substring(0,m);
}
m = line.indexOf("<strong>Current Location:</strong> ");
if (m >= 0) {
location = removeTags( line.substring(m+35) );
line = line.substring(0,m);
}
m = line.indexOf("<strong>Tags:</strong> ");
...
}
...
} |
次に実行したところ、"/?skip=400" で止まってしまいました。
$ ant
$ java -classpath classes:/usr/java/derby/lib/derby.jar LiveJournalFetchPreparator -init
http://kazuomik.livejournal.com/
152249*I 145303*I 151961*I 151707*I 151514*I 135539*I 151218*I 150763*I 150220*I 149897*I
149594*I 151028*I 150495*I 149305*I 149228*I 148921*I 148134*I 148649*I 148363*I 147935*I
http://kazuomik.livejournal.com/?skip=20
...
http://kazuomik.livejournal.com/?skip=380
54835*I 54734*I 54482*I 54085*I 53978*I 53663*I 53377*I 53139*I 52777*I 52543*I
52328*I 52168*I 51855*I 51556*I 51418*I 51044*I 50862*I 50481*I 50411*I 50032*I
http://kazuomik.livejournal.com/?skip=400
54835 54734 54482 54085 53978 53663 53377 53139 52777 52543
52328 52168 51855 51556 51418 51044 50862 50481 50411 50032
$ |
実際、/?skip=380 と /?skip=400 に直接アクセスすると、同じ情報が表示されます。LiveJournal では、"/?skip=NNN" の指定は、400 エントリまでの表示にしか対応していないようです。"/?skip=380" のページを一番下までスクロールさせて、"Previous" のリンクを確認すると、"/YYYY/MM/DD/" という日のページにリンクされています。
このブログのエントリも 400 を超えたため、以前のように、"/?skip=NNN" を使って、すべてのエントリを収集することが不可能になってしまいました。また、Google Search で、昔のエントリについては、月や日のページしか出てこないのは、これが原因のようです。
2007-11-17 update ソースコードの <strong> を < でエスケープしました。<pre> の中ですが、エスケープする必要があります。また、この箇所のために、LiveJournalEntryParser が、このエントリを処理している最中に、Exception が発生してしまいました。Tags: programming
|
|
英辞朗 + Lucene #7 - KazMuzik Blog
2007-11-08 16:02
前回までは、コマンドラインで地味にやってきましたが、一応、Swing ベースの簡単な GUI でアクセスできるようにしました。
import java.awt.BorderLayout;
import static java.awt.BorderLayout.CENTER;
import static java.awt.BorderLayout.EAST;
import static java.awt.BorderLayout.NORTH;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.util.Formatter;
import javax.swing.JButton;
import javax.swing.JFrame;
import static javax.swing.JFrame.EXIT_ON_CLOSE;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import org.apache.lucene.queryParser.ParseException;
public class EijiroLucenePanel extends JPanel implements ActionListener {
private EijiroSimpleIndex index;
private JTextField queryField;
private JButton searchButton;
private JTextArea resultArea;
public EijiroLucenePanel(EijiroSimpleIndex index) {
super();
setLayout(new BorderLayout());
this.index = index;
JPanel searchPanel = new JPanel();
add(searchPanel, NORTH);
searchPanel.setLayout(new BorderLayout());
queryField = new JTextField(32);
searchPanel.add(queryField, CENTER);
queryField.addActionListener(this);
searchButton = new JButton("Search");
searchPanel.add(searchButton, EAST);
searchButton.addActionListener(this);
resultArea = new JTextArea(24, 80);
add(new JScrollPane(resultArea), CENTER);
}
public void actionPerformed(ActionEvent ae) {
String query = queryField.getText();
EijiroSimpleIndex.EijiroSearchResult[] results = null;
try {
results = index.search(query);
}
catch (IOException ex) {
System.err.println(ex.getMessage());
return;
}
catch (ParseException ex) {
System.err.println(ex.getMessage());
return;
}
resultArea.setText("");
for (EijiroSimpleIndex.EijiroSearchResult result : results) {
Formatter f = new Formatter();
f.format("%f %s%n", result.getScore(), result.getLine());
resultArea.append(f.toString());
}
}
public static void main(String[] args) throws Exception {
JFrame frame = new JFrame("Eijiro Lucene");
frame.setLayout(new BorderLayout());
frame.add(new EijiroLucenePanel(new EijiroSimpleIndex()), CENTER);
frame.setDefaultCloseOperation(EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
} |
 Tags: programming
|
|
英辞朗 + Lucene #6 - KazMuzik Blog
2007-11-08 01:17
コマンドラインのパラメータで表示する結果の数を指定できるので、10 にして実行してみます。
$ java -Dsen.home=/usr/java/sen \
-classpath .:lib/lucene-core-2.2.0.jar:lib/lucene-ja.jar:lib/sen.jar:lib/commons-logging.jar \
EijiroSimpleIndex 10
> heaven
1.000000 ■heaven {間投} : 《heavens》これは大変!、まあ!、何てことだ!、とんでもない!\
■・Heavens! One of the biggest typhoons is coming. これは大変だ。最大級の台風が来てるって。
1.000000 ■heaven {名-1} : 天国{てんごく}、神、天■・When I die I want to go to heaven. 死んだら天国へ行きたい。
1.000000 ■heaven {名-2} : 至上{しじょう}の幸福{こうふく}、素晴{すば}らしいもの
1.000000 ■Heaven's will : 天命{てんめい}、天寿{てんじゅ}、神のご意志{いし}
1.000000 ■in heaven {1} : 《be ~》天国にいる、死んでいる
1.000000 ■in heaven {2} : 《be ~》天にも昇る気分{きぶん}[ような気持ち]である
1.000000 ■in heaven : 天に、天国に、死んで
1.000000 ■to heaven : ぜひとも〔~できればと願う〕\
■・I wish to (high) heaven I were with you right now. 今あなたのそばにいられたら、どんなにかよかったのに。
1.000000 ■will of Heaven : 《the ~》神のご意志{いし}、天命{てんめい}
0.625000 ■ascend to heaven : 天に昇る、天国{てんごく}に行く
> 天国
1.000000 ■blessed land : 天国{てんごく}
1.000000 ■divine Kingdom : 天国{てんごく}
1.000000 ■happy land : 天国{てんごく}
1.000000 ■kingdom of Heaven : 天国{てんごく}
1.000000 ■Land of the Leal : 天国{てんごく}
1.000000 ■sky {名-2} : 天国{てんごく}
0.625000 ■on the other side : 天国{てんごく}で\
■・I will talk to my grandfather again when we meet on the other side. もし天国で祖父に会ったら、また話をしたい。
0.625000 ■paradise for criminals : 犯罪天国{はんざい てんごく}
0.625000 ■paradise of fashion : ファッション天国{てんごく}
0.625000 ■paradisiacal {形} : 天国{てんごく}の
> 天国 AND 神 AND 天
1.000000 ■heaven {名-1} : 天国{てんごく}、神、天■・When I die I want to go to heaven. 死んだら天国へ行きたい。
>
$ |
今回は、日本語で検索しても、熟語より単語の方が上位に出てきました。しかし、"天国" と入力しても、"heaven {名-1}" が上位 10以内には出てきません。"天国 AND 神 AND 天" で検索すると、唯一の結果として出てくるので、それぞれの訳語が "ja" の Field となっているのは、間違いなさそうです。しかし、スコアを計算するときに、同一の Field であるため、"天国" が 3つの Term のうちの 1つとなり、ノルムが小さくなり、結果としてスコアが下がってしまいます。
コマンドラインのパラメータを 0 にして、すべての結果を表示してみます。
$ java -Dsen.home=/usr/java/sen \
-classpath .:lib/lucene-core-2.2.0.jar:lib/lucene-ja.jar:lib/sen.jar:lib/commons-logging.jar \
EijiroSimpleIndex 0
> 天国
1.000000 ■blessed land : 天国{てんごく}
1.000000 ■divine Kingdom : 天国{てんごく}
1.000000 ■happy land : 天国{てんごく}
1.000000 ■kingdom of Heaven : 天国{てんごく}
1.000000 ■Land of the Leal : 天国{てんごく}
1.000000 ■sky {名-2} : 天国{てんごく}
0.625000 ■on the other side : 天国{てんごく}で\
■・I will talk to my grandfather again when we meet on the other side. もし天国で祖父に会ったら、また話をしたい。
...
0.500000 ■go to the better place : 死ぬ、天国{てんごく}に行く
0.500000 ■head off to heaven : 天国{てんごく}に向かう、死ぬ
0.500000 ■heaven {名-1} : 天国{てんごく}、神、天\
■・When I die I want to go to heaven. 死んだら天国へ行きたい。
0.500000 ■heaven and hell : 天国{てんごく}と地獄{じごく}
0.500000 ■heaven on earth : 楽園{らくえん}、地上{ちじょう}の天国{てんごく}
...
0.250000 ■with God : 死んで神のみもとに、神とともにある、死んで天国にいる
> ツェッペリン
1.000000 ■Count Ferdinand von Zeppelin {人名} : ツェッペリン◆1838-1917。ドイツの軍人。第二次世界大戦前からの飛行船設計、製造で有名。
0.618718 ■Graf Zeppelin : グラーフ・ツェッペリン、ツェッペリン伯号
0.500000 ■Ferdinand Graf von Zeppelin {人名} : フェルディナンド・グラフ・ツェッペリン\
◆1838-1917。ドイツノ軍人であり発明家。飛行船を発明したことで知られる。
0.500000 ■Zeppelin {名} : 《航空》ツェッペリン型飛行船\
◆ドイツの大型飛行船。第一次大戦でロンドンを爆撃し、空の要塞{ようさい}と呼ばれた。\
第二次世界大戦前は米の戦略的ヘリウム禁輸出で、水素ガスを使用。その為墜落事故を起した。
0.375000 ■Song Remains the Same {映画} : 《The ~》レッド・ツェッペリン/狂熱のライブ◆米1976
> レッドツェッペリン
1.000000 ■Led Zeppelin : レッドツェッペリン◆英国のロックグループ
>
$ |
"heaven {名-1}" の「天国」での検索のスコアは 0.5 でした。
また、日本語で、「ツェッペリン」と入力して検索しても、「レッドツェッペリン」は引っ掛かってきませんでした。"Song Remains the Same" は、日本語フィールドに "レッド・ツェッペリン" とあるので、ヒットしています。これは、Sen をベースにした JapaneseAnalyzer を使っていて、標準の IPA の辞書を使っているためですが、このあたりもコントロールしたい場合は、Tokenizer や TokenFilter (Sen など), またそれが利用する辞書などもカスタマイズする必要がありそうです。
ちなみに、PDIC で「天国」を検索してみました。

たくさんの英訳がありますが、"heaven" はまん中からやや後ろあたりにあります。これは、英辞朗のデータそのものではなく、それからコンバートされた和英辞書用の「和英辞朗」(WAEI-108.TXT) をベースにしたものを使用しているためと思われます。
今回のプロジェクトでは、英和辞書データの英辞朗のデータファイルだけを使い、おまけで日本語からの検索も試してみましたが、本格的に実装するには、やはり和英辞書データも使うのが良さそうです。Tags: programming
|
|
英辞朗 + Lucene #5 - KazMuzik Blog
2007-11-07 23:08
前回(11/2, #4)からの改善点を実装しました。英辞朗 Ver.108 オリジナルデータのバグを回避するために、UTF-8 にコンバートしたものをエディットして、インデックス作成時の入力ファイルとしました。
...
public class EijiroSimpleIndex {
private static final String defaultIndexDirName
= "eijiro_simple_index3";
private static final String eijiroFile = "eiji-108a.txt";
private static final String encoding = "UTF-8";
...
private static Document parse(String line) {
Document doc = new Document();
doc.add(new Field("line", line, Field.Store.COMPRESS, Field.Index.NO));
int n = line.indexOf(" : ");
if (n < 0) {
return null;
}
String en = line.substring(1, n);
String ja = line.substring(n+3);
en = removeParenthesis(en, '{', '}').trim(); // 品詞ラベル
doc.add(new Field("en", en, Field.Store.NO, Field.Index.TOKENIZED));
n = ja.indexOf("\u25c6"); // ◆
if (n >= 0) {
ja = ja.substring(0,n); // 補足説明
}
else {
n = ja.indexOf("\u25a0\u30fb"); // ■・
if (n >= 0) {
ja = ja.substring(0,n); // 文例
}
}
ja = ja.trim();
n = 0;
while (true) {
int m = ja.indexOf('、', n);
if (m < 0) {
addJapaneseField(doc, ja.substring(n));
break;
}
addJapaneseField(doc, ja.substring(n, m));
n = m+1;
}
Field[] fields = doc.getFields("ja");
if (fields == null || fields.length < 1) {
return null;
}
return doc;
}
private static void addJapaneseField(Document doc, String s) {
s = normalizeJapanese(s);
if (s == null) {
return;
}
doc.add(new Field("ja", s, Field.Store.NO, Field.Index.TOKENIZED));
}
private static String normalizeJapanese(String s) {
String s1 = removeParenthesis(s, '【', '】'); // 特殊フィールド
// スピーチラベル?
if (s1.length() != s.length()) {
return null;
}
s = removeParenthesis(s, '{', '}'); // 読み仮名
s = removeParenthesis(s, '《', '》'); // 分野ラベル
s = removeParenthesis(s, '〈', '〉'); // 分野ラベル
return s;
}
private static String removeParenthesis(String s,
char startChar, char endChar) {
String s0 = s;
int n = 0;
while (true) {
n = s.indexOf(startChar, n);
if (n < 0) {
break;
}
int m = s.indexOf(endChar, n+1);
if (m < 0) {
System.err.printf("ERROR: %s%n", s0);
}
if (m < 0 || m == s.length() - 1) {
s = s.substring(0,n);
break;
}
s = s.substring(0,n) + s.substring(m+1);
}
return s;
}
public static class EijiroSearchResult {
private float score;
private String line;
public EijiroSearchResult(float score, String line) {
this.score = score;
this.line = line;
}
public float getScore() {
return score;
}
public String getLine() {
return line;
}
}
public EijiroSearchResult[] search(String q)
throws IOException, ParseException {
return search(q, 0);
}
public EijiroSearchResult[] search(String q, int n)
throws IOException, ParseException {
if (containsNonAsciiChar(q)) {
return search(q, n, false, true);
}
else {
return search(q, n, true, false);
}
}
private static boolean containsNonAsciiChar(String s) {
int n = s.length();
for (int i = 0; i < n; i++) {
if (s.charAt(i) >= '\u0080') {
return true;
}
}
return false;
}
public EijiroSearchResult[] search(String q, int n,
boolean en, boolean ja)
throws IOException, ParseException {
BooleanQuery query = new BooleanQuery();
if (en) {
query.add(enParser.parse(q), BooleanClause.Occur.SHOULD);
}
if (ja) {
query.add(jaParser.parse(q), BooleanClause.Occur.SHOULD);
}
Hits hits = searcher.search(query);
int len = hits.length();
if (n > 0 && len > n) {
len = n;
}
EijiroSearchResult[] results = new EijiroSearchResult[len];
for (int i = 0; i < len; i++) {
Document doc = hits.doc(i);
results[i] = new EijiroSearchResult(hits.score(i), doc.get("line"));
}
return results;
}
public static void main(String[] args) throws Exception {
int maxCount = 10;
if (args.length > 0) {
try {
maxCount = Integer.parseInt(args[0]);
}
catch (NumberFormatException e) {
}
}
EijiroSimpleIndex index = new EijiroSimpleIndex();
BufferedReader in
= new BufferedReader(new InputStreamReader(System.in, "UTF-8"));
while (true) {
System.out.print("> ");
System.out.flush();
String q = in.readLine();
if (q == null) {
break;
}
q = q.trim();
if (q.length() < 1) {
continue;
}
EijiroSearchResult[] results = index.search(q, maxCount);
if (results == null || results.length < 1) {
System.out.println("(no result)");
continue;
}
for (EijiroSearchResult result : results) {
System.out.printf("%f %s%n", result.getScore(), result.getLine());
}
}
System.out.println();
in.close();
index.close();
}
} |
Tags: programming
|
|
英辞朗 + Lucene #4 - KazMuzik Blog
2007-11-02 01:43
#3 のソースをコンパイルして、実行します。
$ javac -classpath lib/lucene-core-2.2.0.jar:lib/lucene-ja.jar EijiroSimpleIndex.java
$ java -Dsen.home=/usr/java/sen \
-classpath .:lib/lucene-core-2.2.0.jar:lib/lucene-ja.jar:lib/sen.jar:lib/commons-logging.jar \
EijiroSimpleIndex
> stairway
1.000000 ■stairway {名} : 階段{かいだん}、階段室{かいだんしつ}\
■・Tom runs up and down the stairway to get exercise. トムは運動のために階段を走って上り下りする。
1.000000 ■stairway : 【レベル】9、【@】ステアウェー、ステアウェイ、【変化】《複》stairways、【分節】stair・way
0.625000 ■ascend the stairway : 階段を上る
0.625000 ■cellar stairway : 地下室{ちかしつ}への階段{かいだん}
0.625000 ■circular stairway : 回り階段{まわりかいだん}
> heaven
1.000000 ■heaven {間投} : 《heavens》これは大変!、まあ!、何てことだ!、とんでもない!\
■・Heavens! One of the biggest typhoons is coming. これは大変だ。最大級の台風が来てるって。
1.000000 ■heaven {名-1} : 天国{てんごく}、神、天■・When I die I want to go to heaven. 死んだら天国へ行きたい。
1.000000 ■heaven {名-2} : 至上{しじょう}の幸福{こうふく}、素晴{すば}らしいもの
1.000000 ■heaven : 【レベル】2、【発音】he'vn、【@】ヘブン、【変化】《複》heavens、【分節】heav・en
1.000000 ■Heaven's will : 天命{てんめい}、天寿{てんじゅ}、神のご意志{いし}
> 天国
1.000000 ■shopper's paradise : 買い物天国
1.000000 ■shopping paradise : 買い物天国
1.000000 ■Zion {名-3} : 天国、ザイオン
0.800000 ■angler's paradise : 釣り人の天国
0.800000 ■blessed land : 天国{てんごく}
>
$ |
今回は、"stairway", "heaven" とも、熟語ではなく、単語がトップになりました。
ただし、日本語の検索では、複数の訳語があったり、基本的なものほど読みが混ざっているため、もう一工夫必要です。Tags: programming
|
|
英辞朗 + Lucene #3 - KazMuzik Blog
2007-11-01 22:04
前回(10/30, #2) に指摘した改善点を実装します。
まず、インデックス作成時には、Document オブジェクトを作るときに、英語("en")と日本語("ja")をそれぞれ別の Field とし、英語からは品詞、日本語からは補足説明と文例を検索対象から外しました。また、一行全体("line")の Field も残していますが、これは検索時の表示用のためで、検索には使用されません。このため、"en" と "ja" Field は、store していません。
なお、最近はあまり Lucene の API を直接使っていなかったので、うっかり "public class EijiroDocument extends org.apache.lucene.document.Document" と実装しようとしましたが、Document クラスは final であるため、コンパイルできません。
検索時には、入力文字列にアスキー文字以外を含んでいるかチェックして、アスキー文字だけの場合は英語の Field を、それ以外の場合は日本語の Field を検索するようにしました。
import org.apache.lucene.analysis.ja.JapaneseAnalyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Searcher;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.IOException;
public class EijiroSimpleIndex {
private static final String defaultIndexDirName
= "eijiro_simple_index2";
private static final String eijiroFile = "EIJI-108.TXT";
private static final String encoding = "Shift-JIS";
private static final int MAX_RESULTS = 10;
private File indexDirName;
private IndexReader reader;
private Searcher searcher;
private QueryParser enParser;
private QueryParser jaParser;
public EijiroSimpleIndex() throws IOException {
this(defaultIndexDirName);
}
public EijiroSimpleIndex(String indexDirName) throws IOException {
this.indexDirName = new File(indexDirName);
if (! (new File(indexDirName).exists())) {
createIndex();
}
reader = IndexReader.open(indexDirName);
searcher = new IndexSearcher(reader);
enParser = new QueryParser("en", new StandardAnalyzer());
jaParser = new QueryParser("ja", new JapaneseAnalyzer());
}
public void close() throws IOException {
reader.close();
}
public void createIndex() throws IOException {
IndexWriter writer
= new IndexWriter(indexDirName, new JapaneseAnalyzer());
BufferedReader in
= new BufferedReader(new InputStreamReader(
new FileInputStream(eijiroFile), encoding));
int count = 0;
while (true) {
String line = in.readLine();
if (line == null) {
break;
}
Document document = parse(line);
if (document == null) {
continue;
}
writer.addDocument(document);
count ++;
if (count >= 1000) {
System.err.print(".");
count = 0;
}
}
in.close();
System.err.println();
writer.optimize();
writer.close();
}
private static Document parse(String line) {
Document doc = new Document();
doc.add(new Field("line", line, Field.Store.COMPRESS, Field.Index.NO));
int n = line.indexOf(" : ");
if (n < 0) {
return null;
}
String en = line.substring(1, n);
String ja = line.substring(n+3);
n = en.lastIndexOf("}");
if (n >= 0) {
int m = en.lastIndexOf(" {", n-1);
en = en.substring(0, m); // 品詞 removed
}
en = en.trim();
doc.add(new Field("en", en, Field.Store.NO, Field.Index.TOKENIZED));
n = ja.indexOf("\u25c6"); // ◆
if (n >= 0) {
ja = ja.substring(0,n); // 補足説明 removed
}
else {
n = ja.indexOf("\u25a0\u30fb"); // ■・
if (n >= 0) {
ja = ja.substring(0,n); // 文例 removed
}
}
ja = ja.trim();
doc.add(new Field("ja", ja, Field.Store.NO, Field.Index.TOKENIZED));
return doc;
}
public static class EijiroSearchResult {
private float score;
private String line;
public EijiroSearchResult(float score, String line) {
this.score = score;
this.line = line;
}
public float getScore() {
return score;
}
public String getLine() {
return line;
}
}
public EijiroSearchResult[] search(String q)
throws IOException, ParseException {
if (containsNonAsciiChar(q)) {
return search(q, false, true);
}
else {
return search(q, true, false);
}
}
private static boolean containsNonAsciiChar(String s) {
int n = s.length();
for (int i = 0; i < n; i++) {
if (s.charAt(i) >= '\u0080') {
return true;
}
}
return false;
}
public EijiroSearchResult[] search(String q, boolean en, boolean ja)
throws IOException, ParseException {
BooleanQuery query = new BooleanQuery();
if (en) {
query.add(enParser.parse(q), BooleanClause.Occur.SHOULD);
}
if (ja) {
query.add(jaParser.parse(q), BooleanClause.Occur.SHOULD);
}
Hits hits = searcher.search(query);
int n = hits.length();
if (n > MAX_RESULTS) {
n = MAX_RESULTS;
}
EijiroSearchResult[] results = new EijiroSearchResult[n];
for (int i = 0; i < n; i++) {
Document doc = hits.doc(i);
results[i] = new EijiroSearchResult(hits.score(i), doc.get("line"));
}
return results;
}
public static void main(String[] args) throws Exception {
EijiroSimpleIndex index = new EijiroSimpleIndex();
BufferedReader in
= new BufferedReader(new InputStreamReader(System.in, "UTF-8"));
while (true) {
System.out.print("> ");
System.out.flush();
String q = in.readLine();
if (q == null) {
break;
}
q = q.trim();
if (q.length() < 1) {
continue;
}
EijiroSearchResult[] results = index.search(q);
if (results == null || results.length < 1) {
System.out.println("(no result)");
continue;
}
int count = 0;
for (EijiroSearchResult result : results) {
System.out.printf("%f %s%n", result.getScore(), result.getLine());
count ++;
if (count >= 5) {
break;
}
}
}
System.out.println();
in.close();
index.close();
}
} |
Tags: programming
|
|
英辞朗 + Lucene #2 - KazMuzik Blog
2007-10-30 01:55
前回 (#1)からの続きです。コンパイルして、実行してみます。
$ javac -classpath /usr/java/lucene-2.2.0/lucene-core-2.2.0.jar:/usr/java/lucene-ja/lib/lucene-ja.jar \
EijiroSimpleIndex.java
$ java -Dsen.home=/usr/java/sen \
-classpath .:/usr/java/lucene-2.2.0/lucene-core-2.2.0.jar:/usr/java/lucene-ja/lib/lucene-ja.jar:\
/usr/java/sen/lib/sen.jar:/usr/java/sen/lib/commons-logging.jar EijiroSimpleIndex
...
> heaven
2007/10/30 01:23:45 net.java.sen.Dictionary
...
情報: time to load connect cost file = 128[ms]
1.000000 ■heaven born {形} : <→heaven-born>
1.000000 ■heaven sent {形} : <→heaven-sent>
1.000000 ■tea of heaven {名} : <→tea-of-heaven>
0.857143 ■heaven appointed task : <→heaven-appointed task>
0.857143 ■heaven kissing mountains : <→heaven-kissing mountains>
> stairway
1.000000 ■ladder like stairway : <→ladder-like stairway>
0.824958 ■ascend the stairway : 階段を上る
0.824958 ■climb a stairway : 階段を上る
0.824958 ■grimy stairway : 汚れた階段
0.824958 ■mount a stairway : 階段を上る
> stairway to heaven
1.000000 ■stairway to heaven : 〈豪俗〉(パンストの)伝線◆【同】run ; ladder
0.621393 ■ladder like stairway : <→ladder-like stairway>
0.512623 ■ascend the stairway : 階段を上る
0.512623 ■climb a stairway : 階段を上る
0.512623 ■grimy stairway : 汚れた階段
> stairway AND heaven
1.000000 ■stairway to heaven : 〈豪俗〉(パンストの)伝線◆【同】run ; ladder
>
$ du -k -s eijiro_simple_index
329136 eijiro_simple_index
$ |
インデックス作成は、マシンのパフォーマンスにもよりますが、数十分から数時間といったところだと思います。データは圧縮したので、インデックスのサイズは、330MB 程度になりました。
”heaven", "stairway", "stairway to heaven" などで検索してみました。"stairway to heaven" は、見出語にマッチした行がトップになりましたが、"heaven" や "stairway" は、それらの熟語が上位を占め、単語そのものは上位 5つには現れていません。これは Lucene のスコアの計算方法のためです。例えば "stairway" については、PDIC の参考画面をみるとわかるように、基本的な単語であるため、複数の訳語や文例などがあるため、もともとの一行の長さが長くなっていますが、"climb a stairway" は熟語であるため、ひとつの訳語しかなく、一行が短くなっています。このため、Similarity クラスのドキュメントにあるスコアの計算式からわかるように、単語の場合は、全体に占める検索文字列の割合が小さくなるため、ノルムが小さくなり、結果としてスコアが小さくなりますが、述語の場合には、逆にノルム、そしてスコアが大きくなり、上位に表示されることになります。特に、"ladder like stairway" の場合には、もう一度、"staiway" が現れているため、頻度も影響して、"stairway" の検索で、トップになっています。
しかし、実際に(英和)辞書として英単語を検索するときには、述語よりも、単語そのものがトップに現れてほしいものです。次回は、これを改善していきます。Tags: programming
|
|
英辞朗 + Lucene #1 - KazMuzik Blog
2007-10-29 22:22
週末に、英辞朗 Ver.108 を購入して、Linux にテキストファイルをコピーしたので、いよいよ、Java からアクセスするためのプログラミングです。まずは、Lucene の Index を作成して、検索してみます。
英辞朗のデータ仕様によると、ひとつの見出語(英語)に対して、一行のデータになっており、複数の訳語(日本語), 補足説明、文例が含まれています。今回は、簡単のため、一行に対して、ひとつの Document オブジェクトを作成し、一行全体そのままを "line" という名前の Field にします。
import org.apache.lucene.analysis.ja.JapaneseAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Searcher;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.IOException;
public class EijiroSimpleIndex {
private static final String defaultIndexDirName
= "eijiro_simple_index";
private static final String eijiroFile = "EIJI-108.TXT";
private static final String encoding = "Shift-JIS";
private static final int MAX_RESULTS = 5;
private File indexDirName;
private IndexReader reader;
private Searcher searcher;
private QueryParser parser;
public EijiroSimpleIndex() throws IOException {
this(defaultIndexDirName);
}
public EijiroSimpleIndex(String indexDirName) throws IOException {
this.indexDirName = new File(indexDirName);
if (! (new File(indexDirName).exists())) {
createIndex();
}
reader = IndexReader.open(indexDirName);
searcher = new IndexSearcher(reader);
parser = new QueryParser("line", new JapaneseAnalyzer());
}
public void close() throws IOException {
reader.close();
}
public void createIndex() throws IOException {
IndexWriter writer
= new IndexWriter(indexDirName, new JapaneseAnalyzer());
BufferedReader in
= new BufferedReader(new InputStreamReader(
new FileInputStream(eijiroFile), encoding));
int count = 0;
while (true) {
String line = in.readLine();
if (line == null) {
break;
}
Document document = new Document();
Field field = new Field("line", line, Field.Store.COMPRESS,
Field.Index.TOKENIZED);
document.add(field);
writer.addDocument(document);
count ++;
if (count >= 1000) {
System.err.print(".");
count = 0;
}
}
in.close();
System.err.println();
writer.optimize();
writer.close();
}
public static class EijiroSearchResult {
private float score;
private String line;
public EijiroSearchResult(float score, String line) {
this.score = score;
this.line = line;
}
public float getScore() {
return score;
}
public String getLine() {
return line;
}
}
public EijiroSearchResult[] search(String q)
throws IOException, ParseException {
Query query = parser.parse(q);
Hits hits = searcher.search(query);
int n = hits.length();
if (n > MAX_RESULTS) {
n = MAX_RESULTS;
}
EijiroSearchResult[] results = new EijiroSearchResult[n];
for (int i = 0; i < n; i++) {
Document doc = hits.doc(i);
results[i] = new EijiroSearchResult(hits.score(i), doc.get("line"));
}
return results;
}
public static void main(String[] args) throws Exception {
EijiroSimpleIndex index = new EijiroSimpleIndex();
BufferedReader in
= new BufferedReader(new InputStreamReader(System.in, "UTF-8"));
while (true) {
System.out.print("> ");
System.out.flush();
String q = in.readLine();
if (q == null) {
break;
}
q = q.trim();
if (q.length() < 1) {
continue;
}
EijiroSearchResult[] results = index.search(q);
if (results == null || results.length < 1) {
System.out.println("(no result)");
continue;
}
for (EijiroSearchResult result : results) {
System.out.printf("%f %s%n", result.getScore(), result.getLine());
}
}
System.out.println();
in.close();
index.close();
}
} |
Tags: programming
|
|
英辞郎 - KazMuzik Blog
2007-10-27 21:21
プログラムからアクセスできる英和辞書を探していましたが、EDP の「英辞郎」という辞書データが、ダウンロード可能で、オンラインで 1,980円支払えばパスワードが入手できることがわかり、なかなか評判も良いようなので、購入してみました。
条件としては、Windows 専用のアプリケーションの GUI からひとつずつ検索しなければいけないようなソフトは対象外で、最低でも API でアクセスできるか、できればテキストファイルのものを探していました。もし API 経由のアクセスになる場合は、プログラム言語は C言語や C++、またスクリプト言語などでもよかったのですが、できれば Java、また OS は Linux 上で扱いたいところでした。この英辞郎は、テキストファイルで提供されているので、私にとっては一番理想的な形でした。
ただし、ダウンロードするファイル自体は、WinRAR で圧縮されている exe (Windows の実行形式) ファイルなので、Windows 上で解凍する必要があります(*)。 起動して、購入したパスワードを入力すると、Shift-JIS のテキストファイルが得られます。最新版は、9/28/2007 にリリースされた Ver. 108 で、1,886,646件のデータがあります。データはどんどん増えていて、だいたい 1ヶ月程で、1万件程度増えて、バージョンが上がっているようです。この場合、基本的には、再びパスワードを購入する必要があります。
Windows で Shift-JIS のテキストファイルが得られれば、後は、Linux に転送して、Java から自由にアクセスできます。Java の Reader を使うので Shift-JIS のままでもいいのですが、UTF-8 locale の Linux の shell 上で、エディタや grep などのコマンドから簡単にアクセスできるように、iconv で UTF-8 に変換しておきました。
$ iconv -f SHIFT-JIS -t UTF-8 EIJI-108.TXT > eiji-108.utf8.txt
iconv: illegal input sequence at position 185586
$ iconv -f SJIS-WIN -t UTF-8 EIJI-108.TXT > eiji-108.utf8.txt
$ wc -l EIJI-108.TXT eiji-108.utf8.txt
1886646 EIJI-108.TXT
1886646 eiji-108.utf8.txt
3773292 total
$ |
やはり、入力エンコーティングは、単なる SHIFT-JIS ではだめで、SJIS-WIN を指定する必要があります。
また、英和辞書データである英辞朗(EIJI-108.TXT)の他にも、和英辞朗(WAEI-108.TXT), 例辞朗(REIJI108.TXT), 略辞朗(RYAKU108.TXT) などがあり、同一のパスワードで解凍できます。
つづく
2007-11-27 update (*) Linux 上でも unrar コマンドで解凍できます。 -> 英辞郎 Ver.109 - unrar on LinuxTags: computer_technology, programming
|
|
org.apache.lucene.index.memory.SynonymMap (#3) - KazMuzik Blog
2007-10-20 20:23
前回は、Lucene WordNet パッケージを紹介しましたが、同じく contrib である Memory パッケージには、SynonymMap というクラスがあります。前回はディスク上にインデックスを作成したのに対し、これは直接 wn_s.pl を読み込み、メモリ上に synonym のマップを展開します。簡単なアプリケーションを書いてみます。
import org.apache.lucene.index.memory.SynonymMap;
import java.io.FileInputStream;
public class SynonymMapTest {
public static void main(String[] args) throws Exception {
SynonymMap map = new SynonymMap(new FileInputStream("prolog/wn_s.pl"));
for (String a : args) {
System.out.printf("%s%n", a);
for (String s : map.getSynonyms(a)) {
System.out.printf("%s : %s%n", a, s);
}
}
}
} |
コンパイルして、前回同様、"stairway to heaven" を渡して実行してみます。
$ javac -classpath /usr/java/lucene-2.2.0/lucene-core-2.2.0.jar:/usr/java/lucene-2.2.0/contrib/memory/lucene-memory-2.2.0.jar \
SynonymMapTest.java
$ java -classpath .:/usr/java/lucene-2.2.0/lucene-core-2.2.0.jar:/usr/java/lucene-2.2.0/contrib/memory/lucene-memory-2.2.0.jar \
SynonymMapTest stairway to heaven
stairway
stairway : staircase
to
heaven
heaven : eden
heaven : nirvana
heaven : paradise
$ |
2007-10-23 update Lucene API Docs is back. I could not access it during this weekend.Tags: programming
|
|
WordNet 3.0 for UNIX and 2.1 for Windows (#2) - KazMuzik Blog
2007-10-19 20:22
前回は、Lucene WordNet package から、WordNet の synonym(s) にアクセスしてみましたが、今日は、直接 WordNet をインストールしてみます。
まずは、Linux からです。Tcl/Tk の Config スクリプトが必要ですが、私の Fedora 7 には tcl-devel と tk-devel パッケージがインストールされておらず、/usr/lib/tclConfig.sh と /usr/lib/tkConfig.sh がありませんでした。まずは、yum でこれらのパッケージからインストールしました。
$ sudo yum install tcl-devel
$ sudo yum install tk-devel
$ cd ~/wordnet
$ wget http://wordnet.princeton.edu/3.0/WordNet-3.0.tar.gz
$ tar zxvpf WordNet-3.0.tar.gz
$ cd WordNet-3.0
$ ./configure
$ make
$ sudo make install
$ /usr/local/WordNet-3.0/bin/wn heaven -synsn
Synonyms/Hypernyms (Ordered by Estimated Frequency) of noun heaven
2 senses of heaven
Sense 1
Eden, paradise, nirvana, heaven, promised land, Shangri-la
=> region, part
Sense 2
Heaven
=> imaginary place, mythical place, fictitious place
$ |
Lucene WordNet パッケージでは、synonym のために、wn_s.pl しか使っていませんでしたが、WordNet の wn コマンドでは、さらに多くの情報がプリントされています。前回と同様、"heaven" で試してみましたが、"Shangri-la" というのも、あります。また、別の sense の "Heaven" もあります。
次は、Windows 版ですが、ダウンロードできる binary は WordNet 2.1 です。Vista でも問題なく、インストールでき、動作しました。
 Tags: programming
|
|
Lucene WordNet package - KazMuzik Blog
2007-10-18 20:21
1月に紹介した Lucene ですが、その後、2/17/2007 に 2.1.0, 6/19/2007 に 2.2.0 がリリースされています。Lucene API document を見ていると、org.apache.lucene.wordnet という WordNet の Synonym (類義語) を扱う contrib package がありました。確か、以前から、少なくとも、2.0.0 のときにはあったように、記憶していますが、個人的には、最近、Synonym に興味があり、いろいろ研究していたところなので、ここで紹介しておきます。
WordNet は、synonym のセットである synset を中心とした英語の概念辞書のひとつで、Princeton 大学で開発されています。日本語の Wikipedia によくまとめられているので、これから読むのが手っ取り早いでしょう。Lucene API Document からは、WordNet 2.0 にリンクが張ってありましたが、最新は 2006年12月にリリースされた WordNet 3.0 になっています。UNIX用、Windows用の他に、Prolog版もあります。Lucene では、Prolog版の wn_s.pl というファイルだけを使います。
まずは、WordNet 3.0 の Prolog版をダウンロードして、wn_s.pl から、Syns2Index で Lucene index を作成します。
$ cd
$ mkdir wordnet
$ cd wordnet
$ wget http://wordnet.princeton.edu/3.0/WNprolog-3.0.tar.gz
$ tar zxvpf WNprolog-3.0.tar.gz
$ java -classpath /usr/java/lucene-2.2.0/lucene-core-2.2.0.jar:/usr/java/lucene-2.2.0/contrib/wordnet/lucene-wordnet-2.2.0.jar \
org.apache.lucene.wordnet.Syns2Index prolog/wn_s.pl index0
Opening Prolog file prolog/wn_s.pl
[1/2] Parsing prolog/wn_s.pl
2 s(100001740,1,'entity',n,1,11). 0 0 ndecent=0
4 s(100002137,1,'abstraction',n,6,0). 1 1 ndecent=1
...
131072 s(113660337,1,'nautical mile',n,2,0). 49957 51928 ndecent=58050
[2/2] Building index to store synonyms, map sizes are 77651 and 90406
row=1/77651 doc= Document<stored/uncompressed<syn:between> stored/uncompressed,indexed<word:>>
row=2/77651 doc= Document<stored/uncompressed<syn:adenine> stored/uncompressed<syn:amp> stored/uncompressed<syn:ampere> \
stored/uncompressed<syn:angstrom> stored/uncompressed<syn:axerophthol> stored/uncompressed,indexed>
...
row=32768/77651 doc= Document<stored/uncompressed<syn:redeeming> stored/uncompressed<syn:redemptional> \
stored/uncompressed<syn:redemptory> stored/uncompressed<syn:saving> stored/uncompressed,indexed<word:redemptive>>
Optimizing..
$ |
このインデックス作成は、かなり時間がかかります。
次は、作成されたインデックスを用いて、Query を展開してみます。SynExpand というクラスが main メソッドも持っているので、手っ取り早く、これを使います。
$ java -classpath /usr/java/lucene-2.2.0/lucene-core-2.2.0.jar:/usr/java/lucene-2.2.0/contrib/wordnet/lucene-wordnet-2.2.0.jar \
org.apache.lucene.wordnet.SynExpand index0 'stairway to heaven'
Query: stairway staircase^0.9 heaven eden^0.9 nirvana^0.9 paradise^0.9
$ |
"stairway to heaven" という query を synonym を用いて展開してみましたが、"stairway" が "staircase" という synonym を、"heaven" が "eden", "nirvana", "paradise" という synonym をもっているのがわかります。これを利用すれば、synonym も含めて、検索できるようになります。
また、もうひとつ SynLookup というクラスがあるので、これも試してみます。
$ java -classpath /usr/java/lucene-2.2.0/lucene-core-2.2.0.jar:/usr/java/lucene-2.2.0/contrib/wordnet/lucene-wordnet-2.2.0.jar \
org.apache.lucene.wordnet.SynLookup index0 paradise
Synonyms found for "paradise":
eden
heaven
nirvana
$ |
Tags: programming
|
|
JRuby - KazMuzik Blog
2007-10-09 05:07
最近、Ruby を Java で実装した JRuby の動きが活発になっているようなので、試してみました。最新の 1.0.1 をダウンロードして、このブログの 1/2/2007 のエントリにある samegame.rb を実行してみました。
$ cd /tmp
$ wget http://dist.codehaus.org/jruby/jruby-bin-1.0.1.tar.gz
$ cd /usr/java
$ tar xvpf /tmp/jruby-bin-1.0.1.tar.gz
$ ln -s jruby-1.0.1 jruby
$ export JAVA_HOME=/usr/java/jdk
$ export JRUBY_HOME=/usr/java/jruby
$ export PATH=$PATH:/$JAVA_HOME/bin:$JRUBY_HOME/bin
$ |
$ cd ~/samegame
$ jruby samegame.rb
52432214111513151444
23454131532512143554
44245352141115145223
52523312143311335533
52452254245324414354
24521442532114413425
53433111453454233214
13331151323152455423
45251554525341152143
45442232232551441331
54533152252132254212
32524132321413323513
#0 51 actions possible.
### 146 (#2)
#0 [9,3] 2 2 2 1 1
524322141_1513151444
234541315_2512143554
44245352111115145223
52523312133311335533
52452254245324414354
24521442542114413425
53433111443454233214
13331151333152455423
45251554555341152143
45442232232551441331
54533152252132254212
32524132321413323513
#1 49 actions possible.
...
__3______
__5_1____
__1_5____
513432355
#59 1 actions possible.
### 1000 (#59)
#59 [7,0] 5 2 228 1 1000
__3____
__5_1__
__1_5__
5134323
##60 turns, 12 remains, 228 erased, 1000 points.
$ |
多少、パフォーマンスは落ちますが、互換性は大丈夫なようです。Tags: programming
|
|
|
|
Mr.J の悲劇 - プログラミングのできないエンジニアは vi エディタで j キーを押し続ける .. #5 - KazMuzik Blog
2007-09-28 20:20
昔々、あるところに、プログラミングのできないエンジニア Mr.J がいました。Mr.J は、同僚にプログラミング上のアドバイスを求めました。同僚がそのソースコードをエディタで開くように言うと、Mr.J は、vi エディタを立ち上げました。スクリーンには 1ページ目が表示されており、カーソルは、1番上の行にあります。次に、同僚は、その画面に表示されていない、ある名前のメソッドのところまで行くように言いました。Mr.J は、j キーを押すと、それをホールドし続けて、画面をにらんでいます。カーソルが画面の一番下まで行っても j キーを押し続けたままで、ついに画面がスクロールし始めました。どれくらい時間が経ったでしょうか。やっと目的のメソッドが画面に現れてきたところで、Mr.J は、おもむろに右手の人差し指を j キーからあげたのでした。
その後、数千行あるファイルの最後の数行をチェックする必要がでてきました。Mr.J は、tail とタイプするかわりに、vi とタイプしました。ファイルが表示されると、今度は、G (shift + g) とやるかわりに、やはり j キーを押し続けました。画面には、ファイルの内容がスクロールし続けています... Mr.J は、vi エディタを 5年以上使っていますが、よほど j が好きなようです。
Mr.J の悲しい物語りでした。Tags: programming Current Mood: sad
|
|
Program を書かない SE #4 - KazMuzik Blog
2007-09-24 06:15
#2 では、「(日本の) プログラマの地位が低い」 = 「プログラミング能力のある人の地位が低い」と、変な誤解をする日本人がいると書きましたが、これがエスカレートすると、笑い話のようなことがおきます。一応「エンジニア」の肩書きはあるけど、プログラミングができない人がいた場合、必然的にプログラムを書かないエンジニアということになりますが、このために日本側からは、そのような人の方が senior とみなされることがあります。プロジェクトを進めるにあたっては、笑い話やただの誤解として済ませるわけにはいかないので、ここでも本質的でない苦労をすることになります。日本の会社と仕事をするときは、プログラミングは、誰か別の人がやっている、ということにしておく方が無難なようです。Tags: programming
|
|
|
|
Program を書かない SE #2 - SE とプログラマ - KazMuzik Blog
2007-09-17 19:19
日本のソフトウェア開発では、大きく SE (システムエンジニア) とプログラマという2つの職種に分かれているようです。SE が設計した仕様書を元に、プログラマがコーディングするというのが、一般のスタイルのようです。このため、一般的に SE の方が地位が高く、プログラマの地位は低いと考えられているようです。これは、業種や会社によって違うかもしれませんが、特に、昔メインフレーム(汎用機)をやっていたような大手の会社や、アプリケーション分野の開発(特に金融系など)ではこのような傾向が強いようです。
ここまでは、日本のソフトウェア開発の特殊性ということで理解できないでもないのですが、困ったことには、「(日本の) プログラマの地位が低い」 = 「プログラミング能力のある人の地位が低い」と、変な誤解をする日本人が多いことです。前回のエントリで書いたように、アメリカでは、特に、最先端の技術を売りにしているスタートアップ企業では、開発エンジニア全員がプログラミングができることがほとんどです。プログラミングが出来ない SE が仕様書を書いて、別の人にコーディングしてもらって、などと悠長なことをやってる余裕はありません。ところが、某日米共同開発プロジェクトで、日本側のパートナーでもある、某大手企業とやりとりをしていて感じたのですが、私がアメリカ側で一手に開発をして、さらにプログラミングもしているということで、そしてこの「プログラムを書いている」という点から、当初は、ただのプログラマーか、という評価をされているようでした。つまり、全般的なアーキテクチャもわからず、単に、誰か(?)が書いた仕様書をコーディングしているだけ、という感じです。もちろん、これではプロジェクトに支障が出るので、日本側にも理解してもらうよう、かなり努力しましたが、まだまだ理解できない方々もいらっしゃるようです。Tags: programming
|
|
Program を書かない SE #1 - クリープのないコーヒーなんて - KazMuzik Blog
2007-09-16 22:22
最近、ちょっと思うことがあり、「program を書かない SE」というテーマで、いくつかエントリを書こうと思います。実は、最初は、「program が書けない software engineer」にしようと思ったのですが、なんとなく、「クリープのないコーヒーなんて」という昔の CM のフレーズが頭に浮かび、あえてタイトルを変更しました。
クリープ(Creap) は、森永乳業の製品で、ミルクの代わりにコーヒーに入れる粉末クリームの一種です。この CM はけっこう印象的なようで、「クリープのないコーヒー」とかで検索すると、たくさんのブログにひっかかったりします。ようするに、クリープやミルクを入れなくても、コーヒーはコーヒーなわけで、同様に、programming というスキルは、software engineer (日本では SE, system(s) engineer) にとって、コーヒー(本質的)か、クリープ(おまけ)か、ということです。
人それぞれ価値観が違っていて当然ですし、会社や立場によっても異なると思いますが、私の感覚では、日本では、SE の 90% ぐらいの人が、programming を、クリープのように考えていて、しかも多くがブラックコーヒーを飲んでいるような気がしますが、アメリカでは逆転して、10% ぐらいかな、といった感じ、つまり 90% ぐらいの software engineer がコーヒーそのものと、とらえているのではないかと思います。まあ、統計をとったわけでもないし、私の経験もだんぶん偏っているので、かなり大雑把な感じですが ... ( しかし、なんか、喩えがいまひとつのエントリになってしまいました。)
つづくTags: programming
|
|
URL Encoding - KazMuzik Blog
2007-07-11 03:33
Web browser は、個人的には、ずっと Netscape, Mozilla, そして最近では Firefox を使っていて、Internet Explorer は、あまり使ったことがありませんでした。幸い、今まで、特別に IE に依存する状況はありませんでした。
ところが、昨日、フォームに入力した日本語が化けるという現象が報告されてきました。私の方では、特にそんな現象は起こりません。状況を確認していくと、Firefox や IE7 では大丈夫ですが、IE6 のときには文字化けするのは事実のようです。さらに、Apache のログや、Tomcat での debug メッセージを調べると、Firefox では、ブラウザ側でちゃんと日本語を %HH という "%" + 16進ペアの URL Encodingで、サーバーに送っていますが、IE はそのままの文字コードを送っているようです。しかも、IE6 の場合、UTF-8 のはずが、コードが少し変です。FORM タグに ENCTYPE="application/x-www-form-urlencoded" を加えても無視されるようです。結局、IE のために、JavaScript の encodeURIComponent() を使った workaround を実装することになりました。
<form name="f" action="s" onsubmit="qs(this);return false;">
<input name="q" type="text">
</form>
<script>
function qs(tf) {
var qv = tf.q.value;
if (window.encodeURIComponent) {
qv = encodeURIComponent(qv);
tf.q.value = qv;
}
tf.action = "s?q=" + qv ;
tf.submit();
}
</script>
|
Tags: programming
|
|
Apache Ant - Kaz Muzik Blog Backup Project #23 - KazMuzik Blog
2007-07-05 00:57
本格的にソースコードを書いていく前に、Apache Ant の環境をセットアップしておきます。
$ cd
$ mkdir kazmuzikblog
$ cd kazmuzikblog
$ mkdir src classes lib
$ mv /usr/local/nutch/LiveJournal*.java /usr/local/nutch/RCS src
$ mv /usr/local/nutch/kazmuzikblog .
$
|
#18 (5/6/2007) の LiveJournalHtmlCreator.java を stdout 以外にも、ant の build.xml でファイル名を指定して出力できるようにしました。また、今後のアップデートに備えて、instance を生成するように変更しました。
import java.io.Writer;
import java.io.OutputStreamWriter;
import java.io.OutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.sql.SQLException;
public final class LiveJournalHtmlCreator {
private LiveJournalEntryDatabase db;
public LiveJournalHtmlCreator() throws SQLException, ClassNotFoundException {
db = new LiveJournalEntryDatabase();
}
public void close() throws SQLException {
db.close();
}
public void printHtml(Writer writer) throws IOException, SQLException {
LiveJournalEntryHtmlWriter out = new LiveJournalEntryHtmlWriter(writer);
out.println("<html><head>");
out.println("<meta http-equiv=\"Content-Type\""
+ " content=\"text/html; charset=utf-8\">");
out.println("</head><body>");
out.println("<table border=0 cellspacing=0 cellpadding=8>");
for (int id : db.getAllEntryIds()) {
LiveJournalEntry entry = db.select(id);
out.println("<tr><td bgcolor=#ff0000>");
out.printf("<a name=\"item%d\"></a>", id);
out.printEntry(entry);
out.println("</td></tr>");
}
out.println("</table>");
out.println("</body></html>");
out.flush();
}
public static void main(String args[]) throws Exception {
OutputStream os = System.out;
if (args.length > 0) {
os = new FileOutputStream(args[0]);
}
Writer out = new OutputStreamWriter(os, "UTF-8");
LiveJournalHtmlCreator creator = new LiveJournalHtmlCreator();
creator.printHtml(out);
creator.close();
out.close();
}
}
|
Ant の build.xml です。
<project name="kazmuzikblog" default="compile">
<property name="src" value="src" />
<property name="build" value="classes" />
<property name="lib" value="lib" />
<property name="nutchPath" value="/usr/local/nutch-0.9" />
<property name="derbyJar" value="/usr/java/db-derby-10.2.2.0-bin/lib/derby.jar" />
<target name="compile">
<javac srcdir="${src}"
destdir="${build}"
includes="*.java">
<classpath>
<pathelement path="${nutchPath}/nutch-0.9.jar" />
<pathelement path="${nutchPath}/lib/hadoop-0.12.2-core.jar" />
</classpath>
</javac>
</target>
<target name="jar" depends="compile">
<jar destfile="${lib}/kazmuzikblog.jar"
basedir="${build}"
/>
</target>
<target name="html" depends="jar">
<java classname="LiveJournalHtmlCreator">
<classpath>
<pathelement path="${lib}/kazmuzikblog.jar" />
<pathelement path="${derbyJar}" />
</classpath>
<arg value="myjournal.html" />
</java>
</target>
</project>
|
実行します。
$ ant html
Buildfile: build.xml
compile:
[javac] Compiling 13 source files to /home/kaz/kazmuzikblog/classes
[javac] Note: Some input files use or override a deprecated API.
[javac] Note: Recompile with -Xlint:deprecation for details.
jar:
[jar] Building jar: /home/kaz/kazmuzikblog/lib/kazmuzikblog.jar
html:
BUILD SUCCESSFUL
Total time: 9 seconds
$ ls -l myjournal.html
-rw-r--r--+ 1 kaz kaz 1541867 2007-07-04 13:09 myjournal.html
$
|
Tags: programming
|
|
HTML per Tag - Kaz Muzik Blog Backup Project #24 - KazMuzik Blog
2007-07-04 19:11
前回の LiveJournalHtmlCreator は、ブログ全体をひとつの HTML ファイルに書き出しましたが、エントリも 400 を超え、サイズが大きくなってきたので、今回は Tag ごとに HTML ファイルを作成することにします。
まずは、SQL と method を追加するため、LiveJournalEntryDatabase をアップデートしておきます。
...
public class LiveJournalEntryDatabase {
...
private static final String sqlSelectAllEntryIdsForTag
= "select a.id from entryTagTable a, entryTable b"
+ " where a.tag = ? and a.id = b.id order by b.date";
private static final String sqlSelectAllTags
= "select distinct tag from entryTagTable order by tag";
...
private PreparedStatement psSelectAllEntryIdsForTag = null;
private PreparedStatement psSelectAllTags = null;
...
private void prepareStatements() throws SQLException {
...
psSelectAllEntryIdsForTag = conn.prepareStatement(sqlSelectAllEntryIdsForTag);
psSelectAllTags = conn.prepareStatement(sqlSelectAllTags);
}
...
public int[] getAllEntryIdsForTag(String tag) throws SQLException {
psSelectAllEntryIdsForTag.setString(1, tag);
ResultSet rs = psSelectAllEntryIdsForTag.executeQuery();
List<Integer> list = new LinkedList<Integer>();
while (rs.next()) {
list.add( rs.getInt(1) );
}
int n = list.size();
int[] ids = new int[n];
for (int i = 0; i < n; i ++) {
ids[i] = list.get(i);
}
return ids;
}
public String[] getAllTags() throws SQLException {
ResultSet rs = psSelectAllTags.executeQuery();
List<String> list = new LinkedList<String>();
while (rs.next()) {
list.add( rs.getString(1) );
}
int n = list.size();
String[] tags = new String[n];
list.toArray(tags);
return tags;
}
...
}
|
次に、これを利用して、LiveJournalHtmlCreator をアップデートします。
...
public final class LiveJournalHtmlCreator {
...
public void printHtml(Writer writer) throws IOException, SQLException {
printHtml(writer, null);
}
public void printHtml(Writer writer, String tag)
throws IOException, SQLException {
LiveJournalEntryHtmlWriter out = new LiveJournalEntryHtmlWriter(writer);
out.println("<html><head>");
out.println("<meta http-equiv=\"Content-Type\""
+ " content=\"text/html; charset=utf-8\">");
out.print("<title>Kaz Muzik Blog");
if (tag != null) {
out.print(" - Tag: " + tag);
}
out.println("</title>");
out.println("</head><body>");
out.println("<table border=0 cellspacing=0 cellpadding=8>");
int[] ids = null;
if (tag == null) {
ids = db.getAllEntryIds();
}
else {
ids = db.getAllEntryIdsForTag(tag);
}
for (int id : ids) {
...
}
...
}
public String[] getAllTags() throws SQLException {
return db.getAllTags();
}
public void createFile(String filename, String tag)
throws SQLException, IOException {
Writer out
= new OutputStreamWriter(new FileOutputStream(filename), "UTF-8");
printHtml(out, tag);
out.close();
}
public static void main(String args[]) throws Exception {
LiveJournalHtmlCreator creator = new LiveJournalHtmlCreator();
if (args.length == 0) {
String filename = "kazmuzikblog-all.html";
System.err.println("all tags");
creator.createFile(filename, null);
for (String tag : creator.getAllTags()) {
filename = "kazmuzikblog-tag-" + tag + ".html";
System.err.println("tag=" + tag);
creator.createFile(filename, tag);
}
}
else {
String filename = null;
String tag = null;
if (args.length > 1) {
tag = args[0];
filename = args[1];
}
else if (args.length > 0) {
// tag = null; // ALL
filename = args[0];
}
System.err.println("tag=" + tag);
creator.createFile(filename, tag);
}
creator.close();
}
}
|
LiveJournalFetchPreparator でデータベースをアップデートしてから、HTML ファイルを作成します。
java -classpath lib/kazmuzikblog.jar:/usr/java/derby/lib/derby.jar LiveJournalFetchPreparator
http://kazuomik.livejournal.com/
104752*I 104659*I 104291*I 104057*I 103902*I 103524 103339 103042 102798 102532
102380 102105 84281 101733 101616 101234 100894 100794 100492 100289
http://kazuomik.livejournal.com/?skip=20
100095 99615 99339 99121 98902 98640 98325 98252 97974 97662
97485 97174 96871 96740 96298 96143 95895 95524 95278 95122
$ java -classpath lib/kazmuzikblog.jar:/usr/java/derby/lib/derby.jar LiveJournalHtmlCreator
all tags
tag=computer technology
tag=music
tag=music and computer
tag=music gear
tag=music technology
tag=programming
tag=test
tag=useful link
tag=アメリカでの転職
tag=アメリカ生活
tag=日本語
$ ls -l kazmuzikblog-*.html
-rw-r--r--+ 1 kaz kaz 1555643 2007-07-04 19:08 kazmuzikblog-all.html
-rw-r--r--+ 1 kaz kaz 874312 2007-07-04 19:08 kazmuzikblog-tag-computer technology.html
-rw-r--r--+ 1 kaz kaz 270594 2007-07-04 19:08 kazmuzikblog-tag-music and computer.html
-rw-r--r--+ 1 kaz kaz 62135 2007-07-04 19:08 kazmuzikblog-tag-music gear.html
-rw-r--r--+ 1 kaz kaz 145081 2007-07-04 19:08 kazmuzikblog-tag-music technology.html
-rw-r--r--+ 1 kaz kaz 1950 2007-07-04 19:08 kazmuzikblog-tag-music.html
-rw-r--r--+ 1 kaz kaz 778115 2007-07-04 19:08 kazmuzikblog-tag-programming.html
-rw-r--r--+ 1 kaz kaz 3524 2007-07-04 19:08 kazmuzikblog-tag-test.html
-rw-r--r--+ 1 kaz kaz 8946 2007-07-04 19:08 kazmuzikblog-tag-useful link.html
-rw-r--r--+ 1 kaz kaz 46437 2007-07-04 19:08 kazmuzikblog-tag-アメリカでの転職.html
-rw-r--r--+ 1 kaz kaz 237594 2007-07-04 19:08 kazmuzikblog-tag-アメリカ生活.html
-rw-r--r--+ 1 kaz kaz 1883 2007-07-04 19:08 kazmuzikblog-tag-日本語.html
$
|
Tags: programming
|
|
Kaz Muzik Blog Backup Project #22 - KazMuzik Blog
2007-07-03 08:26
1ヶ月ぶりなので、6/6/2007 の LiveJournalFetchPreparater に -init オプションをつけて実行して、データベースを初期化して、すべてのエントリを取り込んでみます。
$ java -classpath .:/usr/java/derby/lib/derby.jar LiveJournalFetchPreparator -init
http://kazuomik.livejournal.com/
103524*I 103339*I 103042*I 102798*I 102532*I 102380*I 102105*I 84281*I 101733*I 101616*I
101234*I 100894*I 100794*I 100492*I 100289*I 100095*I 99615*I 99339*I 99121*I 98902*I
http://kazuomik.livejournal.com/?skip=20
98640*I 98325*I 98252*I 97974*I 97662*I 97485*I 97174*I 96871*I 96740*I 96298*I
...
l8640*I 8328*I 8982*I 8161*I 7327*I 7694*I 7456*I 6946*I 6753*I 6600*I
http://kazuomik.livejournal.com/?skip=380
5941 6278 5653 5503 5242 5085 4837 3672 3518 3209
2876 495 722 1960 4044 1598 4195 1332 4582 1120
$ wc -l urls-kazmuzikblog/nutch
400 urls-kazmuzikblog/nutch
$
|
ちょうど 400 のエントリを投稿していました。(*)
nutch には、crawl や readseg などのサブコマンドがあり、crawl サブコマンドは一連の処理を連続して行いますが、他のサブコマンドをいくつか用いても同様のことができます。今回は、inject, generate, fetch サブコマンドを使用してみます。
$ bin/nutch inject tmp-crawldb urls-kazmuzikblog
...
$ bin/nutch generate tmp-crawldb tmp-segments
...
Generator: segment: tmp-segments/20070703074227
...
$ bin/nutch fetch tmp-segments/20070703074227
Fetcher: starting
Fetcher: segment: tmp-segments/20070703074227
Fetcher: threads: 10
fetching http://kazuomik.livejournal.com/88261.html
...
fetching http://kazuomik.livejournal.com/61706.html
fetch of http://kazuomik.livejournal.com/61706.html failed with:\
java.net.SocketTimeoutException: Read timed out
fetching http://kazuomik.livejournal.com/103524.html
...
fetching http://kazuomik.livejournal.com/81466.html
Fetcher: done
$
|
途中、timeout でfetch が fail している箇所もあります。readseg サブコマンドで確認してみます。
$ bin/nutch readseg -list -dir tmp-segments
NAME GENERATED FETCHER START FETCHER END FETCHED PARSED
20070703074227 400 2007-07-03T07:43:29 2007-07-03T08:18:01 400 346
$
|
400 のページが個別に fetch されていのが、確認できましたが、54 = (400 - 346) ページが失敗しているようです。
2007-11-17 update (*) #26 (2007-11-10) に書いたように、この "/?skip=NNN" による方法では、(ちょうど) 400個のエントリしか、得られません。Tags: computer_technology, programming
|
|
Translation by Samples and Pattern Matching #7 - KazMuzik Blog
2007-06-22 05:15
今回は、簡単な文を翻訳します。まずは、"is" だけをハードコードしてみます。
...
public SimpleTranslator {
...
public String translate(String enText) {
if (enText == null || enText.length() < 1) {
return "";
}
//
String jaText = dict.get(enText);
if (jaText != null) {
return jaText;
}
//
int enTextLen = enText.length();
if (enText.charAt(enTextLen-1) == '.') {
return translate(enText.substring(0,enTextLen-1)) + "。";
}
//
String[] enWords = enText.split(" ");
int len = enWords.length;
if (len < 2) {
return enText;
}
//
int m = enText.indexOf(" is ");
if (m > 0) {
return concatJapaneseTerms(
translate( enText.substring(0,m) ),
"は、",
translate( enText.substring(m+4) ),
"です" );
}
//
....
}
...
public static void main(String[] args) throws Exception {
...
String jaTerm = translator.translate(enTerm.toLowerCase());
...
}
}
|
$ cat dict7.txt
this = これ
that = あれ
pen = ペン
notebook = ノート
desk = 机
the =
a =
an =
$ cat en_text7.txt
This is a pen.
That is a desk.
$ cat en_text7.txt | java SimpleTranslator dict7.txt
This is a pen. = これは、ペンです。
That is a desk. = あれは、机です。
$
|
Tags: computer_technology, programming
|
|
Translation by Samples and Pattern Matching #6 - KazMuzik Blog
2007-06-21 06:44
前回は、「天国への階段」のために、"to" をハードコードしてサポートしましたが、それを一般化してサポートするための Map を追加しました。とりあえず、"of" と "on" を登録しました。
また、concatEnglishWords() や concatJapaneseTerms() などのユーティリティメソッドを追加したりしたので、今回はソースをそのまま載せます。
import java.util.Map;
import java.util.HashMap;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.FileInputStream;
import java.io.PrintWriter;
import java.io.OutputStreamWriter;
public class SimpleTranslator {
private static Map<String,String> dictReverse;
static {
dictReverse = new HashMap<String,String>();
dictReverse.put("to", "への");
dictReverse.put("of", "の");
dictReverse.put("on", "の上の");
}
private static final int MAX_WORD_NUMBER = 8;
private Map<String,String> dict;
private Map<String,String>[] dicts;
public SimpleTranslator(String filename) throws IOException {
createDictionaries();
readDictionary(filename);
enhanceDictionary();
}
private void createDictionaries() {
dict = new HashMap<String,String>();
dicts = new Map[MAX_WORD_NUMBER + 1];
for (int i = 0; i < dicts.length; i++) {
dicts[i] = new HashMap<String,String>();
}
}
private void readDictionary(String filename) throws IOException {
BufferedReader in = new BufferedReader( new InputStreamReader(
new FileInputStream(filename), "UTF-8") );
while (true) {
String line = in.readLine();
if (line == null) {
break;
}
String[] terms = line.split(" = ");
if (terms.length < 2 || terms[1] == null) {
putTerm(terms[0], "");
}
else {
putTerm(terms[0], terms[1]);
}
}
in.close();
}
private void putTerm(String enTerm, String jaTerm) {
dict.put(enTerm, jaTerm);
String[] words = enTerm.split(" ");
if (words.length > MAX_WORD_NUMBER) {
dicts[0].put(enTerm, jaTerm);
}
else {
dicts[words.length].put(enTerm, jaTerm);
}
}
private int enhanceDictionary() {
int count = 0;
while (true) {
int n = enhanceDictionary1();
if (n < 1) {
break;
}
count += n;
System.err.printf("(%d entries added to the dictionary)%n", n);
}
return count;
}
private int enhanceDictionary1() {
Map<String,String> map = new HashMap<String,String>();
for (String enTerm : dict.keySet()) {
String[] enWords = enTerm.split(" ");
if (enWords.length < 2) {
continue;
}
String jaTerm = dict.get(enTerm);
String[] jaWords = new String[enWords.length];
int index = -1;
for (int i = 0; i < enWords.length; i++) {
String jaWord = dicts[1].get(enWords[i]);
if (jaWord == null) {
index = i;
break;
}
if (! jaTerm.startsWith(jaWord)) {
break;
}
jaTerm = jaTerm.substring(jaWord.length());
continue;
}
if (index < 0) {
continue;
}
for (int i = enWords.length - 1; i > index; i--) {
String jaWord = dicts[1].get(enWords[i]);
if (jaWord == null) {
index = -1;
break;
}
if (! jaTerm.endsWith(jaWord)) {
index = -1;
break;
}
jaTerm = jaTerm.substring(0, jaTerm.length() - jaWord.length());
continue;
}
if (index < 0) {
continue;
}
if (jaTerm.length() < 1) {
continue;
}
if (dicts[1].get(enWords[index]) != null) {
continue;
}
String oldJaTerm = map.get(enWords[index]);
if (oldJaTerm == null) {
map.put(enWords[index], jaTerm);
}
else if (oldJaTerm.length() < jaTerm.length()) {
map.put(enWords[index], jaTerm);
}
}
for (String enTerm : map.keySet()) {
String jaTerm = map.get(enTerm);
putTerm(enTerm, jaTerm);
System.err.printf("* %s = %s%n", enTerm, jaTerm);
}
return map.size();
}
public String translate(String enText) {
String jaText = dict.get(enText);
if (jaText != null) {
return jaText;
}
//
String[] enWords = enText.split(" ");
int len = enWords.length;
if (len < 2) {
return enText;
}
//
for (String enWord : dictReverse.keySet()) {
int n = enText.indexOf(" "+ enWord + " ");
if (n < 0) {
continue;
}
String jaTerm = dictReverse.get(enWord);
if (jaTerm != null) {
return concatJapaneseTerms(
translate( enText.substring(n + 2 + enWord.length()) ),
jaTerm,
translate( enText.substring(0, n) ) );
}
}
//
if (--len > MAX_WORD_NUMBER) {
len = MAX_WORD_NUMBER;
}
while (len > 0) {
for (int i = 0; i + len <= enWords.length; i++) {
String enTerm = concatEnglishWords(enWords,i,len);
String jaTerm = dicts[len].get( enTerm );
if (jaTerm != null) {
return concatJapaneseTerms(
translate(concatEnglishWords(enWords,0,i)),
jaTerm,
translate(concatEnglishWords(enWords,i+1, enWords.length-len-i)) );
}
}
len --;
}
return enText;
}
private String concatEnglishWords(String[] words, int offset, int len) {
if (words== null || len < 1) {
return "";
}
len += offset;
if (len > words.length) {
len = words.length;
}
String prevWord = null;
StringBuilder sb = new StringBuilder();
for (int i = offset; i < len; i++) {
String word = words[i];
if (word == null || word.length() < 1) {
continue;
}
if (prevWord != null) {
sb.append(" ");
}
sb.append(word);
prevWord = word;
}
return sb.toString();
}
private String concatJapaneseTerms(String... words) {
if (words == null || words.length < 1) {
return "";
}
//
String prevWord = null;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < words.length; i++) {
String word= words[i];
if (word == null || word.length() < 1) {
continue;
}
if (prevWord != null) {
int prevWordLen = prevWord.length();
if (prevWord.charAt(prevWordLen-1) < '\u0100') {
sb.append(" ");
}
else if (word.charAt(0) < '\u0100') {
sb.append(" ");
}
}
sb.append(word);
prevWord= word;
}
return sb.toString();
}
public static void main(String[] args) throws Exception {
BufferedReader in = new BufferedReader(
new InputStreamReader(System.in, "UTF-8"));
PrintWriter out = new PrintWriter(
new OutputStreamWriter(System.out, "UTF-8"));
SimpleTranslator translator = new SimpleTranslator(args[0]);
while (true) {
String enTerm = in.readLine();
if (enTerm == null) {
break;
}
String jaTerm = translator.translate(enTerm);
out.printf("%s = %s%n", enTerm, jaTerm);
}
out.flush();
out.close();
in.close();
}
}
|
$ cat dict6.txt
stairway = 階段
heaven = 天国
one = ひとつ
them = それら
notebook = ノート
desk = 机
the =
a =
an =
$ cat en_text6.txt
the stairway to heaven
one of them
a notebook on the desk
$ cat en_text6.txt | java SimpleTranslator dict6.txt
the stairway to heaven = 天国への階段
one of them = それらのひとつ
a notebook on the desk = 机の上のノート
$
|
Tags: computer_technology, programming
|
|
Stairway to Heaven - Translation by Samples and Pattern Matching #5 - KazMuzik Blog
2007-06-18 21:40
今回は、実行結果から、始めます。
$ cat dict5.txt
stairway = 階段
heaven = 天国
$ cat en_text5.txt
stairway to heaven
$ cat en_text5.txt | java SimpleTranslator dict5.txt
stairway to heaven = 天国への階段
$
|
英語では、"A to B" が、日本語では「B への A」と順序が入れ替わっています。
今回は、これを translate() メソッドの中で、ハードコードします。
...
public class SimpleTranslator {
...
public String translate(String enText) {
String jaText = dict.get(enText);
if (jaText != null) {
return jaText;
}
//
String[] enWords = enText.split(" ");
int len = enWords.length;
if (len < 2) {
return enText;
}
//
int n = enText.indexOf(" to ");
if (n > 0) {
StringBuilder sb = new StringBuilder();
String enTerm = enText.substring(n + 4);
String jaTerm = translate(enTerm);
sb.append(jaTerm);
if (jaTerm.charAt(jaTerm.length()-1) < '\u0100') {
sb.append(" ");
}
sb.append("\u3078\u306e"); // への (to)
enTerm = enText.substring(0, n);
jaTerm = translate(enTerm);
if (jaTerm.charAt(0) < '\u0100') {
sb.append(" ");
}
sb.append(jaTerm);
return sb.toString();
}
//
if (--len > MAX_WORD_NUMBER) {
len = MAX_WORD_NUMBER;
}
while (len > 0) {
for (int i = 0; i + len <= enWords.length; i++) {
String enTerm = concatWords(enWords,i,len);
String jaTerm = dicts[len].get( enTerm );
if (jaTerm != null) {
StringBuilder sb = new StringBuilder();
if (i > 0) {
String jaTerm1 = translate( concatWords(enWords,0,i) );
sb.append(jaTerm1);
if (jaTerm1.charAt(jaTerm1.length()-1) < '\u0100') {
sb.append(" ");
}
}
sb.append(jaTerm);
if (i +len < enWords.length) {
String jaTerm2 = translate( concatWords(enWords,i+1, enWords.length-i-1) );
if (jaTerm2.charAt(0) < '\u0100') {
sb.append(" ");
}
sb.append(jaTerm2);
}
return sb.toString();
}
}
len --;
}
return enText;
}
...
}
|
Tags: computer_technology, programming
|
|
Translation by Samples and Pattern Matching #4 - KazMuzik Blog
2007-06-15 22:42
前回は、長い熟語の優先度を上げるため、word 数ごとに Map を作りましたが、enhanceDictionary() メソッドはまだ反映していなかったため、word 数 1 の Map も同時に更新することにします。
pulibc class SimpleTranslator {
...
private int enhanceDictionary1() {
Map<String,String> map = new HashMap<String,String>();
for (String enTerm : dict.keySet()) {
String[] enWords = enTerm.split(" ");
if (enWords.length < 2) {
continue;
}
String jaTerm = dict.get(enTerm);
String[] jaWords = new String[enWords.length];
int index = -1;
for (int i = 0; i < enWords.length; i++) {
String jaWord = dicts[1].get(enWords[i]);
if (jaWord == null) {
index = i;
break;
}
if (! jaTerm.startsWith(jaWord)) {
break;
}
jaTerm = jaTerm.substring(jaWord.length());
continue;
}
if (index < 0) {
continue;
}
for (int i = enWords.length - 1; i > index; i--) {
String jaWord = dicts[1].get(enWords[i]);
if (jaWord == null) {
index = -1;
break;
}
if (! jaTerm.endsWith(jaWord)) {
index = -1;
break;
}
jaTerm = jaTerm.substring(0, jaTerm.length() - jaWord.length());
continue;
}
if (index < 0) {
continue;
}
if (jaTerm.length() < 1) {
continue;
}
if (dicts[1].get(enWords[index]) != null) {
continue;
}
String oldJaTerm = map.get(enWords[index]);
if (oldJaTerm == null) {
map.put(enWords[index], jaTerm);
}
else if (oldJaTerm.length() < jaTerm.length()) {
map.put(enWords[index], jaTerm);
}
}
for (String enTerm : map.keySet()) {
String jaTerm = map.get(enTerm);
dict.put(enTerm, jaTerm);
dicts[1].put(enTerm, jaTerm);
System.err.printf("* %s = %s%n", enTerm, jaTerm);
}
return map.size();
}
...
}
|
$ cat dict4.txt
black = 黒い
house = 家
dog = 犬
house dog = 番犬
big dog = 大きな犬
$ cat en_text4.txt
big house dog
$ javac SimpleTranslator.java
$ cat en_text4.txt | java SimpleTranslator dict4.txt
* big = 大きな
(1 entries added to the dictionary)
big house dog = 大きな番犬
$
|
なお、現在のところ、単語(word数1)の辞書を使って、新しい単語だけを探していますが、本当は、熟語(word数2以上)の辞書も活用して、さらに新しい熟語も登録したいところです。しかし、これを実装すると、組み合わせの数が飛躍的に増大し、ロジックも複雑になるので、ここでは将来の課題として残しておくことにします。Tags: programming
|
|
Translation by Samples and Pattern Matching #3 - KazMuzik Blog
2007-06-13 20:57
SimpleTranslator を、別の例で実行してみます。
$ cat dict3.txt
black = 黒い
house = 家
dog = 犬
house dog = 番犬
$ cat en_text3.txt
black house dog
$ cat en_text3.txt | java SimpleTranslator dict3.txt
black house dog = 黒い家犬
$
|
辞書には、"house dog" が「番犬」として熟語で登録されていますが、"black house dog" を翻訳しようとすると、熟語ではなく、単語の "house" と "dog" の組み合わせで、「家」+「犬」になってしまいました。これでは面白くないので、熟語を先にチェックするように変更してみます。
今までは、英語から日本語への Map をひとつしか持っていませんでしたが、word の数ごとに Map を作り、word 数が多いものからチェックすることにします。
...
public class SimpleTranslator {
private static final int MAX_WORD_NUMBER = 8;
private Map<String,String> dict;
private Map<String,String>[] dicts;
public SimpleTranslator(String filename) throws IOException {
readDictionary(filename);
enhanceDictionary();
}
private void readDictionary(String filename) throws IOException {
dict = new HashMap<String,String>();
dicts = new Map[MAX_WORD_NUMBER + 1];
for (int i = 0; i < dicts.length; i++) {
dicts[i] = new HashMap<String,String>();
}
//
BufferedReader in = new BufferedReader(
new InputStreamReader(
new FileInputStream(filename), "UTF-8"));
while (true) {
String line = in.readLine();
if (line == null) {
break;
}
String[] terms = line.split(" = ");
dict.put(terms[0], terms[1]);
String[] words = terms[0].split(" ");
if (words.length > MAX_WORD_NUMBER) {
dicts[0].put(terms[0], terms[1]);
}
else {
dicts[words.length].put(terms[0], terms[1]);
}
}
in.close();
}
...
public String translate(String enText) {
String jaText = dict.get(enText);
if (jaText != null) {
return jaText;
}
//
String[] enWords = enText.split(" ");
int len = enWords.length;
if (len < 2) {
return enText;
}
//
if (--len > MAX_WORD_NUMBER) {
len = MAX_WORD_NUMBER;
}
while (len > 0) {
for (int i = 0; i + len <= enWords.length; i++) {
String enTerm = concatWords(enWords,i,len);
String jaTerm = dicts[len].get( enTerm );
if (jaTerm != null) {
StringBuilder sb = new StringBuilder();
if (i > 0) {
String jaTerm1 = translate( concatWords(enWords,0,i) );
sb.append(jaTerm1);
if (jaTerm1.charAt(jaTerm1.length()-1) < '\u0100') {
sb.append(" ");
}
}
sb.append(jaTerm);
if (i +len < enWords.length) {
String jaTerm2
= translate( concatWords(enWords,i+1, enWords.length-i-1) );
if (jaTerm2.charAt(0) < '\u0100') {
sb.append(" ");
}
sb.append(jaTerm2);
}
return sb.toString();
}
}
len --;
}
return enText;
}
private String concatWords(String[] words, int offset, int len) {
if (len < 1) {
return "";
}
StringBuilder sb = new StringBuilder(words[offset]);
for (int i = 1; i < len; i++) {
sb.append(" ");
sb.append(words[offset + i]);
}
return sb.toString();
}
...
}
|
$ javac SimpleTranslator.java
$ cat en_text3.txt | java SimpleTranslator dict3.txt
black house dog = 黒い番犬
$
|
今度は、熟語の "house dog" の日本語「番犬」が使用されました。Tags: programming
|
|
Translation by Samples and Pattern Matching #2 - KazMuzik Blog
2007-06-12 21:26
前回は、辞書を利用した簡単な SimpleTranslator を作成しました。今回は、熟語から単語を類推して、辞書を充実させることを考えてみます。
例えば、"dog" が「犬」ということは知っていますが、"black" の意味がわからないと仮定します。このとき、"black dog" が「黒い犬」ということがわかれば、"black" は「黒い」という意味をもつことが類推されます。
このロジックを使って、辞書を充実させるメソッド enhanceDictionary1() を追加します。
...
public class SimpleTranslator {
...
public SimpleTranslator(String filename) throws IOException {
readDictionary(filename);
enhanceDictionary();
}
...
private int enhanceDictionary() {
int count = 0;
while (true) {
int n = enhanceDictionary1();
if (n < 1) {
break;
}
count += n;
System.err.printf("(%d entries added to the dictionary)%n", n);
}
return count;
}
private int enhanceDictionary1() {
Map<String,String> map = new HashMap<String,String>();
for (String enTerm : dict.keySet()) {
String[] enWords = enTerm.split(" ");
if (enWords.length < 2) {
continue;
}
String jaTerm = dict.get(enTerm);
String[] jaWords = new String[enWords.length];
int index = -1;
for (int i = 0; i < enWords.length; i++) {
String jaWord = dict.get(enWords[i]);
if (jaWord == null) {
index = i;
break;
}
if (! jaTerm.startsWith(jaWord)) {
break;
}
jaTerm = jaTerm.substring(jaWord.length());
continue;
}
if (index < 0) {
continue;
}
for (int i = enWords.length - 1; i > index; i--) {
String jaWord = dict.get(enWords[i]);
if (jaWord == null) {
index = -1;
break;
}
if (! jaTerm.endsWith(jaWord)) {
index = -1;
break;
}
jaTerm = jaTerm.substring(0, jaTerm.length() - jaWord.length());
continue;
}
if (index < 0) {
continue;
}
if (jaTerm.length() < 1) {
continue;
}
if (dict.get(enWords[index]) != null) {
continue;
}
String oldJaTerm = map.get(enWords[index]);
if (oldJaTerm == null) {
map.put(enWords[index], jaTerm);
}
else if (oldJaTerm.length() < jaTerm.length()) {
map.put(enWords[index], jaTerm);
}
}
for (String enTerm : map.keySet()) {
String jaTerm = map.get(enTerm);
dict.put(enTerm, jaTerm);
System.err.printf("* %s = %s%n", enTerm, jaTerm);
}
return map.size();
}
...
}
|
$ javac SimpleTranslator.java
$ cat dict2.txt
dog = 犬
black dog = 黒い犬
white dog = 白い犬
black cat = 黒い猫
$ cat en_text2.txt
white cat
$ cat en_text2.txt | java SimpleTranslator dict2.txt
* white = 白い
* black = 黒い
(2 entries added to the dictionary)
* cat = 猫
(1 entries added to the dictionary)
white cat = 白い猫
$
|
1回目の enhanceDictionary1() で、"dog" をもとにして "white" と "black" の日本語が見つかり、2回目の enhanceDictionary1() で、"cat" の日本語が見つかり、辞書に登録されました。これをもとに、"white cat" が翻訳されました。Tags: programming
|
|
Translation by Samples and Pattern Matching #1 - KazMuzik Blog
2007-06-10 17:26
ソフトウェアやインターネットのサービスを、日本語化する場合に、英語で書かれたある種のテキストを日本語に翻訳する作業が発生します。しばしば、翻訳の遅れや、質の悪さが、プロジェクトの足を引っ張ることがあります。今回からのシリーズでは、ある種の英語から日本語への自動翻訳を Java で実装してみることにします。
まず、次のような英和辞書があるとします。 black = 黒い dog = 犬 このとき、"black dog" は「黒い犬」と翻訳することができます。
これを Java で実装してみます。
import java.util.Map;
import java.util.HashMap;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.FileInputStream;
import java.io.PrintWriter;
import java.io.OutputStreamWriter;
public class SimpleTranslator {
private Map<String,String> dict;
public SimpleTranslator(String filename) throws IOException {
readDictionary(filename);
}
private void readDictionary(String filename) throws IOException {
dict = new HashMap<String,String>();
BufferedReader in = new BufferedReader(
new InputStreamReader(
new FileInputStream(filename), "UTF-8"));
while (true) {
String line = in.readLine();
if (line == null) {
break;
}
String[] terms = line.split(" = ");
dict.put(terms[0], terms[1]);
}
in.close();
}
public String translate(String enText) {
String jaText = dict.get(enText);
if (jaText != null) {
return jaText;
}
//
StringBuilder sb = new StringBuilder();
boolean translated = true;
boolean first = true;
for (String enWord : enText.split(" ")) {
String jaWord = dict.get(enWord);
if (jaWord == null) {
if (! first) {
sb.append(" ");
}
sb.append(enWord);
translated = false;
}
else {
if (! translated) {
sb.append(" ");
}
sb.append(jaWord);
translated = true;
}
first = false;
}
return sb.toString();
}
public static void main(String[] args) throws Exception {
BufferedReader in = new BufferedReader(
new InputStreamReader(System.in, "UTF-8"));
PrintWriter out = new PrintWriter(
new OutputStreamWriter(System.out, "UTF-8"));
SimpleTranslator translator = new SimpleTranslator(args[0]);
while (true) {
String enTerm = in.readLine();
if (enTerm == null) {
break;
}
String jaTerm = translator.translate(enTerm);
out.printf("%s = %s%n", enTerm, jaTerm);
}
out.flush();
out.close();
in.close();
}
}
|
$ javac SimpleTranslator.java
$ cat dict1.txt
black = 黒い
dog = 犬
$ cat en_text1.txt
black dog
white dog
black cat
white cat
$ cat en_text1.txt | java SimpleTranslator dict1.txt
black dog = 黒い犬
white dog = white 犬
black cat = 黒い cat
white cat = white cat
$
|
Tags: programming
|
|
LiveJournalFetchPreparator with LiveJournalDatabaseManager - Kaz Muzik Blog Backup Project #21 - KazMuzik Blog
2007-06-06 22:56
前回の LiveJournalDatabaseManager の updateEntries() メソッドを使って、LiveJournalFetchPreparator を書き直しておきます。
import java.io.PrintWriter;
import java.io.OutputStreamWriter;
import java.io.FileOutputStream;
import java.util.List;
public class LiveJournalFetchPreparator {
public static void main(String[] args) throws Exception {
boolean init = false;
if (args.length > 0 && args[0].equals("-init")) {
init = true;
}
LiveJournalDatabaseManager manager = new LiveJournalDatabaseManager(init);
List list = manager.updateEntries();
PrintWriter out
= new PrintWriter(
new OutputStreamWriter(
new FileOutputStream("urls-kazmuzikblog/nutch"), "US-ASCII"));
for (int id : list) {
out.printf("http://kazuomik.livejournal.com/%d.html%n", id);
}
out.flush();
out.close();
//
out = new PrintWriter(new OutputStreamWriter(
new FileOutputStream("conf/crawl-urlfilter.txt"), "US-ASCII"));
out.println("+http://kazuomik.livejournal.com/[1-9][0-9]*.html$");
out.println("-.");
out.flush();
out.close();
}
}
|
それでは、実行してみます。
$ java -classpath .:/usr/java/derby/lib/derby.jar LiveJournalFetchPreparator
http://kazuomik.livejournal.com/
91272*I 91120*I 84281 90712 90539 90319 89953 89809 89537 89171
88875 88669 88513 88261 87862 87654 87455 87177 86869 86541
http://kazuomik.livejournal.com/?skip=20
86459 86194 85837 85750 85372 85193 84912 84571 59760 84106
83846 83562 83109 70808 83425 82794 82561 82243 81991 81756
$ cat urls-kazmuzikblog/nutch
http://kazuomik.livejournal.com/91272.html
http://kazuomik.livejournal.com/91120.html
$
|
2日前に update したところなので、2回分の entry が追加されました。Tags: computer_technology, programming
|
|
LiveJournalDatabaseManager - Kaz Muzik Blog Backup Project #20 - KazMuzik Blog
2007-06-05 22:35
データベース関連では、いくつかツールを作りましたが、わかりずらくなってきたため、低レベルの LiveJournalEntryDatabase だけを残し、他は新しい LiveJournalDatabaseManager にまとめることにしました。まずは、updateEntries() メソッドを実装しました。前回の LiveJournalEntry の equals() メソッドを使い、必要なときだけ UPDATE するようにしました。
import java.net.URL;
import java.net.URLConnection;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.sql.SQLException;
import java.util.List;
import java.util.ArrayList;
public class LiveJournalDatabaseManager extends LiveJournalEntryDatabase {
private static boolean verbose = true;
public LiveJournalDatabaseManager() throws SQLException, ClassNotFoundException {
this(false);
}
public LiveJournalDatabaseManager(boolean init) throws SQLException, ClassNotFoundException {
super(init);
}
public List updateEntries(int nSkip) throws SQLException, IOException {
String urlString = "http://kazuomik.livejournal.com/";
if (nSkip > 0) {
urlString += ("?skip=" + nSkip);
}
if (verbose) {
System.err.println(urlString);
}
URL url = new URL(urlString);
URLConnection conn = url.openConnection();
InputStream in = conn.getInputStream();
LiveJournalEntryParser parser
= new LiveJournalEntryParser(new InputStreamReader(in, "UTF-8"));
List list = new ArrayList(20);
int count = 0;
while (true) {
LiveJournalEntry entry = parser.parse();
if (entry == null) {
break;
}
if (verbose) {
System.err.printf("%d", entry.getId());
}
int id = entry.getId();
LiveJournalEntry oldEntry = select(id);
if (oldEntry == null) {
insert(entry);
list.add(id);
if (verbose) {
System.err.print("*I");
}
}
else if (! entry.equals(oldEntry)) {
update(entry);
list.add(id);
if (verbose) {
System.err.print("*U");
}
}
else {
}
count ++;
if (verbose) {
if (count % 10 == 0) {
System.err.println();
}
else {
System.err.print(" ");
}
System.err.flush();
}
}
if (verbose) {
if (count % 10 != 0) {
System.err.println();
}
}
return list;
}
public List updateEntries() throws SQLException, IOException {
List allList = new ArrayList();
int n = 0;
while (true) {
List list = updateEntries(n);
if (list.size() == 0)
{
break;
}
allList.addAll(list);
n += 20;
try {
Thread.sleep(5000);
}
catch (InterruptedException e) {
}
}
return allList;
}
}
|
Tags: computer_technology, programming
|
|
LiveJournalEntry equals() method - Kaz Muzik Blog Backup Project #19 - KazMuzik Blog
2007-06-04 22:34
前回(#18 - 5/6/07) から1ヶ月程度たったので、いくつか簡単にアップデートしてから、本題に入っていきたいと思います。
まずは、LiveJournalEntry クラスの equals()を override して、内容が同じかどうかチェックすることにします。
...
public class LiveJournalEntry implements Serializable {
...
public boolean equals(Object other) {
if (! (other instanceof LiveJournalEntry)) {
return false;
}
LiveJournalEntry that = (LiveJournalEntry)other;
if (id != that.id) {
return false;
}
if (date.getTime() / 60000 != that.date.getTime() / 60000)
{
return false;
}
if (! title.equals(that.title)) {
return false;
}
if (! entry.equals(that.entry)) {
return false;
}
if (mood == null) {
if (that.mood != null) {
return false;
}
}
else if (that.mood == null) {
return false;
}
else if (! mood.equals(that.mood)) {
return false;
}
if (location == null) {
if (that.location != null) {
return false;
}
}
else if (that.location == null) {
return false;
}
else if (! location.equals(that.location)) {
return false;
}
if (tags == null) {
if (that.tags != null) {
return false;
}
}
else if (that.tags == null) {
return false;
}
else {
if (tags.length != that.tags.length) {
return false;
}
for (int i = 0 ; i < tags.length; i++) {
if (! tags[i].equals(that.tags[i])) {
return false;
}
}
}
return true;
}
}
|
Tags: computer_technology, programming
|
|
LiveJournalHtmlCreator - Kaz Muzik Blog Backup Project #18 - KazMuzik Blog
2007-05-06 17:15
#12 では、Nutch のセグメントから、ひとつの HTML ファイルを作成しましたが、前回で、Nutch とは独立して、データベースにすべてのエントリを保存したので、今回は、データベースから、ひとつの HTML ファイルを作成します。
import java.io.OutputStreamWriter;
public final class LiveJournalHtmlCreator {
public static void main(String argv[]) throws Exception {
LiveJournalEntryDatabase db = new LiveJournalEntryDatabase();
LiveJournalEntryHtmlWriter out
= new LiveJournalEntryHtmlWriter(
new OutputStreamWriter(System.out, "UTF-8"));
out.println("<html><body>");
out.println("<able border=0 cellspacing=0 cellpadding=8>");
for (int id : db.getAllEntryIds()) {
LiveJournalEntry entry = db.select(id);
out.println("<tr><td bgcolor=#ff0000>");
out.printf("<a name=\"item%d\"></a>", id);
out.printEntry(entry);
out.println("</td></tr>");
}
out.println("</table>");
out.println("</body></html>");
out.close();
db.close();
}
}
|
データベースから、日付順にソートされたエントリをひとつずつ、LiveJournalEntryHtmlWriter でフォーマットしてプリントするだけなので、簡単に書けました。
$ javac LiveJournalHtmlCreator.java
$ java -classpath .:/usr/java/jdk/db/derby.jar LiveJournalHtmlCreator > myjournal3.html
$
|
なお、今回のバックアップでは、エントリの本文は HTML だけを対象にしてきましたが、JPEG や GIF などのイメージもあるので、できればバックアップしたいところです。これは、ひとつにまとめた HTML を Firefox で表示させ、Firefox の Save Page As でまるごと保存することにより、簡単にすませることにします。
最初は Nutch を使って始めた Blog Backup Project ですが、最後には Nutch と独立して、Apache Derby のデータベースにすべてのエントリを保存するところまできました。
次回からは、これらのデータを有効利用して、さらなる情報収集や、検索エンジンに応用していく予定です。Tags: computer_technology, programming
|
|
LiveJournalDatabaseSetup - Kaz Muzik Blog Backup Project #17 - KazMuzik Blog
2007-05-06 10:23
#16 の LiveJournalEntryUpdater は、トップページだけ読んで、データベースを更新しましたが、少し変更して、?skip=NNN (NNN=20,40,...) もすべて処理すれば、Nutch に依存しないで、すべてのエントリを取り込みデータベースを構築することができます。
まずは、LiveJournalEntryDatabase において、テーブルの primary key の定義や、index などを追加しておきます。
...
public class LiveJournalEntryDatabase {
...
private static final String sqlCreateEntryTable
= "create table entryTable "
+ "(id int not null, date timestamp not null, title varchar(256) not null,"
+ " mood varchar(32), location varchar(32),"
+ " primary key(id))";
private static final String sqlCreateEntryBodyTable
= "create table entryBodyTable (id int not null, entry blob,"
+ " primary key(id))";
private static final String sqlCreateEntryTagTable
= "create table entryTagTable (id int not null, tag varchar(64))";
private static final String sqlCreateEntryDateIndex
= "create index entryDateIndex on entryTable (date asc)";
private static final String sqlCreateTagIdIndex
= "create index tagIdIndex on entryTagTable (id asc)";
...
public void createTables() throws SQLException {
executeSql(sqlCreateEntryTable);
executeSql(sqlCreateEntryBodyTable);
executeSql(sqlCreateEntryTagTable);
executeSql(sqlCreateEntryDateIndex);
executeSql(sqlCreateTagIdIndex);
}
...
}
|
次に、#16 の LiveJournalEntryUpdaterに修正を加えて、順にすべての ?skip=NNN というページを読み込み、データベースを構築するクラスを作成します。
import java.net.URL;
import java.net.URLConnection;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.sql.SQLException;
public class LiveJournalDatabaseSetup extends LiveJournalEntryDatabase {
public LiveJournalDatabaseSetup() throws SQLException, ClassNotFoundException {
super(true);
}
public int insertEntries(int nSkip) throws SQLException, IOException {
String urlString = "http://kazuomik.livejournal.com/";
if (nSkip > 0) {
urlString += ("?skip=" + nSkip);
}
System.out.println(urlString);
URL url = new URL(urlString);
URLConnection conn = url.openConnection();
InputStream in = conn.getInputStream();
LiveJournalEntryParser parser
= new LiveJournalEntryParser(new InputStreamReader(in, "UTF-8"));
int count = 0;
while (true) {
LiveJournalEntry entry = parser.parse();
if (entry == null) {
break;
}
System.out.printf("%d", entry.getId());
System.out.flush();
insert(entry);
count ++;
if (count % 10 == 0) {
System.out.println();
}
else {
System.out.print(" ");
}
}
if (count % 10 != 0) {
System.out.println();
}
return count;
}
public static void main(String[] args) throws Exception {
int n = 0;
LiveJournalDatabaseSetup db = new LiveJournalDatabaseSetup();
while (true) {
int m = db.insertEntries(n);
if (m < 20) {
break;
}
n += 20;
try {
Thread.sleep(5000);
}
catch (InterruptedException e) {
}
}
db.close();
}
}
|
それでは、コンパイルして、実行してみます。今回は JDK 6 にバンドルされている derby.jar を使ってみます。
$ javac LiveJournalEntryDatabase.java
$ javac LiveJournalDatabaseSetup.java
$ java -classpath .:/usr/java/jdk1.6.0_01/db/derby.jar LiveJournalEntryUpdater
http://kazuomik.livejournal.com/
80810 80540 80298 79912 59760 79813 79495 79224 79017 78745
78462 78252 77846 77703 77432 77261 77050 76685 76427 76062
http://kazuomik.livejournal.com/?skip=20
76024 75731 75404 75135 74998 74600 70808 74410 74000 73761
...
http://kazuomik.livejournal.com/?skip=280
8776 8640 8328 8982 8161 7327 7694 7456 6946 6753
6600 5941 6278 5653 5503 5242 5085 4837 3672 3518
http://kazuomik.livejournal.com/?skip=300
3209 2876 495 722 1960 4044 1598 4195 1332 4582
1120 1008 2409 2080 2603
$
|
これで、Nutch に依存しないで、いつでもブログのすべてのエントリをデータベースにバックアップすることができます。Tags: computer_technology, programming
|
|
|
|