File
[ class tree: File ] [ index: File ] [ all elements ]

Source for file CSV.php

Documentation is available at CSV.php

  1. <?php
  2. // +----------------------------------------------------------------------+
  3. // | PHP Version 4                                                        |
  4. // +----------------------------------------------------------------------+
  5. // | Copyright (c) 2002-2003 Tomas Von Veschler Cox                       |
  6. // +----------------------------------------------------------------------+
  7. // | This source file is subject to version 2.0 of the PHP license,       |
  8. // | that is bundled with this package in the file LICENSE, and is        |
  9. // | available at through the world-wide-web at                           |
  10. // | http://www.php.net/license/2_02.txt.                                 |
  11. // | If you did not receive a copy of the PHP license and are unable to   |
  12. // | obtain it through the world-wide-web, please send a note to          |
  13. // | license@php.net so we can mail you a copy immediately.               |
  14. // +----------------------------------------------------------------------+
  15. // | Authors: Tomas V.V.Cox <cox@idecnet.com>                             |
  16. // |                                                                      |
  17. // +----------------------------------------------------------------------+
  18. //
  19. // $Id: CSV.php,v 1.15 2005/01/13 17:35:13 mike Exp $
  20.  
  21. require_once 'PEAR.php';
  22. require_once 'File.php';
  23.  
  24. /**
  25. * File class for handling CSV files (Comma Separated Values), a common format
  26. * for exchanging data.
  27. *
  28. * TODO:
  29. *  - Usage example and Doc
  30. *  - Use getPointer() in discoverFormat
  31. *  - Add a line counter for being able to output better error reports
  32. *  - Store the last error in GLOBALS and add File_CSV::getLastError()
  33. *
  34. * Wish:
  35. *  - Support Mac EOL format
  36. *  - Other methods like readAll(), writeAll(), numFields(), numRows()
  37. *  - Try to detect if a CSV has header or not in discoverFormat()
  38. *
  39. * Known Bugs:
  40. * (they has been analyzed but for the moment the impact in the speed for
  41. *  properly handle this uncommon cases is too high and won't be supported)
  42. *  - A field which is composed only by a single quoted separator (ie -> ;";";)
  43. *    is not handled properly
  44. *  - When there is exactly one field minus than the expected number and there
  45. *    is a field with a separator inside, the parser will throw the "wrong count" error
  46. *
  47. @author Tomas V.V.Cox <cox@idecnet.com>
  48. @package File
  49. */
  50. class File_CSV
  51. {
  52.     /**
  53.     * This raiseError method works in a different way. It will always return
  54.     * false (an error occurred) but it will call PEAR::raiseError() before
  55.     * it. If no default PEAR global handler is set, will trigger an error.
  56.     *
  57.     * @param string $error The error message
  58.     * @return bool always false
  59.     */
  60.     function raiseError($error)
  61.     {
  62.         // If a default PEAR Error handler is not set trigger the error
  63.         // XXX Add a PEAR::isSetHandler() method?
  64.         if ($GLOBALS['_PEAR_default_error_mode'== PEAR_ERROR_RETURN{
  65.             PEAR::raiseError($errornullPEAR_ERROR_TRIGGERE_USER_WARNING);
  66.         else {
  67.             PEAR::raiseError($error);
  68.         }
  69.         return false;
  70.     }
  71.  
  72.     /**
  73.     * Checks the configuration given by the user
  74.     *
  75.     * @param array  &$conf  The configuration assoc array
  76.     * @param string &$error The error will be written here if any
  77.     */
  78.     function _conf(&$conf&$error)
  79.     {
  80.         // check conf
  81.         if (!is_array($conf)) {
  82.             return $error "Invalid configuration";
  83.         }
  84.  
  85.         if (!isset($conf['fields']|| !is_numeric($conf['fields'])) {
  86.             return $error 'The number of fields must be numeric (the "fields" key)';
  87.         }
  88.  
  89.         if (isset($conf['sep'])) {
  90.             if (strlen($conf['sep']!= 1{
  91.                 return $error 'Separator can only be one char';
  92.             }
  93.         elseif ($conf['fields'> 1{
  94.             return $error 'Missing separator (the "sep" key)';
  95.         }
  96.  
  97.         if (isset($conf['quote'])) {
  98.             if (strlen($conf['quote']!= 1{
  99.                 return $error 'The quote char must be one char (the "quote" key)';
  100.             }
  101.         else {
  102.             $conf['quote'= null;
  103.         }
  104.  
  105.         if (!isset($conf['crlf'])) {
  106.             $conf['crlf'"\n";
  107.         }
  108.     }
  109.  
  110.     /**
  111.     * Return or create the file descriptor associated with a file
  112.     *
  113.     * @param string $file The name of the file
  114.     * @param array  &$conf The configuration
  115.     * @param string $mode The open node (ex: FILE_MODE_READ or FILE_MODE_WRITE)
  116.     *
  117.     * @return mixed A file resource or false
  118.     */
  119.     function getPointer($file&$conf$mode = FILE_MODE_READ)
  120.     {
  121.         static $resources  = array();
  122.         static $config;
  123.         if (isset($resources[$file])) {
  124.             $conf $config;
  125.             return $resources[$file];
  126.         }
  127.         File_CSV::_conf($conf$error);
  128.         if ($error{
  129.             return File_CSV::raiseError($error);
  130.         }
  131.         $config $conf;
  132.         PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
  133.         $fp &File::_getFilePointer($file$mode);
  134.         PEAR::popErrorHandling();
  135.         if (PEAR::isError($fp)) {
  136.             return File_CSV::raiseError($fp);
  137.         }
  138.         $resources[$file$fp;
  139.  
  140.         if ($mode == FILE_MODE_READ && !empty($conf['header'])) {
  141.             if (!File_CSV::read($file$conf)) {
  142.                 return false;
  143.             }
  144.         }
  145.         return $fp;
  146.     }
  147.  
  148.     /**
  149.     * Unquote data
  150.     *
  151.     * @param string $field The data to unquote
  152.     * @param string $quote The quote char
  153.     * @return string the unquoted data
  154.     */
  155.     function unquote($field$quote)
  156.     {
  157.         // Incase null fields (form: ;;)
  158.         if (!strlen($field)) {
  159.             return $field;
  160.         }
  161.         if ($quote && $field{0== $quote && $field{strlen($field)-1== $quote{
  162.             return substr($field1-1);
  163.         }
  164.         return $field;
  165.     }
  166.  
  167.     /**
  168.     * Reads a row of data as an array from a CSV file. It's able to
  169.     * read memo fields with multiline data.
  170.     *
  171.     * @param string $file   The filename where to write the data
  172.     * @param array  &$conf   The configuration of the dest CSV
  173.     *
  174.     * @return mixed Array with the data read or false on error/no more data
  175.     */
  176.     function readQuoted($file&$conf)
  177.     {
  178.         if (!$fp File_CSV::getPointer($file$confFILE_MODE_READ)) {
  179.             return false;
  180.         }
  181.         $buff $c = null;
  182.         $ret  = array();
  183.         $i = 1;
  184.         $in_quote = false;
  185.         $quote $conf['quote'];
  186.         $f $conf['fields'];
  187.         while (($ch fgetc($fp)) !== false{
  188.             $prev $c;
  189.             $c $ch;
  190.             // Common case
  191.             if ($c != $quote && $c != $conf['sep'&& $c != "\n"{
  192.                 $buff .= $c;
  193.                 continue;
  194.             }
  195.             if ($c == $quote && $quote &&
  196.                 ($prev == $conf['sep'|| $prev == "\n" || $prev === null))
  197.             {
  198.                 $in_quote = true;
  199.             elseif ($in_quote{
  200.                 // When ends quote
  201.                 if ($c == $conf['sep'&& $prev == $conf['quote']{
  202.                     $in_quote = false;
  203.                 elseif ($c == "\n"{
  204.                     $sub ($prev == "\r"? 2 : 1;
  205.                     if ((strlen($buff>= $sub&&
  206.                         ($buff{strlen($buff$sub== $quote))
  207.                     {
  208.                         $in_quote = false;
  209.                     }
  210.                 }
  211.             }
  212.             if (!$in_quote && ($c == $conf['sep'|| $c == "\n")) {
  213.                 // More fields than expected
  214.                 if (($c == $conf['sep']&& ((count($ret+ 1== $f)) {
  215.                     while ($c != "\n"{
  216.                         $c fgetc($fp);
  217.                     }
  218.                     File_CSV::raiseError("Read more fields than the ".
  219.                                          "expected ".$conf['fields']);
  220.                     return true;
  221.                 }
  222.                 // Less fields than expected
  223.                 if (($c == "\n"&& ($i != $f)) {
  224.                     File_CSV::raiseError("Read wrong fields number count: '"$i .
  225.                                          "' expected ".$conf['fields']);
  226.                     return true;
  227.                 }
  228.                 if ($prev == "\r"{
  229.                     $buff substr($buff0-1);
  230.                 }
  231.                 $ret[File_CSV::unquote($buff$quote);
  232.                 if (count($ret== $f{
  233.                     return $ret;
  234.                 }
  235.                 $buff '';
  236.                 $i++;
  237.                 continue;
  238.             }
  239.             $buff .= $c;
  240.         }
  241.         return !feof($fp$ret : false;
  242.     }
  243.  
  244.     /**
  245.     * Reads a "row" from a CSV file and return it as an array
  246.     *
  247.     * @param string $file The CSV file
  248.     * @param array  &$conf The configuration of the dest CSV
  249.     *
  250.     * @return mixed Array or false
  251.     */
  252.     function read($file&$conf)
  253.     {
  254.         if (!$fp File_CSV::getPointer($file$confFILE_MODE_READ)) {
  255.             return false;
  256.         }
  257.         // The size is limited to 4K
  258.         if (!$line   fgets($fp4096)) {
  259.             return false;
  260.         }
  261.  
  262.         $fields $conf['fields'== 1 ? array($lineexplode($conf['sep']$line);
  263.  
  264.         if ($conf['quote']{
  265.             $last =$fields[count($fields- 1];
  266.             // Fallback to read the line with readQuoted when guess
  267.             // that the simple explode won't work right
  268.             if (($last{strlen($last- 1== "\n"
  269.                 && $last{0== $conf['quote']
  270.                 && $last{strlen(rtrim($last)) - 1!= $conf['quote'])
  271.                 ||
  272.                 (count($fields!= $conf['fields'])
  273.                 // XXX perhaps there is a separator inside a quoted field
  274.                 //preg_match("|{$conf['quote']}.*{$conf['sep']}.*{$conf['quote']}|U", $line)
  275.                 )
  276.             {
  277.                 fseek($fp-1 * strlen($line)SEEK_CUR);
  278.                 return File_CSV::readQuoted($file$conf);
  279.             else {
  280.                 $last rtrim($last);
  281.                 foreach ($fields as $k => $v{
  282.                     $fields[$kFile_CSV::unquote($v$conf['quote']);
  283.                 }
  284.             }
  285.         }
  286.         if (count($fields!= $conf['fields']{
  287.             File_CSV::raiseError("Read wrong fields number count: '"count($fields.
  288.                                   "' expected ".$conf['fields']);
  289.             return true;
  290.         }
  291.         return $fields;
  292.     }
  293.  
  294.     /**
  295.     * Internal use only, will be removed in the future
  296.     *
  297.     * @param string $str The string to debug
  298.     * @access private
  299.     */
  300.     function _dbgBuff($str)
  301.     {
  302.         if (strpos($str"\r"!== false{
  303.             $str str_replace("\r""_r_"$str);
  304.         }
  305.         if (strpos($str"\n"!== false{
  306.             $str str_replace("\n""_n_"$str);
  307.         }
  308.         if (strpos($str"\t"!== false{
  309.             $str str_replace("\t""_t_"$str);
  310.         }
  311.         echo "buff: ($str)\n";
  312.     }
  313.  
  314.     /**
  315.     * Writes a struc (array) in a file as CSV
  316.     *
  317.     * @param string $file   The filename where to write the data
  318.     * @param array  $fields Ordered array with the data
  319.     * @param array  &$conf   The configuration of the dest CSV
  320.     *
  321.     * @return bool True on success false otherwise
  322.     */
  323.     function write($file$fields&$conf)
  324.     {
  325.         if (!$fp File_CSV::getPointer($file$confFILE_MODE_WRITE)) {
  326.             return false;
  327.         }
  328.         if (count($fields!= $conf['fields']{
  329.             File_CSV::raiseError("Wrong fields number count: '"count($fields.
  330.                                   "' expected ".$conf['fields']);
  331.             return true;
  332.         }
  333.         $write '';
  334.         for ($i = 0; $i count($fields)$i++{
  335.             if (!is_numeric($fields[$i]&& $conf['quote']{
  336.                 $write .= $conf['quote'$fields[$i$conf['quote'];
  337.             else {
  338.                 $write .= $fields[$i];
  339.             }
  340.             if ($i (count($fields- 1)) {
  341.                 $write .= $conf['sep'];
  342.             else {
  343.                 $write .= $conf['crlf'];
  344.             }
  345.         }
  346.         if (!fwrite($fp$write)) {
  347.             return File_CSV::raiseError('Can not write to file');
  348.         }
  349.         return true;
  350.     }
  351.  
  352.     /**
  353.     * Discover the format of a CSV file (the number of fields, the separator
  354.     * and if it quote string fields)
  355.     *
  356.     * @param string the CSV file name
  357.     * @param array extra separators that should be checked for.
  358.     * @return mixed Assoc array or false
  359.     */
  360.     function discoverFormat($file$extraSeps = array())
  361.     {
  362.         if (!$fp @fopen($file'r')) {
  363.             return File_CSV::raiseError("Could not open file: $file");
  364.         }
  365.         $seps = array("\t"';'':'',');
  366.         $seps array_merge($seps$extraSeps);
  367.         $matches = array();
  368.         // Take the first 10 lines and store the number of ocurrences
  369.         // for each separator in each line
  370.         for ($i = 0; ($i < 10&& ($line fgets($fp4096))$i++{
  371.             foreach ($seps as $sep{
  372.                 $matches[$sep][$isubstr_count($line$sep);
  373.             }
  374.         }
  375.         $final = array();
  376.         // Group the results by amount of equal ocurrences
  377.         foreach ($matches as $sep => $res{
  378.             $times = array();
  379.             $times[0= 0;
  380.             foreach ($res as $k => $num{
  381.                 if ($num > 0{
  382.                     $times[$num(isset($times[$num])) $times[$num+ 1 : 1;
  383.                 }
  384.             }
  385.             arsort($times);
  386.             $fields[$sepkey($times);
  387.             $amount[$sep$times[key($times)];
  388.         }
  389.         arsort($amount);
  390.         $sep    key($amount);
  391.  
  392.         $conf['fields'$fields[$sep+ 1;
  393.         $conf['sep']    $sep;
  394.         // Test if there are fields with quotes arround in the first 5 lines
  395.         $quotes '"\'';
  396.         $quote  = null;
  397.         rewind($fp);
  398.         for ($i = 0; ($i < 5&& ($line fgets($fp4096))$i++{
  399.             if (preg_match("|$sep([$quotes]).*([$quotes])$sep|U"$line$match)) {
  400.                 if ($match[1== $match[2]{
  401.                     $quote $match[1];
  402.                     break;
  403.                 }
  404.             }
  405.             if (preg_match("|^([$quotes]).*([$quotes])$sep|"$line$match)
  406.                 || preg_match("|([$quotes]).*([$quotes])$sep\s$|Us"$line$match))
  407.             {
  408.                 if ($match[1== $match[2]{
  409.                     $quote $match[1];
  410.                     break;
  411.                 }
  412.             }
  413.         }
  414.         $conf['quote'$quote;
  415.         fclose($fp);
  416.         // XXX What about trying to discover the "header"?
  417.         return $conf;
  418.     }
  419. }
  420. ?>

Documentation generated on Mon, 11 Mar 2019 14:13:02 -0400 by phpDocumentor 1.4.4. PEAR Logo Copyright © PHP Group 2004.