即兴添加一下

.NET使用FastDBF读写DBF

FastDBF源代码地址:https://github.com/SocialExplorer/FastDBF

第一步在解决方案中新建一个类库的项目:取名为SocialExplorer.FastDBF

 

 

 

第二步:引入FASTDBF的源文件  源代码可以通过github地址下载引入

 

源文件:DbfColumn.cs

///
/// Author: Ahmed Lacevic
/// Date: 12/1/2007
/// 
/// Revision History:
/// -----------------------------------
///   Author:
///   Date:
///   Desc:

using System;
using System.Collections.Generic;
using System.Text;


namespace SocialExplorer.IO.FastDBF
{ 
  
  /// <summary>
  /// This class represents a DBF Column.
  /// </summary>
  /// 
  /// <remarks>
  /// Note that certain properties can not be modified after creation of the object. 
  /// This is because we are locking the header object after creation of a data row,
  /// and columns are part of the header so either we have to have a lock field for each column,
  /// or make it so that certain properties such as length can only be set during creation of a column.
  /// Otherwise a user of this object could modify a column that belongs to a locked header and thus corrupt the DBF file.
  /// </remarks>
  public class DbfColumn : ICloneable
  { 
    
    /*
     (FoxPro/FoxBase) Double integer *NOT* a memo field
     G     General     (dBASE V: like Memo) OLE Objects in MS Windows versions 
     P     Picture     (FoxPro) Like Memo fields, but not for text processing. 
     Y     Currency     (FoxPro)
     T     DateTime     (FoxPro)
     I     Integer     Length: 4 byte little endian integer     (FoxPro)
    */
    
    /// <summary>
    ///  Great information on DBF located here: 
    ///  http://www.clicketyclick.dk/databases/xbase/format/data_types.html
    ///  http://www.clicketyclick.dk/databases/xbase/format/dbf.html
    /// </summary>
    public enum DbfColumnType
    { 
      
      /// <summary>
      /// Character  less than 254 length
      /// ASCII text less than 254 characters long in dBASE. 
      /// 
      /// Character fields can be up to 32 KB long (in Clipper and FoxPro) using decimal 
      /// count as high byte in field length. It's possible to use up to 64KB long fields 
      /// by reading length as unsigned.
      /// 
      /// </summary>
      Character = 0,
      
      /// <summary>
      /// Number     Length: less than 18 
      ///   ASCII text up till 18 characters long (include sign and decimal point). 
      /// 
      /// Valid characters: 
      ///    "0" - "9" and "-". Number fields can be up to 20 characters long in FoxPro and Clipper. 
      /// </summary>
      /// <remarks>
      /// We are not enforcing this 18 char limit.
      /// </remarks>
      Number = 1,
      
      /// <summary>
      ///  L  Logical  Length: 1    Boolean/byte (8 bit) 
      ///  
      ///  Legal values: 
      ///   ?     Not initialised (default)
      ///   Y,y     Yes
      ///   N,n     No
      ///   F,f     False
      ///   T,t     True
      ///   Logical fields are always displayed using T/F/?. Some sources claims 
      ///   that space (ASCII 20h) is valid for not initialised. Space may occur, but is not defined.      
      /// </summary>
      Boolean = 2,
      
      /// <summary>
      /// D     Date     Length: 8  Date in format YYYYMMDD. A date like 0000-00- 00 is *NOT* valid. 
      /// </summary>
      Date = 3,
      
      /// <summary>
      /// M     Memo     Length: 10     Pointer to ASCII text field in memo file 10 digits representing a pointer to a DBT block (default is blanks). 
      /// </summary>
      Memo = 4,
      
      /// <summary>
      /// B     Binary          (dBASE V) Like Memo fields, but not for text processing.
      /// </summary>
      Binary = 5,
      
      /// <summary>
      /// I     Integer     Length: 4 byte little endian integer     (FoxPro)
      /// </summary>
      Integer = 6, 
      
    }
    
    
    /// <summary>
    /// Column (field) name
    /// </summary>
    private string mName;
    
    
    /// <summary>
    /// Field Type (Char, number, boolean, date, memo, binary)
    /// </summary>
    private DbfColumnType mType;
    
    
    /// <summary>
    /// Offset from the start of the record
    /// </summary>
    internal int mDataAddress;
    
    
    /// <summary>
    /// Length of the data in bytes; some rules apply which are in the spec (read more above).
    /// </summary>
    private int mLength;
    
    
    /// <summary>
    /// Decimal precision count, or number of digits afer decimal point. This applies to Number types only.
    /// </summary>
    private int mDecimalCount;
    
    
    
    /// <summary>
    /// Full spec constructor sets all relevant fields.
    /// </summary>
    /// <param name="sName"></param>
    /// <param name="type"></param>
    /// <param name="nLength"></param>
    /// <param name="nDecimals"></param>
    public DbfColumn(string sName, DbfColumnType type, int nLength, int nDecimals)
    { 
      
      Name = sName;
      mType = type;
      mLength = nLength;
      
      if(type == DbfColumnType.Number)
        mDecimalCount = nDecimals;
      else
        mDecimalCount = 0;
      
      
      
      //perform some simple integrity checks...
      //-------------------------------------------
      
      //decimal precision:
      //we could also fix the length property with a statement like this: mLength = mDecimalCount + 2;
      //lyq修改源码取消判断
      //if (mDecimalCount > 0 && mLength - mDecimalCount <= 1)
      //  throw new Exception("Decimal precision can not be larger than the length of the field.");
      
      if(mType == DbfColumnType.Integer)
        mLength = 4;
      
      if(mType == DbfColumnType.Binary)
        mLength = 1;
      
      if(mType == DbfColumnType.Date)
        mLength = 8;  //Dates are exactly yyyyMMdd
      
      if(mType == DbfColumnType.Memo)
        mLength = 10;  //Length: 10 Pointer to ASCII text field in memo file. pointer to a DBT block.
      
      if(mType == DbfColumnType.Boolean)
        mLength = 1;
      
      //field length:
      if (mLength <= 0)
        throw new Exception("Invalid field length specified. Field length can not be zero or less than zero.");
      else if (type != DbfColumnType.Character && type != DbfColumnType.Binary && mLength > 255)
        throw new Exception("Invalid field length specified. For numbers it should be within 20 digits, but we allow up to 255. For Char and binary types, length up to 65,535 is allowed. For maximum compatibility use up to 255.");
      else if((type == DbfColumnType.Character || type == DbfColumnType.Binary) && mLength > 65535)
        throw new Exception("Invalid field length specified. For Char and binary types, length up to 65535 is supported. For maximum compatibility use up to 255.");
      
      
    }
    
    
    /// <summary>
    /// Create a new column fully specifying all properties.
    /// </summary>
    /// <param name="sName">column name</param>
    /// <param name="type">type of field</param>
    /// <param name="nLength">field length including decimal places and decimal point if any</param>
    /// <param name="nDecimals">decimal places</param>
    /// <param name="nDataAddress">offset from start of record</param>
    internal DbfColumn(string sName, DbfColumnType type, int nLength, int nDecimals, int nDataAddress): this(sName, type, nLength, nDecimals)
    { 
      
      mDataAddress = nDataAddress;
      
    }
    
    
    public DbfColumn(string sName, DbfColumnType type): this(sName, type, 0, 0)
    { 
      if(type == DbfColumnType.Number || type == DbfColumnType.Character )
        throw new Exception("For number and character field types you must specify Length and Decimal Precision.");
      
    }
    
    
    /// <summary>
    /// Field Name.
    /// </summary>
    public string Name
    { 
      get
      { 
        return mName;
      }
      
      set
      { 
        //name:
        if (string.IsNullOrEmpty(value))
          throw new Exception("Field names must be at least one char long and can not be null.");
        
        if (value.Length > 11)
          throw new Exception("Field names can not be longer than 11 chars.");
        
        mName = value;
        
      }
      
    }
    
    
    /// <summary>
    /// Field Type (C N L D or M).
    /// </summary>
    public DbfColumnType ColumnType
    { 
      get
      { 
        return mType;
      }
    }
    
    
    /// <summary>
    /// Returns column type as a char, (as written in the DBF column header)
    /// N=number, C=char, B=binary, L=boolean, D=date, I=integer, M=memo
    /// </summary>
    public char ColumnTypeChar
    { 
      get
      { 
        switch(mType)
        {
          case DbfColumnType.Number:
            return 'N';
          
          case DbfColumnType.Character:
            return 'C';
          
          case DbfColumnType.Binary:
            return 'B';
            
          case DbfColumnType.Boolean:
            return 'L';
          
          case DbfColumnType.Date:
            return 'D';
          
          case DbfColumnType.Integer:
            return 'I';

          case DbfColumnType.Memo:
            return 'M';
          
        }
        
        throw new Exception("Unrecognized field type!");
        
      }
    }
    
    
    /// <summary>
    /// Field Data Address offset from the start of the record.
    /// </summary>
    public int DataAddress
    { 
      get
      { 
        return mDataAddress;
      }
    }

    /// <summary>
    /// Length of the data in bytes.
    /// </summary>
    public int Length
    { 
      get
      { 
        return mLength;
      }
    }

    /// <summary>
    /// Field decimal count in Binary, indicating where the decimal is.
    /// </summary>
    public int DecimalCount
    { 
      get
      { 
        return mDecimalCount;
      }
      
    }
    
    
    
    
    
