PHPを起動するとhttpdがSegmentation faultになる場合の対処法

Apache 2.0.52でPHPが動いてないウェブサーバがあった。
GETするといきなりEOFでコンテンツが何もこない。

httpdのerror_logにセグメンテーション違反が出てた。

[Mon Feb 16 13:30:24 2009] [notice] child pid 7897 exit signal Segmentation fault (11)

gdbhttpdを起動してみるとバックトレースはこんな感じ。
やり方はここを参考にした。http://dozo.rgr.jp/log/eid454.html

Program received signal SIGSEGV, Segmentation fault.
[Switching to Thread -1209084224 (LWP 3094)]
0x00000000 in ?? ()
(gdb)
(gdb) bt
#0  0x00000000 in ?? ()
#1  0x01250aa7 in php_hash_environment ()
    at /usr/local/src/php-5.1.6/main/php_variables.c:656
#2  0x01245e76 in php_request_startup ()
    at /usr/local/src/php-5.1.6/main/main.c:1118
#3  0x012f4d46 in php_handler (r=0x9eadde0)
    at /usr/local/src/php-5.1.6/sapi/apache2handler/sapi_apache2.c:542
#4  0x00dea9c7 in ap_run_handler () from /usr/sbin/httpd
#5  0x00deae33 in ap_invoke_handler () from /usr/sbin/httpd
#6  0x00de78b5 in ap_process_request () from /usr/sbin/httpd
#7  0x00de263f in _start () from /usr/sbin/httpd
#8  0x09eadde0 in ?? ()
#9  0x00000004 in ?? ()
#10 0x09eadde0 in ?? ()
#11 0x00927d89 in modperl_process_connection_handler ()
   from /etc/httpd/modules/mod_perl.so
#12 0x00df5359 in ap_run_process_connection () from /usr/sbin/httpd
#13 0x00df56c4 in ap_process_connection () from /usr/sbin/httpd
#14 0x00de8909 in ap_graceful_stop_signalled () from /usr/sbin/httpd
#15 0x00de8b00 in ap_graceful_stop_signalled () from /usr/sbin/httpd
#16 0x00de8c1f in ap_graceful_stop_signalled () from /usr/sbin/httpd
#17 0x00de94e4 in ap_mpm_run () from /usr/sbin/httpd
#18 0x00df035a in main () from /usr/sbin/httpd

原因は単純。Apacheの設定でPHP5とPHP4のモジュールをどっちもロードしてた。
PHP4のほうをコメントアウトしてhttpd再起動で終わり。

conf/httpd.confで

LoadModule php5_module modules/libphp5.so

conf.d/php.confで

#LoadModule php4_module modules/libphp4.so

PHP5のモジュール入れたら動作確認は必ずやりましょうね。

NicoClipをはてなブックマークのリニューアルに対応させました

最新版はこちらからインストールできます。マイリストインポートツールも対応しました(12月8日)。
http://svn.coderepos.org/share/lang/javascript/userscripts/nicoclip.user.js
http://svn.coderepos.org/share/lang/javascript/userscripts/nicoclip_import.user.js

一応 b.hatena.ne.jp を使うようになっているのですが、個人的に利用する分には bbeta.hatena.ne.jp のほうが古いキャッシュも出ずレスポンスも速くて良いような気がしています。
ソースの頭のほうで手動で変更できるようにしておきました。
27行目のコメント外して28行目をコメントアウトすると bbeta を参照するように変更できます。

this.hatenaHost = 'http://bbeta.hatena.ne.jp/';
// this.hatenaHost = 'http://b.hatena.ne.jp/';

お好みでどうぞ。ただしいつまで使えるかはわかりません。

その他主な変更点は、はてなブックマークのURLやHTML変更への対応に加えて、Atomフィードでタグで絞れなくなったりしてましたのでRSSを使うように更新したりしました。

