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

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