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.18 2005/02/18 11:16:14 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.     * @access private
  74.     * @param string &$error The error will be written here if any
  75.     * @param array  &$conf  The configuration assoc array
  76.     * @return string error    Returns a error message
  77.     */
  78.     function _conf(&$error&$conf)
  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.         if (!isset($conf['eol2unix'])) {
  110.             $conf['eol2unix'= true;
  111.         }
  112.     }
  113.  
  114.     /**
  115.     * Return or create the file descriptor associated with a file
  116.     *
  117.     * @param string $file The name of the file
  118.     * @param array  &$conf The configuration
  119.     * @param string $mode The open node (ex: FILE_MODE_READ or FILE_MODE_WRITE)
  120.     *
  121.     * @return mixed A file resource or false
  122.     */
  123.     function getPointer($file&$conf$mode = FILE_MODE_READ)
  124.     {
  125.         static $resources  = array();
  126.         static $config;
  127.         if (isset($resources[$file])) {
  128.             $conf $config;
  129.             return $resources[$file];
  130.         }
  131.         File_CSV::_conf($error$conf);
  132.         if ($error{
  133.             return File_CSV::raiseError($error);
  134.         }
  135.         $config $conf;
  136.         PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
  137.         $fp &File::_getFilePointer($file$mode);
  138.         PEAR::popErrorHandling();
  139.         if (PEAR::isError($fp)) {
  140.             return File_CSV::raiseError($fp);
  141.         }
  142.         $resources[$file$fp;
  143.  
  144.         if ($mode == FILE_MODE_READ && !empty($conf['header'])) {
  145.             if (!File_CSV::read($file$conf)) {
  146.                 return false;
  147.             }
  148.         }
  149.         return $fp;
  150.     }
  151.  
  152.     /**
  153.     * Unquote data
  154.     *
  155.     * @param string $field The data to unquote
  156.     * @param string $quote The quote char
  157.     * @return string the unquoted data
  158.     */
  159.     function unquote($field$quote)
  160.     {
  161.         // Trim first the string.
  162.         $field trim($field);
  163.         $quote trim($quote);
  164.  
  165.         // Incase null fields (form: ;;)
  166.         if (!strlen($field)) {
  167.             return $field;
  168.         }
  169.  
  170.         if ($quote && $field{0== $quote && $field{strlen($field)-1== $quote{
  171.             return substr($field1-1);
  172.         }
  173.         return $field;
  174.     }
  175.  
  176.     /**
  177.     * Reads a row of data as an array from a CSV file. It's able to
  178.     * read memo fields with multiline data.
  179.     *
  180.     * @param string $file   The filename where to write the data
  181.     * @param array  &$conf   The configuration of the dest CSV
  182.     *
  183.     * @return mixed Array with the data read or false on error/no more data
  184.     */
  185.     function readQuoted($file&$conf)
  186.     {
  187.         if (!$fp File_CSV::getPointer($file$confFILE_MODE_READ)) {
  188.             return false;
  189.         }
  190.  
  191.         $buff $c '';
  192.         $ret  = array();
  193.         $i = 1;
  194.         $in_quote = false;
  195.         $quote $conf['quote'];
  196.         $f $conf['fields'];
  197.         $eol2unix $conf['eol2unix'];
  198.         while (($ch fgetc($fp)) !== false{
  199.             $prev $c;
  200.             $c $ch;
  201.             // Common case
  202.             if ($c != $quote && $c != $conf['sep'&& $c != "\n" && $c != "\r"{
  203.                 $buff .= $c;
  204.                 continue;
  205.             }
  206.  
  207.             // Start quote.
  208.             if ($quote && $c == $quote &&
  209.                 ($prev == $conf['sep'|| $prev == "\n" || $prev === null ||
  210.                  $prev == "\r" || $prev == ''))
  211.             {
  212.                 $in_quote = true;
  213.             }
  214.  
  215.             if ($in_quote{
  216.                 // When ends quote
  217.                 if ($c == $conf['sep'&& $prev == $conf['quote']{
  218.                     $in_quote = false;
  219.                 elseif ($c == "\n" || $c == "\r"{
  220.                     $sub ($prev == "\r"? 2 : 1;
  221.                     if ((strlen($buff>= $sub&&
  222.                         ($buff{strlen($buff$sub== $quote))
  223.                     {
  224.                         $in_quote = false;
  225.                     }
  226.                 }
  227.             }
  228.  
  229.             if (!$in_quote && ($c == $conf['sep'|| $c == "\n" || $c == "\r"&& $prev != ''{
  230.                 // More fields than expected
  231.                 if (($c == $conf['sep']&& ((count($ret+ 1== $f)) {
  232.                     // Seek the pointer into linebreak character.
  233.                     while (true{
  234.                         $c fgetc($fp);
  235.                         if  ($c == "\n" || $c == "\r"{
  236.                             break;
  237.                         }
  238.                     }
  239.  
  240.                     // Insert last field value.
  241.                     $ret[File_CSV::unquote($buff$quote);
  242.                     return $ret;
  243.                 }
  244.  
  245.                 // Less fields than expected
  246.                 if (($c == "\n" || $c == "\r"&& ($i != $f)) {
  247.                     // Insert last field value.
  248.                     $ret[File_CSV::unquote($buff$quote);
  249.  
  250.                     // Pair the array elements to fields count.
  251.                     return array_merge($ret,
  252.                                        array_fill(count($ret),
  253.                                                  ($f - 1(count($ret- 1),
  254.                                                  '')
  255.                     );
  256.                 }
  257.  
  258.                 if ($prev == "\r"{
  259.                     $buff substr($buff0-1);
  260.                 }
  261.  
  262.                 // Convert EOL character to Unix EOL (LF).
  263.                 if ($eol2unix{
  264.                     $buff preg_replace('/(\r\n|\r)$/'"\n"$buff);
  265.                 }
  266.  
  267.                 $ret[File_CSV::unquote($buff$quote);
  268.                 if (count($ret== $f{
  269.                     return $ret;
  270.                 }
  271.                 $buff '';
  272.                 $i++;
  273.                 continue;
  274.             }
  275.             $buff .= $c;
  276.         }
  277.         return !feof($fp$ret : false;
  278.     }
  279.  
  280.     /**
  281.     * Reads a "row" from a CSV file and return it as an array
  282.     *
  283.     * @param string $file The CSV file
  284.     * @param array  &$conf The configuration of the dest CSV
  285.     *
  286.     * @return mixed Array or false
  287.     */
  288.     function read($file&$conf)
  289.     {
  290.         if (!$fp File_CSV::getPointer($file$confFILE_MODE_READ)) {
  291.             return false;
  292.         }
  293.         // The size is limited to 4K
  294.         if (!$line   fgets($fp4096)) {
  295.             return false;
  296.         }
  297.  
  298.         $fields $conf['fields'== 1 ? array($lineexplode($conf['sep']$line);
  299.  
  300.         if ($conf['quote']{
  301.             $last =$fields[count($fields- 1];
  302.             // Fallback to read the line with readQuoted when guess
  303.             // that the simple explode won't work right
  304.             if (($last{strlen($last- 1== "\n"
  305.                 && $last{0== $conf['quote']
  306.                 && $last{strlen(rtrim($last)) - 1!= $conf['quote'])
  307.                 ||
  308.                 (count($fields!= $conf['fields'])
  309.                 // XXX perhaps there is a separator inside a quoted field
  310.                 //preg_match("|{$conf['quote']}.*{$conf['sep']}.*{$conf['quote']}|U", $line)
  311.                 )
  312.             {
  313.                 fseek($fp-1 * strlen($line)SEEK_CUR);
  314.                 return File_CSV::readQuoted($file$conf);
  315.             else {
  316.                 $last rtrim($last);
  317.                 foreach ($fields as $k => $v{
  318.                     $fields[$kFile_CSV::unquote($v$conf['quote']);
  319.                 }
  320.             }
  321.         }
  322.  
  323.         if (count($fields!= $conf['fields']{
  324.             File_CSV::raiseError("Read wrong fields number count: '"count($fields.
  325.                                   "' expected ".$conf['fields']);
  326.             return true;
  327.         }
  328.         return $fields;
  329.     }
  330.  
  331.     /**
  332.     * Internal use only, will be removed in the future
  333.     *
  334.     * @param string $str The string to debug
  335.     * @access private
  336.     */
  337.     function _dbgBuff($str)
  338.     {
  339.         if (strpos($str"\r"!== false{
  340.             $str str_replace("\r""_r_"$str);
  341.         }
  342.         if (strpos($str"\n"!== false{
  343.             $str str_replace("\n""_n_"$str);
  344.         }
  345.         if (strpos($str"\t"!== false{
  346.             $str str_replace("\t""_t_"$str);
  347.         }
  348.         echo "buff: ($str)\n";
  349.     }
  350.  
  351.     /**
  352.     * Writes a struc (array) in a file as CSV
  353.     *
  354.     * @param string $file   The filename where to write the data
  355.     * @param array  $fields Ordered array with the data
  356.     * @param array  &$conf   The configuration of the dest CSV
  357.     *
  358.     * @return bool True on success false otherwise
  359.     */
  360.     function write($file$fields&$conf)
  361.     {
  362.         if (!$fp File_CSV::getPointer($file$confFILE_MODE_WRITE)) {
  363.             return false;
  364.         }
  365.         if (count($fields!= $conf['fields']{
  366.             File_CSV::raiseError("Wrong fields number count: '"count($fields.
  367.                                   "' expected ".$conf['fields']);
  368.             return true;
  369.         }
  370.         $write '';
  371.         for ($i = 0; $i count($fields)$i++{
  372.             if (!is_numeric($fields[$i]&& $conf['quote']{
  373.                 $write .= $conf['quote'$fields[$i$conf['quote'];
  374.             else {
  375.                 $write .= $fields[$i];
  376.             }
  377.             if ($i (count($fields- 1)) {
  378.                 $write .= $conf['sep'];
  379.             else {
  380.                 $write .= $conf['crlf'];
  381.             }
  382.         }
  383.         if (!fwrite($fp$write)) {
  384.             return File_CSV::raiseError('Can not write to file');
  385.         }
  386.         return true;
  387.     }
  388.  
  389.     /**
  390.     * Discover the format of a CSV file (the number of fields, the separator
  391.     * and if it quote string fields)
  392.     *
  393.     * @param string the CSV file name
  394.     * @param array extra separators that should be checked for.
  395.     * @return mixed Assoc array or false
  396.     */
  397.     function discoverFormat($file$extraSeps = array())
  398.     {
  399.         if (!$fp @fopen($file'r')) {
  400.             return File_CSV::raiseError("Could not open file: $file");
  401.         }
  402.         $seps = array("\t"';'':'',');
  403.         $seps array_merge($seps$extraSeps);
  404.         $matches = array();
  405.  
  406.         // Set auto detect line ending for Mac EOL support if < PHP 4.3.0.
  407.         $phpver version_compare('4.1.0'phpversion()'<');
  408.         if ($phpver{
  409.             $oldini ini_get('auto_detect_line_endings');
  410.             ini_set('auto_detect_line_endings''1');
  411.         }
  412.  
  413.         // Take the first 10 lines and store the number of ocurrences
  414.         // for each separator in each line
  415.  
  416.         $lines file($file);
  417.         if (count($lines> 10{
  418.             $lines array_slice($lines010);
  419.         }
  420.  
  421.         if ($phpver{
  422.             ini_set('auto_detect_line_endings'$oldini);
  423.         }
  424.  
  425.         foreach ($lines as $line{
  426.             foreach ($seps as $sep{
  427.                 $matches[$sep][substr_count($line$sep);
  428.             }
  429.         }
  430.  
  431.         $final = array();
  432.         // Group the results by amount of equal ocurrences
  433.         foreach ($matches as $sep => $res{
  434.             $times = array();
  435.             $times[0= 0;
  436.             foreach ($res as $k => $num{
  437.                 if ($num > 0{
  438.                     $times[$num(isset($times[$num])) $times[$num+ 1 : 1;
  439.                 }
  440.             }
  441.             arsort($times);
  442.  
  443.             // Use max fields count.
  444.             $fields[$sepmax(array_flip($times));
  445.             $amount[$sep$times[key($times)];
  446.         }
  447.  
  448.         arsort($amount);
  449.         $sep    key($amount);
  450.  
  451.         $conf['fields'$fields[$sep+ 1;
  452.         $conf['sep']    $sep;
  453.  
  454.         // Test if there are fields with quotes arround in the first 5 lines
  455.         $quotes '"\'';
  456.         $quote  = null;
  457.         if (count($lines> 5{
  458.             $lines array_slice($lines05);
  459.         }
  460.  
  461.         foreach ($lines as $line{
  462.             if (preg_match("|$sep([$quotes]).*([$quotes])$sep|U"$line$match)) {
  463.                 if ($match[1== $match[2]{
  464.                     $quote $match[1];
  465.                     break;
  466.                 }
  467.             }
  468.             if (preg_match("|^([$quotes]).*([$quotes])$sep|"$line$match)
  469.                 || preg_match("|([$quotes]).*([$quotes])$sep\s$|Us"$line$match))
  470.             {
  471.                 if ($match[1== $match[2]{
  472.                     $quote $match[1];
  473.                     break;
  474.                 }
  475.             }
  476.         }
  477.         $conf['quote'$quote;
  478.         fclose($fp);
  479.         // XXX What about trying to discover the "header"?
  480.         return $conf;
  481.     }
  482. }
  483. ?>

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