日本語(シフトJIS)を含んだzipやlzhをUTF-8に変換しながら解凍

シフトJISの日本語ファイル名が入ったzipファイルを受け取ってしまったとき、UTF-8環境のLinux上でunzipすると文字化けして大変なことになります…
解凍後に変換しようとしてもどうしようもないので、SJISからUTF-8にファイル名を変換した上で解凍するPerlスクリプトを勉強がてら作ってみました。
CPANモジュールの Archive::Zip などが必要です。

#!/usr/bin/perl
# ファイル名をSJISからUTF-8に変換しながらzipを解凍する

use strict;
use warnings;
use Archive::Zip qw( :ERROR_CODES :CONSTANTS );
use Encode qw/from_to/;
use Getopt::Long;
use DateTime;
use DateTime::TimeZone::Local;

GetOptions('list|l' => \ my $mode_list);
if (scalar @ARGV < 1) {
  print "Usage: $0 [-l] <zip file>\n";
  exit 1;
}
my $zipfile = shift @ARGV;

my $zip = Archive::Zip->new();
unless ( $zip->read($zipfile) == AZ_OK ) {
  die 'read error';
}
print "Archive: $zipfile\n";
if ($mode_list) {
  print <<HERE;
   Length     Date    Time   Name
 --------  ---------- -----  ----
HERE
}
my $total_size = 0;
my @members = $zip->members();
my $members_count = scalar @members;
foreach my $member (@members) {
  my $filename = $member->fileName;
  from_to($filename, 'cp932', 'utf8');
  if ($mode_list) { # リスト表示モード
    my $dt = DateTime->from_epoch(
      epoch => $member->lastModTime
    )->set_time_zone(DateTime::TimeZone::Local->TimeZone());
    $total_size += $member->uncompressedSize;
    printf "%9d  %s  %s\n",
      $member->uncompressedSize,
      $dt->ymd('-') . ' ' . sprintf("%02d", $dt->hour) . ':' . sprintf("%02d", $dt->min),
      $filename;
  } else { # 解凍モード
    print "  inflating: $filename\n";
    unless (defined $zip->extractMember($member, $filename)) {
      warn "  failed: $filename\n";
    }
  }
}
my $members_count_str = "$members_count file" . ($members_count > 1 ? 's' : '');
if ($mode_list) {
  print <<HERE;
 --------                    -------
HERE
printf "%9d                    %s\n", $total_size, $members_count_str;
}

これを /usr/local/bin/unzips とかに保存して下のコマンドで展開できます。
ファイルの上書き確認なんていう上等な機能はまだないです。

$ unzips <zip file>

-l オプションで中身の一覧表示ができます。

$ unzips -l <zip file>


ついでに日本語ファイル名を含むlzhファイルを送ってこられる方々が私の周りに少なからずいるのでそのとき用に下のスクリプトも作りました。
CPANモジュールの Archive::Lha などが必要です。

#!/usr/bin/perl
# ファイル名をSJISからUTF-8に変換しながらlzhを解凍する

use strict;
use warnings;
use Archive::Lha::Stream;
use Archive::Lha::Header;
use Archive::Lha::Decode;
use Encode qw/from_to/;
use File::Basename;
use File::Path;
use Getopt::Long;
use DateTime;
use DateTime::TimeZone::Local;

GetOptions('list|l' => \ my $mode_list);
if (scalar @ARGV < 1) {
  print "Usage: $0 [-l] <lzh file>\n";
  exit 1;
}
my $lzhfile = shift @ARGV;

