ラベル PHP の投稿を表示しています。 すべての投稿を表示
ラベル PHP の投稿を表示しています。 すべての投稿を表示

2013-12-25

【PHP】マニュアルのShift_JIS変換と整形・容量削減について

PHPの関数・言語マニュアルはWindows用のCHMファイル形式でも配布されていますが、このファイルがまったくの役立たずです。
ダウンロードしただけでは、中のヘルプHTML内容を見ることすらできないものなのです。

そこで、HTML Help Workshopというソフトで一度CHMファイルをバラして修正する必要があります。
2つのファイル名を変更するくらいならば手でも簡単にできますが、すべてのファイル(2013-12現在1万3千ファイル程度あります)の文字コードとメタタグ変更を手でやるのは時間の無駄なので、これを一気に行えるプログラムを作ってみました。
ただ、自分の環境ではうまくいっただけなので、注意して使ってくださいね。


 define( 'DEST_PATH', 'HTML Help WorkshopでデコンパイルしたHTMLファイルのディレクトリパス' );
 
 $d = dir( DEST_PATH );
 
 $i = 0;
 while( false != ($entry = $d->read()) ) {
  
  if( substr_count( $entry, '.html' ) > 0 ) {
   
   $get_file_path = DEST_PATH . $entry;
   
   $contents = file_get_contents( $get_file_path );
   print( '[' . $get_file_path . "] has input. \r\n" );
   
   $contents = mb_convert_encoding( $contents, 'sjis-win', 'UTF-8' );
   $contents = preg_replace( '/utf\-8/i', 'Shift_JIS', $contents );
   $contents = preg_replace( '/\"+/i', '"', $contents );
   $contents = preg_replace( '/\s+/i', ' ', $contents );
   
   file_put_contents( $get_file_path, $contents );
   print( '[' . $get_file_path . "] has output. \r\n" );
   
   $i += 1;
  }
 }
 $d->close();
 
 exit();



以上です。

2013-10-21

【テキスト】正規表現パターン作成代行サービス開始!

久しぶりに何かお客様から依頼されてモノを作りたくなったので、ちっちゃいところからやり直そうと言うことで正規表現を作成代行してみようと思います。
あまりにも複雑な正規表現パターン(たとえば、規約どおりのメールアドレスのパターンとか)じゃ無ければ作りますので、良かったらオーダーしてみてください。

→依頼ページ:正規表現についての簡単なものを作成します!

□依頼時の注意点
・料金は一つの依頼につき500円です。
・処理対象テキストデータを送ってもらいます。
 たとえばHTML、Aタグのhref属性を取得したいなら具体的なAタグのテキストを複数ください。
・処理内容の具体的な内容
 たとえば、XMLデータならどのタグの要素が欲しい、とか書いてください。

□依頼してからパターンが納品される日数
・簡単なものなら1日、比較的複雑なもので2~3日程度で納品します。
・パターン個数は基本的に1つの依頼で1つにしようと思っています。

以上です。それでは。

2013-10-05

【青空文庫】青空文庫のXMLをパースしてみた

【青空文庫】青空文庫のXMLをパースしてみた 自分の読書用に青空文庫に掲載してあるXMLをパースしてみたよ。
青空文庫のXMLは仕様通りにキッチリ書いてあるものと、そうでないものがあり、ちょっと苦労した。

以下のクラスは基本的に「大見出し」と「中見出し」で区切って配列にして返すものだけど、面倒な処理は正規表現やexplodeでばっさり切り分けたりしてます。

---
このクラスを使って小説を転載したものは、以下のサイトで読めます。
 ・多賀城[たがのき]小説投稿サイト
良かったら見てくださいませ。
---

/*
*   青空文庫XMLパーサ
*     2013/10/01
*/
class Aozora
{
  const         FROM_ENCODING = 'sjis-win';
  const         TO_ENCODING   = 'UTF-8';
  
  public        $filename;
  public        $title;
  public        $author;
  /*
    本文の階層構造
    
    大見出しがある場合
    bodies = array(
      '大見出し_01' => array( '中見出し_01' => '本文', '中見出し_02' => '本文', ... '中見出し_nn' => '本文' ),
      '大見出し_02' => array( '中見出し_01' => '本文', '中見出し_02' => '本文', ... '中見出し_nn' => '本文' ),
      '大見出し_03' => array( '中見出し_01' => '本文', '中見出し_02' => '本文', ... '中見出し_nn' => '本文' ),
              ...
      '大見出し_nn' => array( '中見出し_01' => '本文', '中見出し_02' => '本文', ... '中見出し_nn' => '本文' ) );
      
    大見出しが無い場合
    bodies = array( '中見出し_01' => '本文', '中見出し_02' => '本文', ... '中見出し_nn' => '本文' );
  */
  public        $bodies;
  public        $infomation;          // 小説情報
  public        $notes;               // 注
  public        $is_omidasi;
  public        $is_tyumidasi;
  
  private       $raw_text;
  private       $main_text;
  private       $biblio;
  
  public function __construct( $filename )
  {
    $this->filename = $filename;
    $this->raw_text = file_get_contents( $this->filename );
    $this->raw_text = mb_convert_encoding( $this->raw_text, self::TO_ENCODING, self::FROM_ENCODING );
    $this->bodies = array();
  }
  
  public function get_title()
  {
    return sprintf( '%s (作者:%s)', $this->title, $this->author );
  }
  
  public function get_bodies()
  {
    return $this->bodies;
  }
  