    /// <summary>
    /// Returns corresponding dbf field type given a .net Type.
    /// </summary>
    /// <param name="type"></param>
    /// <returns></returns>
    public static DbfColumnType GetDbaseType(Type type)
    { 
      
      if (type == typeof(string))
        return DbfColumnType.Character;
      else if (type == typeof(double) || type == typeof(float))
        return DbfColumnType.Number;
      else if (type == typeof(bool))
        return DbfColumnType.Boolean;
      else if (type == typeof(DateTime))
        return DbfColumnType.Date;
      
      throw new NotSupportedException(String.Format("{0} does not have a corresponding dbase type.", type.Name));
      
    }
    
    public static DbfColumnType GetDbaseType(char c)
    { 
      switch(c.ToString().ToUpper())
      { 
        case "C": return DbfColumnType.Character;
        case "N": return DbfColumnType.Number;
        case "B": return DbfColumnType.Binary;
        case "L": return DbfColumnType.Boolean;
        case "D": return DbfColumnType.Date;
        case "I": return DbfColumnType.Integer;
        case "M": return DbfColumnType.Memo;
      }
      
      throw new NotSupportedException(String.Format("{0} does not have a corresponding dbase type.", c));
      
    }
    
    /// <summary>
    /// Returns shp file Shape Field.
    /// </summary>
    /// <returns></returns>
    public static DbfColumn ShapeField()
    { 
      return new DbfColumn("Geometry", DbfColumnType.Binary);
      
    }
    
    
    /// <summary>
    /// Returns Shp file ID field.
    /// </summary>
    /// <returns></returns>
    public static DbfColumn IdField()
    { 
      return new DbfColumn("Row", DbfColumnType.Integer);
      
    }



    public object Clone()
    {
        return this.MemberwiseClone();
    }
  }
}
View Code

源文件:DbfDataTruncateException.cs

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Serialization;


namespace SocialExplorer.IO.FastDBF
{ 
  
  public class DbfDataTruncateException: Exception
  { 
    
    public DbfDataTruncateException(string smessage): base(smessage)
    { 
    }

    public DbfDataTruncateException(string smessage, Exception innerException)
      : base(smessage, innerException)
    { 
    }

    public DbfDataTruncateException(SerializationInfo info, StreamingContext context)
      : base(info, context)
    {
    }
    
  }
}
View Code

源文件:DbfFile.cs

///
/// Author: Ahmed Lacevic
/// Date: 12/1/2007
/// Desc: This class represents a DBF file. You can create, open, update and save DBF files using this class and supporting classes.
/// Also, this class supports reading/writing from/to an internet forward only type of stream!
/// 
/// Revision History:
/// -----------------------------------
///   Author:
///   Date:
///   Desc:


using System;
using System.Collections.Generic;
using System.Text;
using System.IO;


namespace SocialExplorer.IO.FastDBF
{

    /// <summary>
    /// This class represents a DBF file. You can create new, open, update and save DBF files using this class and supporting classes.
    /// Also, this class supports reading/writing from/to an internet forward only type of stream!
    /// </summary>
    /// <remarks>
    /// TODO: add end of file byte '0x1A' !!!
    /// We don't relly on that byte at all, and everything works with or without that byte, but it should be there by spec.
    /// </remarks>
    public class DbfFile
    {

        /// <summary>
        /// Helps read/write dbf file header information.
        /// </summary>
        protected DbfHeader mHeader;


        /// <summary>
        /// flag that indicates whether the header was written or not...
        /// </summary>
        protected bool mHeaderWritten = false;


        /// <summary>
        /// Streams to read and write to the DBF file.
        /// </summary>
        protected Stream mDbfFile = null;
        protected BinaryReader mDbfFileReader = null;
        protected BinaryWriter mDbfFileWriter = null;

        private Encoding encoding = Encoding.ASCII;

        /// <summary>
        /// File that was opened, if one was opened at all.
        /// </summary>
        protected string mFileName = "";


        /// <summary>
        /// Number of records read using ReadNext() methods only. This applies only when we are using a forward-only stream.
        /// mRecordsReadCount is used to keep track of record index. With a seek enabled stream, 
        /// we can always calculate index using stream position.
        /// </summary>
        protected int mRecordsReadCount = 0;


        /// <summary>
        /// keep these values handy so we don't call functions on every read.
        /// </summary>
        protected bool mIsForwardOnly = false;
        protected bool mIsReadOnly = false;

        [Obsolete]
        public DbfFile()
            : this(Encoding.ASCII)
        {
        }

        public DbfFile(Encoding encoding)
        {
            this.encoding = encoding;
            mHeader = new DbfHeader(encoding);
        }

        /// <summary>
        /// Open a DBF from a FileStream. This can be a file or an internet connection stream. Make sure that it is positioned at start of DBF file.
        /// Reading a DBF over the internet we can not determine size of the file, so we support HasMore(), ReadNext() interface. 
        /// RecordCount information in header can not be trusted always, since some packages store 0 there.
        /// </summary>
        /// <param name="ofs"></param>
        public void Open(Stream ofs)
        {
            if (mDbfFile != null)
                Close();

            mDbfFile = ofs;
            mDbfFileReader = null;
            mDbfFileWriter = null;

            if (mDbfFile.CanRead)
                mDbfFileReader = new BinaryReader(mDbfFile, encoding);

            if (mDbfFile.CanWrite)
                mDbfFileWriter = new BinaryWriter(mDbfFile, encoding);

            //reset position
            mRecordsReadCount = 0;

            //assume header is not written
            mHeaderWritten = false;

            //read the header
            if (ofs.CanRead)
            {
                //try to read the header...
                try
                {
                    mHeader.Read(mDbfFileReader);
                    mHeaderWritten = true;

                }
                catch (EndOfStreamException)
                {
                    //could not read header, file is empty
                    mHeader = new DbfHeader(encoding);
                    mHeaderWritten = false;
                }


            }

            if (mDbfFile != null)
            {
                mIsReadOnly = !mDbfFile.CanWrite;
                mIsForwardOnly = !mDbfFile.CanSeek;
            }


        }



        /// <summary>
        /// Open a DBF file or create a new one.
        /// </summary>
        /// <param name="sPath">Full path to the file.</param>
        /// <param name="mode"></param>
        public void Open(string sPath, FileMode mode, FileAccess access, FileShare share)
        {
            mFileName = sPath;
            Open(File.Open(sPath, mode, access, share));
        }

        /// <summary>
        /// Open a DBF file or create a new one.
        /// </summary>
        /// <param name="sPath">Full path to the file.</param>
        /// <param name="mode"></param>
        public void Open(string sPath, FileMode mode, FileAccess access)
        {
            mFileName = sPath;
            Open(File.Open(sPath, mode, access));
        }

        /// <summary>
        /// Open a DBF file or create a new one.
        /// </summary>
        /// <param name="sPath">Full path to the file.</param>
        /// <param name="mode"></param>
        public void Open(string sPath, FileMode mode)
        {
            mFileName = sPath;
            Open(File.Open(sPath, mode));
        }


        /// <summary>
        /// Creates a new DBF 4 file. Overwrites if file exists! Use Open() function for more options.
        /// </summary>
        /// <param name="sPath"></param>
        public void Create(string sPath)
        {
            Open(sPath, FileMode.Create, FileAccess.ReadWrite);
            mHeaderWritten = false;

        }



        /// <summary>
        /// Update header info, flush buffers and close streams. You should always call this method when you are done with a DBF file.
        /// </summary>
        public void Close()
        {

            //try to update the header if it has changed
            //------------------------------------------
            if (mHeader.IsDirty)
                WriteHeader();



            //Empty header...
            //--------------------------------
            mHeader = new DbfHeader(encoding);
            mHeaderWritten = false;


            //reset current record index
            //--------------------------------
            mRecordsReadCount = 0;


            //Close streams...
            //--------------------------------
            if (mDbfFileWriter != null)
            {
                mDbfFileWriter.Flush();
                mDbfFileWriter.Close();
            }

            if (mDbfFileReader != null)
                mDbfFileReader.Close();

            if (mDbfFile != null)
                mDbfFile.Close();


            //set streams to null
            //--------------------------------
            mDbfFileReader = null;
            mDbfFileWriter = null;
            mDbfFile = null;

            mFileName = "";

        }



        /// <summary>
        /// Returns true if we can not write to the DBF file stream.
        /// </summary>
        public bool IsReadOnly
        {
            get
            {
                return mIsReadOnly;
                /*
                if (mDbfFile != null)
                  return !mDbfFile.CanWrite; 
                return true;
                */

            }

        }


        /// <summary>
        /// Returns true if we can not seek to different locations within the file, such as internet connections.
        /// </summary>
        public bool IsForwardOnly
        {
            get
            {
                return mIsForwardOnly;
                /*
                if(mDbfFile!=null)
                  return !mDbfFile.CanSeek;
        
                return false;
                */
            }
        }


        /// <summary>
        /// Returns the name of the filestream.
        /// </summary>
        public string FileName
        {
            get
            {
                return mFileName;
            }
        }



