投稿日:

CSVファイルをモデルのfindメソッドで読み込む方法

CakePHPの勉強会で、英語に負けずコードをばんばん読んでいこう!という話があったので、Bakeryの中を覗いて見つけたソースを動かすというのをやってみました。
今回はCSVファイルをfind一発で読んでしまおうというヤツです。
元はSiegfriedHirschさんが作成されました。

CSVファイルをデータベースっぽくする為に、いくつか準備をします。


CakePHPの勉強会で、英語に負けずコードをばんばん読んでいこう!という話があったので、Bakeryの中を覗いて見つけたソースを動かすというのをやってみました。
今回はCSVファイルをfind一発で読んでしまおうというヤツです。
元はSiegfriedHirschさんが作成されました。

CSVファイルをデータベースっぽくする為に、いくつか準備をします。

1./models/datasources/csv_source.phpを設置する。
下記のファイルになります。

<?php
/**
 * CSV class
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @author Siegfried Hirsch <siegfried.hirsch@gmail.com>
 * @copyright Copyright 2008-2009, Siegfried Hirsch
 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
 * @created Januar 21, 2009
 * @version 1.0
 **/
class CsvSource extends DataSource {

/**
 * Description string for this Data Source.
 *
 * @var unknown_type
 */
    var $description = "CSV Data Source";
    var $delimiter = ';';                     // delimiter between the columns
    var $maxCol = 0;
    var $fields = null;                     // fieldnames
    var $handle = false;                     // handle of the open csv file
    var $page = 1;                             // start always on the first page
    var $limit = 99999;                     // just to make the chunks not too big

/**
 * Default configuration.
 *
 * @var unknown_type
 */
    var $__baseConfig = array(
            'datasource' => 'csv',
            'path' => '.',                     // local path on the server relative to WWW_ROOT
            'extension' => 'csv',             // file extension of CSV Files
            'readonly' => true,             // only for reading
            'recursive' => false,             // only false is supported at the moment
            );


/**
 * Constructor
 */
    function __construct( $config = null, $autoConnect = true ){

        // Configure::write('debug', 1);
        $this->debug = Configure::read( 'debug' ) > 0;
        $this->fullDebug = Configure::read( 'debug' ) > 1;

        // debug($config);
        parent::__construct( $config );

        if( $autoConnect ){
            return $this->connect();
        } else {
            return true;
        }

    }


/**
 * Connects to the mailbox using options in the given configuration array.
 *
 * @return boolean True if the mailbox could be connected, else false
 */
    function connect() {

        $config = $this->config;
        $this->connected = false;

        uses( 'Folder' );

        if( $config['readonly'] ){
            $create = false;
            $mode = 0;
        } else {
            $create = true;
            $mode = 0777;
        }

        //$config['path'] = WWW_ROOT . $config['path'];
        $config['path'] = TMP . $config['path'];        // Added by Imaoka on 26-Jun-2009

        // debug($config['path']);
        $this->connection = &new Folder( $path = $config['path'], $create, $mode );
        if( $this->connection ){
            $this->handle = false;
            $this->connected = true;
        }

        return $this->connected;
    }


    /**
     * listSources
     *
     * @author: SHirsch
     * @created: 21.01.2009
     * @return array of available CSV files
     */
    function listSources() {

        $config = $this->config;

        if( $this->_sources !== null ){
            return $this->_sources;
        }

        if( $config['recursive'] ){
            // not supported yet -> has to use Folder::findRecursive()
        } else {
            // すべてにCSVファイル名を取得し、拡張子を省いてテーブル名にしたものを$listに格納する
            // list all .csv files and remove the extension to get only "tablenames"
            $list = $this->connection->find( '.*' . $config['extension'], false );
            foreach( $list as &$l ){
                if( stripos( $l,  '.' . $config['extension'] ) > 0 ){
                    $l = str_ireplace( '.' . $config['extension'], '', $l );
                }
            }
            $this->_sources = $list;
        }

        //debug( $list );

        return $list;
    }


/**
 * Convenience method for DboSource::listSources().  Returns source names in lowercase.
 *
 * @return array
 */
    function sources( $reset = false ){

        if( $reset === true ){
            $this->_sources = null;
        }

        return array_map( 'strtolower', $this->listSources() );
    }


/**
 * Returns a Model description (metadata) or null if none found.
 * デリミッタ、列数、各列名をセット。各列名を返す。
 *
 * @return mixed
 **/
    function describe( $model ){

        //debug( $model->table );
        $this->__getDescriptionFromFirstLine( $model );

        //debug( $this->fields );
        return $this->fields;
    }