  /**
    XMLパース本体
  */
  public function parse()
  {
    // 改行を全て消す
    $this->raw_text = preg_replace( '#[\n\r]+#i', '', $this->raw_text );
    $this->raw_text = preg_replace( '#[\s]+#i', ' ', $this->raw_text );
    
    // タイトルと著者を取得する
    $ret = preg_match( '#

(.*?)

#i', $this->raw_text, $temp_text ); $this->title = $this->replace_imgtag( $temp_text[ 1 ] ); $this->title = trim( $this->title ); $ret = preg_match( '#

(.*?)

#i', $this->raw_text, $temp_text ); $this->author = $this->replace_imgtag( $temp_text[ 1 ] ); $this->author = trim( $this->author ); // 付加情報を取得する $ret = preg_match( '#
(.*?)
#i', $this->raw_text, $temp_text ); $this->infomation = $this->remove_htmltag( $temp_text[ 1 ] ); $this->infomation = $this->add_return( $this->infomation ); $this->infomation = trim( $this->infomation ); // 本文を全て取得する $ret = preg_match( '#
(.*)
#i', $this->raw_text, $temp_text ); $this->main_text = trim( $temp_text[ 1 ] ); // 大見出しから切り出し $this->parse_omidasi(); } /* 大見出しがある場合の本文切り出し */ private function parse_omidasi() { if( $this->is_omidasi( $this->main_text ) ) { // 大見出しがある場合 $temp_text = explode( '

', $this->main_text ); foreach( $temp_text AS $temp ) { if( trim( $temp ) == "" ) { continue; } if( !$this->is_omidasi( $temp ) ) { continue; } $ret = preg_match( '#(.*?)

(.*)$#', $temp, $temp_omidasi ); $omidasi = $this->remove_htmltag( $temp_omidasi[ 1 ] ); $omidasi = $this->replace_imgtag( $omidasi ); $omidasi = trim( $omidasi ); $this->bodies[ $omidasi ] = $this->parse_tyumidasi( $temp_omidasi[ 2 ] ); } } else { // 大見出しがない場合 $this->bodies[] = $this->parse_tyumidasi( $this->main_text ); } } /* 中見出しから本文切り出し @param $html 大見出し1つ分のテキスト または 本文全体のテキスト */ private function parse_tyumidasi( $html ) { $tyumidasi_array = array(); if( $this->is_tyumidasi( $html ) ) { $temp_text = explode( '

', $html ); foreach( $temp_text AS $idx => $temp ) { if( trim( $temp ) == "" ) { continue; } if( !$this->is_tyumidasi( $temp ) ) { continue; } $ret = preg_match( '#(.*?)

(.*)$#', $temp, $temp_tyumidasi ); $tyumidasi = $this->remove_htmltag( $temp_tyumidasi[ 1 ] ); $tyumidasi = $this->replace_imgtag( $tyumidasi ); $tyumidasi = trim( $tyumidasi ); $tyumidasi_array[ $tyumidasi ] = $this->remove_htmltag( $temp_tyumidasi[ 2 ] ); $tyumidasi_array[ $tyumidasi ] = $this->replace_imgtag( $tyumidasi_array[ $tyumidasi ] ); $tyumidasi_array[ $tyumidasi ] = $this->add_return( $tyumidasi_array[ $tyumidasi ] ); } } else { $tyumidasi_array[ 1 ] = $this->remove_htmltag( $html ); $tyumidasi_array[ 1 ] = $this->replace_imgtag( $tyumidasi_array[ 1 ] ); $tyumidasi_array[ 1 ] = $this->add_return( $tyumidasi_array[ 1 ] ); } return $tyumidasi_array; } /* 大見出しがあるかどうか */ private function is_omidasi( $html ) { $ret = substr_count( $html, '' ); if( $ret > 0 ) { $this->is_omidasi = true; } else { $this->is_omidasi = false; } return $this->is_omidasi; } /* 中見出しがあるかどうか */ private function is_tyumidasi( $html ) { $ret = substr_count( $html, '' ); if( $ret > 0 ) { $this->is_tyumidasi = true; } else { $this->is_tyumidasi = false; } return $this->is_tyumidasi; } /* 余分なHTMLタグを除去する */ private function remove_htmltag( $text ) { return preg_replace( '#<(?:|/)(?:em|div|span|a|h3|h4|hr)[^>]*>#', '', $text ); } /* 画像タグを置き換える */ private function replace_imgtag( $text ) { return preg_replace( '#]+>#', "$1", $text ); } /* * 改行を元に戻す */ private function add_return( $text ) { return preg_replace( '#]+>#', "\r\n", $text ); } }


以上です。

2013-09-16

【PHPで画像処理】ガンマ補正を行うクラス

自分で撮った写真や画像の明るさを偏向するのに便利なのがガンマ補正なのですが、映像からキャプチャした連続画像などを手で補正するのがダルいわけです。 そこで、ちょこっとPHPで組んでみました。

ちなみにガンマ補正とは、画像の「暗すぎ」とか「明るすぎ」というものを補正するのが目的の画像処理のことです。 Windows用のディスプレイはガンマ補正値が「2.2」となっているそうです。

/*
* ガンマ補正用クラス
*/
class Gamma
{
  private   $image;       // 入力画像リソース
  private   $new_image;   // 出力画像リソース
  private   $size_x;
  private   $size_y;
  
  /**
    コンストラクタ
    @param $image 入力画像のリソース
  */
  public function __construct( $image )
  {
    $this->image      = $image;
    $this->size_x     = imagesx( $this->image );
    $this->size_y     = imagesy( $this->image );
    $this->new_image  = imagecreatetruecolor( $this->size_x, $this->size_y );
  }
  
  /**
    デストラクタ
  */
  public function __destruct()
  {
    imagedestroy( $this->image );
  }

  /**
    変更後の画像リソース取得
    @return 画像リソース
  */
  public function get_image()
  {
    return $this->new_image;
  }
  
  /**
    出力用画像リソースを新しく作成する
  */
  public function renew_image()
  {
    unset( $this->new_image );
    $this->new_image  = imagecreatetruecolor( $this->size_x, $this->size_y );
  }
  
  /**
    ガンマ補正
    @param $gamma ガンマ補正値 ※ 0 < $gamma <= 5
    @return 成功:true 失敗:false
  */
  public function gamma( $gamma )
  {
    if( $gamma < 0 ) {
      return false;
    }
    
    if( $gamma > 5 ) {
      return false;
    }
    
    for( $x = 0 ; $x < $this->size_x ; $x += 1 ) {
      for( $y = 0 ; $y < $this->size_y ; $y += 1 ) {
        $this->culc_gamma( $x, $y, $gamma );
      }
    }
    
    return true;
  }
  
  /*
    ガンマ補正用計算
  */
  private function culc_gamma( $x, $y, $gamma )
  {
    $rgb = imagecolorat( $this->image, $x, $y );
    $r = ($rgb >> 16) & 0xFF;
    $g = ($rgb >> 8) & 0xFF;
    $b = $rgb & 0xFF;
    
    $temp_gamma = 1 / $gamma;
    $r_base     = $r / 255;
    $g_base     = $g / 255;
    $b_base     = $b / 255;
    $nr = (int)floor( 255 * pow( $r_base, $temp_gamma ) );
    $ng = (int)floor( 255 * pow( $g_base, $temp_gamma ) );
    $nb = (int)floor( 255 * pow( $b_base, $temp_gamma ) );
    
    $nrgb = imagecolorexact( $this->image, $nr, $ng, $nb );
    imagesetpixel( $this->new_image, $x, $y, $nrgb );
  }
}


ちなみに、以下にプログラムを使って編集した画像を載せてみます。
1枚目が元画像、2枚目が補正値0.5のもの(暗くしたもの)、3枚目が補正値2.0のもの(明るくしたもの)です。




2012-06-19

【PHP】PC用・スマートフォン用・携帯用のページ振り分けする方法

PC、スマートフォンおよび携帯電話のページを自動的に切り替える方法を考えてみました。
SEO的にどのような影響が出るかはわかりませんw


◆仕様
・(1)切り替え用のプログラムは、公開しているディレクトリ直下に置く
・レスポンスされるHTMLを生成するプログラムは公開されていないディレクトリに、「pc/」、「smart/」および「mobile/」という名前にしておく
・レスポンスされるHTMLを生成するプログラムは、(1)に置いてある切り替え用のプログラムで読み込む(インクルードする)
・PC用ページは「/pc」以下のURLでアクセスする
・スマートフォン用ページは「/smart」以下のURLでアクセスする
・携帯電話用ページは「/mobile」以下のURLでアクセスする
・振り分けのは「ユーザエージェント」を検査して行うものとする



参考)ディレクトリ構成
├─public/ (公開ディレクトリ)
│  │
│  ├foo.php(振り分けプログラム1)
│  └bar.php(振り分けプログラム2)
│
└─template/ (非公開ディレクトリ))
    │
    ├─pc/
    ├─smart/
    └─mobile/
        │
        ├foo.php (携帯HTML生成用プログラム1)
        └bar.php (携帯HTML生成用プログラム2)
          ※この2つのプログラムはpc/、smart/ディレクトリにも存在する。



◆.htaccess
mod_rewriteを使用して、/pc、/smart、/mobileにアクセスしてきたリクエストを仕様の(1)にアクセスさせます。
例として、
---
RewriteRule /pc/([a-zA-Z\.]+)$ /$1
RewriteRule /smart/([a-zA-Z\.]+)$ /$1
RewriteRule /mobile/([a-zA-Z\.]+)$ /$1
---
といった感じでしょうか。

◆振り分けプログラムのすること
振り分けを厳密にするのは大変なので、今回はユーザエージェントを使用して振り分けすることにします。
たとえば、以下のようなプログラムになるでしょう。