        /// <summary>
        /// Read next record and fill data into parameter oFillRecord. Returns true if a record was read, otherwise false.
        /// </summary>
        /// <param name="oFillRecord"></param>
        /// <returns></returns>
        public bool ReadNext(DbfRecord oFillRecord)
        {

            //check if we can fill this record with data. it must match record size specified by header and number of columns.
            //we are not checking whether it comes from another DBF file or not, we just need the same structure. Allow flexibility but be safe.
            if (oFillRecord.Header != mHeader && (oFillRecord.Header.ColumnCount != mHeader.ColumnCount || oFillRecord.Header.RecordLength != mHeader.RecordLength))
                throw new Exception("Record parameter does not have the same size and number of columns as the " +
                                    "header specifies, so we are unable to read a record into oFillRecord. " +
                                    "This is a programming error, have you mixed up DBF file objects?");

            //DBF file reader can be null if stream is not readable...
            if (mDbfFileReader == null)
                throw new Exception("Read stream is null, either you have opened a stream that can not be " +
                                    "read from (a write-only stream) or you have not opened a stream at all.");

            //read next record...
            bool bRead = oFillRecord.Read(mDbfFile);

            if (bRead)
            {
                if (mIsForwardOnly)
                {
                    //zero based index! set before incrementing count.
                    oFillRecord.RecordIndex = mRecordsReadCount;
                    mRecordsReadCount++;
                }
                else
                    oFillRecord.RecordIndex = ((int)((mDbfFile.Position - mHeader.HeaderLength) / mHeader.RecordLength)) - 1;

            }

            return bRead;

        }


        /// <summary>
        /// Tries to read a record and returns a new record object or null if nothing was read.
        /// </summary>
        /// <returns></returns>
        public DbfRecord ReadNext()
        {
            //create a new record and fill it.
            DbfRecord orec = new DbfRecord(mHeader);

            return ReadNext(orec) ? orec : null;

        }



        /// <summary>
        /// Reads a record specified by index into oFillRecord object. You can use this method 
        /// to read in and process records without creating and discarding record objects.
        /// Note that you should check that your stream is not forward-only! If you have a forward only stream, use ReadNext() functions.
        /// </summary>
        /// <param name="index">Zero based record index.</param>
        /// <param name="oFillRecord">Record object to fill, must have same size and number of fields as thid DBF file header!</param>
        /// <remarks>
        /// <returns>True if read a record was read, otherwise false. If you read end of file false will be returned and oFillRecord will NOT be modified!</returns>
        /// The parameter record (oFillRecord) must match record size specified by the header and number of columns as well.
        /// It does not have to come from the same header, but it must match the structure. We are not going as far as to check size of each field.
        /// The idea is to be flexible but safe. It's a fine balance, these two are almost always at odds.
        /// </remarks>
        public bool Read(int index, DbfRecord oFillRecord)
        {

            //check if we can fill this record with data. it must match record size specified by header and number of columns.
            //we are not checking whether it comes from another DBF file or not, we just need the same structure. Allow flexibility but be safe.
            if (oFillRecord.Header != mHeader && (oFillRecord.Header.ColumnCount != mHeader.ColumnCount || oFillRecord.Header.RecordLength != mHeader.RecordLength))
                throw new Exception("Record parameter does not have the same size and number of columns as the " +
                                    "header specifies, so we are unable to read a record into oFillRecord. " +
                                    "This is a programming error, have you mixed up DBF file objects?");

            //DBF file reader can be null if stream is not readable...
            if (mDbfFileReader == null)
                throw new Exception("ReadStream is null, either you have opened a stream that can not be " +
                                    "read from (a write-only stream) or you have not opened a stream at all.");


            //move to the specified record, note that an exception will be thrown is stream is not seekable! 
            //This is ok, since we provide a function to check whether the stream is seekable. 
            long nSeekToPosition = mHeader.HeaderLength + (index * mHeader.RecordLength);

            //check whether requested record exists. Subtract 1 from file length (there is a terminating character 1A at the end of the file)
            //so if we hit end of file, there are no more records, so return false;
            if (index < 0 || mDbfFile.Length - 1 <= nSeekToPosition)
                return false;

            //move to record and read
            mDbfFile.Seek(nSeekToPosition, SeekOrigin.Begin);

            //read the record
            bool bRead = oFillRecord.Read(mDbfFile);
            if (bRead)
                oFillRecord.RecordIndex = index;

            return bRead;

        }

        public bool ReadValue(int rowIndex, int columnIndex, out string result)
        {

            result = String.Empty;

            DbfColumn ocol = mHeader[columnIndex];

            //move to the specified record, note that an exception will be thrown is stream is not seekable! 
            //This is ok, since we provide a function to check whether the stream is seekable. 
            long nSeekToPosition = mHeader.HeaderLength + (rowIndex * mHeader.RecordLength) + ocol.DataAddress;

            //check whether requested record exists. Subtract 1 from file length (there is a terminating character 1A at the end of the file)
            //so if we hit end of file, there are no more records, so return false;
            if (rowIndex < 0 || mDbfFile.Length - 1 <= nSeekToPosition)
                return false;

            //move to position and read
            mDbfFile.Seek(nSeekToPosition, SeekOrigin.Begin);

            //read the value
            byte[] data = new byte[ocol.Length];
            mDbfFile.Read(data, 0, ocol.Length);
            result = new string(encoding.GetChars(data, 0, ocol.Length));

            return true;
        }

        /// <summary>
        /// Reads a record specified by index. This method requires the stream to be able to seek to position. 
        /// If you are using a http stream, or a stream that can not stream, use ReadNext() methods to read in all records.
        /// </summary>
        /// <param name="index">Zero based index.</param>
        /// <returns>Null if record can not be read, otherwise returns a new record.</returns>
        public DbfRecord Read(int index)
        {
            //create a new record and fill it.
            DbfRecord orec = new DbfRecord(mHeader);

            return Read(index, orec) ? orec : null;

        }




        /// <summary>
        /// Write a record to file. If RecordIndex is present, record will be updated, otherwise a new record will be written.
        /// Header will be output first if this is the first record being writen to file. 
        /// This method does not require stream seek capability to add a new record.
        /// </summary>
        /// <param name="orec"></param>
        public void Write(DbfRecord orec)
        {

            //if header was never written, write it first, then output the record
            if (!mHeaderWritten)
                WriteHeader();

            //if this is a new record (RecordIndex should be -1 in that case)
            if (orec.RecordIndex < 0)
            {
                if (mDbfFileWriter.BaseStream.CanSeek)
                {
                    //calculate number of records in file. do not rely on header's RecordCount property since client can change that value.
                    //also note that some DBF files do not have ending 0x1A byte, so we subtract 1 and round off 
                    //instead of just cast since cast would just drop decimals.
                    int nNumRecords = (int)Math.Round(((double)(mDbfFile.Length - mHeader.HeaderLength - 1) / mHeader.RecordLength));
                    if (nNumRecords < 0)
                        nNumRecords = 0;

                    orec.RecordIndex = nNumRecords;
                    Update(orec);
                    mHeader.RecordCount++;

                }
                else
                {
                    //we can not position this stream, just write out the new record.
                    orec.Write(mDbfFile);
                    mHeader.RecordCount++;
                }
            }
            else
                Update(orec);

        }

        public void Write(DbfRecord orec, bool bClearRecordAfterWrite)
        {

            Write(orec);

            if (bClearRecordAfterWrite)
                orec.Clear();

        }


        /// <summary>
        /// Update a record. RecordIndex (zero based index) must be more than -1, otherwise an exception is thrown.
        /// You can also use Write method which updates a record if it has RecordIndex or adds a new one if RecordIndex == -1.
        /// RecordIndex is set automatically when you call any Read() methods on this class.
        /// </summary>
        /// <param name="orec"></param>
        public void Update(DbfRecord orec)
        {

            //if header was never written, write it first, then output the record
            if (!mHeaderWritten)
                WriteHeader();


            //Check if record has an index
            if (orec.RecordIndex < 0)
                throw new Exception("RecordIndex is not set, unable to update record. Set RecordIndex or call Write() method to add a new record to file.");


            //Check if this record matches record size specified by header and number of columns. 
            //Client can pass a record from another DBF that is incompatible with this one and that would corrupt the file.
            if (orec.Header != mHeader && (orec.Header.ColumnCount != mHeader.ColumnCount || orec.Header.RecordLength != mHeader.RecordLength))
                throw new Exception("Record parameter does not have the same size and number of columns as the " +
                                    "header specifies. Writing this record would corrupt the DBF file. " +
                                    "This is a programming error, have you mixed up DBF file objects?");

            //DBF file writer can be null if stream is not writable to...
            if (mDbfFileWriter == null)
                throw new Exception("Write stream is null. Either you have opened a stream that can not be " +
                                    "writen to (a read-only stream) or you have not opened a stream at all.");


            //move to the specified record, note that an exception will be thrown if stream is not seekable! 
            //This is ok, since we provide a function to check whether the stream is seekable. 
            long nSeekToPosition = (long)mHeader.HeaderLength + (long)((long)orec.RecordIndex * (long)mHeader.RecordLength);

            //check whether we can seek to this position. Subtract 1 from file length (there is a terminating character 1A at the end of the file)
            //so if we hit end of file, there are no more records, so return false;
            if (mDbfFile.Length < nSeekToPosition)
                throw new Exception("Invalid record position. Unable to save record.");

            //move to record start
            mDbfFile.Seek(nSeekToPosition, SeekOrigin.Begin);

            //write
            orec.Write(mDbfFile);


        }