    /**
     * __getDescriptionFromFirstLine and store into class variables
     *
     * @author: SHirsch
     * @created: 21.01.2009
     *
     * @param $model
     * @set CsvSource::fields array with fieldnames from the first line
     * @set CsvSource::delimiter char the delimiter of this CSV file
     *
     * @return true
     */
    private function __getDescriptionFromFirstLine( $model ){

        $config = $this->config;

        //$config['path'] = WWW_ROOT . $config['path'];        // Added by Imaoka on 26-Jun-2009
        $config['path'] = TMP . $config['path'];            // Modified by Imaoka on 26-Jun-2009

        $filename = $model->table . "." . $config['extension'];
        $handle = fopen( $config['path'] . DS .  $filename, "r" );

        $line = rtrim( fgets( $handle ) );     // remove \n\r
        $data_comma = explode( ",", $line );
        $data_semicolon = explode( ";", $line );

        if( count( $data_comma ) > count( $data_semicolon ) ){
            $this->delimiter = ',';
            $this->fields = $data_comma;
            $this->maxCol = count( $data_comma );
        } else {
            $this->delimiter = ";";
            $this->fields = $data_semicolon;
            $this->maxCol = count( $data_semicolon );
        }

        fclose( $handle );

        return true;
    }


    /* close
    **
    ** @created: 21.01.2009 14:59:08
    **
    */
    function close(){

        if( $this->connected ){
            if( $this->handle ){
                @fclose( $this->handle );
                $this->handle = false;
            }
            $this->connected = false;
        }
    }


/**
 * The "R" in CRUD
 *
 * @param Model $model
 * @param array $queryData
 * @param integer $recursive Number of levels of association
 * @return unknown
 */
    function read( &$model, $queryData = array(), $recursive = null ){

        $config = $this->config;

        //$config['path'] = WWW_ROOT . $config['path'];        // Added by Imaoka on 26-Jun-2009
        $config['path'] = TMP . $config['path'];            // Modified by Imaoka on 26-Jun-2009

        // デリミッタ、列情報の取得
        $this->fields = $this->describe( $model );            // Added by Imaoka on 26-Jun-2009

        $filename = $config['path'] . DS . $model->table . "." . $config['extension'];

        if( $this->handle === false ){
            $this->handle = fopen( $filename,  "r" );
        }

        $queryData = $this->__scrubQueryData( $queryData );

        // get the limit
        if( isset( $queryData['limit'] ) && !empty( $queryData['limit'] ) ){
            $this->limit = $queryData['limit'];
            //debug( $this->limit );
        }

        // get the page#
        if( isset( $queryData['page'] ) && !empty( $queryData['page'] ) ){
            $this->page = $queryData['page'];
            //debug( $this->page );
        }

        if( empty( $queryData['fields'] ) ){
            $fields = $this->fields;
            $allFields = true;
        } else {
            $fields = $queryData['fields'];
            $allFields = false;
            $_fieldIndex = array();
            $index = 0;

            // generate an index array of all wanted fields
            foreach( $this->fields as $field ){
                 if( in_array( $field, $fields ) ){
                    $_fieldIndex[] = $index;
                }
                $index++;
            }
        }

        $lineCount = 0;
        $recordCount = 0;
        $resultSet = array();

        // Daten werden aus der Datei in ein Array $data gelesen
        // Modified by Imaoka on 26-Jun-2009
        //while( ( $data = fgetcsv( $this->handle, 8192, $this->delimiter ) ) !== FALSE ){
        while( ( $data = fgetcsv_reg( $this->handle, 8192, $this->delimiter ) ) !== FALSE ){
            if( $lineCount == 0 ) {
                // throw away the first line
                $lineCount++;
                continue;
                // $_page = 1;
            } else {
                // compute the virtual pagenumber
                // Modified by Imaoka on 26-Jun-2009
                //$_page = floor( $lineCount / $this->limit ) + 1;
                $_page = floor( $lineCount / $this->limit );
                if( $lineCount % $this->limit > 0 )
                    $_page++;

                // do have have reached our requested page ?
                if( $this->page > $_page ){
                    $lineCount++;
                    continue;
                }
                // skip over records, that are not complete
                if( count( $data ) < $this->maxCol ){
                    $lineCount++;
                    continue;
                }

                $record = array();
                if( $allFields ){
                    $i = 0;
                    $record['id'] = $lineCount;
                    foreach( $fields as $field ){
                        $record[$field] = $data[$i++];
                    }
                    $resultSet[] = $record;
                } else {
                    $record['id'] = $lineCount;
                    if( count( $_fieldIndex ) > 0 ){
                        foreach( $_fieldIndex as $i ){
                            $record[$this->fields[$i]] = $data[$i];
                        }
                    }
                    $resultSet[] = $record;
                }
                unset( $record );

                // now count every record
                $recordCount++;
                $lineCount++;

                // is our page filled with records, then stop
                if( $recordCount >= $this->limit ){
                    break;
                }
            }
        }
        $result[$model->table] = $resultSet;
        return $result;
    }

/**
 * Private helper method to remove query metadata in given data array.
 *
 * @param array $data
 * @return array
 */
    function __scrubQueryData( $data ) {

        foreach( array( 'conditions', 'fields', 'joins', 'order', 'limit', 'offset', 'group') as $key ){
            if( !isset( $data[$key] ) || empty( $data[$key] ) ){
                $data[$key] = array ();
            }
        }
        return $data;
    }

}
?>