  define( 'PC',       0 );
  define( 'SMART',    1 );
  define( 'MOBILE',   2 );
 
  switch( true ) {
    case preg_match( '/DoCoMo/i',    $_SERVER[ 'HTTP_USER_AGENT' ] ):
    case preg_match( '/KDDI/i',    $_SERVER[ 'HTTP_USER_AGENT' ] ):
    case preg_match( '/J\-PHONE|Vodafone|SoftBank/i',  
      $_SERVER[ 'HTTP_USER_AGENT' ] ):
      define( 'USER',   MOBILE );
      break;
    case preg_match( '/Android/i',  $_SERVER[ 'HTTP_USER_AGENT' ] ):
    case preg_match( '/iPhone/i',   $_SERVER[ 'HTTP_USER_AGENT' ] ):
    case preg_match( '/iPod/i',     $_SERVER[ 'HTTP_USER_AGENT' ] ):
    case preg_match( '/iPad/i',     $_SERVER[ 'HTTP_USER_AGENT' ] ):
      define( 'USER',   SMART );
      break;
    default:
      define( 'USER',   PC );
  }


これを振り分けプログラムで判別し、それにあったHTML生成プログラムをインクルードすればOKです。
たとえば、


  switch( USER ) {
    case MOBILE:
      include( '/template/mobile/' . basename( __FILE__ ) );
      break;
    case SMART:
      include( '/template/smart/' . basename( __FILE__ ) );
      break;
    default:
      include( '/template/pc/' . basename( __FILE__ ) );
      break;
  }


というような感じになるでしょう。
HTMLを変更するときは、必要となるtemplate/以下のファイルを修正するだけでOKです。

2012-05-23

【サービス】家計簿システム作ってみた。

久しぶりに専門である「お金計算」のプログラム作ってみました。

今回は慣らしということで簡単な「家計簿」のシステムにしてみました。
開発期間は設計から賞味2日位です。
以前にも同じようなシステムを作っていたのですが、そちらはすでに古いバージョンのデータベースやPHPを使っていたので保守性も悪く、長いプログラムになっていました。
ですので、全面的にプログラムを書き直したうえでデータベースのバージョンもあげています。
あと、プライバシーとセキュリティを考えると、「名前」や「住所」は入力してもらわないほうが良いと判断して、ユーザ登録の際の入力には「メールアドレス」「パスワード」「ニックネーム」のみお願いしています

「TagajoTown Household Account Book System (TgagajoTown家計簿システム)」
紹介サイトURL  : http://kakei.tagajo.tv/
システムの使い方: http://kakei.tagajo.tv/how_to_use.php

というわけで、以下システムの目的と特徴紹介です。

■システムの目的
この家計簿システムは「現金出納」に利用するための物です(世間一般の家計簿はみな現金出納だと思いますが)。
基本的に、
1)預金からお金を引き出し
2)買い物をし
3)あまったら預金へ戻す
という流れを記録することで、現金の流れ(キャッシュフロー)を把握します。
つまり、無駄なお金を使ってないか確認するのが目的のひとつです。

あと重要なのが3)の「あまったら預金へ戻す」ことです。
これは「貯蓄を増やす」こととイコールなので「ヘソクリを増やしたい」という方にもお勧めの方法です。
このシステムでは、摘要が自由に設定できるので「預金預入」の摘要を追加して、その金額が増えるように家計を管理していけば、このシステムの目的達成、ということになります。

■特徴1.加算式家計簿
自分が家計簿(お小遣い帳でもいいですが)をつけるときに気になることがあります。
それは、家計簿をつけるとき「摘要」に合致する出納を「事前に集計して」からでないと家計簿に記載できないのがイヤだったということです。
たとえば、「食費」の摘要にはその日買った「昼食代」や「おやつ代」などを一旦「集計」するというひと手間です。