        /// <summary>
        /// Save header to file. Normally, you do not have to call this method, header is saved 
        /// automatically and updated when you close the file (if it changed).
        /// </summary>
        public bool WriteHeader()
        {

            //update header if possible
            //--------------------------------
            if (mDbfFileWriter != null)
            {
                if (mDbfFileWriter.BaseStream.CanSeek)
                {
                    mDbfFileWriter.Seek(0, SeekOrigin.Begin);
                    mHeader.Write(mDbfFileWriter);
                    mHeaderWritten = true;
                    return true;
                }
                else
                {
                    //if stream can not seek, then just write it out and that's it.
                    if (!mHeaderWritten)
                        mHeader.Write(mDbfFileWriter);

                    mHeaderWritten = true;

                }
            }

            return false;

        }



        /// <summary>
        /// Access DBF header with information on columns. Use this object for faster access to header. 
        /// Remove one layer of function calls by saving header reference and using it directly to access columns.
        /// </summary>
        public DbfHeader Header
        {
            get
            {
                return mHeader;
            }
        }



    }
}
View Code

源文件:DbfHeader.cs

///
/// Author: Ahmed Lacevic
/// Date: 12/1/2007
/// Desc: 
/// 
/// Revision History:
/// -----------------------------------
///   Author:
///   Date:
///   Desc:


using System;
using System.Collections.Generic;
using System.Text;
using System.IO;



namespace SocialExplorer.IO.FastDBF
{

    /// <summary>
    /// This class represents a DBF IV file header.
    /// </summary>
    /// 
    /// <remarks>
    /// DBF files are really wasteful on space but this legacy format lives on because it's really really simple. 
    /// It lacks much in features though.
    /// 
    /// 
    /// Thanks to Erik Bachmann for providing the DBF file structure information!!
    /// http://www.clicketyclick.dk/databases/xbase/format/dbf.html
    /// 
    ///           _______________________  _______
    /// 00h /   0| Version number      *1|  ^
    ///          |-----------------------|  |
    /// 01h /   1| Date of last update   |  |
    /// 02h /   2|      YYMMDD        *21|  |
    /// 03h /   3|                    *14|  |
    ///          |-----------------------|  |
    /// 04h /   4| Number of records     | Record
    /// 05h /   5| in data file          | header
    /// 06h /   6| ( 32 bits )        *14|  |
    /// 07h /   7|                       |  |
    ///          |-----------------------|  |
    /// 08h /   8| Length of header   *14|  |
    /// 09h /   9| structure ( 16 bits ) |  |
    ///          |-----------------------|  |
    /// 0Ah /  10| Length of each record |  |
    /// 0Bh /  11| ( 16 bits )     *2 *14|  |
    ///          |-----------------------|  |
    /// 0Ch /  12| ( Reserved )        *3|  |
    /// 0Dh /  13|                       |  |
    ///          |-----------------------|  |
    /// 0Eh /  14| Incomplete transac.*12|  |
    ///          |-----------------------|  |
    /// 0Fh /  15| Encryption flag    *13|  |
    ///          |-----------------------|  |
    /// 10h /  16| Free record thread    |  |
    /// 11h /  17| (reserved for LAN     |  |
    /// 12h /  18|  only )               |  |
    /// 13h /  19|                       |  |
    ///          |-----------------------|  |
    /// 14h /  20| ( Reserved for        |  |            _        |=======================| ______
    ///          |   multi-user dBASE )  |  |           / 00h /  0| Field name in ASCII   |  ^
    ///          : ( dBASE III+ - )      :  |          /          : (terminated by 00h)   :  |
    ///          :                       :  |         |           |                       |  |
    /// 1Bh /  27|                       |  |         |   0Ah / 10|                       |  |
    ///          |-----------------------|  |         |           |-----------------------| For
    /// 1Ch /  28| MDX flag (dBASE IV)*14|  |         |   0Bh / 11| Field type (ASCII) *20| each
    ///          |-----------------------|  |         |           |-----------------------| field
    /// 1Dh /  29| Language driver     *5|  |        /    0Ch / 12| Field data address    |  |
    ///          |-----------------------|  |       /             |                     *6|  |
    /// 1Eh /  30| ( Reserved )          |  |      /              | (in memory !!!)       |  |
    /// 1Fh /  31|                     *3|  |     /       0Fh / 15| (dBASE III+)          |  |
    ///          |=======================|__|____/                |-----------------------|  |  -
    /// 20h /  32|                       |  |  ^          10h / 16| Field length       *22|  |   |
    ///          |- - - - - - - - - - - -|  |  |                  |-----------------------|  |   | *7
    ///          |                    *19|  |  |          11h / 17| Decimal count      *23|  |   |
    ///          |- - - - - - - - - - - -|  |  Field              |-----------------------|  |  -
    ///          |                       |  | Descriptor  12h / 18| ( Reserved for        |  |
    ///          :. . . . . . . . . . . .:  |  |array     13h / 19|   multi-user dBASE)*18|  |
    ///          :                       :  |  |                  |-----------------------|  |
    ///       n  |                       |__|__v_         14h / 20| Work area ID       *16|  |
    ///          |-----------------------|  |    \                |-----------------------|  |
    ///       n+1| Terminator (0Dh)      |  |     \       15h / 21| ( Reserved for        |  |
    ///          |=======================|  |      \      16h / 22|   multi-user dBASE )  |  |
    ///       m  | Database Container    |  |       \             |-----------------------|  |
    ///          :                    *15:  |        \    17h / 23| Flag for SET FIELDS   |  |
    ///          :                       :  |         |           |-----------------------|  |
    ///     / m+263                      |  |         |   18h / 24| ( Reserved )          |  |
    ///          |=======================|__v_ ___    |           :                       :  |
    ///          :                       :    ^       |           :                       :  |
    ///          :                       :    |       |           :                       :  |
    ///          :                       :    |       |   1Eh / 30|                       |  |
    ///          | Record structure      |    |       |           |-----------------------|  |
    ///          |                       |    |        \  1Fh / 31| Index field flag    *8|  |
    ///          |                       |    |         \_        |=======================| _v_____
    ///          |                       | Records
    ///          |-----------------------|    |
    ///          |                       |    |          _        |=======================| _______
    ///          |                       |    |         / 00h /  0| Record deleted flag *9|  ^
    ///          |                       |    |        /          |-----------------------|  |
    ///          |                       |    |       /           | Data               *10|  One
    ///          |                       |    |      /            : (ASCII)            *17: record
    ///          |                       |____|_____/             |                       |  |
    ///          :                       :    |                   |                       | _v_____
    ///          :                       :____|_____              |=======================|
    ///          :                       :    |
    ///          |                       |    |
    ///          |                       |    |
    ///          |                       |    |
    ///          |                       |    |
    ///          |                       |    |
    ///          |=======================|    |
    ///          |__End_of_File__________| ___v____  End of file ( 1Ah )  *11
    /// 
    /// </remarks>
    public class DbfHeader : ICloneable
    {

        /// <summary>
        /// Header file descriptor size is 33 bytes (32 bytes + 1 terminator byte), followed by column metadata which is 32 bytes each.
        /// </summary>
        public const int FileDescriptorSize = 33;


        /// <summary>
        /// Field or DBF Column descriptor is 32 bytes long.
        /// </summary>
        public const int ColumnDescriptorSize = 32;


        //type of the file, must be 03h
        private const int mFileType = 0x03;

        //Date the file was last updated.
        private DateTime mUpdateDate;

        //Number of records in the datafile, 32bit little-endian, unsigned 
        private uint mNumRecords = 0;

        //Length of the header structure
        private ushort mHeaderLength = FileDescriptorSize;  //empty header is 33 bytes long. Each column adds 32 bytes.

        //Length of the records, ushort - unsigned 16 bit integer
        private int mRecordLength = 1;  //start with 1 because the first byte is a delete flag

        //DBF fields/columns
        internal List<DbfColumn> mFields = new List<DbfColumn>();


        //indicates whether header columns can be modified!
        bool mLocked = false;

        //keeps column name index for the header, must clear when header columns change.
        private Dictionary<string, int> mColumnNameIndex = null;

        /// <summary>
        /// When object is modified dirty flag is set.
        /// </summary>
        bool mIsDirty = false;


        /// <summary>
        /// mEmptyRecord is an array used to clear record data in CDbf4Record.
        /// This is shared by all record objects, used to speed up clearing fields or entire record.
        /// <seealso cref="EmptyDataRecord"/>
        /// </summary>
        private byte[] mEmptyRecord = null;


        public readonly Encoding encoding = Encoding.ASCII;


        [Obsolete]
        public DbfHeader()
        {
        }

        public DbfHeader(Encoding encoding)
        {
            this.encoding = encoding;
        }


        /// <summary>
        /// Specify initial column capacity.
        /// </summary>
        /// <param name="nInitialFields"></param>
        public DbfHeader(int nFieldCapacity)
        {
            mFields = new List<DbfColumn>(nFieldCapacity);

        }


        /// <summary>
        /// Gets header length.
        /// </summary>
        public ushort HeaderLength
        {
            get
            {
                return mHeaderLength;
            }
        }


        /// <summary>
        /// Add a new column to the DBF header.
        /// </summary>
        /// <param name="oNewCol"></param>
        public void AddColumn(DbfColumn oNewCol)
        {

            //throw exception if the header is locked
            if (mLocked)
                throw new InvalidOperationException("This header is locked and can not be modified. Modifying the header would result in a corrupt DBF file. You can unlock the header by calling UnLock() method.");

            //since we are breaking the spec rules about max number of fields, we should at least 
            //check that the record length stays within a number that can be recorded in the header!
            //we have 2 unsigned bytes for record length for a maximum of 65535.
            if (mRecordLength + oNewCol.Length > 65535)
                throw new ArgumentOutOfRangeException("oNewCol", "Unable to add new column. Adding this column puts the record length over the maximum (which is 65535 bytes).");


            //add the column
            mFields.Add(oNewCol);

            //update offset bits, record and header lengths
            oNewCol.mDataAddress = mRecordLength;
            mRecordLength += oNewCol.Length;
            mHeaderLength += ColumnDescriptorSize;

            //clear empty record
            mEmptyRecord = null;

            //set dirty bit
            mIsDirty = true;
            mColumnNameIndex = null;

        }