2./app/config/database.phpに追記する。
CSV用のデータソースを定義します。

    var $csv = array(
        'datasource' => 'csv',
        'path' => 'csv',                 // local path on the server
        'extension' => 'csv',             // file extension of CSV Files
        'readonly' => true,             // only for reading
        'recursive' => false,             // only false is supported at the moment
        );

datasource…CSVファイル用なので、’csv’としてください。
path…これは、読み込むCSVファイルを置くフォルダ名です。ソースコードも参照して、適宜変更可です。
extension…CSVファイルの拡張子なので、’csv’です。
readonly…読み込むだけなので、trueです。
recursive…リレーショナルではないので、falseです。

3.CSVファイルを読み込むモデルを用意します。
読み込むCSVファイルは、それ自体をDBテーブルのように扱いますので、ファイル名をテーブル名のように命名し、普通のモデルファイルを用意します。
そのモデルを使用するコントローラ、モデルを使用して取得したデータを表示するビューも用意しました。
それぞれのサンプルコード・ファイル名は下記になります。

モデル名:Price
モデルファイル名:/models/price.php
コントローラ名:prices_controller.php
ビューファイル名:/views/prices/read.php
CSVファイル名:/app/tmp/csv/prices.csv (複数形です。テーブル名と同じ)

CSVファイルの中身は下記です。
1行目は列名、デリミッタはカンマ、ファイルの文字コードはUTF-8、改行コードはLFです。
スクリプトでファイルをアップロードすることを想定し、CSVファイルの置き場所は/tmpの中に/csvというフォルダを作成してやることにしました。

id,name,dep,device,favourite,price
1,山田太郎,代表,自転車,サボテン,1000
2,鈴木一郎,技術,電車,タバコ,1200
3,福山雅治,技術,電車,じゃがりこ,600
4,木村拓哉,営業,電車,デニム,750
5,岡田准一,技術,自転車,お酒,850

4.モデルを書きます。
モデルでは、’csv’というデータソースを使用することを宣言します。
コードは下記。

<?php 
class Price extends AppModel {
    var $name = 'Price';
    var $useDbConfig = 'csv';
    var $useTable = false;
}
?>

5.コントローラを書きます。
Priceモデルの使用を宣言し、findでデータを読み込みます。

<?php
class PricesController extends AppController
{
    var $name = 'Prices';
    var $uses = array( 'Price' );

    function read(){

           $list = $this->Price->find( 'all' );
           $this->set( 'list', $list['prices'] );
    }
?>

6.ビューを書きます。
取得したデータの確認に、テーブルに吐き出すように書いています。

<h2>CSVデータ一覧</h2>
<table>
    <tr>
        <th>ID</th>
        <th>名前</th>
        <th>部署・役職</th>
        <th>通勤手段</th>
        <th>好きなもの</th>
        <th>価格</th>
    </tr>
<?php

    foreach( $list as $data )
        {
        print <<<EOD
    <tr>
        <td>{$data['id']}</td>
        <td>{$data['name']}</td>
        <td>{$data['dep']}</td>
        <td>{$data['device']}</td>
        <td>{$data['favourite']}</td>
        <td>{$data['price']}</td>
    </tr>
EOD;
        }
?>
</table>

さて、findした結果、データがどのように入ってきたのかを確認してみましょう。

csv_data.JPG


そして、テーブルに表示すると下記のような感じです。

csv_table.JPG

読み込む際のパラメータとしては、読み込み件数(limit)、件数指定が指定されている場合にどのページを読み込むか(page)、どの列を読み込むか(fields)の三つが使用可能です。

$list = $this->Price->find( 'all', array(    'limit' => 4,     // read only 100 records
                                                      'page' => 1,    // start at page 2, limit is needed to make sense
                                                      'fields' => array( 'id', 'name', 'price' )
                                                )
                            );

CSVファイルの場所など少し気をつけるポイントはありますが、find一発で読み込めるのは便利です。
是非お試しください。