5

CSV ファイルを検索し、特定の検索機能を実行するスクリプトを作成する必要があります。

  1. 列の重複エントリを見つける
  2. 別の列の禁止エントリのリストに一致するものを見つける
  3. 指定された列での正規表現マッチングによるエントリの検索

今、私はこれを手続き的にコーディングすることにまったく問題はありませんが、オブジェクト指向プログラミングに移行しているので、代わりにオブジェクトのクラスとインスタンスを使用したいと考えています。

ただし、OOP で考えるのはまだ自然にできていないので、どちらに進むべきか完全にはわかりません。特定のコードを探しているのではなく、スクリプトをどのように設計できるかについての提案を探しています。

私の現在の考えはこれです。

  1. ファイル クラスを作成します。これにより、データのインポート/エクスポートが処理されます
  2. 検索クラスを作成します。file の子クラス。これには、さまざまな検索方法が含まれます

index.php でどのように機能するか:

  1. index.php のファイル オブジェクトの csv から配列を取得します
  2. 配列の値を反復処理するループを作成します
  3. 検索オブジェクトからループ内のメソッドを呼び出し、それらをエコーアウトします

このアプローチで私が目にする問題はこれです。

  • 特定の「列」を見るために、配列内のさまざまな要素をポイントしたいと思います。ループを関数に入れ、これをパラメーターとして渡すこともできますが、この種の OOP のポイントを無効にしているように感じます。
  • 私の検索方法はさまざまな方法で機能します。ネストされたループを使用すると、重複するエントリを検索するのはかなり簡単ですが、単純な単語または正規表現の検索を行うには、ネストされたループは必要ありません。

代わりにこのように行くべきですか?

  1. ファイル クラスを作成します。これにより、データのインポート/エクスポートが処理されます
  2. ループ クラス ファイルのクラスの子を作成します。これには、配列の反復処理を処理するメソッドが含まれます
  3. 検索クラスを作成します。ループの子クラス。これには、さまざまな検索方法が含まれます

これに関する私の主な問題は、複数の検索オブジェクトが必要であり、ループ クラス内でこれを反復処理するように見えることです。

どんな助けでも大歓迎です。私は OOP に非常に慣れていないため、個々の部分は理解していますが、全体像をまだ把握できていません。私がやろうとしていることを複雑にしすぎているのかもしれませんし、まだ見えていないもっと簡単な方法があるかもしれません。

4

3 に答える 3

13

PHP は、SplFileObject を使用してオブジェクト指向の方法で CSV ファイルを読み取る方法を既に提供しています。

$file = new SplFileObject("data.csv");

// tell object that it is reading a CSV file
$file->setFlags(SplFileObject::READ_CSV);
$file->setCsvControl(',', '"', '\\');

// iterate over the data
foreach ($file as $row) {
    list ($fruit, $quantity) = $row;
    // Do something with values
}

SplFileObject は CSV データをストリーミングするため、メモリ消費は非常に少なく、大きな CSV ファイルを効率的に処理できますが、ファイル I/O であるため、最速ではありません。ただし、SplFileObject は Iterator インターフェイスを実装しているため、その $file インスタンスを他の反復子にラップして反復を変更できます。たとえば、ファイルの I/O を制限するには、それを CachingIterator にラップできます。

$cachedFile = new CachingIterator($file, CachingIterator::FULL_CACHE);

キャッシュを埋めるには、$cachedFile を反復処理します。これでキャッシュがいっぱいになります