        /// <summary>
        /// Create and add a new column with specified name and type.
        /// </summary>
        /// <param name="sName"></param>
        /// <param name="type"></param>
        public void AddColumn(string sName, DbfColumn.DbfColumnType type)
        {
            AddColumn(new DbfColumn(sName, type));
        }


        /// <summary>
        /// Create and add a new column with specified name, type, length, and decimal precision.
        /// </summary>
        /// <param name="sName">Field name. Uniqueness is not enforced.</param>
        /// <param name="type"></param>
        /// <param name="nLength">Length of the field including decimal point and decimal numbers</param>
        /// <param name="nDecimals">Number of decimal places to keep.</param>
        public void AddColumn(string sName, DbfColumn.DbfColumnType type, int nLength, int nDecimals)
        {
            AddColumn(new DbfColumn(sName, type, nLength, nDecimals));
        }


        /// <summary>
        /// Remove column from header definition.
        /// </summary>
        /// <param name="nIndex"></param>
        public void RemoveColumn(int nIndex)
        {
            //throw exception if the header is locked
            if (mLocked)
                throw new InvalidOperationException("This header is locked and can not be modified. Modifying the header would result in a corrupt DBF file. You can unlock the header by calling UnLock() method.");


            DbfColumn oColRemove = mFields[nIndex];
            mFields.RemoveAt(nIndex);


            oColRemove.mDataAddress = 0;
            mRecordLength -= oColRemove.Length;
            mHeaderLength -= ColumnDescriptorSize;

            //if you remove a column offset shift for each of the columns 
            //following the one removed, we need to update those offsets.
            int nRemovedColLen = oColRemove.Length;
            for (int i = nIndex; i < mFields.Count; i++)
                mFields[i].mDataAddress -= nRemovedColLen;

            //clear the empty record
            mEmptyRecord = null;

            //set dirty bit
            mIsDirty = true;
            mColumnNameIndex = null;

        }


        /// <summary>
        /// Look up a column index by name. Note that this is case sensitive, internally it does a lookup using a dictionary.
        /// </summary>
        /// <param name="sName"></param>
        public DbfColumn this[string sName]
        {
            get
            {
                int colIndex = FindColumn(sName);
                if (colIndex > -1)
                    return mFields[colIndex];

                return null;

            }
        }


        /// <summary>
        /// Returns column at specified index. Index is 0 based.
        /// </summary>
        /// <param name="nIndex">Zero based index.</param>
        /// <returns></returns>
        public DbfColumn this[int nIndex]
        {
            get
            {
                return mFields[nIndex];
            }
        }


        /// <summary>
        /// Finds a column index by using a fast dictionary lookup-- creates column dictionary on first use. Returns -1 if not found. Note this is case sensitive!
        /// </summary>
        /// <param name="sName">Column name</param>
        /// <returns>column index (0 based) or -1 if not found.</returns>
        public int FindColumn(string sName)
        {

            if (mColumnNameIndex == null)
            {
                mColumnNameIndex = new Dictionary<string, int>(mFields.Count);

                //create a new index
                for (int i = 0; i < mFields.Count; i++)
                {
                    mColumnNameIndex.Add(mFields[i].Name, i);
                }
            }

            int columnIndex;
            if (mColumnNameIndex.TryGetValue(sName, out columnIndex))
                return columnIndex;

            return -1;

        }


        /// <summary>
        /// Returns an empty data record. This is used to clear columns 
        /// </summary>
        /// <remarks>
        /// The reason we put this in the header class is because it allows us to use the CDbf4Record class in two ways.
        /// 1. we can create one instance of the record and reuse it to write many records quickly clearing the data array by bitblting to it.
        /// 2. we can create many instances of the record (a collection of records) and have only one copy of this empty dataset for all of them.
        ///    If we had put it in the Record class then we would be taking up twice as much space unnecessarily. The empty record also fits the model
        ///    and everything is neatly encapsulated and safe.
        /// 
        /// </remarks>
        protected internal byte[] EmptyDataRecord
        {
            get { return mEmptyRecord ?? (mEmptyRecord = encoding.GetBytes("".PadLeft(mRecordLength, ' ').ToCharArray())); }
        }


        /// <summary>
        /// Returns Number of columns in this dbf header.
        /// </summary>
        public int ColumnCount
        {
            get { return mFields.Count; }

        }


        /// <summary>
        /// Size of one record in bytes. All fields + 1 byte delete flag.
        /// </summary>
        public int RecordLength
        {
            get
            {
                return mRecordLength;
            }
        }


        /// <summary>
        /// Get/Set number of records in the DBF.
        /// </summary>
        /// <remarks>
        /// The reason we allow client to set RecordCount is beause in certain streams 
        /// like internet streams we can not update record count as we write out records, we have to set it in advance,
        /// so client has to be able to modify this property.
        /// </remarks>
        public uint RecordCount
        {
            get
            {
                return mNumRecords;
            }

            set
            {
                mNumRecords = value;

                //set the dirty bit
                mIsDirty = true;

            }
        }


        /// <summary>
        /// Get/set whether this header is read only or can be modified. When you create a CDbfRecord 
        /// object and pass a header to it, CDbfRecord locks the header so that it can not be modified any longer.
        /// in order to preserve DBF integrity.
        /// </summary>
        internal bool Locked
        {
            get
            {
                return mLocked;
            }

            set
            {
                mLocked = value;
            }

        }


        /// <summary>
        /// Use this method with caution. Headers are locked for a reason, to prevent DBF from becoming corrupt.
        /// </summary>
        public void Unlock()
        {
            mLocked = false;
        }


        /// <summary>
        /// Returns true when this object is modified after read or write.
        /// </summary>
        public bool IsDirty
        {
            get
            {
                return mIsDirty;
            }

            set
            {
                mIsDirty = value;
            }
        }


        /// <summary>
        /// Encoding must be ASCII for this binary writer.
        /// </summary>
        /// <param name="writer"></param>
        /// <remarks>
        /// See class remarks for DBF file structure.
        /// </remarks>
        public void Write(BinaryWriter writer)
        {

            //write the header
            // write the output file type.
            writer.Write((byte)mFileType);

            //Update date format is YYMMDD, which is different from the column Date type (YYYYDDMM)
            writer.Write((byte)(mUpdateDate.Year - 1900));
            writer.Write((byte)mUpdateDate.Month);
            writer.Write((byte)mUpdateDate.Day);

            // write the number of records in the datafile. (32 bit number, little-endian unsigned)
            writer.Write(mNumRecords);

            // write the length of the header structure.
            writer.Write(mHeaderLength);

            // write the length of a record
            writer.Write((ushort)mRecordLength);

            // write the reserved bytes in the header
            for (int i = 0; i < 20; i++)
                writer.Write((byte)0);

            // write all of the header records
            byte[] byteReserved = new byte[14];  //these are initialized to 0 by default.
            foreach (DbfColumn field in mFields)
            {
                //char[] cname = field.Name.PadRight(11, (char)0).ToCharArray();
                byte[] bName = encoding.GetBytes(field.Name);
                byte[] cname = new byte[11];
                Array.ConstrainedCopy(bName, 0, cname, 0, bName.Length > 11 ? 11 : bName.Length);

                writer.Write(cname);

                // write the field type
                writer.Write((char)field.ColumnTypeChar);

                // write the field data address, offset from the start of the record.
                writer.Write(field.DataAddress);


                // write the length of the field.
                // if char field is longer than 255 bytes, then we use the decimal field as part of the field length.
                if (field.ColumnType == DbfColumn.DbfColumnType.Character && field.Length > 255)
                {
                    //treat decimal count as high byte of field length, this extends char field max to 65535
                    writer.Write((ushort)field.Length);

                }
                else
                {
                    // write the length of the field.
                    writer.Write((byte)field.Length);

                    // write the decimal count.
                    writer.Write((byte)field.DecimalCount);
                }

                // write the reserved bytes.
                writer.Write(byteReserved);

            }

            // write the end of the field definitions marker
            writer.Write((byte)0x0D);
            writer.Flush();

            //clear dirty bit
            mIsDirty = false;


            //lock the header so it can not be modified any longer, 
            //we could actually postpond this until first record is written!
            mLocked = true;


        }