my $stream = Archive::Lha::Stream->new(file => $lzhfile);
print "Archive: $lzhfile\n";
if ($mode_list) {
  print <<HERE;
   Length     Date    Time   Name
 --------  ---------- -----  ----
HERE
}
my $total_size = 0;
my $members_count = 0;
while (defined(my $level = $stream->search_header)) {
  $members_count++;
  my $header = Archive::Lha::Header->new(
    level  => $level,
    stream => $stream,
  );
  my $filename = $header->pathname;
  $filename =~ s{\\}{/}g;
  $filename =~ s/\xff//g;
  from_to($filename, 'cp932', 'utf8');
  if ($mode_list) { # リスト表示モード
    my $dt = DateTime->from_epoch(
      epoch => $header->timestamp
    )->set_time_zone(DateTime::TimeZone::Local->TimeZone());
    $total_size += $header->original_size;
    printf "%9d  %s  %s\n",
      $header->original_size,
      $dt->ymd('-') . ' ' . sprintf("%02d", $dt->hour) . ':' . sprintf("%02d", $dt->min),
      $filename;
    $stream->seek( $header->next_header );
  } else { # 解凍モード
    print "  inflating: $filename\n";
    my $dir = dirname($filename);
    mkpath($dir) unless -e $dir;
    open my $fh, '>:raw', $filename;
    binmode $fh;
    $stream->seek($header->data_top);
    my $decoder = Archive::Lha::Decode->new(
      header => $header,
      read   => sub { $stream->read(@_) },
      write  => sub { print $fh @_ },
    );
    my $crc16 = $decoder->decode;
    close $fh;
    warn "crc mismatch" if $crc16 != $header->crc16;
  }
}
my $members_count_str = "$members_count file" . ($members_count > 1 ? 's' : '');
if ($mode_list) {
  print <<HERE;
 --------                    -------
HERE
printf "%9d                    %s\n", $total_size, $members_count_str;
}

これも /usr/local/bin/unlzhs とかに保存して下のコマンドで実行できます。
くれぐれも、上書き確認機能はないですので使う際はご注意ください。
機能的には lha x のようなものです。

$ unlzhs <lzh file>

-l オプションで中身を一覧表示できます。
機能的には lha l のようなものです。

$ unlzhs -l <lzh file>

ヤフオクの出品商品のアクセス数とかを監視するPerlスクリプト

初めてYahoo!オークションに出品したんですが、アクセス総数やウォッチリスト追加総数の伸びが気になってしょうがないので作りました。cronに登録して動かしてます。CPANにもこういうのなくてヤフオクAPI使ってもアクセス数とかは取れないんですよね。

ログはこんなふうに取れてます。データは左から時刻、アクセス総数、友だちにメールを送った総数、ウォッチリストに追加した総数、違反商品として申告された総数、現在価格、入札件数です。

time                    views   friend  watch   illegal price   bids
2008-06-02T15:30:06     39      0       16      0       1,100   4
2008-06-02T15:40:06     39      0       16      0       1,100   4
2008-06-02T15:50:06     41      0       16      0       1,100   4
2008-06-02T16:00:05     44      0       16      0       1,100   4
2008-06-02T16:10:06     44      0       16      0       1,100   4

スクリプトは下の通りです。
今思うと普通に正規表現でスクレープでもいい気がしますが、Web::Scraper 良いですね。miyagawaさん最高っすね!
ヤフオクだと空白行はさむのに単一の <p> タグが使われてたりするのですが、おそらく HTML::TreeBuilder で p_strict がデフォルトで false になってるせいで単一の <p> をちゃんとXPathの階層に入れないと解釈できません。Firebugから取ったXPathだと p が入らないので(おまけに tbody がなくても入っちゃいますが)そのまま使えず、仕方なく自分でHTML構造たどりました。不便やなー

Cookieの処理が肝心です。最初作ったスクリプトで、面倒くさいので毎回ログインフォームからログインしてたんですが、10分おきに動かしててその日40回くらいアクセスしたところでいきなりログイン時にCaptchaを要求されるようになり、ログインできなくなってしまいました。いくらUser-Agent変えようが10分おきにログインしてたらbotって丸わかりですね、すいません。Cookieでログインしてれば回避できるみたいです。

