みんな、Taggerにはnilとか”"(空文字)とかを引数に渡しちゃだめですよ!
>> MeCab::Tagger.new.parseToNode(nil) (irb):1: [BUG] Segmentation fault ruby 1.8.7 (2009-04-08 patchlevel 160) [x86_64-linux]
単体で動かしていればすぐに気が付いたんだろうけど、デプロイした後で起こっていたのでなかなか気付きませんでした。
みんな、Taggerにはnilとか”"(空文字)とかを引数に渡しちゃだめですよ!
>> MeCab::Tagger.new.parseToNode(nil) (irb):1: [BUG] Segmentation fault ruby 1.8.7 (2009-04-08 patchlevel 160) [x86_64-linux]
単体で動かしていればすぐに気が付いたんだろうけど、デプロイした後で起こっていたのでなかなか気付きませんでした。
HpricotのXPATHではlower-caseとupper-caseは使えなかったんですね。アイター。
ということでさっくりとコードいじりました。
例えばmetaタグのdescriptionを取り出すときに、以下のように大文字になっていると
<meta NAME="DESCRIPTION" ...>
次のようなxpathでは取り出せません。
//head/meta[@name=description]
これを解決するには以下のようにxpathの関数を使うのが理想なのですが
//head/meta[lower-case(@name)=description]
Hpricotでの実現方法が分からなかったのでElem::Travを直でいじるという乱暴な方法で解決しました。
#
# 属性へのアクセスは全て小文字に揃えます。
#
module Hpricot
module Elem::Trav
def has_attribute?(name)
self.raw_attributes && self.raw_attributes.has_key?(name.to_s.downcase)
end
def get_attribute(name)
a = self.raw_attributes && self.raw_attributes[name.to_s.downcase]
a = Hpricot.uxs(a) if a
a = a.downcase if (a && self.name.downcase == "meta" && name == "name")
a
end
alias_method :[], :get_attribute
def set_attribute(name, val)
altered!
self.raw_attributes ||= {}
self.raw_attributes[name.to_s.downcase] = val.fast_xs
end
alias_method :[]=, :set_attribute
def remove_attribute(name)
name = name.to_s.downcase
if has_attribute? name
altered!
self.raw_attributes.delete(name)
end
end
end
end
これでmetaタグのname属性だけが全て小文字に揃います。
この内容はEclipse3.4で行っています。
Eclipseでrubyのソースコードを編集するにはAptanaを使うのが一般的ですが、railsを使うわけでもなく、cssを編集するわけでもなく、ただ単純にrubyで10行程度のスクリプトを書くためにAptanaを入れるのはちょっと大げさですよね。
そんなときにはAptanaに含まれるRDTだけをインストールしてあげます。
公式での説明。
http://update1.aptana.org/rdt/3.2/index.html
プラグインのURL。
http://update1.aptana.org/rdt/3.2/site.xml
インストールが完了すれば、あとは Winodw > Open Perspective > Other から Ruby を選ぶだけ。eclipseでさくさくrubyのソースコードをいじれるようになり快適です。
微妙な関数をperlからjavaに移植しました。前はrubyに移植しています。いったいどれだけnph-proxy.cgiが大好きなんだって話です。
public static String warpProxyDecode(String s) {
Pattern p = Pattern.compile("^([^?#]*)([^#]*)(.*)");
Matcher m = p.matcher(s);
if (m.matches() == false) {
throw new IllegalArgumentException(s);
}
String uri = m.group(1);
String query = m.group(2);
String frag = m.group(3);
Pattern uriPattern = Pattern.compile("=(..)");
Matcher uriMatcher = uriPattern.matcher(uri);
StringBuffer buff = new StringBuffer();
while (uriMatcher.find()) {
uriMatcher.appendReplacement(buff, toHexChr(uriMatcher.group(1)));
}
uriMatcher.appendTail(buff);
return buff + query + frag;
}
こうやって見るとjavaの正規表現って手間かかりますね。
元のperlのコード。
sub wrap_proxy_decode {
my($enc_URL)= @_ ;
my($uri, $query, $frag)= $enc_URL=~ /^([^?#]*)([^#]*)(.*)/ ;
# First, un-encode =xx chars.
$uri=~ s/=(..)/chr(hex($1))/ge ;
$uri= &proxy_decode($uri) ;
return $uri . $query . $frag ;
}
移植したrubyのコード。
def wrap_proxy_decode(arg)
/^([^?#]*)([^#]*)(.*)/ =~ arg
(uri, query, frag) = $1, $2, $3
uri.gsub!(/=(..)/){$1.hex.chr}
uri + query + frag
end
log4jでエラーメールを送る必要があり、ささっと設定ファイルだけ書き足せば終るだろと思っていたのですがすんなりとは行きませんでした。
デフォルトの SMTPAppender を使うと、日本語でおくれなかったり、ポート番号の指定が設定ファイルにかけません。
ですのでこれを解決するために、SMTPAppender を拡張した SMTPAppenderEx を作りました。
以下設定ファイルの内容です。SMTPPortというのが増えています。
log4j.appender.mail=hoge.log.SMTPAppenderEx
log4j.appender.mail.threshold=ERROR
log4j.appender.mail.LocationInfo=true
log4j.appender.mail.SMTPHost=xxxx
log4j.appender.mail.SMTPUsername=xxxx
log4j.appender.mail.SMTPPassword=xxxx
log4j.appender.mail.SMTPPort=xxxx
log4j.appender.mail.Subject=xxxx
log4j.appender.mail.From=xxxx
log4j.appender.mail.To=xxxx
log4j.appender.mail.BufferSize=256
log4j.appender.mail.layout=org.apache.log4j.PatternLayout
log4j.appender.mail.layout.ConversionPattern=%d %5p %c{1} - %m%n
以下はソースコードです。charset=ISO-2022-JPを指定しているのと、hostnameを取り出している次にportも取り出すように変更を加えています。
package hoge.log;
import java.util.Date;
import java.util.Properties;
import javax.mail.Authenticator;
import javax.mail.Multipart;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMultipart;
import org.apache.log4j.Layout;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.net.SMTPAppender;
import org.apache.log4j.spi.LoggingEvent;
public class SMTPAppenderEx extends SMTPAppender {
private String smtpPort;
@Override
protected void sendBuffer() {
try {
MimeBodyPart part = new MimeBodyPart();
StringBuffer sbuf = new StringBuffer();
String t = super.layout.getHeader();
if (t != null) {
sbuf.append(t);
}
int len = cb.length();
for (int i = 0; i < len; i++) {
LoggingEvent event = cb.get();
sbuf.append(super.layout.format(event));
if (!super.layout.ignoresThrowable()) {
continue;
}
String s[] = event.getThrowableStrRep();
if (s == null) {
continue;
}
for (int j = 0; j < s.length; j++) {
sbuf.append(s[j]);
sbuf.append(Layout.LINE_SEP);
}
}
t = super.layout.getFooter();
if (t != null) {
sbuf.append(t);
}
part.setContent(sbuf.toString(), super.layout.getContentType() + "; charset=ISO-2022-JP");
Multipart mp = new MimeMultipart();
mp.addBodyPart(part);
msg.setContent(mp);
msg.setSentDate(new Date());
Transport.send(msg);
} catch (Exception e) {
LogLog.error("Error occured while sending e-mail notification.", e);
}
}
@Override
protected Session createSession() {
Properties props = null;
try {
props = new Properties(System.getProperties());
} catch (SecurityException ex) {
props = new Properties();
}
if (getSMTPHost() != null) {
props.put("mail.smtp.host", getSMTPHost());
}
if (getSMTPPort() != null) {
props.put("mail.smtp.port", getSMTPPort());
}
Authenticator auth = null;
if (getSMTPPassword() != null && getSMTPUsername() != null) {
props.put("mail.smtp.auth", "true");
auth = new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(getSMTPUsername(), getSMTPPassword());
}
};
}
Session session = Session.getInstance(props, auth);
if (getSMTPDebug()) {
session.setDebug(getSMTPDebug());
}
return session;
}
public void setSMTPPort(String smtpPort) {
this.smtpPort = smtpPort;
}
public String getSMTPPort() {
return smtpPort;
}
}
送られてくるメールのヘッダは以下のようになっています。
Return-Path:X-Original-To: xxxx@xxxx Delivered-To: xxxx@xxxx Received: from tkosuga2 (unknown [192.168.5.196]) by xxxx (Postfix) with ESMTP id E193DF18001 for ; Thu, 17 Sep 2009 12:17:54 +0900 (JST) Date: Thu, 17 Sep 2009 12:16:12 +0900 (JST) From: xxxx@xxxx To: xxxx@xxxx Message-ID: <12528990.1.1253157372727.JavaMail.tkosuga@tkosuga> Subject: grisette.ambree Error MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="----=_Part_0_25392791.1253157372696" X-Antivirus: avast! (VPS 090916-0, 2009/09/16), Inbound message X-Antivirus-Status: Clean ------=_Part_0_25392791.1253157372696 Content-Type: text/plain; charset=ISO-2022-JP Content-Transfer-Encoding: quoted-printable
charsetがちゃんと書き換わっていますね。これでSMTPで、log4jから日本語エラーメールをさくっと送れるようになりました。
こんな会話があったのが、12日の夜。
[2009/09/12 22:58:25] えがちゃんの発言: ひまだなーw [2009/09/12 22:58:27] えがちゃんの発言: なにします?w [2009/09/12 22:58:29] えがちゃんの発言: なんかサービスつくりたいw [2009/09/12 23:03:57] tkosugaの発言: なんのサービス? [2009/09/12 23:04:07] tkosugaの発言: この話に食いつく [2009/09/12 23:04:25] えがちゃんの発言: pimoteのAPIをつかったサービスですねー
そこからbuzzterみたいなのを作りたいと言う話になりましたので、
颯爽とpimotterをリリースしました!この公開が13日19時30分ですので、話しが始まってからリリースまで21時間という速さです。
精度はまだまだ低いのですが、雰囲気は十分に感じ取ってもられるのではないかなと思います。
pimoteで流行しているキーワードを自動的に抽出し、それをピックアップして見ることができます。これでどんな事が流行しているのか一目で分かりますね!
このウェブサービスはrails 2.3.2 を使って作られています。pimoteサーバーには5分間隔で最新のタイムラインを取得しに行っています。画面に表示されるタイムライン解析結果はmemcachedを使って3分間キャッシュしてます。タイムラインの解析にはMeCabを使いました。
pimoteといいますか、twitterで流れる文章はほとんど口語で流れてるプラス、名詞だけ抜きだせば良いというものではないという事が分かってきました。
アスキーアートも抜き出せると良いのですが、パターンで一致させるぐらいしか方法が思いつきませんでした(ここは未実装です)
さっくりと作る分には誰が作っても同じような実装になると思うので、ここにこのサービスの中核となるMeCabで構文解析された単語の取り捨てをするコードを貼り付けます。
#
# もっとちゃんと作らないと。後で時間かけてかなー。
#
def self.necessary?(surface, feature, prev_feature)
#p surface + ", feature:" + feature + ", prev_feature: " + prev_feature.to_s
return false if surface.blank?
return false if surface.split(//u).collect{|c| MARKS.include?(c)}.all?
return false if /[[:cntrl:]|[:blank:]|[:punct:]]+/ =~ surface
return true if (/^名詞,(一般|固有名詞|サ変接続|形容動詞語幹|ナイ形容詞語幹)/ =~ feature)
return true if (/^形容詞,(自立)/ =~ feature)
return true if (/^接頭詞,名詞接続/ =~ feature)
return true if (/^感動詞/ =~ feature)
return false if prev_feature.blank?
#
# この2つ次は絶対に拾う
#
if (/^接頭詞,名詞接続/ =~ prev_feature)
return true
end
if (/^名詞/ =~ prev_feature)
return true if (/^助詞,(.*,連語|終助詞|係助詞)/ =~ feature)
return true if (/^動詞,(非自立)/ =~ feature)
end
if (/^助詞,格助詞,連語/ =~ prev_feature)
return true if (/^助動詞/ =~ feature)
end
if (/^助詞,係助詞/ =~ prev_feature)
return true if (/^助詞,終助詞/ =~ feature)
end
if (/^動詞,非自立/ =~ prev_feature)
return true if (/^動詞,非自立/ =~ feature)
end
false
end
ASCII_ART_MARKS = %w{゚ ノ Д 冫}
ASCII_MARKS = %w{… ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ ^ _ ` { | } ~ > <}
ZENKAKU_MARKS = %w{ 、 。 , . ・ : ; ? ! ゛ ゜ ´ ` ¨ ^  ̄ _ ヽ ヾ ゝ ゞ 〃 仝 々 〆 〇 ー ― ‐ / \ ~ ∥ | … ‥ ‘ ’ “ ” ( ) 〔 〕 [ ] { } 〈 〉 《 》 「 」 『 』 【 】 + - ± × ÷ = ≠ < > ≦ ≧ ∞ ∴ ♂ ♀ ° ′ ″ ℃ ¥ $ ¢ £ % # & * @ § ☆ ★ ○ ● ◎ ◇ ◆ □ ■ △ ▲ ▽ ▼ ※ 〒 → ← ↑ ↓ 〓 ∈ ∋ ⊆ ⊇ ⊂ ⊃ ∪ ∩ ∧ ∨ ¬ ⇒ ⇔ ∀ ∃ ∠ ⊥ ⌒ ∂ ∇ ≡ ≒ ≪ ≫ √ ∽ ∝ ∵ ∫ ∬ Å ‰ # ♭ ♪ † ‡ ¶ ◯ ─ │ ┌ ┐ ┘ └ ├ ┬ ┤ ┴ ┼ ━ ┃ ┏ ┓ ┛ ┗ ┣ ┳ ┫ ┻ ╋ ┠ ┯ ┨ ┷ ┿ ┝ ┰ ┥ ┸ ╂ ∮ ∑ ∟ ⊿ ¦ ' " }
MARKS = ASCII_MARKS + ZENKAKU_MARKS + ASCII_ART_MARKS
よそで書かれたテキストを自分のサーバーにもってきて加工するだけのサービスって楽ですね。負荷の問題もありませんし。接続先は1つですし。次からのサービスを作るときに、このあまりの楽々さに溺れてしまわないよう気をつけたいと思います。
1つ前の記事と同じように、クレジットカード・メーカーが実稼動するcentos5.2でrmagickが動くようにします。
まず依存するライブラリをインストールします。
sudo yum install bzip2-devel freetype-devel libpng-devel libtiff-devel freetype-devel libjpeg-devel
ソースコードもってきてconfigureしてmakeしてinstall
wget ftp://ftp.imagemagick.org/pub/ImageMagick/ImageMagick-6.5.3-10.tar.gz
tar xvfz ImageMagick-6.5.3-10.tar.gz
cd ImageMagick-6.5.3-10
./configure --disable-static --with-modules --without-perl \
--without-magick-plus-plus --with-quantum-depth=8 --disable-openmp
make
sudo make install
共有ライブラリを認識させます。
ldconfig
あとは以下を~/.bashrcに追記します。
export LD_LIBRARY_PATH=/usr/local/lib
gemでRMagickをインストール。
gem install rmagick -v 2.11.0
以上です。centosに入れるのは特に問題なくできました。
クレジットカード・メーカーの開発はUbuntuで行ないました。そこでRMagickをインストールできるまですんなりとは行かなかったので備忘録としてブログ記事にします。
まず依存するライブラリをインストール。
sudo apt-get install libfreetype6 sudo apt-get install libfreetype6-dev sudo apt-get install libjpeg62-dev sudo apt-get install libpng12-dev sudo apt-get install libwmf-dev sudo apt-get install libperl-dev sudo apt-get install libbz2-dev sudo apt-get install libz-dev sudo apt-get install libx11-dev sudo apt-get install libxt-dev sudo apt-get install libxext-dev sudo apt-get install libxml2-dev sudo apt-get install liblcms1-dev sudo apt-get install libexif-dev sudo apt-get install libjasper-dev sudo apt-get install libltdl3-dev sudo apt-get install graphviz sudo apt-get install gs-gpl sudo apt-get install pkg-config
wgetでソースコードを取得します。
wget ftp://ftp.imagemagick.org/pub/ImageMagick/ImageMagick-6.5.3-10.tar.gz tar xvfz ImageMagick-6.5.3-10.tar.gz cd ImageMagick-6.5.3-10
configure するときに –disable-openmpを足しました。
./configure --disable-static --with-modules --without-perl \
--without-magick-plus-plus --with-quantum-depth=8 --disable-openmp
makeしてインストールします。
make sudo make install
シンボリックリンクを更新します。
ldconfig
これをやらないとibMagickCore.so.2が見つからないとエラーがでます。
$ convert --version convert: error while loading shared libraries: libMagickCore.so.2: cannot open shared object file: No such file or directory
正常にインストールできたことを確認。
$ convert --version Version: ImageMagick 6.5.3-10 2009-08-04 Q8 http://www.imagemagick.org Copyright: Copyright (C) 1999-2009 ImageMagick Studio LLC
次にgemでRMagickをインストールします。
sudo gem install rmagick -v 2.11.0
irbから動作確認してみましょう。
require 'rubygems' require 'RMagick' require 'pp' include Magick pp colors
ずらーっと色情報が出力されれば完了です。
今回、クレジットカード・メーカーを作るにあたって、アップロードされたファイルの取り扱いに意外と気を使う必要がありました。上限を超えたばかでかいファイルが送られてくるケースや、ファイルの拡張子はpngでも内容はexeファイルであったりなどです。
ここではそれらに対して行なった設定と実装を説明します。
Apacheにアップロードするファイルサイズの上限を指定
クレジットカード・メーカーではアップロードできるファイルサイズの上限を1Mまでとしています。
このファイルサイズ以上をアップロードされても捨てるしかありませんので、リソースの無駄使いになってしまいます。
これを防ぐためにはApacheのLimitRequestBodyディレクティブを使います。
以下、上記ページからの抜粋です。
このディレクティブは、 管理者にクライアントからの異常なリクエストを制御できるようにし、 何らかの形のサービス拒否攻撃 (訳注:DoS) を避けるのに有効です。 ある場所へのファイルアップロードを許可する場合に、 アップロードできるファイルのサイズを 100K に制限したければ、 以下のように指定します: LimitRequestBody 102400
アップロードされたファイルの種類を指定
アップロードされてきたファイルのMIMEタイプを判別し、それが背景画像として使えるかを判定しなければいけません。
MIMEタイプを判定するライブラリにはshared-mime-infoを使いました。
yumを使ってインストールするには以下のコマンドを実行します。
yum install shared-mime-info
rubyからshared-mime-infoを使えるように以下のコマンドを実行します。
gem install shared-mime-info
これでMIMEパッケージが使えるようになりますので、以下のコードでファイルの種類を知ることができます。
require 'rubygems'
require 'shared-mime-info'
MIME.check_magics('images/sample.jpg').type
=> "image/jpeg"
shared-mime-infoの仕様に、推奨するMIMEタイプのチェックする順番について書かれています。
簡単に書きますと、次の3ステップになります。
1. MIMEタイプを見る
2. glob(ファイルの拡張子)を見る
3. magic(ヘッダ部分のバイナリ)を見る
attachment-fuの使い方
クレジットカード・メーカーはカード画像の生成にImageMagickを使っています。railsプラグインのattachment-fuではサムネイルの生成にImageMagickを使っているためとても相性がよかったです。
以下のようにthumbnailsにファイルのサイズを書くだけでサムネイルを勝手に生成してくれます。
class PublishedImageFile < ActiveRecord::Base
has_attachment(:content_type => :image,
:max_size => 1024.kilobytes,
:thumbnails => { :middle => '387x249', :small => '258x166'}
)
end
このモデルのスキーマは以下のようになっています。
create_table "published_image_files", :force => true do |t|
t.integer "parent_id"
t.string "content_type"
t.string "filename"
t.string "thumbnail"
t.integer "size"
t.integer "width"
t.integer "height"
t.string "name", :default => "オリジナルの"
t.string "comment", :limit => 1024, :default => "自分だけのクレカ出来た!"
t.boolean "fixed"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "published_image_files", ["content_type"], :name => "index_published_image_files_on_content_type"
add_index "published_image_files", ["created_at"], :name => "index_published_image_files_on_created_at"
add_index "published_image_files", ["parent_id"], :name => "index_published_image_files_on_parent_id"
テーブル作って、モデル作って、has_attachmentと書くだけでアタッチメントの管理できるattachment-fuは素晴らしいです。モデルを削除すると保存先のストレージからも消してくれます。
以上です、railsでファイルをアップロードするサービスを作る方の参考になれば幸いです。
railsでscript/consoleを動かすとこのエラーがでる。うーんreadlineが入っていないらしい。
# ruby script/console
Loading development environment (Rails 2.2.2)
/usr/local/lib/ruby/1.8/irb/completion.rb:10:in `require': no such file to load -- readline (LoadError)
from /usr/local/lib/ruby/1.8/irb/completion.rb:10
from /usr/local/lib/ruby/1.8/irb/init.rb:252:in `require'
from /usr/local/lib/ruby/1.8/irb/init.rb:252:in `load_modules'
from /usr/local/lib/ruby/1.8/irb/init.rb:250:in `each'
from /usr/local/lib/ruby/1.8/irb/init.rb:250:in `load_modules'
from /usr/local/lib/ruby/1.8/irb/init.rb:21:in `setup'
from /usr/local/lib/ruby/1.8/irb.rb:54:in `start'
from /usr/local/bin/irb:13
rubyのソースコードのext/readlineに移動しておもむろにextconf.rb
# ruby extconf.rb checking for tgetnum() in -lncurses... no checking for tgetnum() in -ltermcap... no checking for tgetnum() in -lcurses... no checking for readline/readline.h... yes checking for readline/history.h... yes checking for readline() in -lreadline... no checking for readline() in -ledit... no checking for editline/readline.h... no
えええ?分かんないなあ。あれ?ncursesが入っていない?
yumでncurses-develを入れる。
yum install ncurses-devel
再度実行。
# ruby extconf.rb checking for tgetnum() in -lncurses... yes checking for readline/readline.h... yes checking for readline/history.h... yes checking for readline() in -lreadline... yes checking for rl_filename_completion_function()... yes checking for rl_username_completion_function()... yes checking for rl_completion_matches()... yes checking for rl_deprep_term_function in stdio.h,readline/readline.h,readline/history.h... yes checking for rl_completion_append_character in stdio.h,readline/readline.h,readline/history.h... yes checking for rl_basic_word_break_characters in stdio.h,readline/readline.h,readline/history.h... yes checking for rl_completer_word_break_characters in stdio.h,readline/readline.h,readline/history.h... yes checking for rl_basic_quote_characters in stdio.h,readline/readline.h,readline/history.h... yes checking for rl_completer_quote_characters in stdio.h,readline/readline.h,readline/history.h... yes checking for rl_filename_quote_characters in stdio.h,readline/readline.h,readline/history.h... yes checking for rl_attempted_completion_over in stdio.h,readline/readline.h,readline/history.h... yes checking for rl_library_version in stdio.h,readline/readline.h,readline/history.h... yes checking for rl_event_hook in stdio.h,readline/readline.h,readline/history.h... yes checking for rl_cleanup_after_signal()... yes checking for rl_clear_signals()... yes checking for rl_vi_editing_mode()... yes checking for rl_emacs_editing_mode()... yes checking for replace_history_entry()... yes checking for remove_history()... yes creating Makefile
あとはmake && make install。これで動きました。