        /// <summary>
        /// Read header data, make sure the stream is positioned at the start of the file to read the header otherwise you will get an exception.
        /// When this function is done the position will be the first record.
        /// </summary>
        /// <param name="reader"></param>
        public void Read(BinaryReader reader)
        {

            // type of reader.
            int nFileType = reader.ReadByte();

            if (nFileType != 0x03)
                throw new NotSupportedException("Unsupported DBF reader Type " + nFileType);

            // parse the update date information.
            int year = (int)reader.ReadByte();
            int month = (int)reader.ReadByte();
            int day = (int)reader.ReadByte();
            mUpdateDate = new DateTime(year + 1900, month, day);

            // read the number of records.
            mNumRecords = reader.ReadUInt32();

            // read the length of the header structure.
            mHeaderLength = reader.ReadUInt16();

            // read the length of a record
            mRecordLength = reader.ReadInt16();

            // skip the reserved bytes in the header.
            reader.ReadBytes(20);

            // calculate the number of Fields in the header
            int nNumFields = (mHeaderLength - FileDescriptorSize) / ColumnDescriptorSize;

            //offset from start of record, start at 1 because that's the delete flag.
            int nDataOffset = 1;

            // read all of the header records
            mFields = new List<DbfColumn>(nNumFields);
            for (int i = 0; i < nNumFields; i++)
            {

                // read the field name                
                char[] buffer = new char[11];
                buffer = reader.ReadChars(11);
                string sFieldName = new string(buffer);
                int nullPoint = sFieldName.IndexOf((char)0);
                if (nullPoint != -1)
                    sFieldName = sFieldName.Substring(0, nullPoint);


                //read the field type
                char cDbaseType = (char)reader.ReadByte();

                // read the field data address, offset from the start of the record.
                int nFieldDataAddress = reader.ReadInt32();


                //read the field length in bytes
                //if field type is char, then read FieldLength and Decimal count as one number to allow char fields to be
                //longer than 256 bytes (ASCII char). This is the way Clipper and FoxPro do it, and there is really no downside
                //since for char fields decimal count should be zero for other versions that do not support this extended functionality.
                //-----------------------------------------------------------------------------------------------------------------------
                int nFieldLength = 0;
                int nDecimals = 0;
                if (cDbaseType == 'C' || cDbaseType == 'c')
                {
                    //treat decimal count as high byte
                    nFieldLength = (int)reader.ReadUInt16();
                }
                else
                {
                    //read field length as an unsigned byte.
                    nFieldLength = (int)reader.ReadByte();

                    //read decimal count as one byte
                    nDecimals = (int)reader.ReadByte();

                }


                //read the reserved bytes.
                reader.ReadBytes(14);

                //Create and add field to collection
                mFields.Add(new DbfColumn(sFieldName, DbfColumn.GetDbaseType(cDbaseType), nFieldLength, nDecimals, nDataOffset));

                // add up address information, you can not trust the address recorded in the DBF file...
                nDataOffset += nFieldLength;

            }

            // Last byte is a marker for the end of the field definitions.
            reader.ReadBytes(1);


            //read any extra header bytes...move to first record
            //equivalent to reader.BaseStream.Seek(mHeaderLength, SeekOrigin.Begin) except that we are not using the seek function since
            //we need to support streams that can not seek like web connections.
            int nExtraReadBytes = mHeaderLength - (FileDescriptorSize + (ColumnDescriptorSize * mFields.Count));
            if (nExtraReadBytes > 0)
                reader.ReadBytes(nExtraReadBytes);



            //if the stream is not forward-only, calculate number of records using file size, 
            //sometimes the header does not contain the correct record count
            //if we are reading the file from the web, we have to use ReadNext() functions anyway so
            //Number of records is not so important and we can trust the DBF to have it stored correctly.
            if (reader.BaseStream.CanSeek && mNumRecords == 0)
            {
                //notice here that we subtract file end byte which is supposed to be 0x1A,
                //but some DBF files are incorrectly written without this byte, so we round off to nearest integer.
                //that gives a correct result with or without ending byte.
                if (mRecordLength > 0)
                    mNumRecords = (uint)Math.Round(((double)(reader.BaseStream.Length - mHeaderLength - 1) / mRecordLength));

            }


            //lock header since it was read from a file. we don't want it modified because that would corrupt the file.
            //user can override this lock if really necessary by calling UnLock() method.
            mLocked = true;

            //clear dirty bit
            mIsDirty = false;

        }



        public object Clone()
        {
            return this.MemberwiseClone();
        }
    }
}
View Code

源文件:DbfRecord.cs

///
/// Author: Ahmed Lacevic
/// Date: 12/1/2007
/// Desc: 
/// 
/// Revision History:
/// -----------------------------------
///   Author:
///   Date:
///   Desc:


using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Globalization;


namespace SocialExplorer.IO.FastDBF
{

    /// <summary>
    /// Use this class to create a record and write it to a dbf file. You can use one record object to write all records!!
    /// It was designed for this kind of use. You can do this by clearing the record of all data 
    /// (call Clear() method) or setting values to all fields again, then write to dbf file. 
    /// This eliminates creating and destroying objects and optimizes memory use.
    /// 
    /// Once you create a record the header can no longer be modified, since modifying the header would make a corrupt DBF file.
    /// </summary>
    public class DbfRecord
    {

        /// <summary>
        /// Header provides information on all field types, sizes, precision and other useful information about the DBF.
        /// </summary>
        private DbfHeader mHeader = null;

        /// <summary>
        /// Dbf data are a mix of ASCII characters and binary, which neatly fit in a byte array.
        /// BinaryWriter would esentially perform the same conversion using the same Encoding class.
        /// </summary>
        private byte[] mData = null;

        /// <summary>
        /// Zero based record index. -1 when not set, new records for example.
        /// </summary>
        private int mRecordIndex = -1;

        /// <summary>
        /// Empty Record array reference used to clear fields quickly (or entire record).
        /// </summary>
        private readonly byte[] mEmptyRecord = null;


        /// <summary>
        /// Specifies whether we allow strings to be truncated. If false and string is longer than we can fit in the field, an exception is thrown.
        /// </summary>
        private bool mAllowStringTruncate = true;

        /// <summary>
        /// Specifies whether we allow the decimal portion of numbers to be truncated. 
        /// If false and decimal digits overflow the field, an exception is thrown.
        /// </summary>
        private bool mAllowDecimalTruncate = false;

        /// <summary>
        /// Specifies whether we allow the integer portion of numbers to be truncated.
        /// If false and integer digits overflow the field, an exception is thrown.
        /// </summary>
        private bool mAllowIntegerTruncate = false;


        //array used to clear decimals, we can clear up to 40 decimals which is much more than is allowed under DBF spec anyway.
        //Note: 48 is ASCII code for 0.
        private static readonly byte[] mDecimalClear = new byte[] {48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,
                                                               48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,
                                                               48,48,48,48,48,48,48,48,48,48,48,48,48,48,48};


        //Warning: do not make this one static because that would not be thread safe!! The reason I have 
        //placed this here is to skip small memory allocation/deallocation which fragments memory in .net.
        private int[] mTempIntVal = { 0 };


        //Ascii Encoder
        private readonly Encoding encoding = Encoding.ASCII;

        /// <summary>
        /// Column Name to Column Index map
        /// </summary>
        private readonly Dictionary<string, int> mColNameToConIdx = new Dictionary<string, int>(StringComparer.InvariantCulture);



        /// <summary>
        /// 
        /// </summary>
        /// <param name="oHeader">Dbf Header will be locked once a record is created 
        /// since the record size is fixed and if the header was modified it would corrupt the DBF file.</param>
        public DbfRecord(DbfHeader oHeader)
        {
            mHeader = oHeader;
            mHeader.Locked = true;

            //create a buffer to hold all record data. We will reuse this buffer to write all data to the file.
            mData = new byte[mHeader.RecordLength];
            mEmptyRecord = mHeader.EmptyDataRecord;
            encoding = oHeader.encoding;

            for (int i = 0; i < oHeader.mFields.Count; i++)
                mColNameToConIdx[oHeader.mFields[i].Name] = i;
        }