#!/usr/bin/perl
use strict;
use warnings;
use WWW::Mechanize;
use Web::Scraper;
use DateTime;
use Encode qw/encode decode/;

# ヤフオクユーザ名
my $username = 'ユーザ名';
# ヤフオクパスワード
my $password = 'パスワード';
# 監視対象のヤフオク商品ページ
my $target_page = 'http://page--.auctions.yahoo.co.jp/jp/auction/---------';

# 抽出したデータを記録するファイル
my $logfile = 'yalog.txt';
# Cookieを保存するファイル
my $cookie_file = 'cookies.txt';
# ログインできたかどうか確認するための検索文字列
my $login_string = encode('euc-jp', decode('utf8', "こんにちは、 $username さん"));

# ログインを試みる
# Cookieを使ってだめなら通常のログインを行う
sub try_login($) {
  my $mech = shift;
  unless (&cookie_login($mech)) {
    unless (&login($mech)) {
      return 0;
    }
  }
  return 1;
}

# Cookieを使ってログインを試みる
sub cookie_login($) {
  print "cookie_login\n";
  my $mech = shift;
  if (-e $cookie_file) {
    $mech->cookie_jar({ file => $cookie_file });
    $mech->get('http://auctions.yahoo.co.jp/');
    if ($mech->content =~ $login_string) { # ログイン成功
      print " - 成功\n";
      return 1;
    }
  } else {
    print "cookieファイルがありません \"$cookie_file\"\n";
  }
  # ログイン失敗
  print " - 失敗\n";
  return 0;
}

# フォームからのログインを試みる
# 既存のCookieが別アカウントのものならば一旦ログアウトする
sub login($) {
  print "login\n";
  my $mech = shift;
  $mech->get('https://login.yahoo.co.jp/config/login?.lg=jp&.intl=jp&.src=auc&.done=http://auctions.yahoo.co.jp/jp');
  unless ($mech->find_all_inputs( name => 'login' )) {
    # ログインフォームがない=別アカウントでログインしている?
    print "違うユーザでログインしていると思われるのでまずログアウトします\n";
    &logout($mech);
    $mech->get('https://login.yahoo.co.jp/config/login?.lg=jp&.intl=jp&.src=auc&.done=http://auctions.yahoo.co.jp/jp');
  }
  $mech->submit_form(
    fields => {
      login  => $username,
      passwd => $password
    },
  );
  $mech->get('http://auctions.yahoo.co.jp/jp');
  if ($mech->content =~ $login_string) { # ログイン成功
    print " - 成功, Cookieをファイルに保存します\n";
    open(FILE, ">$cookie_file") or die $!;
    print FILE "#LWP-Cookies-1.0\n";
    # discardなCookieもダンプが必要
    print FILE $mech->cookie_jar->as_string;
    close(FILE);
    return 1;
  } else { # ログイン失敗
    print " - 失敗\n";
    return 0;
  }
}

# ログアウトする
sub logout($) {
  my $mech = shift;
  print "logout\n";
  $mech->get('http://login.yahoo.co.jp/config/login?.lg=jp&.intl=jp&logout=1&.src=auc&.done=http://auctions.yahoo.co.jp/jp');
}

# main
my $mech = new WWW::Mechanize();
$mech->agent_alias('Windows IE 6');

unless (&try_login($mech)) {
  die "ログインできませんでした";
}

$mech->get($target_page);

my $scraper = scraper {
  # 0: アクセス総数
  # 1: 友だちにメールを送った総数
  # 2: ウォッチリストに追加した総数
  # 3: 違反商品として申告された総数
  # 4: 入札者評価制限
  process '/html/body/p/table/tr/td/table/tr/td/table/tr/td/table/tr/td/font', 'nums[]' => 'TEXT';
  # 5: 現在価格
  # 7: 残り時間
  # 12: 入札件数
  process '/html/body/p/table/tr/td/table/tr/td/font', 'info[]' => 'TEXT'; # 13th
};