今回、それをなくすために以下のような方針で入力画面を設計しました。
1)1日の出納入力は「摘要」ごとに行う。つまりある日の出納データ数は設定した摘要の数までとなる。
2)摘要に入力した出納の数値は、すでにデータがあるときは「データに加算」、無いときは「その数値のデータを作成」とする。
3)摘要のデータは削除できないようにする(このシステムでの「出納データ削除」とは「摘要の削除」を意味するため)
4)削除できないため、出納の数値から「減算」もできるように「マイナスの数値」を受け入れるようにする。
という感じにしました。
このような設計にすると、「買った金額」ごとに入力できます
買い物の後レシートをもらったとして「その商品ごと」に金額を入力(しかも同じ摘要へ何度も)を行えるようになります。

こうしたおかげで「日毎に増えるデータ数」が限定され、ディスクの容量も節約できるようになり、集計演算もまぁまぁのスピードで行えるようになりました。

■特徴2.摘要のカテゴリ分類
摘要を「カテゴリ分け」できるようになっています。
カテゴリ分けすると、システムトップの集計一覧表に「小計」が表示されるようになります。
たとえば「収入」カテゴリに「預金引出」と「給与(手渡)」の摘要があったとして、2つの摘要の合計が「小計」欄に表示されるようになります。

また、これら「カテゴリ」「摘要」ともに「登録、変更、削除」ができるようになっています。
ただし、出納データが登録された摘要やカテゴリを削除すると同時に出納データも削除されてしまうので注意が必要です。

■特徴3.グラフ表示
システムのトップページには、「カテゴリ・摘要の集計一覧表」が表示されますが、そのほかに「摘要ごと」の折れ線グラフが表示されます。
このグラフ自体はGoogle Chart APIを使用しています。

以上です。

2012-04-29

【電脳卸】全てのカテゴリをパースする。

電脳卸のWebAPIで「全てのカテゴリを返す」XMLファイルがあるので、それをパースしてみたよ。
PHPの組み込み関数にもXMLをパースする関数があるけど、自分で実装したくなったので自分で書いてみたよ。

APIから返されるXMLの構造が微妙に難しかったので、再帰関数とスタックでやってみた。

APIのURLは→「http://webservice.d-064.com/3.x/category.xml」です。

/**
  電脳卸用全てのカテゴリをパースして配列して、シリアライズ後保存する。
  パース結果の配列は以下のような仕様で出力される
  
  結果配列仕様:
    array( 'カテゴリID 1' => array( 'name' => 'カテゴリ名', 'parent' => '親カテゴリID' ) );
    array( 'カテゴリID 2' => array( 'name' => 'カテゴリ名', 'parent' => '親カテゴリID' ) );
    array( 'カテゴリID 3' => array( 'name' => 'カテゴリ名', 'parent' => '親カテゴリID' ) );
    ...
    array( 'カテゴリID n' => array( 'name' => 'カテゴリ名', 'parent' => '親カテゴリID' ) );
    ※一番上のカテゴリは親カテゴリIDが「-1」となっている。
  
  商品のカテゴリIDから親カテゴリ全てを取得するには、    
    function get_category_tree( $category_id, &$category_tree )
    {
      global $global_category_array;
      
      $id = $category_id;
      $name   = $global_category_array[ $id ][ 'name' ];
      $parent = $global_category_array[ $id ][ 'parent' ];
      
      $category_tree[ $id ] = 
        array( 'name'   => $name, 'parent' => $parent );
      
      if( $parent == '-1' ) {
        return ;
      } else {
        get_category_tree( $parent, $category_tree );
      }
    }
    
    $global_category_array = unserialize( file_get_contents( './category_array.dat' ) );
    $tree = array();
    get_category_tree( '1269', $tree );
    var_dump( $tree );
  といったプログラムで返せる。
*/
define( 'CATEGORY_XML_URL',         'http://webservice.d-064.com/3.x/category.xml' );
define( 'CATEGORY_XML_FILENAME',    dirname( __FILE__ ) . DIRECTORY_SEPARATOR . 'category.xml' );
define( 'CATEGORY_ARRAY_FILENAME',  dirname( __FILE__ ) . DIRECTORY_SEPARATOR . 'category_array.dat' );
define( 'CATEGORY_SRC_ENCODING',    'UTF-8' );
define( 'CATEGORY_DIST_ENCODING',   'SJIS' );
date_default_timezone_set( 'Asia/Tokyo' );

/*
  個々のカテゴリデータを取得する
  @return array [ 'category_id', 'category_depth', 'category_name' ]
*/
function get_id_depth_name()
{
  global $xmlp;
  return array(
    preg_replace( '/<[^>]+>(.*?)<\/[^>]+>/', '$1', trim( fgets( $xmlp ) ) ),        // カテゴリID
    preg_replace( '/<[^>]+>(.*?)<\/[^>]+>/', '$1', trim( fgets( $xmlp ) ) ),        // カテゴリ深さ
    preg_replace( '/<[^>]+>(.*?)<\/[^>]+>/', '$1', trim( fgets( $xmlp ) ) ) );      // カテゴリ名
}
 /**
  カテゴリXMLをパースする。再帰関数。
  @param $parent 親カテゴリID
*/
function parse( $parent = -1 ) 
{
  global $xmlp;
  global $global_category_array;
  global $global_parent_ids;        // 親IDのスタック
  
  $line = trim( fgets( $xmlp ) );
  switch( true ) {
    // 空行
    case ($line == ''):
      parse( $parent );
      break;
    
    // XML定義
    case preg_match( '/<\?xml(?:[^>]+)>/', $line ):
      parse( $parent );
      break;
    
    // 一番外側のタグ
    case ($line == ''):
      parse( $parent );
      break;
    case ($line == ''):
      fclose( $xmlp );
      return;
    
    // 1つのカテゴリ
    case ($line == ''):
      list( $id, $depth, $name ) = get_id_depth_name();
      $name = mb_convert_encoding( $name, CATEGORY_DIST_ENCODING, CATEGORY_SRC_ENCODING );
      // 親IDを退避
      $parent = $global_parent_ids[ 0 ];
      // スタックに自分を積む
      array_unshift( $global_parent_ids, $id );
      
      $global_category_array[ $id ][ 'name' ]    = $name;
      $global_category_array[ $id ][ 'parent' ]  = $parent;
      
      parse( $parent );
      break;
    case ($line == ''):
      // 自分のIDを捨てる
      array_shift( $global_parent_ids );
      parse( $global_parent_ids[ 0 ] );
      break;
      
    // サブカテゴリ
    case ($line == ''):
      parse( $global_parent_ids[ 0 ] );
      break;
    case ($line == ''):
      parse( $global_parent_ids[ 0 ] );
      break;
    
    // ファイルが終わる前に「/Categorys」が来るはずなので使わないかも。
    case feof( $xmlp ):
      fclose( $xmlp );
      return;
      
    // 何かあったときのための保険。そのまま終了。
    default:
      die( '[' . $line . '] Exception.' );
  }
}


