【问题标题】:PHP: Searching through a CSV file the OOP wayPHP:以 OOP 方式搜索 CSV 文件
【发布时间】:2012-10-26 06:53:20
【问题描述】:

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

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

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

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

我现在的想法是这样的;

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

它在 index.php 中的作用:

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

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

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

我应该这样去吗?

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

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

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

【问题讨论】:

    标签: php oop object loops csv


    【解决方案1】:

    我将说明一种合理的方法来设计满足您陈述的需求的 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。虽然这不会是世界末日,但这正是您想要避免的事情。

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

    【讨论】:

    • 我认为这正是我正在寻找的答案。我现在在工作,所以我没有太多时间尝试,但我今晚肯定会尝试这种方法。如果我能理解这一点,我认为这将是一个很好的起点。
    • @MartynShutt:刚写完,希望对您有所帮助。 :)
    • 这是一个很好的答案。那里有一点我需要弄清楚(例如,以前从未使用过 array_map() ),但从逻辑上讲它很有意义。非常感谢您为此付出的时间。我期待着努力!
    • 我现在正在使用您提供的代码,但我在理解其中一些方面有点困难。我已将其作为一个单独的问题发布在这里:stackoverflow.com/questions/13575246/… 如果您能提供一些专业知识,我将不胜感激。再次感谢。
    【解决方案2】:

    你实际上选择了一个不好的例子来学习 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;
      }
    

    【讨论】:

    • 我想我明白你在说什么。我认为通过实践和时间,我将学会区分何时最好使用函数和何时最好使用类。我仍然从程序的角度思考,我承认看起来非常相关的类更具可读性。试图从心理上阅读多个班级如何相互影响的过程会让我的大脑有点麻木。也许我在这个意义上错过了封装的意义。
    【解决方案3】:

    PHP 已经提供了read a CSV file in an OO manner with SplFileObject 的方法:

    $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);
    

    您可以堆叠多个 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
            }
        }
    );
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-15
      • 1970-01-01
      • 2012-08-21
      • 2015-05-14
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多