my $res = $scraper->scrape($mech->content, $mech->uri);

my $price = $res->{'info'}->[5];
$price =~ s/[^0-9,]//g;
my $bid = $res->{'info'}->[12];
$bid =~ s/[^0-9,]//g;

# 出力は左から以下の項目をタブ区切り
#  取得時刻
#  アクセス総数
#  友だちにメールを送った総数
#  ウォッチリストに追加した総数
#  違反商品として申告された総数
#  現在価格
#  入札件数
my $line = DateTime->now->set_time_zone('Asia/Tokyo') . "\t"
            . join("\t", @{$res->{'nums'}}[0..3]) . "\t"
            . $price . "\t"
            . $bid . "\n";
open(FILE, ">>$logfile") or die $!;
flock(FILE, 2);
print FILE $line;
close(FILE);

フォルダペインが消える件

Thunderbird使ってたら、なぜか今日起動したときにフォルダペイン(アカウント一覧)が表示されなくなって困った。

自分のプロファイルディレクトリの localstore.rdf を消して(退避させて)起動したら直った。
これってウインドウのレイアウトを制御してる設定ファイルなんですね

NicoClip更新

いつのまにかニコニコ動画のタイトル文字列のパターンが変更されていたので(汗)対応しておきました。
最新版はこちらから
http://svn.coderepos.org/share/lang/javascript/userscripts/nicoclip.user.js


Changeset
http://coderepos.org/share/changeset/7056
http://coderepos.org/share/changeset/7057

diff

--- nicoclip.user.js    (リビジョン 7055)
+++ nicoclip.user.js    (リビジョン 7057)
@@ -212,7 +212,9 @@
   var videos = [];
   for (var i = 0; i < entriesNum; i++) {
     var title = (entries[i].getElementsByTagName('title'))[0].firstChild.nodeValue;
-    title = title.replace(/^.*?‐/, '');
+    title = title.replace(/^ニコニコ動画\(.*?\)\s*‐/, ''); // RC2/RC
+    title = title.replace(/‐ニコニコ動画\(.*?\)$/, ''); // RC2
+    title = title.replace(/^ニコニコ動画\(.*?\) \| /, ''); // gamma
     // あとでcreateTextNodeするのでエンティティを解除
     title = title.replace('&lt;', '<');
     title = title.replace('&gt;', '>');

WWW::Mechanizeのsubmitとclickの違い

これははまりました・・
CPANのWWW::Mechanize 1.34で click() は <input type="submit" name="name1" value="value1"> の分の name1=value1 も送信しますが、submit() ではこれを送信しません。

たとえば下のようなフォームの場合。

<form method="get" action="do.php" name="loginform">
<input type="hidden" name="login" value="123">
<input type="hidden" name="login" value="abc">
<input type="submit" name="login" value="xyz">
</form>

Firefox 2.0等のブラウザでsubmitボタンをクリックすると下のURLに飛びます。

do.php?login=123&login=abc&login=xyz

ところが WWW::Mechanize で submit() すると下のURLに飛びます。

do.php?login=123&login=abc

submit() では login=xyz が送られないのです。しかし click() を使えば送ることができます。
WWW::Mechanizeのドキュメントによると submit() だとどのボタンもクリックされないとのことです。(→参考


これが原因となって、submit_form() の際に一工夫しないとdotProjectにログインできませんでした。dotProjectはこの login=xyz に相当するものが送られないとログインできないようです。(変な仕様だな・・)
submit_form() を使う場合、下のようにbuttonパラメータを指定すると内部で click() を呼んでくれるので login=xyz が送れます。

$mech->submit_form(
  fields => {
    username => $username,
    password => $password,
  },
  button => 'login'
);

もちろん field() とかでパラメータをセットしてから click() でもいいと思いますです。


結局、フォームでは submit() じゃなくて極力 click() を使ったほうがいいということですかね。