// XMLファイルダウンロードと保存
file_put_contents( CATEGORY_XML_FILENAME, file_get_contents( CATEGORY_XML_URL ) );

// XMLファイルのポインタ取得
$xmlp = fopen( CATEGORY_XML_FILENAME, 'r' );
if( !$xmlp ) {
  die( 'XML File Dont Open.' );
}

// 親IDスタックと結果配列準備
$global_parent_ids      = array( -1 );
$global_category_array  = array();

// パース本体
parse();

// 結果配列をシリアライズして保存
file_put_contents( CATEGORY_ARRAY_FILENAME, serialize( $global_category_array ) );

exit( 0 );

こんな感じ。

2012-04-07

【Lisp/Scheme】Pythonで実装されたSchemeインタプリタをPHPにコンバートしてみた。

関数型言語勉強の一環として、比較的簡単なSchemeのインタプリタをいつも使ってるPHPで実装してみることを考えたのですが、参考になる書籍がなかったのでネットで探したところ以下のページを見つけました。
((Pythonで) 書く (Lisp) インタプリタ) http://www.aoky.net/articles/peter_norvig/lispy.htm

このページのプログラムはPythonで書かれています。PythonとPHPはかなり違う言語なので、なぜかPythonの勉強もしてしまいましたw

評価器以外には特に苦労しなかったので、評価器以外を直接見たい場合はこの記事一番下からダウンロードしてください。

■評価器(Evaluator)
評価器は標準入力から入力されたS式(Lisp系言語のプログラムのこと)を読取り器から得るシンボルや定数の列を受け取って解釈しプログラムを実行するものです。
以下具体的に見ていきます。実際のPythonプログラムは上のページを見てください。
評価器は再帰関数になってて、Schemeのスペシャルフォーム(評価器が式の内容全てを評価しない式のこと)の処理と、普通の式を処理するかなり簡単なプログラムです。
が、これをPHPにコンバートするのに苦労しました。以下が自分の書いた評価器プログラムです。

・コンバートしたPHPプログラム
/**
  定数、シンボルまたはシンボルの配列を評価する。再帰関数。
  @param $symbols 定数、シンボル、またはシンボルの配列
  @param $env     環境オブジェクト。指定されない場合はトップレベル環境。
*/
public static function lisp_eval( $symbols, $env = null )
{
  // 環境が指定されていないときはトップレベルの環境を指定
  if( $env == null ) {
    global $global_environment;
    $env = $global_environment;
  }
  
  // シンボルの場合
  if( is_a( $symbols, 'Symbol' ) ) {
    return $env->get( $symbols->value );
  
  // 定数の場合
  } elseif( is_scalar( $symbols ) ) {
    return $symbols;
  
  // 以降リストの場合
  } elseif( $symbols[ 0 ]->value == 'quote' ) {     // (quote exp)
    list( $quote, $exp ) = $symbols;
    return $exp;
    
  } elseif( $symbols[ 0 ]->value == 'if' ) {        // (if test conseq alt)
    list( $if, $test, $conseq, $alt ) = $symbols;
    if( self::lisp_eval( $test, $env ) ) {
      return self::lisp_eval( $conseq, $env );
    } else {
      return self::lisp_eval( $alt, $env );
    }
    
  } elseif( $symbols[ 0 ]->value == 'define' ) {      // (define var (lambda exp))
    list( $define, $var, $exp ) = $symbols;
    $env->add( $var->value, self::lisp_eval( $exp, $env ) );
    
  } elseif( $symbols[ 0 ]->value == 'lambda' ) {      // (lambda vars exp)
    list( $lambda, $vars, $exp ) = $symbols;
    $str_lambda  = '$args = func_get_args();' . "\n";
    $str_lambda .= '$env  = array_pop( $args );' . "\n";
    $str_lambda .= '$vars = unserialize( stripslashes( ' . "'" . addslashes( serialize( $vars ) ) . "'" . ' ) ); ' . "\n";
    $str_lambda .= '$exp  = unserialize( stripslashes( ' . "'" . addslashes( serialize( $exp ) ) . "'" . ' ) ); ' . "\n";
    $str_lambda .= '$new_env = new Environment( &$env );' . "\n";
    $str_lambda .= '$new_env->zip( $vars, $args );' . "\n";
    $str_lambda .= 'return Evaluator::lisp_eval( $exp, $new_env );' . "\n";
    $lambda_function = create_function( '', $str_lambda );
    return $lambda_function;
  
  } elseif( $symbols[ 0 ]->value == 'begin' ) {       // (begin exps)
    each( $symbols );
    foreach( $symbols AS $symbol ) {
      $val = self::lisp_eval( $symbol->value, $env );
    }
    return $val;
    
  } else {                                            // 関数評価
    $exps = array();
    
    foreach( $symbols AS $symbol ) {
      $exps[] = self::lisp_eval( $symbol, $env );
    }
    $proc = array_shift( $exps );
    
    /*
      小細工
      関数が使う環境オブジェクトを関数の引数として渡す。
    */
    array_push( $exps, $env );
    $ret  = call_user_func_array( $proc, $exps );
    return $ret;
  }
}


Pythonの評価器には「S式をリスト構造に直した」リストと、環境オブジェクト(後述)が渡されるようですが、PHPにはリストのように扱える組み込みデータ構造が用意されていません。
この関数のためにいちいちリスト構造を実装するのも面倒なので、配列をリストの代わりにしました。
引数の「$symbols」がその配列です。この配列に対して「each」「array_shift」「list」などを使えば、案外簡単に配列をリストのように扱えます。

