SJISで組まれたPHPスクリプトをUTF-8で出力する。

【追記 2015年2月9日】
文字化け対策で、mb_convert_encoding関数に渡す漢字コードは’SJIS’ ではなく、’SJIS-WIN’ ( ‘CP932’) を使えとのお達し。ごめんなさい(m_m)
【追記ここまで】

年初からなんやかんやと忙しく更新する時間が取れないので、放置状態でした(ーー;;
ここ一ヶ月、年度末でなかなかの作業量。

備忘録のメモです。

外注している、SJISで組まれたPHPスクリプトをヘッダとフッターを変えUTF-8にする、という簡単な仕事。時間が迫っているので、外注先に投げる時間がない。ヘッダとフッタを変えてスクリプトファイルの文字コードをUTF8にしただけでは、文字化けしてしまう。そりゃ当然、 データーベースに保存されているデータはSJISのままなんだから・・・。

しかもデーターベースには手が出せない。それに外注先のスクリプトをこちらで勝手にいじると、まずい。何かと問題がある。

というわけで・・・

元のPHPスクリプトファイルを XXXX-sjis.phpのようにして、XXXX.phpから以下のようにしてXXXX-sjis.phpを読み込んでエンコード変換して出力することで、一応解決。

<?php
//XXXX-sjis.php
echo 'これはSJISファイルスクリプト';
?>

<?php
// XXXX.php
$sjis_contents = file_get_contents('http://host/XXXX-sjis.php');
echo mb_convert_encoding($sjis_contents,'UTF-8','SJIS');

本当は以下のように、元のファイルに出力バッファリング・ハンドラを追加すれば解決するんですが・・・元のファイルはあまりいじりたくないので・・・(^^;;;;

<?php
function output_handler($buf)
{
  return mb_convert_encoding($buf,'UTF-8','SJIS');
}

ob_start('output_handler');
//XXXX-sjis.php
echo 'これはSJISファイルスクリプトですが、出力はUTF-8になります。

ob_end_flush();
?>

とりあえずの応急処置・・・でなんとか切り抜けた(^^ゞ

でも・・・わざわざfile_get_contents関数なんて使わないで、requireすれば良かったのか????

<?php
// XXXX.php
function output_handler($buf)
{
  return mb_convert_encoding($buf,'UTF-8','SJIS');
}

ob_start('output_handler');

require 'XXXX-sjis.php';

ob_end_flush();

まぁ、いいや。

PHPの配列は9ソだ!

PHPの仕様に関して文句は別にないんだが・・・、あのPHPの配列だけは、どうにかならないものか・・・。

Perlだと、配列と連想配列は、配列なら、$array[0] 、連想配列なら、$hash{’00’}と、表記と共に明確に区別されているのでわかりやすいし、まぁ、間違えることは、少ない。

が、PHPのコードを書いてると、ホントにイライラする。ま、言語仕様を斜め読みしかしていない、あやふやな知識で書いているオイラが全面的に悪いんだけど。。。

データベースのとあるカラムからフェッチした、数字の文字列を、つい、うっかり、配列に入れて、foreachとかでぶん回していたら、あるはずのデータが未定義になる、という現象の解読に数時間費やしてしまった・・・。

$ar = array();

$ar[1] = 'ほげほげ';
$ar[23] = 'ほむほむ';

//$recordには、以下の値が入っていた。
//$record['date_begin_day'] = '01';

echo $ar[$record['date_begin_day']];

さて、僕は、当然 「ほげほげ」と出力されると信じて疑わなかった。
PHPで仕事している人は、たぶん、こう思うでしょう。

「おまえはアホか!もう一回、リファレンスを読めよ」

と。

echo $ar[intval($record['date_begin_day'])];
//intval関数を通さないといけなかった・・・。

perlなら配列の添字は数字に暗黙的に変換されるので何ら問題無い。

$ar[1] = 'ほげほげ';
$ar[23] = 'ほむほむ';

$record{'date_begin_day'} = '01';

print $ar[$record{'date_begin_day'}];

しかし、別の問題を引き起こすが・・・ま、一長一短ですかな・・・。

気を付けよう。。。

WORDPRESSプラグインのテンプレート

私的記録。

WordPressのテーマやプラグインは便利ですよね。
既存のプラグインを組み合わせて使うと、あら不思議、それなりにWEBシステムが出来てしまうではありませんか(^_^;)

あまりにも便利なので、自分でも足りない機能を作ってしまいたい、と思うのは当然でしょう。

というわけで、プラグインをすぐ作れるようにスケルトン的なテンプレートを記録。

Add Html Code

このスケルトン・プラグインは、wp_head,wp_footerのアクションを登録するプラグインである。
要するにテーマのヘッダとフッタに好きなHTMLコードをインジェクトするプラグイン。

このスケルトンは4つのファイルで構成されています。

■プラグイン本体 (addhtml.php) まずはこれがないと。

<?php
/*
Plugin Name: Add HTML code for WordPress
Description: Add head or foot html code
Version: 1.0
Author: Kenji Nakagawa
License: none
*/

//start up!
require_once(dirname( __FILE__ ) .'/addhtml-common.php');

if(is_admin())
{
  require_once(dirname( __FILE__ ) .'/addhtml-setting.php');
  AddHtmlCodeSetting::register(plugin_basename(__FILE__));
}
else
{
  require_once(dirname( __FILE__ ) .'/addhtml-doaction.php');
  AddHtmlCode::register();
}

?>

以下、これまで散々関数名のバッティングに悩まされてきたので、片っ端からclass作って、staticメンバ関数に放り込んでます。

■共通変数と関数 (addhtml-common.php)

<?php
class AddHtmlCodeCommon
{
  protected static $options;

  protected static function unescape($str)
    {
      $str = str_replace("\\\"","\"",$str);
      $str = str_replace("\\'","'",$str);

      return $str;
    }
}

?>

■設定管理ページ (addhtml-setting.php)

<?php
class AddHtmlCodeSetting extends AddHtmlCodeCommon
{
  private static $plugin_file;

  public static function register($pfile)
    {
      self::$plugin_file = $pfile;
      // addon check
      if ( !function_exists( 'add_action' ) )
        {
          echo "I'm just a plugin, not much I can do when called directly.";
          exit;
        }

      add_action('admin_menu', __CLASS__.'::option');
      add_filter( 'plugin_action_links', __CLASS__.'::action', 10, 2 );
    }

  public static function action( $links, $file )
    {
      if($file !== self::$plugin_file)
        return $links;
      
      array_unshift( $links, '<a href="options-general.php?page=addhtmlcode">設定</a>');
      return $links;
    }
  
  public static function option()
    {
      add_option('addhtmlcode');
      add_options_page('Add Html Code設定', 'Add Html Code', 10, 'addhtmlcode', __CLASS__.'::options_page');
    }

  public static function options_page()
    {
      // フィールドと設定項目名のための変数
      $opt_name = 'addhtmlcode';
      self::$options = get_option($opt_name);

      // ユーザが何かの情報を投稿したかどうかをチェックする
      // 投稿していれば、このhiddenフィールドの値は'Y'にセットされる
      if($_POST['action'] === 'update')
        {
          // 投稿された値を読む
          self::$options = array();
         
          self::$options['header']  = $_POST['header'];
          self::$options['footer']  = $_POST['footer'];

          // データベースに値を設定する
          update_option( $opt_name, self::$options);

          // 画面に更新されたことを伝えるメッセージを表示
          echo '<div class="updated"><p><strong>設定が保存されました。</strong></p></div>';
        }

      self::$options['header'] = self::unescape(self::$options['header']);
      self::$options['footer'] = self::unescape(self::$options['footer']);

      // 設定変更画面を表示する
?>
<div class="wrap">
<div id="icon-options-general" class="icon32"><br></div>
<h2>Adding HTML Code</h2>
<p>
 ※ヘッダー、フッターに任意のHTMLコードを挿入します。<br>
 このプラグインを適用させるには、テンプレートヘッダ(header.php)・フッター(footer.php)にそれぞれ、
 wp_head(),wp_footer()を記述する必要があります。
</p>
<form name="form1" method="post" action="<?php echo str_replace( '%7E', '~', $_SERVER['REQUEST_URI']); ?>">
<?php wp_nonce_field('update-options'); ?>

<h3 class="text-box-title">ヘッダー&nbsp;<span>&lt;head&gt;タグ内に挿入されます。</span></h3>
<textarea name="header" class="text-box"><?php echo self::$options['header']; ?></textarea>

<h3 class="text-box-title">フッター&nbsp;<span>&lt;/body&gt;直前付近に挿入されます。</span></h3>
<textarea name="footer" class="text-box"><?php echo self::$options['footer']; ?></textarea>

<p class="submit">
<input type="hidden" name="action" value="update">
<input type="hidden" name="page_options" value="header,footer">
<input type="submit" name="Submit" value="設定を更新する">
</p>

</form>
</div>
<style type="text/css"><!--
form { padding: 1em; margin-top: 1em;}
.text-box { display: block;width: 80%; height: 10em; margin-bottom: 3em;padding: 0.5em;}
.text-box-title { margin-bottom: 5px;}
.text-box-title span { font-size: 80%; color: green;}
--></style>
<?php
    }
}

?>

■実際の出力 (addhtml-doaction.php)

<?php
class AddHtmlCode extends AddHtmlCodeCommon
{
  public static function register()
    {
      // addon check
      if ( !function_exists( 'add_action' ) )
        {
          echo "I'm just a plugin, not much I can do when called directly.";
          exit;
        }
      
      self::$options = get_option('addhtmlcode');
      
      add_action('wp_head', __CLASS__.'::add_header');
      add_action('wp_footer', __CLASS__.'::add_footer');
    }

  public static function add_header()
    {
      echo self::unescape(self::$options['header']),"\n";
    }

  public static function add_footer()
    {
      echo self::unescape(self::$options['footer']),"\n";
    }
}

?>

PHPで形態素解析とMySQLで全文検索

備忘録メモです。長ったらしいタイトルっす。

ブログの簡易版みたいなスクリプト(管理者だけが書き込める掲示板みたいなやつ)の改造をちょっと前に依頼されたんですが、その中で検索機能(全文検索)を付けるというのがありました。全文検索っていっても、入力された単語にマッチしたレコードを全部表示する、要はSQLクエリーのselect文でlike演算子でマッチさせるだけでいい、ということだったんですが、ただでさえ、面白くないPHPの仕事だし(^^;;;、それだけでは僕にとっても得るものが少ないので(^^;、もうちょっと勉強になるものを作ってみよう、ということで調べました。

仕事しながら勉強って・・・ま、いいか。

日本語の文章をMySQLで全文検索させるには(FULLTEXTインデックスってことね。)、まず日本語の文章を形態素解析にかけて、名詞・動詞・助詞・・・といった風に分解することから始めなければいけません。英文などでは単語間は必ずスペースもしくはカンマで区切られますから、特に意識しなくても済むのですが、日本語の文章や主にアジア圏の言語では、そう簡単にはいきません。

幸いにもフリーで使用できる形態素解析エンジンは結構豊富にあります。有名なものとして、KAKASIやMeCab、Igoなどがあります。が、一般人が実際に使用するには、結構ハードルが高いものです。

まず、何よりレンタルサーバーなどの一般的なサーバーではほぼこのようなライブラリはインストールされていませんし、新たに追加できることは不可能でしょう。でも、最近では非常に低コストのVPSサーバーがあるので、それなりに知識がある人は導入できるでしょうけど、サーバー管理の知識がない方にとっては難しいでしょう。

ただ、PHP、しかも、レンタルサーバーでも利用できる・・・という条件だと、選択肢は非常に限られると思います。その中でも小規模なサイトに必要十分なものとして、Igo-PHPが手軽に利用できて、サイトへの組み込みも少ない工数で行えると思います。

Igo-PHPは、Javaの形態素解析エンジンIgoのPHP移植版で、Igo同様、MITライセンスで自由に利用できるというありがたいライブラリです。作者に感謝です。

流れとしては・・・

  1. Igo-PHPのダウンロード
  2. Igo本体のダウンロード(辞書生成に必要。別途Java実行環境が必要)
  3. 辞書の元となるファイルをダウンロード(MeCabサイト→ダウンロード→Mecab用の辞書(IPA辞書)
    (2017-10-22 リンク先修正)
  4. 2)でダウンロードしたIgo(Javaプログラム)を使用して辞書を生成。
    3)でダウンロードしたファイルを展開し、以下のコマンドをうつ。Windowsだと java.exeがあるディレクトリが%WINDIR%や%PROGRAMFILES%にあったりと環境によって違うと思います。

    >> java -cp igo-0.4.3.jar net.reduls.igo.bin.BuildDic ipadic mecab-ipadic-2.7.0-20070801 EUC-JP

といった感じになります。これらは全部Windows上で行えます。あとは・・・PHPスクリプトからIgo-PHP、生成した辞書を使って形態素解析ができます。

生成されたipadicディレクトリに辞書がビルドされていますので、以後、このipadicディレクトリとIgo-PHPだけを使用します。

WindowsにPHPをインストールされている方は、下記のようなスクリプトを作成して実行してみてください。

<?php
// test.php

// Igo-PHPとipadicディレクトリを 'lib'というディレクトリにまとめて置いとく。
require_once 'lib/Igo.php';
 
$igo = new Igo("./lib/ipadic");
$text = "私には夢がある。";

// 詳細な結果が欲しい場合は、parseメソッド
//$result = $igo->parse($text);

//単に区切ればいいだけなら、wakatiメソッド
$result = $igo->wakati($text);

//それぞれ、単語の配列が返ります。

echo mb_convert_encoding(implode('/',$result),'SJIS');

実行すると、こんな結果がでました。ちゃんと分解されてますね。

>>php test.php
私/に/は/夢/が/ある/。

さて、検索される文章・記事は、MySQLのデータベースに入れることが多いのでMySQL + PHPでの組み込みを中心に。

MySQLでテーブルを作成する際に、記事などを格納させるカラムとは別に、検索用のカラムを一個追加して、そのカラムにFULLTEXTインデックスを張ります。以下のような感じですかね?

CREATE TABLE  posts (id INT,title TEXT,content TEXT,content_s TEXT,FULLTEXT(content_s));

というようなテーブルを用意しておいて、INSERT,UPDATEする際に、contentの内容を形態素解析にかけ、名詞のみ抜き出し、content_sに抜き出した単語を半角スペース区切りでつなげた文字列を格納しておく。要は検索インデックスをcontent_sに貯めておくという方法です。

英語ならば、語と語は必ずスペースで区切られるから、こんなめんどっちーことをしなくてもいいんですが・・・。
データをINSERTするときは、こんな感じでしょうか。

<?php
$igo = new Igo("./lib/ipadic");
$pdo = new PDO('mysql:dbname=testdb;host=localhost','dbuser','password');

//テーブル作成
$pdo->exec('CREATE TABLE posts (id INT,title TEXT,content TEXT,content_s TEXT,FULLTEXT(content_s))');

//テストデータを用意
$id = 1;
$title = '私には夢がある。';
$content = '私には夢がある。私の四人の幼い子ども達が、いつの日か肌の色ではなく人格そのものによって評価される国に住めるようになるという夢です。';

//形態素解析
$result = $igo->wakati($content);
$content_s = implode(' ',$result);

//INSERT文組み立て
$sql = sprintf("INSERT INTO posts(id,title,content,content_s) VALUES(%d,'%s','%s','%s')",
               $id,
               $pdo->quote($title),
               $pdo->quote($content),
               $pdo->quote($content_s));

//クエリー実行
$pdo->exec($sql);

$pdo = null;
$igo = null;

こんな感じでデータをどんどん追加して、

で、全文検索させたいときは、contentカラムを検索するのではなく、content_sカラムを全文検索させるように、

SELECT * FROM posts WHERE MATCH(content_s) AGAINST('検索単語',IN BOOLEAN MODE);

と、するだけ。

ただ、これだけだと、ちょっと不便なことがあります。形態素解析にかけると、辞書にある単語を元に解析するので、二つ以上の名詞がくっついて一つの名詞になるような語がバラバラになってしまいます。

たとえば、「神戸市」の結果は、「神戸」と「市」に分かれてしまいます。「神戸市」と一つの単語で登録したい場合などは、ひと手間かける必要があります。

全文検索以外の他の用途でも使えそう。漢字混じりの文章をすべて平仮名や片仮名に変換したりも可能なので、使い道は結構あると思います。

またデスクトップアプリの機能として形態素解析エンジンを使いたい場合は、.NETアプリで使用できるNMeCabというMeCabの.NET移植版がありますし、形態素解析というと、ものすごく難しい、というイメージですが結構簡単に自分のアプリにも組み込めたりできますので、もっと活用の場があってもいいと思います。