【问题标题】:How to read FoxPro Memo with PHP?如何用 PHP 阅读 FoxPro 备忘录?
【发布时间】:2009-12-22 16:10:04
【问题描述】:

我必须将 .DBF 和 .FPT 文件从 Visual FoxPro 转换为 MySQL。现在我的脚本适用于 .DBF 文件,它使用 dbase_open() 和 dbase_get_record_with_names() 打开并读取它们,然后执行 MySQL INSERT 命令。

但是,这些 .DBF 文件的某些字段属于 MEMO 类型,因此存储在以 .FPT 结尾的单独文件中。如何阅读此文件?

我在MSDN 中找到了此文件类型的规范,但我不知道如何使用 PHP 逐字节读取此文件(另外,我更喜欢更简单的解决方案)。

有什么想法吗?

【问题讨论】:

    标签: php mysql foxpro dbf


    【解决方案1】:

    好吧,我仔细研究了 DBF 和 FPT 文件结构的 MSDN 规范,结果是一个漂亮的 PHP 类,它可以同时打开一个 DBF 和(可选)一个 FPT 备忘录文件。这个类会给你一个又一个的记录,从而从备忘录文件中获取任何备忘录——如果打开的话。

    class Prodigy_DBF {
        private $Filename, $DB_Type, $DB_Update, $DB_Records, $DB_FirstData, $DB_RecordLength, $DB_Flags, $DB_CodePageMark, $DB_Fields, $FileHandle, $FileOpened;
        private $Memo_Handle, $Memo_Opened, $Memo_BlockSize;
    
        private function Initialize() {
    
            if($this->FileOpened) {
                fclose($this->FileHandle);
            }
    
            if($this->Memo_Opened) {
                fclose($this->Memo_Handle);
            }
    
            $this->FileOpened = false;
            $this->FileHandle = NULL;
            $this->Filename = NULL;
            $this->DB_Type = NULL;
            $this->DB_Update = NULL;
            $this->DB_Records = NULL;
            $this->DB_FirstData = NULL;
            $this->DB_RecordLength = NULL;
            $this->DB_CodePageMark = NULL;
            $this->DB_Flags = NULL;
            $this->DB_Fields = array();
    
            $this->Memo_Handle = NULL;
            $this->Memo_Opened = false;
            $this->Memo_BlockSize = NULL;
        }
    
        public function __construct($Filename, $MemoFilename = NULL) {
            $this->Prodigy_DBF($Filename, $MemoFilename);
        }
    
        public function Prodigy_DBF($Filename, $MemoFilename = NULL) {
            $this->Initialize();
            $this->OpenDatabase($Filename, $MemoFilename);
        }
    
        public function OpenDatabase($Filename, $MemoFilename = NULL) {
            $Return = false;
            $this->Initialize();
    
            $this->FileHandle = fopen($Filename, "r");
            if($this->FileHandle) {
                // DB Open, reading headers
                $this->DB_Type = dechex(ord(fread($this->FileHandle, 1)));
                $LUPD = fread($this->FileHandle, 3);
                $this->DB_Update = ord($LUPD[0])."/".ord($LUPD[1])."/".ord($LUPD[2]);
                $Rec = unpack("V", fread($this->FileHandle, 4));
                $this->DB_Records = $Rec[1];
                $Pos = fread($this->FileHandle, 2);
                $this->DB_FirstData = (ord($Pos[0]) + ord($Pos[1]) * 256);
                $Len = fread($this->FileHandle, 2);
                $this->DB_RecordLength = (ord($Len[0]) + ord($Len[1]) * 256);
                fseek($this->FileHandle, 28); // Ignoring "reserved" bytes, jumping to table flags
                $this->DB_Flags = dechex(ord(fread($this->FileHandle, 1)));
                $this->DB_CodePageMark = ord(fread($this->FileHandle, 1));
                fseek($this->FileHandle, 2, SEEK_CUR);    // Ignoring next 2 "reserved" bytes
    
                // Now reading field captions and attributes
                while(!feof($this->FileHandle)) {
    
                    // Checking for end of header
                    if(ord(fread($this->FileHandle, 1)) == 13) {
                        break;  // End of header!
                    } else {
                        // Go back
                        fseek($this->FileHandle, -1, SEEK_CUR);
                    }
    
                    $Field["Name"] = trim(fread($this->FileHandle, 11));
                    $Field["Type"] = fread($this->FileHandle, 1);
                    fseek($this->FileHandle, 4, SEEK_CUR);  // Skipping attribute "displacement"
                    $Field["Size"] = ord(fread($this->FileHandle, 1));
                    fseek($this->FileHandle, 15, SEEK_CUR); // Skipping any remaining attributes
                    $this->DB_Fields[] = $Field;
                }
    
                // Setting file pointer to the first record
                fseek($this->FileHandle, $this->DB_FirstData);
    
                $this->FileOpened = true;
    
                // Open memo file, if exists
                if(!empty($MemoFilename) and file_exists($MemoFilename) and preg_match("%^(.+).fpt$%i", $MemoFilename)) {
                    $this->Memo_Handle = fopen($MemoFilename, "r");
                    if($this->Memo_Handle) {
                        $this->Memo_Opened = true;
    
                        // Getting block size
                        fseek($this->Memo_Handle, 6);
                        $Data = unpack("n", fread($this->Memo_Handle, 2));
                        $this->Memo_BlockSize = $Data[1];
                    }
                }
            }
    
            return $Return;
        }
    
        public function GetNextRecord($FieldCaptions = false) {
            $Return = NULL;
            $Record = array();
    
            if(!$this->FileOpened) {
                $Return = false;
            } elseif(feof($this->FileHandle)) {
                $Return = NULL;
            } else {
                // File open and not EOF
                fseek($this->FileHandle, 1, SEEK_CUR);  // Ignoring DELETE flag
                foreach($this->DB_Fields as $Field) {
                    $RawData = fread($this->FileHandle, $Field["Size"]);
                    // Checking for memo reference
                    if($Field["Type"] == "M" and $Field["Size"] == 4 and !empty($RawData)) {
                        // Binary Memo reference
                        $Memo_BO = unpack("V", $RawData);
                        if($this->Memo_Opened and $Memo_BO != 0) {
                            fseek($this->Memo_Handle, $Memo_BO[1] * $this->Memo_BlockSize);
                            $Type = unpack("N", fread($this->Memo_Handle, 4));
                            if($Type[1] == "1") {
                                $Len = unpack("N", fread($this->Memo_Handle, 4));
                                $Value = trim(fread($this->Memo_Handle, $Len[1]));
                            } else {
                                // Pictures will not be shown
                                $Value = "{BINARY_PICTURE}";
                            }
                        } else {
                            $Value = "{NO_MEMO_FILE_OPEN}";
                        }
                    } else {
                        $Value = trim($RawData);
                    }
    
                    if($FieldCaptions) {
                        $Record[$Field["Name"]] = $Value;
                    } else {
                        $Record[] = $Value;
                    }
                }
    
                $Return = $Record;
            }
    
            return $Return;
        }
    
        function __destruct() {
            // Cleanly close any open files before destruction
            $this->Initialize();
        }
    }
    

    类可以这样使用:

        $Test = new Prodigy_DBF("customer.DBF", "customer.FPT");
        while(($Record = $Test->GetNextRecord(true)) and !empty($Record)) {
            print_r($Record);
        }
    

    这可能不是一门完美的课程,但它对我有用。随意使用此代码,但请注意,该类非常宽容 - 它不关心 fread() 和 fseek() 是否返回 true 或其他任何内容 - 因此您可能需要在使用前对其进行一些改进。

    另外请注意,有许多私有变量,例如记录数、记录大小等,目前未使用。

    【讨论】:

    【解决方案2】:

    我替换了这段代码:

    fseek($this->Memo_Handle, (trim($RawData) * $this->Memo_BlockSize)+8);
    $Value = trim(fread($this->Memo_Handle, $this->Memo_BlockSize));
    

    使用此代码:

    fseek($this->Memo_Handle, (trim($RawData) * $this->Memo_BlockSize)+4);
    $Len  = unpack("N", fread($this->Memo_Handle, 4));
    $Value = trim(fread($this->Memo_Handle, $Len[1] ));
    

    这对我有帮助

    【讨论】:

      【解决方案3】:

      虽然不是 PHP,但 VFP 是从 1 开始的引用,我认为 PHP 是从零开始的引用,因此您必须进行相应的解密和调整,但这很有效,希望您能够 完成后发布您对这部分的版本。

      VFP 中的 FILETOSTR() 将打开一个文件,并将整个内容读入 作为字符串的单个内存变量——所有转义键、高字节字符等都完好无损。您可能需要依赖 FOPEN()、FSEEK()、FCLOSE() 等。

      MemoTest.FPT 是我的示例备忘录表/文件 fpt1 = FILETOSTR("MEMOTEST.FPT")

      首先,您必须检测创建文件时使用的 MEMO BLOCK SIZE。通常这将是 64 字节,但根据您在帖子中的链接。

      标头位置 6-7 标识大小(VFP,位置 7 和 8)。第一个字节是高位

      nBlockSize = ASC( SUBSTR( fpt1, 7, 1 )) * 256 + ASC( SUBSTR( fpt1, 8, 1 ))
      

      现在,在您的个人记录中。无论您的 DBF 结构中哪里有备注 FIELD(并且每个记录结构可以有多个),都会有 4 个字节。在记录字段中,它标识了存储内容的备忘录文件中的“块”。

      MemoBytes = 4 个字节,位于您确定的字段位置。这些将存储为 0-255 的 ASCII。该字段以第一个字节作为低位存储,第 4 个字节作为 256^3 = 16777216 存储。第一个使用的“块”将从头占用的备忘录 .fpt 文件规范的 512 位置偏移开始位置 0-511。

      因此,如果您的第一个备注字段的内容为“8000”,其中 8 是实际的 0x08,而不是数字“8”,即 0x38,零是 0x00。

      YourMemoField = "8000"(实际使用 ascii,但为了便于阅读,显示 hex 预期值)

      First Byte is ASCII value  * 1   ( 256 ^ 0 )
      Second Byte is ASCII value * 256   (256 ^ 1)
      Third Byte is ASCII value * 65536   (256 ^ 2)
      Fourth Byte is ASCII value * 16777216 (256 ^ 3)
      
      nMemoBlock =  byte1 + ( byte2 * 256 ) + ( byte3 * 65536 ) + ( byte4 * 16777216 )
      

      现在,您需要 FSEEK() 到

      FSEEK( handle, nMemoBlock * nBlockSize +1 )
      

      对于您要查找的块的第一个字节。这将指向 BLOCK 标头。在这种情况下,根据规范,前 4 个字节标识块签名,后 4 个字节是内容的长度。对于这两个,字节首先以 HIGH-BYTE 存储。

      从您的 FSEEK() 中,它与上面的 nMemoBlock 的高字节相反。这里的“Byte1-4”来自你的 FSEEK() 位置

      nSignature = ( byte1 * 16777216 ) + ( byte2 * 65536 ) + ( byte3 * 256 ) + byte4
      
      nMemoLength = ( byte5 * 16777216 ) + ( byte6 * 65536 ) + ( byte7 * 256 ) + byte8
      

      现在,FSEEK() 到第 9 个字节(在您刚刚读取的标头的 8 个字节之后的数据的第一个实际字符,用于签名和备忘录长度)。这是您数据的开始。

      现在,阅读剩下的内容...

      FSEEK() +9 characters to new position
      
      cFinalMemoData = FREAD( handle, nMemoLength )
      

      我知道这并不完美,PHP 脚本也不完美,但它的伪代码足以说明事物的存储方式,希望能助您一臂之力。

      再次提醒您,在您逐步调试过程时请考虑到确保 0 或 1 个偏移量的基础。为了帮助简化和测试这一点,我创建了一个简单的 .DBF,其中包含 2 个字段...一个字符字段和一个备注字段,添加了一些记录和一些基本内容以确认所有内容、位置等。

      【讨论】:

      • 感谢您的详细解释。我正在开发的代码大致如您所描述的那样工作。但是,数据似乎存在问题,因为某些记录的字段比其他记录少,这意味着我无法判断记录何时开始以及记录何时结束。你说 DBF 会识别 FPT 中的块,你有更多关于这方面的信息吗?我正在使用 PHP 的 dbase 函数,我认为它们不会返回二进制数据... DBF 中的备注字段只是 NULL
      • 如果为 null,则不会有与该条目关联的备忘录内容。但是,为备注字段分配的字段字节将始终为 4 个字节。如果它有一个值,它将是我所描述的指向 .FPT 文件中的块的解析。
      【解决方案4】:
      <?
      class Prodigy_DBF {
          private $Filename, $DB_Type, $DB_Update, $DB_Records, $DB_FirstData, $DB_RecordLength, $DB_Flags, $DB_CodePageMark, $DB_Fields, $FileHandle, $FileOpened;
          private $Memo_Handle, $Memo_Opened, $Memo_BlockSize;
      
          private function Initialize() {
      
              if($this->FileOpened) {
                  fclose($this->FileHandle);
              }
      
              if($this->Memo_Opened) {
                  fclose($this->Memo_Handle);
              }
      
              $this->FileOpened = false;
              $this->FileHandle = NULL;
              $this->Filename = NULL;
              $this->DB_Type = NULL;
              $this->DB_Update = NULL;
              $this->DB_Records = NULL;
              $this->DB_FirstData = NULL;
              $this->DB_RecordLength = NULL;
              $this->DB_CodePageMark = NULL;
              $this->DB_Flags = NULL;
              $this->DB_Fields = array();
      
              $this->Memo_Handle = NULL;
              $this->Memo_Opened = false;
              $this->Memo_BlockSize = NULL;
          }
      
          public function __construct($Filename, $MemoFilename = NULL) {
              $this->Prodigy_DBF($Filename, $MemoFilename);
          }
      
          public function Prodigy_DBF($Filename, $MemoFilename = NULL) {
              $this->Initialize();
              $this->OpenDatabase($Filename, $MemoFilename);
          }
      
          public function OpenDatabase($Filename, $MemoFilename = NULL) {
              $Return = false;
              $this->Initialize();
      
              $this->FileHandle = fopen($Filename, "r");
              if($this->FileHandle) {
                  // DB Open, reading headers
                  $this->DB_Type = dechex(ord(fread($this->FileHandle, 1)));
                  $LUPD = fread($this->FileHandle, 3);
                  $this->DB_Update = ord($LUPD[0])."/".ord($LUPD[1])."/".ord($LUPD[2]);
                  $Rec = unpack("V", fread($this->FileHandle, 4));
                  $this->DB_Records = $Rec[1];
                  $Pos = fread($this->FileHandle, 2);
                  $this->DB_FirstData = (ord($Pos[0]) + ord($Pos[1]) * 256);
                  $Len = fread($this->FileHandle, 2);
                  $this->DB_RecordLength = (ord($Len[0]) + ord($Len[1]) * 256);
                  fseek($this->FileHandle, 28); // Ignoring "reserved" bytes, jumping to table flags
                  $this->DB_Flags = dechex(ord(fread($this->FileHandle, 1)));
                  $this->DB_CodePageMark = ord(fread($this->FileHandle, 1));
                  fseek($this->FileHandle, 2, SEEK_CUR);    // Ignoring next 2 "reserved" bytes
      
                  // Now reading field captions and attributes
                  while(!feof($this->FileHandle)) {
      
                      // Checking for end of header
                      if(ord(fread($this->FileHandle, 1)) == 13) {
                          break;  // End of header!
                      } else {
                          // Go back
                          fseek($this->FileHandle, -1, SEEK_CUR);
                      }
      
                      $Field["Name"] = trim(fread($this->FileHandle, 11));
                      $Field["Type"] = fread($this->FileHandle, 1);
                      fseek($this->FileHandle, 4, SEEK_CUR);  // Skipping attribute "displacement"
                      $Field["Size"] = ord(fread($this->FileHandle, 1));
                      fseek($this->FileHandle, 15, SEEK_CUR); // Skipping any remaining attributes
                      $this->DB_Fields[] = $Field;
                  }
      
                  // Setting file pointer to the first record
                  fseek($this->FileHandle, $this->DB_FirstData);
      
                  $this->FileOpened = true;
      
                  // Open memo file, if exists
                  if(!empty($MemoFilename) and file_exists($MemoFilename) and preg_match("%^(.+).fpt$%i", $MemoFilename)) {
                      $this->Memo_Handle = fopen($MemoFilename, "r");
                      if($this->Memo_Handle) {
                          $this->Memo_Opened = true;
      
                          // Getting block size
                          fseek($this->Memo_Handle, 6);
                          $Data = unpack("n", fread($this->Memo_Handle, 2));
                          $this->Memo_BlockSize = $Data[1];
                      }
                  }
              }
      
              return $Return;
          }
      
          public function GetNextRecord($FieldCaptions = false) {
              $Return = NULL;
              $Record = array();
      
              if(!$this->FileOpened) {
                  $Return = false;
              } elseif(feof($this->FileHandle)) {
                  $Return = NULL;
              } else {
                  // File open and not EOF
                  fseek($this->FileHandle, 1, SEEK_CUR);  // Ignoring DELETE flag
                  foreach($this->DB_Fields as $Field) {
                      $RawData = fread($this->FileHandle, $Field["Size"]);
                      // Checking for memo reference
                      if($Field["Type"] == "M" and $Field["Size"] == 4 and !empty($RawData)) {
                          // Binary Memo reference
                          $Memo_BO = unpack("V", $RawData);
                          if($this->Memo_Opened and $Memo_BO != 0) {
                              fseek($this->Memo_Handle, $Memo_BO[1] * $this->Memo_BlockSize);
                              $Type = unpack("N", fread($this->Memo_Handle, 4));
                              if($Type[1] == "1") {
                                  $Len = unpack("N", fread($this->Memo_Handle, 4));
                                  $Value = trim(fread($this->Memo_Handle, $Len[1]));
                              } else {
                                  // Pictures will not be shown
                                  $Value = "{BINARY_PICTURE}";
                          }
                      } else {
                          $Value = "{NO_MEMO_FILE_OPEN}";
                      }
                  } else {
                      if($Field["Type"] == "M"){
                          if(trim($RawData) > 0)   {
                              fseek($this->Memo_Handle, (trim($RawData) * $this->Memo_BlockSize)+8);
                              $Value = trim(fread($this->Memo_Handle, $this->Memo_BlockSize));
                          }
                      }else{
                          $Value = trim($RawData);
                      }
                  }
      
                  if($FieldCaptions) {
                      $Record[$Field["Name"]] = $Value;
                  } else {
                      $Record[] = $Value;
                  }
              }
      
                  $Return = $Record;
              }
      
              return $Return;
          }
      
          function __destruct() {
              // Cleanly close any open files before destruction
              $this->Initialize();
          }
      }
      ?>
      

      【讨论】:

      • 您的问题解决了吗?如果是这样,您修改了哪些行?
      • 是的..解决了我的问题.......改变了这个“$Value = trim($RawData);”到下面...我在这里添加: if($Field["Type"] == "M"){ if(trim($RawData) > 0) { fseek($this->Memo_Handle, (trim($RawData) * $this->Memo_BlockSize)+8); $Value = trim(fread($this->Memo_Handle, $this->Memo_BlockSize)); } }else{ $Value = trim($RawData);是 rite 做正确的事吗?但我解决了我的问题..
      【解决方案5】:

      我认为 PHP 中不太可能有 FoxPro 库。

      您可能需要从头开始编写代码。如需逐字节阅读,请联系fopen() fread() and colleagues

      编辑:似乎有一个Visual FoxPro ODBC driver。您可能能够通过 PDO 和该连接器连接到 FoxPro 数据库。我不知道成功的机会有多大,需要做多少工作。

      【讨论】:

      • 感谢您的快速回答。现在我正在尝试使用 fopen() 和 fread() 读取文件。我在这里面临的唯一问题是我不知道如何将 Byte 转换为 Integer(无论如何我如何将 BYTE 存储在 PHP 变量中?)。
      • OK 我做了一些研究,发现 unpack() 函数可以将原始数据转换为多种整数或其他格式。如果我能解决这个问题,我会告诉你的。否则 - 欢迎进一步的建议:)
      【解决方案6】:

      FPT 文件包含备忘录数据。在 DBF 中有 memo 类型的列,该列中的信息是指向 FPT 文件中条目的指针。

      如果您从表中查询数据,您只需引用备注列即可获取数据。您不需要单独解析 FPT 文件中的数据。 OLE DB 驱动程序(或 ODBC 驱动程序,如果您的文件是 VFP 6 或更早版本)应该只为您提供此信息。

      有一个工具可以自动将您的 Visual FoxPro 数据迁移到 MySQL。您可能想看看是否可以节省一些时间:

      转到http://leafe.com/dls/vfp

      并搜索“Stru2MySQL_2”以获取用于迁移数据结构的工具,并搜索“VFP2MySQL Data Upload program”以获取有助于迁移的工具。

      里克·舒默 VFP MVP

      【讨论】:

        【解决方案7】:

        您可能还想检查 PHP dbase 库。它们与 DBF 文件配合得很好。

        【讨论】:

        • 是的,您可以使用 dbase 函数轻松读取它们,但我发现这些函数不显示来自 MEMO 字段的数据(很可能存储在单独的文件中)。这就是为什么我编写了自己的类,它可以相应地自动读取 MEMO 文件。
        猜你喜欢
        • 2014-09-26
        • 1970-01-01
        • 2018-11-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多