        /// <summary>
        /// Set string data to a column, if the string is longer than specified column length it will be truncated!
        /// If dbf column type is not a string, input will be treated as dbf column 
        /// type and if longer than length an exception will be thrown.
        /// </summary>
        /// <param name="nColIndex"></param>
        /// <returns></returns>
        public string this[int nColIndex]
        {

            set
            {

                DbfColumn ocol = mHeader[nColIndex];
                DbfColumn.DbfColumnType ocolType = ocol.ColumnType;


                //
                //if an empty value is passed, we just clear the data, and leave it blank.
                //note: test have shown that testing for null and checking length is faster than comparing to "" empty str :)
                //------------------------------------------------------------------------------------------------------------
                if (string.IsNullOrEmpty(value))
                {
                    //this is like NULL data, set it to empty. i looked at SAS DBF output when a null value exists 
                    //and empty data are output. we get the same result, so this looks good.
                    Buffer.BlockCopy(mEmptyRecord, ocol.DataAddress, mData, ocol.DataAddress, ocol.Length);

                }
                else
                {

                    //set values according to data type:
                    //-------------------------------------------------------------
                    if (ocolType == DbfColumn.DbfColumnType.Character)
                    {
                        if (!mAllowStringTruncate && value.Length > ocol.Length)
                            throw new DbfDataTruncateException("Value not set. String truncation would occur and AllowStringTruncate flag is set to false. To supress this exception change AllowStringTruncate to true.");

                        //BlockCopy copies bytes.  First clear the previous value, then set the new one.
                        Buffer.BlockCopy(mEmptyRecord, ocol.DataAddress, mData, ocol.DataAddress, ocol.Length);
                        encoding.GetBytes(value, 0, value.Length > ocol.Length ? ocol.Length : value.Length, mData, ocol.DataAddress);

                    }
                    else if (ocolType == DbfColumn.DbfColumnType.Number)
                    {

                        if (ocol.DecimalCount == 0)
                        {

                            //integers
                            //----------------------------------

                            //throw an exception if integer overflow would occur
                            if (!mAllowIntegerTruncate && value.Length > ocol.Length)
                                throw new DbfDataTruncateException("Value not set. Integer does not fit and would be truncated. AllowIntegerTruncate is set to false. To supress this exception set AllowIntegerTruncate to true, although that is not recomended.");


                            //clear all numbers, set to [space].
                            //-----------------------------------------------------
                            Buffer.BlockCopy(mEmptyRecord, 0, mData, ocol.DataAddress, ocol.Length);


                            //set integer part, CAREFUL not to overflow buffer! (truncate instead)
                            //-----------------------------------------------------------------------
                            int nNumLen = value.Length > ocol.Length ? ocol.Length : value.Length;
                            encoding.GetBytes(value, 0, nNumLen, mData, (ocol.DataAddress + ocol.Length - nNumLen));

                        }
                        else
                        {

                            ///TODO: we can improve perfomance here by not using temp char arrays cDec and cNum,
                            ///simply direcly copy from source string using encoding!


                            //break value down into integer and decimal portions
                            //--------------------------------------------------------------------------
                            int nidxDecimal = value.IndexOf('.'); //index where the decimal point occurs
                            char[] cDec = null; //decimal portion of the number
                            char[] cNum = null; //integer portion

                            if (nidxDecimal > -1)
                            {
                                cDec = value.Substring(nidxDecimal + 1).Trim().ToCharArray();
                                cNum = value.Substring(0, nidxDecimal).ToCharArray();

                                //throw an exception if decimal overflow would occur
                                if (!mAllowDecimalTruncate && cDec.Length > ocol.DecimalCount)
                                    throw new DbfDataTruncateException("Value not set. Decimal does not fit and would be truncated. AllowDecimalTruncate is set to false. To supress this exception set AllowDecimalTruncate to true.");

                            }
                            else
                                cNum = value.ToCharArray();


                            //throw an exception if integer overflow would occur
                            if (!mAllowIntegerTruncate && cNum.Length > ocol.Length - ocol.DecimalCount - 1)
                                throw new DbfDataTruncateException("Value not set. Integer does not fit and would be truncated. AllowIntegerTruncate is set to false. To supress this exception set AllowIntegerTruncate to true, although that is not recomended.");



                            //clear all decimals, set to 0.
                            //-----------------------------------------------------
                            Buffer.BlockCopy(mDecimalClear, 0, mData, (ocol.DataAddress + ocol.Length - ocol.DecimalCount), ocol.DecimalCount);

                            //clear all numbers, set to [space].
                            Buffer.BlockCopy(mEmptyRecord, 0, mData, ocol.DataAddress, (ocol.Length - ocol.DecimalCount));



                            //set decimal numbers, CAREFUL not to overflow buffer! (truncate instead)
                            //-----------------------------------------------------------------------
                            if (nidxDecimal > -1)
                            {
                                int nLen = cDec.Length > ocol.DecimalCount ? ocol.DecimalCount : cDec.Length;
                                encoding.GetBytes(cDec, 0, nLen, mData, (ocol.DataAddress + ocol.Length - ocol.DecimalCount));
                            }

                            //set integer part, CAREFUL not to overflow buffer! (truncate instead)
                            //-----------------------------------------------------------------------
                            int nNumLen = cNum.Length > ocol.Length - ocol.DecimalCount - 1 ? (ocol.Length - ocol.DecimalCount - 1) : cNum.Length;
                            encoding.GetBytes(cNum, 0, nNumLen, mData, ocol.DataAddress + ocol.Length - ocol.DecimalCount - nNumLen - 1);


                            //set decimal point
                            //-----------------------------------------------------------------------
                            mData[ocol.DataAddress + ocol.Length - ocol.DecimalCount - 1] = (byte)'.';


                        }


                    }
                    else if (ocolType == DbfColumn.DbfColumnType.Integer)
                    {
                        //note this is a binary Integer type!
                        //----------------------------------------------

                        ///TODO: maybe there is a better way to copy 4 bytes from int to byte array. Some memory function or something.
                        mTempIntVal[0] = Convert.ToInt32(value);
                        Buffer.BlockCopy(mTempIntVal, 0, mData, ocol.DataAddress, 4);

                    }
                    else if (ocolType == DbfColumn.DbfColumnType.Memo)
                    {
                        //copy 10 digits...
                        ///TODO: implement MEMO

                        throw new NotImplementedException("Memo data type functionality not implemented yet!");

                    }
                    else if (ocolType == DbfColumn.DbfColumnType.Boolean)
                    {
                        if (String.Compare(value, "true", true) == 0 || String.Compare(value, "1", true) == 0 ||
                            String.Compare(value, "T", true) == 0 || String.Compare(value, "yes", true) == 0 ||
                            String.Compare(value, "Y", true) == 0)
                            mData[ocol.DataAddress] = (byte)'T';
                        else if (value == " " || value == "?")
                            mData[ocol.DataAddress] = (byte)'?';
                        else
                            mData[ocol.DataAddress] = (byte)'F';

                    }
                    else if (ocolType == DbfColumn.DbfColumnType.Date)
                    {
                        //try to parse out date value using Date.Parse() function, then set the value
                        DateTime dateval;
                        if (DateTime.TryParse(value, out dateval))
                        {
                            SetDateValue(nColIndex, dateval);
                        }
                        else
                            throw new InvalidOperationException("Date could not be parsed from source string! Please parse the Date and set the value (you can try using DateTime.Parse() or DateTime.TryParse() functions).");

                    }
                    else if (ocolType == DbfColumn.DbfColumnType.Binary)
                        throw new InvalidOperationException("Can not use string source to set binary data. Use SetBinaryValue() and GetBinaryValue() functions instead.");

                    else
                        throw new InvalidDataException("Unrecognized data type: " + ocolType.ToString());

                }

            }

            get
            {
                DbfColumn ocol = mHeader[nColIndex];
                return new string(encoding.GetChars(mData, ocol.DataAddress, ocol.Length));

            }
        }

        /// <summary>
        /// Set string data to a column, if the string is longer than specified column length it will be truncated!
        /// If dbf column type is not a string, input will be treated as dbf column 
        /// type and if longer than length an exception will be thrown.
        /// </summary>
        /// <param name="nColName"></param>
        /// <returns></returns>
        public string this[string nColName]
        {
            get
            {
                if (mColNameToConIdx.ContainsKey(nColName))
                    return this[mColNameToConIdx[nColName]];
                throw new InvalidOperationException(string.Format("There's no column with name '{0}'", nColName));
            }
            set
            {
                if (mColNameToConIdx.ContainsKey(nColName))
                    this[mColNameToConIdx[nColName]] = value;
                else
                    throw new InvalidOperationException(string.Format("There's no column with name '{0}'", nColName));
            }
        }

        /// <summary>
        /// Get date value.
        /// </summary>
        /// <param name="nColIndex"></param>
        /// <returns></returns>
        public DateTime GetDateValue(int nColIndex)
        {
            DbfColumn ocol = mHeader[nColIndex];

            if (ocol.ColumnType == DbfColumn.DbfColumnType.Date)
            {
                string sDateVal = encoding.GetString(mData, ocol.DataAddress, ocol.Length);
                return DateTime.ParseExact(sDateVal, "yyyyMMdd", CultureInfo.InvariantCulture);

            }
            else
                throw new Exception("Invalid data type. Column '" + ocol.Name + "' is not a date column.");

        }


        /// <summary>
        /// Get date value.
        /// </summary>
        /// <param name="nColIndex"></param>
        /// <returns></returns>
        public void SetDateValue(int nColIndex, DateTime value)
        {

            DbfColumn ocol = mHeader[nColIndex];
            DbfColumn.DbfColumnType ocolType = ocol.ColumnType;


            if (ocolType == DbfColumn.DbfColumnType.Date)
            {

                //Format date and set value, date format is like this: yyyyMMdd
                //-------------------------------------------------------------
                encoding.GetBytes(value.ToString("yyyyMMdd"), 0, ocol.Length, mData, ocol.DataAddress);

            }
            else
                throw new Exception("Invalid data type. Column is of '" + ocol.ColumnType.ToString() + "' type, not date.");


        }


        /// <summary>
        /// Clears all data in the record.
        /// </summary>
        public void Clear()
        {
            Buffer.BlockCopy(mEmptyRecord, 0, mData, 0, mEmptyRecord.Length);
            mRecordIndex = -1;

        }


        /// <summary>
        /// returns a string representation of this record.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return new string(encoding.GetChars(mData));
        }


        /// <summary>
        /// Gets/sets a zero based record index. This information is not directly stored in DBF. 
        /// It is the location of this record within the DBF. 
        /// </summary>
        /// <remarks>
        /// This property is managed from outside this object,
        /// CDbfFile object updates it when records are read. The reason we don't set it in the Read() 
        /// function within this object is that the stream can be forward-only so the Position property 
        /// is not available and there is no way to figure out what index the record was unless you 
        /// count how many records were read, and that's exactly what CDbfFile does.
        /// </remarks>
        public int RecordIndex
        {
            get
            {
                return mRecordIndex;
            }
            set
            {
                mRecordIndex = value;
            }
        }


        /// <summary>
        /// Returns/sets flag indicating whether this record was tagged deleted. 
        /// </summary>
        /// <remarks>Use CDbf4File.Compress() function to rewrite dbf removing records flagged as deleted.</remarks>
        /// <seealso cref="CDbf4File.Compress() function"/>
        public bool IsDeleted
        {
            get { return mData[0] == '*'; }
            set { mData[0] = value ? (byte)'*' : (byte)' '; }
        }