・環境オブジェクトのこと
環境オブジェクト(以下環境)はトップレベルやレキシカルな環境を実現するためにあります。評価器に渡される環境オブジェクトには現在自分がいる階層の変数、関数が見えています。
また、環境には1つ外側の環境のポインタを持っていて、「あるシンボルを参照したいときに自分のいる環境に無い場合はひとつ外側の環境を参照する」という再帰メソッドが定義されています。
自分のいる階層の環境には自由に関数やシンボルを登録することができますが、自分より下(よりレキシカルな)環境にはアクセスできません。

・関数定義と環境オブジェクトに関する小細工
評価器にはS式のリストと環境が渡されます。リスト内に関数定義があると、評価する際微妙な問題を起こしました。
Pythonのプログラムは普通にクロージャ内に直接実行時の環境を渡せるのですが、自分が使っているPHP(Ver5.1.4)のクロージャには現在の環境をそのままでは渡せません。
なので、関数実行時に「関数の引数とともに現在の環境を押し込む」というなんともチカラ技なプログラムになってしまいました。
そんな感じなので、クロージャでは「引数のチェック」なんてものはしてませんw

こんな感じです。


Pythonのプログラムページに載っている以下のプログラムも「遅い」ですけど、一応動作します。
lis.py> (define area (lambda (r) (* 3.141592653 (* r r))))
lis.py> (define fact (lambda (n) (if (<= n 1) 1 (* n (fact (- n 1)))))) lis.py> (define first car)
lis.py> (define rest cdr)
lis.py> (define count (lambda (item L) (if L (+ (equal? item (first L)) (count item (rest L))) 0)))


PHPのソース全部は以下からダウンロード可能です。
http://www.tagajo.tv/lisphp_1.0.zip

■使い方
シェルに以下のように入力してください。
$ php -f lisphp.php

2012-02-22

【Gumroad】プログラム販売をしてみることにした

Gunroadというサービスがあります。
このサービスは「ファイルをアップロードする」あるいは自分のサーバ上にファイルを置いてその「URLを登録するだけ」でプログラムを販売するページが作成できる、というものです。
サービスへ払う必要がある手数料は「商品金額の5%+$0.30」ということになっています。
詳細はインプレスのInternet Watchの記事↓を参照してみてください。
http://internet.watch.impress.co.jp/docs/special/20120215_512149.html

とりあえず、自分が作ったPHPの自作ライブラリを販売していってみることにしました。
使い方はファイルの最初に「コメント」として記載しておきました。ちょっとしたサンプルプログラムも載っています。

1つ目:PHPのMySQL接続&結果セットクラス
2つ目:PHPカレンダー作成クラス
3つ目:Linuxサーバで自動的にFTPダウンロードするシェルスクリプト

2つ目は使い方がわからなくて「2ドル」の価格設定としてしまいましたが、1つ目の方は「200円」にしています。
さて、どのくらい売れるでしょうか。

それでは。

2011-11-07

【PHP】カレンダーの配列を作るプログラム

ブログ等に貼り付ける「カレンダーの配列」を簡単に作るクラスを作ってみた。
2次元の配列になってて、1次元目は「何週目」かをあらわす添え字で、2次元目は「曜日」の添え字。詳しくはコメントを読んでみてください。

可変引数の「$current_date」を指定するとその日付だったときに「c」へ「true」がセットされます。
これを利用すれば、「今日」や「どの日付のページを表示しているか」など応用が利くかな?とおもいます。

・使用方法
1)以下のプログラムを適当なファイルに保存
2)利用したいプログラム上でincludeやrequireを行い読み込む
3)$cal_array = Calendar::get_calendar( '2011-11-07' ); などど記載する。

・注意事項
引数の「$date」「$current_date」ともにチェックしてないので、必ずコメントにかかれたフォーマットで渡してください。
フォーマットを守らなかったときのテストはしていませんw

・販売中
プログラムはGumroadで販売中です。
※使い方はプログラムのコメントに書いてあります。

いわゆる、コントロールブレイク処理というやつなんですが、面倒くさくて嫌いっちゃ嫌いですねw

2011-01-20

【MySQL】バックアップ&レストアツール作ってみた。

ちょっと前からブログのネタにしてたMySQLだけど、結局MySQL附属の「mysqldump」とかではうまくいかなかったんだよね。
で、しょうがないからMySQLのバックアップとレストアツールを作ってみたよ。

まぁ、大規模なデータベースのバックアップはちょっとムリかもしれないけど、数百MBくらいのテーブルなら何とかなるかもしれない。

●仕様
・最大1つのデータベースのみバックアップ・レストアできる。
・必ずしも元のデータに復元できないことに注意すること!!
※複数のデータベースをバックアップしたいときは、このプログラムを複数の場所に入れればOK。

●バックアップの処理内容
全てのテーブル名を取得し、データを全件SELECTしてbase64でエンコードし保存するだけ。
テーブル1つに対しファイル2つが出来る。「CREATE文が入るファイル」と「データが入るファイル」となる。
たとえば「hoge」というテーブルがあるのであれば、「hoge_create.dat」と「hoge_data.dat」というファイルが出来る。
各レコードを各1行に収めるために改行文字を「¥r¥n」などに変換していることに注意。

●レストア処理の内容
ファイル名から「CREATE文」か「データ」かを区別しCREATE文から処理を始める。フラグを設定すれば「DROP TABLE IF EXISTS文」も入れられる。
データはbase64デコードしINSERT文を作って入れるだけ。データ項目に「'(シングルクォート)」が入ってる場合は二重化して回避。

●使い方
▲ダウンロードする
http://www.tagajo.tv/backuptool_ver01.zip
↑からツールをダウンロードして、展開する。このときディレクトリ構造をいじらないこと。


▲設定を環境に合わせる
「ダウンロードする」で展開したファイルの中に「config.php」というファイルがあるので、その中身を変更する。
具体的には以下の部分のみを変更すればOK。