foreach ($cachedFile as $row) {

次に、キャッシュを反復するには、次のようにします

foreach ($cachedFile->getCache() as $row) {

トレードオフは明らかにメモリの増加です。

ここで、クエリを実行するために、その CachingIterator または SplFileObject を FilterIterator にラップして、csv データを反復処理するときに出力を制限することができます。

class BannedEntriesFilter extends FilterIterator
{
    private $bannedEntries = array();

    public function setBannedEntries(array $bannedEntries)
    {
        $this->bannedEntries = $bannedEntries;
    }

    public function accept()
    {
        foreach ($this->current() as $key => $val) {
            return !$this->isBannedEntryInColumn($val, $key);
        }
    }

    public function $isBannedEntryInColumn($entry, $column)
    {
        return isset($this->bannedEntries[$column])
            && in_array($this->bannedEntries[$column], $entry);
    }
}

FilterIterator は、FilterIterator の accept メソッドのテストを満たさない内側の Iterator からすべてのエントリを省略します。上記では、禁止されたエントリの配列に対して csv ファイルの現在の行をチェックし、一致する場合、データは反復に含まれません。次のように使用します。

$filteredCachedFile = new BannedEntriesFilter(
    new ArrayIterator($cachedFile->getCache())
)

キャッシュされた結果は常に配列であるため、配列を FilterIterator にラップする前に、その配列を ArrayIterator にラップする必要があります。キャッシュを使用するには、CachingIterator を少なくとも 1 回繰り返す必要があることに注意してください。上記ですでにそれを行っていると仮定します。次のステップは、禁止されたエントリを構成することです

$filteredCachedFile->setBannedEntries(
    array(
        // banned entries for column 0
        array('foo', 'bar'),
        // banned entries for column 1
        array( …
    )
);

それはかなり簡単だと思います。禁止されたエントリを保持する CSV データの各列に 1 つのエントリを持つ多次元配列があります。次に、インスタンスを反復処理するだけで、禁止されたエントリを持たない行のみが得られます

foreach ($filteredCachedFile as $row) {
    // do something with filtered rows
}

または、結果を配列に取得するだけの場合:

$results = iterator_to_array($filteredCachedFile);

結果をさらに制限するために、複数の FilterIterator を積み重ねることができます。フィルタリングごとにクラスを書きたくない場合は、CallbackFilterIterator を見てください。これにより、実行時に受け入れロジックを渡すことができます。

$filteredCachedFile = new CallbackFilterIterator(
    new ArrayIterator($cachedFile->getCache()),
    function(array $row) {
        static $bannedEntries = array(
            array('foo', 'bar'),
            …
        );
        foreach ($row as $key => $val) {
            // logic from above returning boolean if match is found
        }
    }
);
于 2012-11-08T10:31:19.167 に答える
3

あなたは実際に OOP を学ぶための悪い例を選んでいます。ファイルを「インポート」および「検索」するために探している機能は、オブジェクト指向の方法ではなく、手続き型の方法で実装するのが最適だからです。世界のすべてが「オブジェクト」ではないことに注意してください。オブジェクトの他に、「プロシージャ」、「アクション」などがあります。この機能をクラスで実装することもできますが、実際にはこれが推奨される方法です。しかし、機能をクラスに入れるだけでは、それが自動的に実際の OOP に変わるわけではありません。

私が言おうとしている点は、OOP の観点からこの機能を理解するのに苦労している理由の 1 つは、実際にはオブジェクト指向の性質ではないということです。Java Math クラス (PHP にも同様のものがあるかもしれません) に精通している場合は、abs や log などのメソッド/関数がたくさんあります。これはクラスですが、実際にはオブジェクト指向のクラスではありません。検出。それは単なる関数の集まりです。

オブジェクト指向の意味でのクラスとは実際には何ですか? これは大きなトピックですが、少なくとも 1 つの一般的な基準は、状態 (属性/フィールド) と動作 (メソッド) の両方があり、動作と状態の間に本質的な結合があるということです。その場合、たとえば、メソッドへの呼び出しは状態にアクセスします (それらは非常に結びついているため)。以下は単純な OOP クラスです。

Class person {

  // State
  name;
  age;
  income;

  // Behavior
  getName();
  setName()
  . 
  .
  .
  getMonthlyIncome() {
    return income / 12;
  }


}

そして、ここにクラスがありますが、その外観(クラスとして)にもかかわらず、実際には手続き型です:

class Math {

  multiply(double x, double y) {
    return x * y;
  }

  divide(double x, double y) {
    return x / y;
   }

  exponentiate(double x, double y) {
     return x^y;
  }
于 2012-11-06T15:58:19.540 に答える
3

ここでは、指定されたニーズを満たす OOP コードを設計するための合理的なアプローチを説明します。以下に示すアイデアは正しいと確信していますが、次の点に注意してください。

  • 設計は改善できます。ここでの目的は、最終製品ではなく、アプローチを示すことです。
  • 実装はとしてのみ意図されています-それが(ほとんど)機能しない場合、それで十分です

これを行う方法

高度に設計されたソリューションは、データへのインターフェイスを定義することから始まります。つまり、すべてのクエリ操作を実行できるデータの表現は何かを考えてください。これがうまくいくものです:

  • データセットの有限コレクションです。ゼロベースのインデックスを指定すると、各行にアクセスできます。
  • の有限集合です。各値は文字列であり、0 から始まるインデックス (列インデックス) を指定してアクセスできます。データセット内のすべての行には、まったく同じ数の値があります。

この定義は、行をループし、特定の列の値に対して何らかのタイプのテストを実行することにより、言及した 3 種類のクエリすべてを実装するのに十分です。

次の動きは、上記をコードで記述するインターフェイスを定義することです。特に良いとは言えませんが、それでも適切なアプローチは次のとおりです。

interface IDataSet {
    public function getRowCount();
    public function getValueAt($row, $column);
}

この部分が完了したので、このインターフェイスを実装し、状況で使用できる具象クラスを定義できます。

class InMemoryDataSet implements IDataSet {
    private $_data = array();

    public function __construct(array $data) {
        $this->_data = $data;
    }

    public function getRowCount() {
        return count($this->_data);
    }

    public function getValueAt($row, $column) {
        if ($row >= $this->getRowCount()) {
            throw new OutOfRangeException();
        }

        return isset($this->_data[$row][$column])
            ? $this->_data[$row][$column]
            : null;
    }
}

次のステップは、入力データを何らかの種類に変換するコードを作成することですIDataSet

function CSVToDataSet($file) {
    return new InMemoryDataSet(array_map('str_getcsv', file($file)));
}

これで、CSV ファイルから簡単に を作成できるようになりました。また、その目的のために明示的に設計されてIDataSetいるため、クエリを実行できることがわかります。IDataSetもうすぐそこです。

唯一欠けているのは、 でクエリを実行できる再利用可能なクラスを作成することですIDataSet。ここにそれらの1つがあります:

class DataQuery {
    private $_dataSet;

    public function __construct(IDataSet $dataSet) {
        $this->_dataSet = $dataSet;
    }

    public static function getRowsWithDuplicates($columnIndex) {
        $values = array();
        for ($i = 0; $i < $this->_dataSet->getRowCount(); ++$i) {
            $values[$this->_dataSet->->getValueAt($i, $columnIndex)][] = $i;
        }

        return array_filter($values, function($row) { return count($row) > 1; });
    }
}

このコードは、キーが CSV データの値である配列を返します。値は、各値が表示される行のゼロベースのインデックスを持つ配列です。重複する値のみが返されるため、各配列には少なくとも 2 つの要素が含まれます。

したがって、この時点で準備は完了です。

$dataSet = CSVToDataSet("data.csv");
$query = new DataQuery($dataSet);
$dupes = $query->getRowsWithDuplicates(0);

これを行うことで得られるもの

アプリケーション全体を編集する必要なく、将来の変更をサポートするクリーンで保守可能なコード。

さらにクエリ操作を追加したい場合は、それらを追加するDataQueryと、すべての具体的なタイプのデータ セットですぐに使用できます。データ セットとその他の外部コードを変更する必要はありません。

データの内部表現を変更する場合は、InMemoryDataSetそれに応じて変更するか、実装する別のクラスを作成しIDataSetて、 の代わりにそのクラスを使用しCSVToDataSetます。クエリ クラスとその他の外部コードを変更する必要はありません。

データ セットの定義を変更する必要がある場合(おそらく、より多くの種類のクエリを効率的に実行できるようにするため) を変更する必要がありますIDataSet。これにより、すべての具体的なデータ セット クラスも全体像に反映DataQueryされます。これは世界の終わりではありませんが、まさに避けたいことです。

そして、これこそまさに、私がこれから始めることを提案した理由です。データセットの適切な定義を思いつくと、他のすべてがうまくいきます。

于 2012-11-06T10:43:19.333 に答える