        /// <summary>
        /// Specifies whether strings can be truncated. If false and string is longer than can fit in the field, an exception is thrown.
        /// Default is True.
        /// </summary>
        public bool AllowStringTurncate
        {
            get { return mAllowStringTruncate; }
            set { mAllowStringTruncate = value; }
        }

        /// <summary>
        /// Specifies whether to allow the decimal portion of numbers to be truncated. 
        /// If false and decimal digits overflow the field, an exception is thrown. Default is false.
        /// </summary>
        public bool AllowDecimalTruncate
        {
            get { return mAllowDecimalTruncate; }
            set { mAllowDecimalTruncate = value; }
        }


        /// <summary>
        /// Specifies whether integer portion of numbers can be truncated.
        /// If false and integer digits overflow the field, an exception is thrown. 
        /// Default is False.
        /// </summary>
        public bool AllowIntegerTruncate
        {
            get { return mAllowIntegerTruncate; }
            set { mAllowIntegerTruncate = value; }
        }


        /// <summary>
        /// Returns header object associated with this record.
        /// </summary>
        public DbfHeader Header
        {
            get
            {
                return mHeader;
            }
        }


        /// <summary>
        /// Get column by index.
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        public DbfColumn Column(int index)
        {
            return mHeader[index];
        }

        /// <summary>
        /// Get column by name.
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        public DbfColumn Column(string sName)
        {
            return mHeader[sName];
        }

        /// <summary>
        /// Gets column count from header.
        /// </summary>
        public int ColumnCount
        {
            get
            {
                return mHeader.ColumnCount;
            }
        }

        /// <summary>
        /// Finds a column index by searching sequentially through the list. Case is ignored. Returns -1 if not found.
        /// </summary>
        /// <param name="sName">Column name.</param>
        /// <returns>Column index (0 based) or -1 if not found.</returns>
        public int FindColumn(string sName)
        {
            return mHeader.FindColumn(sName);
        }

        /// <summary>
        /// Writes data to stream. Make sure stream is positioned correctly because we simply write out the data to it.
        /// </summary>
        /// <param name="osw"></param>
        protected internal void Write(Stream osw)
        {
            osw.Write(mData, 0, mData.Length);

        }


        /// <summary>
        /// Writes data to stream. Make sure stream is positioned correctly because we simply write out data to it, and clear the record.
        /// </summary>
        /// <param name="osw"></param>
        protected internal void Write(Stream obw, bool bClearRecordAfterWrite)
        {
            obw.Write(mData, 0, mData.Length);

            if (bClearRecordAfterWrite)
                Clear();

        }


        /// <summary>
        /// Read record from stream. Returns true if record read completely, otherwise returns false.
        /// </summary>
        /// <param name="obr"></param>
        /// <returns></returns>
        protected internal bool Read(Stream obr)
        {
            return obr.Read(mData, 0, mData.Length) >= mData.Length;
        }

        protected internal string ReadValue(Stream obr, int colIndex)
        {
            DbfColumn ocol = mHeader[colIndex];
            return new string(encoding.GetChars(mData, ocol.DataAddress, ocol.Length));

        }

    }
}
View Code

 

第三步:创建DBF文件

  public void CreateDBF(string dbfPath, string DBFName)
        {
            var odbf = new DbfFile(Encoding.GetEncoding(936));
            odbf.Open(Path.Combine(dbfPath, DBFName + ".dbf"), FileMode.Create);

            //创建列头
            odbf.Header.AddColumn(new DbfColumn("YHBH", DbfColumn.DbfColumnType.Character, 20, 0));
            odbf.Header.AddColumn(new DbfColumn("SBBH", DbfColumn.DbfColumnType.Character, 20, 0));
            odbf.Header.AddColumn(new DbfColumn("YHMC", DbfColumn.DbfColumnType.Character, 64, 0));

            odbf.Header.AddColumn(new DbfColumn("YHZZ", DbfColumn.DbfColumnType.Character, 100, 0));
            odbf.Header.AddColumn(new DbfColumn("SBWZ", DbfColumn.DbfColumnType.Character, 100, 0));
            odbf.Header.AddColumn(new DbfColumn("DH", DbfColumn.DbfColumnType.Character, 50, 0));
            odbf.Header.AddColumn(new DbfColumn("YDDH", DbfColumn.DbfColumnType.Character, 50, 0));
            odbf.Header.AddColumn(new DbfColumn("CBBH", DbfColumn.DbfColumnType.Character, 8, 0));
            odbf.Header.AddColumn(new DbfColumn("CBXH", DbfColumn.DbfColumnType.Number, 8, 0));
            odbf.Header.AddColumn(new DbfColumn("YSXZ", DbfColumn.DbfColumnType.Number, 12, 0));
            odbf.Header.AddColumn(new DbfColumn("SBQD", DbfColumn.DbfColumnType.Number, 12, 0));
            odbf.Header.AddColumn(new DbfColumn("BJSL", DbfColumn.DbfColumnType.Number, 12, 0));
            odbf.Header.AddColumn(new DbfColumn("SBZD", DbfColumn.DbfColumnType.Number, 12, 0));
            odbf.Header.AddColumn(new DbfColumn("SJYS", DbfColumn.DbfColumnType.Number, 12, 0));
            odbf.Header.AddColumn(new DbfColumn("CBRQ", DbfColumn.DbfColumnType.Character, 20, 0));
            odbf.Header.AddColumn(new DbfColumn("SBYXZT", DbfColumn.DbfColumnType.Character, 30, 0));
            odbf.Header.AddColumn(new DbfColumn("SFGS", DbfColumn.DbfColumnType.Character, 1, 0));
            odbf.Header.AddColumn(new DbfColumn("CBBZ", DbfColumn.DbfColumnType.Character, 1, 0));
            odbf.Header.AddColumn(new DbfColumn("SBKJ", DbfColumn.DbfColumnType.Number, 8, 0));
            odbf.Header.AddColumn(new DbfColumn("QYL", DbfColumn.DbfColumnType.Number, 8, 0));
            odbf.Header.AddColumn(new DbfColumn("QFJE", DbfColumn.DbfColumnType.Number, 15, 2));
            odbf.Header.AddColumn(new DbfColumn("YHDJ", DbfColumn.DbfColumnType.Number, 8, 2));
            odbf.Header.AddColumn(new DbfColumn("SCCBRQ", DbfColumn.DbfColumnType.Character, 20, 0));
            odbf.Header.AddColumn(new DbfColumn("SCSL", DbfColumn.DbfColumnType.Number, 12, 0));
            odbf.Header.AddColumn(new DbfColumn("SCCJD", DbfColumn.DbfColumnType.Number, 12, 0));
            odbf.Header.AddColumn(new DbfColumn("ISUPDATE", DbfColumn.DbfColumnType.Character, 1, 0));

第四步:将数据写入DBF

        var orec = new DbfRecord(odbf.Header) { AllowDecimalTruncate = true };

            foreach (var item in WriteDTOlist)
            {
                orec[0] = item.YHBH;
                orec[1] = item.SBBH;
                orec[2] = item.YHMC;
                orec[3] = item.YHZZ;
                orec[4] = item.SBWZ;
                orec[5] = item.CBBH;
                orec[6] = item.CBXH.ToString();
                orec[7] = item.YSXZ;
                orec[8] = item.SBQD.ToString();
                orec[9] = item.BJSL.ToString();
                orec[10] = item.SBZD.ToString();
                orec[11] = item.SJYS.ToString();
                orec[12] = item.CBRQ;
                orec[13] = item.SBYXZT;
                orec[14] = item.SFGS;
                orec[15] = item.CBBZ;
               orec[16] = item.QYL.ToString();
                orec[17] = item.YHDJ.ToString(); 
                odbf.Write(orec, true);
            } 
            odbf.Close();

WriteDTOlist: List<WriteDTO> WriteDTOlist = new List<WriteDTO>();

 

读取DBF,将DBF转化为Datatable

          /// <summary>
        /// 从DBF读取文件到DataTable
        /// </summary>
        /// <param name="fileName">DBF的完整路径:如E:\2222.dbf</param>
        /// <returns></returns>
        public static DataTable DbfToDataTable(string fileName)
        {
            try
            {
                //返回的结果集
                DataTable dt = new DataTable();
                //获取一个DBF文件对象
                DbfFile dbf = new DbfFile(Encoding.Default);
                dbf.Open(fileName, FileMode.Open);

                //创建DataTable的结构(列名)
                DbfHeader dh = dbf.Header;
                for (int index = 0; index < dh.ColumnCount; index++)
                {
                    dt.Columns.Add(dh[index].Name);
                }

                //加载数据到DataTable里
                int i = 0;
                while (dbf.Read(i) != null)
                {
                    //获取一行
                    DbfRecord record = dbf.Read(i);
                    //将改行数据放到DataRow里
                    DataRow dr = dt.NewRow();
                    Object[] objs = new Object[record.ColumnCount];
                    for (int index = 0; index < record.ColumnCount; index++)
                    {
                        objs[index] = record[index];
                    }
                    dr.ItemArray = objs;
                    dt.Rows.Add(dr);
                    i++;
                }
                dbf.Close();
                return dt;
            }
            catch (Exception ex)
            {
                throw new Exception(ex.Message);
            }

        }

 

 

WriteDTO模型:

View Code
posted @ 2020-01-19 09:42  我本梁人  阅读(1230)  评论(1编辑  收藏  举报