---引用開始---
 // バックアップするデータベースの設定
 define( 'DATABASE_HOST',  'xxxxxxxxxxxxx' );
 define( 'DATABASE_USER',  'xxxxxxx' );
 define( 'DATABASE_PASS',  'xxxxxxx' );
 define( 'DATABASE_NAME',  'xxxxxxx' );

 // レストアするデータベースの設定
 define( 'RESTORE_HOST',   'xxxxxxxxxxxxx' );
 define( 'RESTORE_USER',   'xxxxxxx' );
 define( 'RESTORE_PASS',   'xxxxxxx' );
 define( 'RESTORE_NAME',   'xxxxxxx' );

 ~中略~

 // MySQLのクライアント文字コード(お使いのデータ本体の文字コードへ変更してください)
 // SET NAMES *** に使用されます。
 define( 'DATA_CHARACTER_CODE',  'sjis' );
---引用終了---

※注意
「DATABASE_XXXX」はバックアップするデータベースの設定
「RESTORE_XXXX」はレストアするデータベースの設定
なのを間違わないこと。

▲バックアップするサーバへアップロードする
設定を環境に合わせたら、バックアップ対象のデータベースがあるサーバへアップロードする。
アップロードする場所はどこでもOKだけど、Webで公開されているディレクトリはやめること。
セキュリティなんぞこのプログラムは考慮してないので。
アップロードするときも、ディレクトリ構造をいじらないこと。
アップロード先の「dat」ディレクトリには「700」等の書き込みできるパーミッションを設定しておくこと。

▲実行方法
PHPで記述されているのでPHPのインタプリタで実行する。
以下、PHPが「/usr/local/bin/」の下にあるとする。

・バックアップ実行コマンド
$/usr/local/bin/php -f /アップロードしたディレクトリ/backup.php

・レストア実行コマンド
$/usr/local/bin/php -f /アップロードしたディレクトリ/restore.php

以上。

2011-01-11

【MySQL】異なるサーバ間でのデータ移行方法について

Kagoyaのサーバで運営していたサイトを、さくらインターネットのレンタルサーバへ移動したときの作業メモです。
MySQLを使用してサイトを運営しているのですが、基本的にMySQL附属のダンプツールは文字化けなどの問題がたくさんあってあまり使えないですね。
さらにKagoyaサーバのmysqldumpは文字コードの指定を受け付けないようになってるみたいだし。

しょうがないので自分でダンプツールとレストアツールを作成し移行してみることにしました。

---
○サイトの仕様
・データベース数:1つ
・データベース容量:100MB程度
・レコード総数:26万件程度

○テーブルの移行方法
・テーブル自体の構造はphpMyAdminで構造だけ取り出し、移行する

○ダンプツールの仕様
・テーブル1つごとにダンプファイル1つで、テーブル名をファイル名とする
・ファイル内容は1行ごとに1レコード
・文字コードはデータに合わせる
・1行ごとに1レコードなので改行文字は「\r」や「\n」と表記し無理やり1行に収める

○レストアツールの仕様
・ダンプファイルから1行を読み出しINSERT文を作成しテーブルへINSERTする
・文字化けしないように、PHPの「mysql_escape_string関数」をかませる
・mysql_escape_stringを使用すると、ダンプツールで変換した「\n」が「\\n」と変換されてしまうので「\n」へ戻す
---

といった感じにしました。

---
○問題点と回避方法
・ダンプツールで大容量のテーブルをファイルに吐き出そうとするとメモリが足りない、とPHPに怒られる
→mysql_query関数を使用していたため、メモリに結果をバッファしようとして大量のメモリを使用していたことが分かり、メモリを極力使用しない「mysql_unbufferd_query関数」を使用することで回避。

・CSVでデータを吐き出すとデータ内のカンマなどで不整合がおきメンドイ
→「mysql_fetch_assoc関数」を使用し、「連想配列」で結果を返してもらうことにする。それを「serialize関数」で直列化して保存しておけば、読み出し側で「unserialize関数」で元に戻せる。
---

これでデータが全てダンプファイルに落とされたので、移動先のサーバへ持っていってレストアツールでレストアすれば作業完了。文字化けもしないはず。
あと、オマケとしてツールのソースコードを以下に掲載しておきますね。



○ダンプツールのソース
---
  function nl_2_char( &$item, $key )
  {
    $item = preg_replace( '/\r\n/i', '\r\n', $item );
    $item = preg_replace( '/\r/i', '\r', $item );
    $item = preg_replace( '/\n/i', '\n', $item );
  }
 
  define( 'DATABASE_HOST',    'ホスト名' );
  define( 'DATABASE_USER',    'ユーザ名' );
  define( 'DATABASE_PASS',    'パスワード' );
  define( 'DATABASE_NAME',    'データベース名' );
 
  $con = @mysql_connect( DATABASE_HOST, DATABASE_USER, DATABASE_PASS, false );
  mysql_select_db( DATABASE_NAME );
 
  $tables = array( テーブル名の配列 );
 
  foreach( $tables AS $tab    le ) {
    $file_name = dirname( __FILE__ ) . "/dat/" . $table . '.dat';
    @unlink( $file_name );
   
    $strSql  = "SELECT * FROM " . $table;
    $resource = mysql_unbuffered_query( $strSql, $con );
    if( !$resource ) {
      print( "ERROR!" );
      exit();
    }
    $table_data = array();
   
    print( $table . " START!\n " );
   
    while( $row = mysql_fetch_assoc( $resource ) ) {
      array_walk( $row, 'nl_2_char' );
      $buff = serialize( $row ) . "\n";
      file_put_contents( $file_name, $buff, FILE_APPEND );
    }
    unset( $resource );
   
    print( $table . " END!\n " );
  }
?>
---

