home *** CD-ROM | disk | FTP | other *** search
/ Cricao de Sites - 650 Layouts Prontos / WebMasters.iso / Servidores / xampp-win32-1.6.7-installer.exe / php / PEAR / File / CSV.php next >
Encoding:
PHP Script  |  2008-07-02  |  21.1 KB  |  628 lines

  1. <?php
  2. /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
  3.  
  4. /**
  5.  * File::CSV
  6.  *
  7.  * PHP versions 4 and 5
  8.  *
  9.  * LICENSE: This source file is subject to version 3.0 of the PHP license
  10.  * that is available through the world-wide-web at the following URI:
  11.  * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
  12.  * the PHP License and are unable to obtain it through the web, please
  13.  * send a note to license@php.net so we can mail you a copy immediately.
  14.  *
  15.  * @category    File
  16.  * @package     File
  17.  * @author      Tomas V.V.Cox <cox@idecnet.com>
  18.  * @author      Helgi ▐ormar <dufuz@php.net>
  19.  * @copyright   2004-2005 The Authors
  20.  * @license     http://www.php.net/license/3_0.txt  PHP License 3.0
  21.  * @version     CVS: $Id: CSV.php,v 1.41 2007/05/20 12:25:14 dufuz Exp $
  22.  * @link        http://pear.php.net/package/File
  23.  */
  24.  
  25. require_once 'PEAR.php';
  26. require_once 'File.php';
  27.  
  28. /**
  29. * File class for handling CSV files (Comma Separated Values), a common format
  30. * for exchanging data.
  31. *
  32. * TODO:
  33. *  - Usage example and Doc
  34. *  - Use getPointer() in discoverFormat
  35. *  - Add a line counter for being able to output better error reports
  36. *  - Store the last error in GLOBALS and add File_CSV::getLastError()
  37. *
  38. * Wish:
  39. *  - Other methods like readAll(), writeAll(), numFields(), numRows()
  40. *  - Try to detect if a CSV has header or not in discoverFormat() (not possible with CSV)
  41. *
  42. * Known Bugs:
  43. * (they has been analyzed but for the moment the impact in the speed for
  44. *  properly handle this uncommon cases is too high and won't be supported)
  45. *  - A field which is composed only by a single quoted separator (ie -> ;";";)
  46. *    is not handled properly
  47. *  - When there is exactly one field minus than the expected number and there
  48. *    is a field with a separator inside, the parser will throw the "wrong count" error
  49. *
  50. * Info about CSV and links to other sources
  51. * http://www.shaftek.org/publications/drafts/mime-csv/draft-shafranovich-mime-csv-00.html#appendix
  52. *
  53. * @author Tomas V.V.Cox <cox@idecnet.com>
  54. * @author Helgi ▐ormar <dufuz@php.net>
  55. * @package File
  56. */
  57. class File_CSV
  58. {
  59.     /**
  60.     * This raiseError method works in a different way. It will always return
  61.     * false (an error occurred) but it will call PEAR::raiseError() before
  62.     * it. If no default PEAR global handler is set, will trigger an error.
  63.     *
  64.     * @param string $error The error message
  65.     * @return bool always false
  66.     */
  67.     function raiseError($error)
  68.     {
  69.         // If a default PEAR Error handler is not set trigger the error
  70.         // XXX Add a PEAR::isSetHandler() method?
  71.         if ($GLOBALS['_PEAR_default_error_mode'] == PEAR_ERROR_RETURN) {
  72.             PEAR::raiseError($error, null, PEAR_ERROR_TRIGGER, E_USER_WARNING);
  73.         } else {
  74.             PEAR::raiseError($error);
  75.         }
  76.         return false;
  77.     }
  78.  
  79.     /**
  80.     * Checks the configuration given by the user
  81.     *
  82.     * @access private
  83.     * @param string &$error The error will be written here if any
  84.     * @param array  &$conf  The configuration assoc array
  85.     * @return string error    Returns a error message
  86.     */
  87.     function _conf(&$error, &$conf)
  88.     {
  89.         // check conf
  90.         if (!is_array($conf)) {
  91.             return $error = 'Invalid configuration';
  92.         }
  93.  
  94.         if (!isset($conf['fields']) || !(int)$conf['fields']) {
  95.             return $error = 'The number of fields must be numeric (the "fields" key)';
  96.         }
  97.  
  98.         if (isset($conf['sep'])) {
  99.             if (strlen($conf['sep']) != 1) {
  100.                 return $error = 'Separator can only be one char';
  101.             }
  102.         } elseif ($conf['fields'] > 1) {
  103.             return $error = 'Missing separator (the "sep" key)';
  104.         }
  105.  
  106.         if (isset($conf['quote'])) {
  107.             if (strlen($conf['quote']) != 1) {
  108.                 return $error = 'The quote char must be one char (the "quote" key)';
  109.             }
  110.         } else {
  111.             $conf['quote'] = null;
  112.         }
  113.  
  114.         if (!isset($conf['crlf'])) {
  115.             $conf['crlf'] = "\n";
  116.         }
  117.  
  118.         if (!isset($conf['eol2unix'])) {
  119.             $conf['eol2unix'] = true;
  120.         }
  121.     }
  122.  
  123.     /**
  124.     * Return or create the file descriptor associated with a file
  125.     *
  126.     * @param string $file The name of the file
  127.     * @param array  &$conf The configuration
  128.     * @param string $mode The open node (ex: FILE_MODE_READ or FILE_MODE_WRITE)
  129.     * @param boolean $reset if passed as true and resource for the file exists
  130.     *                       than the file pointer will be moved to the beginning
  131.     *
  132.     * @return mixed A file resource or false
  133.     */
  134.     function getPointer($file, &$conf, $mode = FILE_MODE_READ, $reset = false)
  135.     {
  136.         static $resources = array();
  137.         static $config;
  138.         if (isset($resources[$file][$mode])) {
  139.             $conf = $config;
  140.             if ($reset) {
  141.                 fseek($resources[$file][$mode], 0);
  142.             }
  143.             return $resources[$file][$mode];
  144.         }
  145.         File_CSV::_conf($error, $conf);
  146.         if ($error) {
  147.             return File_CSV::raiseError($error);
  148.         }
  149.         $config = $conf;
  150.         PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
  151.         $fp = File::_getFilePointer($file, $mode);
  152.         PEAR::popErrorHandling();
  153.         if (PEAR::isError($fp)) {
  154.             return File_CSV::raiseError($fp);
  155.         }
  156.         $resources[$file][$mode] = $fp;
  157.         return $fp;
  158.     }
  159.  
  160.     /**
  161.     * Unquote data
  162.     *
  163.     * @param string $field The data to unquote
  164.     * @param string $quote The quote char
  165.     * @return string the unquoted data
  166.     */
  167.     function unquote($field, $quote)
  168.     {
  169.         // Trim first the string.
  170.         $field = trim($field);
  171.         $quote = trim($quote);
  172.  
  173.         // Incase null fields (form: ;;)
  174.         if (!strlen($field)) {
  175.             return $field;
  176.         }
  177.  
  178.         // excel compat
  179.         if ($field[0] == '=' && $field[1] == '"') {
  180.             $field = str_replace('="', '"', $field);
  181.         }
  182.  
  183.         $field_len = strlen($field);
  184.         if ($quote && $field[0] == $quote && $field[$field_len - 1] == $quote) {
  185.             // Get rid of escaping quotes
  186.             $new = $prev = $c = '';
  187.             for ($i = 0; $i < $field_len; ++$i) {
  188.                 $prev = $c;
  189.                 $c = $field[$i];
  190.                 // Deal with escaping quotes
  191.                 if ($c == $quote && $prev == $quote) {
  192.                     $c = '';
  193.                 }
  194.  
  195.                 $new .= $c;
  196.             }
  197.             $field = substr($new, 1, -1);
  198.         }
  199.  
  200.         return $field;
  201.     }
  202.  
  203.     /**
  204.     * Reads a row of data as an array from a CSV file. It's able to
  205.     * read memo fields with multiline data.
  206.     *
  207.     * @param string $file   The filename where to write the data
  208.     * @param array  &$conf   The configuration of the dest CSV
  209.     *
  210.     * @return mixed Array with the data read or false on error/no more data
  211.     */
  212.     function readQuoted($file, &$conf)
  213.     {
  214.         if (!$fp = File_CSV::getPointer($file, $conf, FILE_MODE_READ)) {
  215.             return false;
  216.         }
  217.  
  218.         $buff = $old = $prev = $c = '';
  219.         $ret  = array();
  220.         $i = 1;
  221.         $in_quote = false;
  222.         $quote = $conf['quote'];
  223.         $f     = $conf['fields'];
  224.         $sep   = $conf['sep'];
  225.         while (false !== $ch = fgetc($fp)) {
  226.             $old  = $prev;
  227.             $prev = $c;
  228.             $c    = $ch;
  229.  
  230.             // Common case
  231.             if ($c != $quote && $c != $sep && $c != "\n" && $c != "\r") {
  232.                 $buff .= $c;
  233.                 continue;
  234.             }
  235.  
  236.             // Start quote.
  237.             if (
  238.                 $in_quote === false &&
  239.                 $quote && $c == $quote &&
  240.                 (
  241.                  $prev == $sep || $prev == "\n" || $prev === null ||
  242.                  $prev == "\r" || $prev == '' || $prev == ' '
  243.                  || $prev == '=' //excel compat
  244.                 )
  245.             ) {
  246.                 $in_quote = true;
  247.                 // excel compat, removing the = part but only if we are in a quote
  248.                 if ($prev == '=') {
  249.                     $buff{strlen($buff) - 1} = '';
  250.                 }
  251.             }
  252.  
  253.             if ($in_quote) {
  254.  
  255.                 // When does the quote end, make sure it's not double quoted
  256.                 if ($c == $sep && $prev == $quote && $old != $quote) {
  257.                     $in_quote = false;
  258.                 } elseif ($c == $sep && $buff == $quote.$quote) {
  259.                     // In case we are dealing with double quote but empty value
  260.                     $in_quote = false;
  261.                 } elseif ($c == "\n" || $c == "\r") {
  262.                     $sub = ($prev == "\r") ? 2 : 1;
  263.                     $buff_len = strlen($buff);
  264.                     if (
  265.                         $buff_len >= $sub &&
  266.                         $buff[$buff_len - $sub] == $quote
  267.                     ) {
  268.                         $in_quote = false;
  269.                     }
  270.                 }
  271.             }
  272.  
  273.             if (!$in_quote && ($c == $sep || $c == "\n" || $c == "\r") && $prev != '') {
  274.                 // More fields than expected
  275.                 if ($c == $sep && (count($ret) + 1) == $f) {
  276.                     // Seek the pointer into linebreak character.
  277.                     while (true) {
  278.                         $c = fgetc($fp);
  279.                         if  ($c == "\n" || $c == "\r" || $c == '') {
  280.                             break;
  281.                         }
  282.                     }
  283.  
  284.                     // Insert last field value.
  285.                     $ret[] = File_CSV::unquote($buff, $quote);
  286.                     return $ret;
  287.                 }
  288.  
  289.                 // Less fields than expected
  290.                 if (($c == "\n" || $c == "\r") && $i != $f) {
  291.                     // Insert last field value.
  292.                     $ret[] = File_CSV::unquote($buff, $quote);
  293.                     if (count($ret) == 1 && empty($ret[0])) {
  294.                         return array();
  295.                     }
  296.  
  297.                     // Pair the array elements to fields count. - inserting empty values
  298.                     $ret_count = count($ret);
  299.                     $sum = ($f - 1) - ($ret_count - 1);
  300.                     $data = array_merge($ret, array_fill($ret_count, $sum, ''));
  301.                     return $data;
  302.                 }
  303.  
  304.                 if ($prev == "\r") {
  305.                     $buff = substr($buff, 0, -1);
  306.                 }
  307.  
  308.                 // Convert EOL character to Unix EOL (LF).
  309.                 if ($conf['eol2unix']) {
  310.                     $buff = preg_replace('/(\r\n|\r)$/', "\n", $buff);
  311.                 }
  312.  
  313.                 $ret[] = File_CSV::unquote($buff, $quote);
  314.                 if (count($ret) == $f) {
  315.                     return $ret;
  316.                 }
  317.                 $buff = '';
  318.                 ++$i;
  319.                 continue;
  320.             }
  321.             $buff .= $c;
  322.         }
  323.  
  324.         /* If it's the end of the file and we still have something in buffer
  325.          * then we process it since files can have no CL/FR at the end
  326.          */
  327.         $feof = feof($fp);
  328.         if ($feof && !in_array($buff, array("\r", "\n", "\r\n")) && strlen($buff) > 0) {
  329.             $ret[] = File_CSV::unquote($buff, $quote);
  330.             if (count($ret) == $f) {
  331.                 return $ret;
  332.             }
  333.         }
  334.  
  335.         return !$feof ? $ret : false;
  336.     }
  337.  
  338.     /**
  339.     * Reads a "row" from a CSV file and return it as an array
  340.     *
  341.     * @param string $file The CSV file
  342.     * @param array  &$conf The configuration of the dest CSV
  343.     *
  344.     * @return mixed Array or false
  345.     */
  346.     function read($file, &$conf)
  347.     {
  348.         static $headers = array();
  349.         if (!$fp = File_CSV::getPointer($file, $conf, FILE_MODE_READ)) {
  350.             return false;
  351.         }
  352.  
  353.         // The size is limited to 4K
  354.         if (!$line = fgets($fp, 4096)) {
  355.             return false;
  356.         }
  357.  
  358.         $fields = $conf['fields'] == 1 ? array($line) : explode($conf['sep'], $line);
  359.  
  360.         $nl = array("\n", "\r", "\r\n");
  361.         if (in_array($fields[count($fields) - 1], $nl)) {
  362.             array_pop($fields);
  363.         }
  364.  
  365.         $field_count = count($fields);
  366.         $last =& $fields[$field_count - 1];
  367.         $len = strlen($last);
  368.         if (
  369.             $field_count != $conf['fields'] ||
  370.             $conf['quote'] &&
  371.             (
  372.              $len !== 0 && $last[$len - 1] == "\n"
  373.              &&
  374.                 (
  375.                     ($last[0] == $conf['quote']
  376.                     && $last[strlen(rtrim($last)) - 1] != $conf['quote'])
  377.                     ||
  378.                     // excel support
  379.                     ($last[0] == '=' && $last[1] == $conf['quote'])
  380.                     ||
  381.                     // if the row has spaces before the quote
  382.                     preg_match('|^\s+'.preg_quote($conf['quote']) .'|Ums', $last, $match)
  383.                 )
  384.             )
  385.             // XXX perhaps there is a separator inside a quoted field
  386.             //preg_match("|{$conf['quote']}.*{$conf['sep']}.*{$conf['quote']}|U", $line)
  387.         ) {
  388.             fseek($fp, -1 * strlen($line), SEEK_CUR);
  389.             return File_CSV::readQuoted($file, $conf);
  390.         } else {
  391.             foreach ($fields as $k => $v) {
  392.                 $fields[$k] = File_CSV::unquote($v, $conf['quote']);
  393.             }
  394.         }
  395.  
  396.         if (isset($conf['header']) && empty($headers)) {
  397.             // read the first row and assign to $headers
  398.             $headers = $fields;
  399.             return $headers;
  400.         }
  401.  
  402.         if ($field_count != $conf['fields']) {
  403.             File_CSV::raiseError("Read wrong fields number count: '". $field_count .
  404.                                   "' expected ".$conf['fields']);
  405.             return true;
  406.         }
  407.  
  408.         if (!empty($headers)) {
  409.             $tmp = array();
  410.             foreach ($fields as $k => $v) {
  411.                 $tmp[$headers[$k]] = $v;
  412.             }
  413.             $fields = $tmp;
  414.         }
  415.  
  416.         return $fields;
  417.     }
  418.  
  419.     /**
  420.     * Internal use only, will be removed in the future
  421.     *
  422.     * @param string $str The string to debug
  423.     * @access private
  424.     */
  425.     function _dbgBuff($str)
  426.     {
  427.         if (strpos($str, "\r") !== false) {
  428.             $str = str_replace("\r", "_r_", $str);
  429.         }
  430.         if (strpos($str, "\n") !== false) {
  431.             $str = str_replace("\n", "_n_", $str);
  432.         }
  433.         if (strpos($str, "\t") !== false) {
  434.             $str = str_replace("\t", "_t_", $str);
  435.         }
  436.         if ($str === null) {
  437.             $str = '_NULL_';
  438.         }
  439.         if ($str === '') {
  440.             $str = 'Empty string';
  441.         }
  442.         echo "buff: ($str)\n";
  443.     }
  444.  
  445.     /**
  446.     * Writes a struc (array) in a file as CSV
  447.     *
  448.     * @param string $file   The filename where to write the data
  449.     * @param array  $fields Ordered array with the data
  450.     * @param array  &$conf   The configuration of the dest CSV
  451.     *
  452.     * @return bool True on success false otherwise
  453.     */
  454.     function write($file, $fields, &$conf)
  455.     {
  456.         if (!$fp = File_CSV::getPointer($file, $conf, FILE_MODE_WRITE)) {
  457.             return false;
  458.         }
  459.  
  460.         $field_count = count($fields);
  461.         if ($field_count != $conf['fields']) {
  462.             File_CSV::raiseError("Wrong fields number count: '". $field_count .
  463.                                   "' expected ".$conf['fields']);
  464.             return true;
  465.         }
  466.  
  467.         $write = '';
  468.         for ($i = 0; $i < $field_count; ++$i) {
  469.             // only quote if the field contains a sep
  470.             if (!is_numeric($fields[$i]) && $conf['quote']
  471.                 && isset($conf['sep']) && strpos($fields[$i], $conf['sep'])
  472.             ) {
  473.                 $write .= $conf['quote'] . $fields[$i] . $conf['quote'];
  474.             } else {
  475.                 $write .= $fields[$i];
  476.             }
  477.  
  478.             $write .= ($i < ($field_count - 1)) ? $conf['sep']: $conf['crlf'];
  479.         }
  480.  
  481.         if (!fwrite($fp, $write, strlen($write))) {
  482.             return File_CSV::raiseError('Can not write to file');
  483.         }
  484.  
  485.         return true;
  486.     }
  487.  
  488.     /**
  489.     * Discover the format of a CSV file (the number of fields, the separator
  490.     * and if it quote string fields)
  491.     *
  492.     * @param string the CSV file name
  493.     * @param array extra separators that should be checked for.
  494.     * @return mixed Assoc array or false
  495.     */
  496.     function discoverFormat($file, $extraSeps = array())
  497.     {
  498.         if (!$fp = @fopen($file, 'rb')) {
  499.             return File_CSV::raiseError("Could not open file: $file");
  500.         }
  501.  
  502.         // Set auto detect line ending for Mac EOL support
  503.         $oldini = ini_get('auto_detect_line_endings');
  504.         if ($oldini != '1') {
  505.             ini_set('auto_detect_line_endings', '1');
  506.         }
  507.  
  508.         // Take the first 30 lines and store the number of ocurrences
  509.         // for each separator in each line
  510.         $lines = '';
  511.         for ($i = 0; $i < 30 && $line = fgets($fp, 4096); $i++) {
  512.             $lines .= $line;
  513.         }
  514.         fclose($fp);
  515.  
  516.         if ($oldini != '1') {
  517.             ini_set('auto_detect_line_endings', $oldini);
  518.         }
  519.  
  520.         $seps = array("\t", ';', ':', ',');
  521.         $seps = array_merge($seps, $extraSeps);
  522.         $matches = array();
  523.         $quotes = '"\'';
  524.         
  525.         $lines = str_replace('""', '', $lines);
  526.         while ($lines != ($newLines = preg_replace('|((["\'])[^"]*(\2))|', '\2_\2', $lines))){
  527.             $lines = $newLines;
  528.         }
  529.  
  530.         $eol   = strpos($lines, "\r") ? "\r" : "\n";
  531.         $lines = explode($eol, $lines);
  532.         foreach ($lines as $line) {
  533.             $orgLine = $line;
  534.             foreach ($seps as $sep) {
  535.                 $line = preg_replace("|^[^$quotes$sep]*$sep*([$quotes][^$quotes]*[$quotes])|sm", '_', $orgLine);
  536.                 // Find all seps that are within qoutes
  537.                 ///FIXME ... counts legitimit lines as bad ones
  538.  
  539.                  // In case there's a whitespace infront the field
  540.                 $regex = '|\s*?';
  541.                  // Match the first quote (optional), also optionally match = since it's excel stuff
  542.                 $regex.= "(?:\=?[$quotes])";
  543.                 $regex.= '(.*';
  544.                 // Don't match a sep if we are inside a quote
  545.                 // also don't accept the sep if it has a quote on the either side
  546.                 ///FIXME has to be possible if we are inside a quote! (tests fail because of this)
  547.                 $regex.= "(?:[^$quotes])$sep(?:[^$quotes])";
  548.                 $regex.= '.*)';
  549.                 // Close quote (if it's present) and the sep (optional, could be end of line)
  550.                 $regex.= "(?:[$quotes](?:$sep?))|Ums";
  551.                 preg_match_all($regex, $line, $match);
  552.                 // Finding all seps, within quotes or not
  553.                 $sep_count = substr_count($line, $sep);
  554.                 // Real count
  555.                 $matches[$sep][] = $sep_count - count($match[0]);
  556.             }
  557.         }
  558.  
  559.         $final = array();
  560.         // Group the results by amount of equal ocurrences
  561.         foreach ($matches as $sep => $res) {
  562.             $times = array();
  563.             $times[0] = 0;
  564.             foreach ($res as $k => $num) {
  565.                 if ($num > 0) {
  566.                     $times[$num] = (isset($times[$num])) ? $times[$num] + 1 : 1;
  567.                 }
  568.             }
  569.             arsort($times);
  570.  
  571.             // Use max fields count.
  572.             $fields[$sep] = max(array_flip($times));
  573.             $amount[$sep] = $times[key($times)];
  574.         }
  575.  
  576.         arsort($amount);
  577.         $sep = key($amount);
  578.  
  579.         $conf['fields'] = $fields[$sep] + 1;
  580.         $conf['sep']    = $sep;
  581.  
  582.         // Test if there are fields with quotes around in the first 30 lines
  583.         $quote  = null;
  584.  
  585.         $string = implode('', $lines);
  586.         foreach (array('"', '\'') as $q) {
  587.             if (preg_match_all("|$sep(?:\s*?)(\=?[$q]).*([$q])$sep|Us", $string, $match)) {
  588.                 if ($match[1][0] == $match[2][0]) {
  589.                     $quote = $match[1][0];
  590.                     break;
  591.                 }
  592.             }
  593.  
  594.             if (
  595.                 preg_match_all("|^(\=?[$q]).*([$q])$sep{0,1}|Ums", $string, $match)
  596.                 || preg_match_all("|(\=?[$q]).*([$q])$sep\s$|Ums", $string, $match)
  597.             ) {
  598.                 if ($match[1][0] == $match[2][0]) {
  599.                     $quote = $match[1][0];
  600.                     break;
  601.                 }
  602.             }
  603.         }
  604.  
  605.         $conf['quote'] = $quote;
  606.         return $conf;
  607.     }
  608.  
  609.     /**
  610.      * Front to call getPointer and moving the resource to the
  611.      * beginning of the file
  612.      * Reset it if you like.
  613.      *
  614.      * @param string $file The name of the file
  615.      * @param array  &$conf The configuration
  616.      * @param string $mode The open node (ex: FILE_MODE_READ or FILE_MODE_WRITE)
  617.      *
  618.      * @return boolean true on success false on failure
  619.      */
  620.     function resetPointer($file, &$conf, $mode)
  621.     {
  622.         if (!File_CSV::getPointer($file, $conf, $mode, true)) {
  623.             return false;
  624.         }
  625.  
  626.         return true;
  627.     }
  628. }