5

我需要编写一个脚本来搜索 CSV 文件,并对其执行某些搜索功能;

  1. 在列中查找重复条目
  2. 在另一列中查找与禁止条目列表的匹配项
  3. 在指定的列上通过正则表达式匹配查找条目

现在,我在程序上对此进行编码完全没有问题,但是当我现在转向面向对象编程时,我想改用对象的类和实例。

然而,在 OOP 中思考对我来说还不是很自然,所以我不完全确定该走哪条路。我不是在寻找特定的代码,而是关于如何设计脚本的建议。

我目前的想法是这样的;

  1. 创建一个文件类。这将处理数据的导入/导出
  2. 创建一个搜索类。文件的子类。这将包含各种搜索方法

它在 index.php 中的作用:

  1. 从 index.php 文件对象中的 csv 获取一个数组
  2. 创建一个循环来遍历数组的值
  3. 从搜索对象调用循环中的方法并回显它们

我用这种方法看到的问题是这样的;

  • 我将要指向数组中的不同元素以查看特定的“列”。我可以将我的循环放在一个函数中并将其作为参数传递,但是这种方式违背了 OOP 的意义,我觉得
  • 我的搜索方法将以不同的方式工作。使用嵌套循环搜索重复条目相当简单,但我不需要嵌套循环来执行简单的单词或正则表达式搜索。

我应该这样去吗?

  1. 创建一个文件类。这将处理数据的导入/导出
  2. 创建一个循环类文件类的子类。这将包含处理遍历数组的方法
  3. 创建一个搜索类。循环的子类。这将包含各种搜索方法

我的主要问题是我可能需要多个搜索对象并在我的循环类中遍历它。

任何帮助将非常感激。我对 OOP 很陌生,虽然我了解各个部分,但我还不能看到更大的图景。我可能使我正在尝试做的事情过于复杂,或者可能有一种我还看不到的更简单的方法。

4

3 回答 3

13

PHP 已经提供了一种使用 SplFileObject 以 OO 方式读取 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 将忽略内部 Iterator 中不满足 FilterIterator 的 accept 方法中的测试的所有条目。上面,我们检查 csv 文件中的当前行与一组禁止条目,如果匹配,则数据不包含在迭代中。你像这样使用它:

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

由于缓存的结果始终是一个数组,因此我们需要将该数组包装到一个 ArrayIterator 中,然后才能将其包装到我们的 FilterIterator 中。请注意,要使用缓存,您还需要至少迭代一次 CachingIterator。我们只是假设你已经在上面做了。下一步是配置禁止条目

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

我想这很简单。您有一个多维数组,其中包含禁止条目的 CSV 数据中的每一列都有一个条目。然后,您只需遍历实例,它只会为您提供没有被禁止条目的行

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

或者,如果您只想将结果放入数组中:

$results = iterator_to_array($filteredCachedFile);

您可以堆叠多个 FilterIterators 以进一步限制结果。如果您不想为每个过滤编写一个类,请查看 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 代码。虽然我坚信以下提出的想法是合理的,但请注意:

  • 设计可以改进——这里的目的是展示方法,而不是最终产品
  • 该实现仅作为示例- 如果它(几乎)有效,那就足够了

如何去做这件事

一个高度工程化的解决方案将从尝试定义数据接口开始。也就是说,考虑一下允许您执行所有查询操作的数据表示形式。这是一个可行的方法:

  • 数据集的有限集合。给定从零开始的索引,可以访问每一行。
  • 的有限集合。每个值都是一个字符串,并且可以根据其从零开始的索引(即列索引)来访问。数据集中的所有行都具有完全相同数量的值。

通过遍历行并对特定列的值执行某种类型的测试,此定义足以实现您提到的所有三种类型的查询。

下一步是定义一个在代码中描述上述内容的接口。一个不是特别好但仍然足够的方法是:

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. 这是其中之一:

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 数据中的值,值是数组,其中每个值出现的行的索引从零开始。由于只返回重复值,因此每个数组至少有两个元素。

所以此时你已经准备好了:

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

你这样做的收获

干净、可维护的代码,支持将来修改,而无需在整个应用程序中进行编辑。

如果您想添加更多查询操作,请将它们添加到DataQuery您可以立即在所有具体类型的数据集上使用它们。数据集和任何其他外部代码不需要任何修改。

如果要更改数据的内部表示,请相应地修改InMemoryDataSet或创建另一个实现IDataSet并使用该类的类,而不是从CSVToDataSet. 查询类和任何其他外部代码不需要任何修改。

如果您需要更改数据集的定义(可能允许更有效地执行更多类型的查询),那么您必须修改IDataSet,这也将所有具体的数据集类都带入图片中,并且可能DataQuery也是如此。虽然这不会是世界末日,但这正是你想要避免的事情。

这正是我建议从这个开始的原因:如果您为数据集提出了一个好的定义,那么其他一切都会水到渠成。

于 2012-11-06T10:43:19.333 回答
3

您实际上选择了一个不好的例子来学习 OOP。因为,您正在寻找“导入”和“搜索”文件的功能,最好以过程方式实现,而不是面向对象的方式。请记住,并非世界上的所有事物都是“对象”。除了对象,我们还有“过程”、“动作”等。你仍然可以用类来实现这个功能,实际上这是推荐的方式。但是,仅仅将一个功能放在一个类中并不会自动将它变成真正的 OOP。

我要说明的一点是,您可能难以理解 OOP 方面的此功能的原因之一是,它实际上并不是面向对象的性质。如果你熟悉Java Math类(PHP可能也有类似的东西),它有abs,log等b一堆方法/函数。这虽然是一个类,但在面向对象中并不是真正的类感觉。它只是一堆功能。

什么是面向对象意义上的真正的类?这是一个很大的话题,但至少有一个通用标准是它同时具有状态(属性/字段)和行为(方法),从而在行为和状态之间存在内在联系。如果是这样,例如,对方法的调用会访问状态(因为它们是如此紧密地联系在一起)。这是一个简单的 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 回答