○レストアツールのソース
---
  define( 'DATA_PATH',    dirname( __FILE__ ) . '/dat/' );
 
  define( 'DATABASE_HOST',    'ホスト名' );
  define( 'DATABASE_USER',    'ユーザ名' );
  define( 'DATABASE_PASS',    'パスワード' );
  define( 'DATABASE_NAME',    'データベース名' );

  // ファイルネーム取得
  $file_names = array();
  $d = dir( DATA_PATH );
  while ( false !== ($entry = $d->read()) ) {
    if( preg_match( '/[a-z0-9\_]+/', $entry ) ) {
      $file_names[] = $entry;
    }
  }
  $d->close();
 
  $con = @mysql_connect( DATABASE_HOST, DATABASE_USER, DATABASE_PASS, false );
  mysql_select_db( DATABASE_NAME );
  mysql_query( 'SET NAMES sjis' );    // データベースの文字コードにあわせて変更すること!
 
  foreach( $file_names AS $file_name ) {
    $table_name = basename( $file_name, '.dat' );
   
    // $rows = file( DATA_PATH . $file_name );
    $fp = fopen( DATA_PATH . $file_name, 'r' );
    if( !$fp ) {
      die( "DONT OPEN!\n" );
    }
   
    while( !feof( $fp ) ) {
      $row = fgets( $fp );
      if( trim( $row ) == "" ) {
        continue;
      }
      $fields = unserialize( trim( $row ) );
     
      $srcSql  = "INSERT INTO %s (%s) VALUES (%s)";
      $field_names = array();
      $field_values = array();
      foreach( $fields AS $field_name => $value ) {
        $field_names[]  = $field_name;
        $value = mysql_escape_string( $value );
        $value = str_replace( '\n', 'n', $value );
        $value = str_replace( '\r', 'r', $value );
        $field_values[]  = sprintf( ' %s ', $value );
      }
      $field_list = implode( ", ", $field_names );
      $value_list = sprintf( "'%s'", implode( "', '", $field_values ) );
      $strSql = sprintf( $srcSql, $table_name, $field_list, $value_list );
     
      $ret = @mysql_query( $strSql, $con );
      if( !$ret ) {
        print( $strSql . "\n" );
        print( mysql_error() );
        exit();
      }
    }
  }
?>
---

2010-12-22

【Amazon】AWSを使ってすばやい表示をするサイトを作る

表題の通り、AmazonのWebAPIであるAWS(名前がころころ変わるからこれでいいのか分からないw)を使ってすばやい表示のサイトを作った時のメモ。

まず、ページを表示するたびにAPIへ接続しデータを取得するようなサイトは表示が「遅い」し、ページビュー数が増えてくるとAPI提供側から規制が掛かったりする。
そこで、すばやくするためにはどうしたらよいかを考えた。

考えた結果、以下の2つを目標とすることにした。
・表示を早くする
・APIへのアクセスを低減する


という2つの目的を同時に実現するために以下のことをした。

---
1.自サイトを置いているサーバにAPIの結果をキャッシュとして保持し、それをファイルに保存しておく

2.APIへアクセスする時のURLはほぼ一意(ユニーク)なので、URLをmd5やsha1等でハッシュした結果をキャッシュのファイル名とした

3.APIのデータはXMLで返されるので、キャッシュに保存するのは「XMLをパースした結果のシリアライズされたもの」とする。読み込み時にアンシリアライズすればデータ取得が容易になる
---

1.で保存したキャッシュが「存在するならばAPIへはアクセスしない」というプログラムにすれば「APIアクセス低減」と「表示の高速化」が実現する。
しかし、APIで返されるデータは日々更新されるので、いつかは古いデータとなってしまう。これを回避するには「キャッシュを更新」する必要がある。
しかし、更新間隔を狭めて頻繁にAPIにアクセスするようなプログラムにすると結局「遅い」し「規制に掛かる」可能性がある。
この問題には以下のように対処した

---
1.個々の商品詳細データは「ほぼ更新されない」という「前提にした」(実際は価格などが更新される)ので、キャッシュの更新はしないこととした

2.商品一覧データは売れ筋が上に来る(APIで異なる指定が可能だが)ので、サイト構成上重要だから1週間で更新されるようにした

3.更新自体はプログラムがキャッシュへアクセスするときに「ファイル作成日付を確認し」1週間より前なら「APIへアクセスしデータを取得する」、とした
---

これでもAPIが規制に掛かってしまってエラーが返されたら、それは保存せずに従前のキャッシュを返すようにもした。
これで表示に大体1ページ数秒掛かっていたのが、1秒以内で表示されるようになった。


---
データベースが使えるならそちらのほうがプログラムが簡単かもしれない。データベースに全てのデータを突っ込んでおけばメンドウはないかもしれないし。
自分はデータベースを使用する方がメンドウだ、と作ったときは感じたのでそうしたけどね。

あと、この方法は小規模のそれほどページビューが多くないサイトを想定してます。具体的には1日1万ページビューくらい。
なので、もっと大規模なサイトならそれにあわせたものにしないとだめでしょう。

2010-12-15

【ブログ】BloggerにPHPからメールで投稿する方法

翌日発売するマンガやラノベのデータを自動でBloggerに投稿するシステムを開発して運用しています。
プログラミング言語はPHPを使っているのですが、ちょっとめんどくさかったのでメモを残しておきます。

基本的にPHPでメールを送信するには「mb_send_mail」という関数で送信します。また、日本語メールは一般的にJISコードが使用されています。
ですが、Bloggerのメール投稿は日本語エンコードがUTF-8なので、メールもUTFエンコードに変換して送らなければなりません。

例としては、

----
mb_language( 'uni' );
mb_internal_encoding( 'UTF-8' );

$to = 'xxxx@xxxx.jp';
$subject = 'ブログタイトル';
$body = 'ブログ本文';

mb_send_mail( $to, $subject, $text );
exit();
----

といったプログラムになると思います。
ですが、これだけではBloggerに投稿した際に文字化けを起こしてしまいます。

そこで、改良版として以下のようにしています。

----
mb_language( 'uni' );
mb_internal_encoding( 'UTF-8' );

$to = 'xxxx@xxxx.jp';
$subject = 'ブログタイトル';
$body = 'ブログ本文';
$from = 'From: xxxx@xxxxx.jp' . "\r\n";
$from .= 'Content-Type: text/html; charset=UTF-8' . "\r\n";

mb_send_mail( $to, $subject, $text, $from );
exit();
----

$from変数には、Fromヘッダだけではなくて他のいろんなメールヘッダを挿入することができます。
例の通り、「Content-Type」ヘッダを追加してcharset属性でもUTF-8を指定しています。これで文字化けなく投稿することが出来ました。
注意する点は、$fromに挿入するヘッダの各行ごとに「\r\n」を末尾へ挿入することぐらいでしょうか。

ちなみに、メールで投稿するブログ本文にもHTMLタグを挿入することができ、投稿してから編集する必要はありません。
ですが、「ラベル」と呼ばれるBloogerのタグをメール投稿に付加することが出来ないので、これは投稿してから編集する必要がありますね。

ZenBack

WebMoney ぷちカンパ