愿你出走半生,归来仍是少年!
环境:.NET FrameWork4.5、.Net 6
在地理信息系统中,有很多的常用数据格式,类似Shapefile、Dxf等等,在不同的商业或开源平台中都有对其可靠的支持。DBF数据文件作为Shapefile文件的属性存储文件,出现的情况特别多;除此之外,现如今特别多的测绘公司对于数据的内业处理也还是保存在DBF中通过VFP进行处理。
虽然都是DBF文件,但是其内部存在不同的文件类型,这个会导致现如今在Git上找到的开源库有时候会出现有些dbf在打开时出现错误的情况。为了能够方便、快捷的读写DBF数据,不少开发者需要安装驱动,通过OLE DB的方式连接操作。
本文通过二进制流的方式进行DBF文件的解析、读取、写入,同时说明DBF文件的构成。
数据类型 | 所占字节数 |
---|---|
Byte | 1 |
Short | 2 |
Int | 4 |
Char | 1 |
后续的文件读写操作都是基于C#中的BinaryReader进行。
每个DBF文件依次存在以下几个部分:头文件区域、字段描述区域、后链信息(VFP存在,其它版本没发现)、数据内容区域。
通过一下代码创建面向指定DBF文件的流读取器:
new BinaryReader(new FileStream(Filename, FileMode.Open, FileAccess.Read, FileShare.Read, 100000));
头文件区域共计占32字节,具体分布如下。
字节范围 | 字节数 | 读取类型 | 说明 |
---|---|---|---|
0 | 1 | Byte | 文件类型 |
1 | 1 | Byte | 文件最后修改日期,年 YY |
2 | 1 | Byte | 文件最后修改日期,月 MM |
3 | 1 | Byte | 文件最后修改日期,日 DD |
4-7 | 4 | Int | 数据记录数量,总行数 |
8-9 | 2 | Short | 文件头的字节长度(头文件区域、字段描述区域、结束符(0x0D)、后链信息(VFP存在,其它版本没发现)) |
10-11 | 2 | short | 每一行数据记录的字节长度 |
12-27 | 16 | 未使用 | 保留区域 |
28 | 1 | Byte | 表的标记 |
29 | 1 | Byte | Language Driver ID (LDID),文件编码代码页标记 |
30-31 | 2 | 未使用 | 保留区域 |
// 获取Dbf的第一个字节,代表dbf文件类型,参照 DbfFileType 0
FileType = reader.ReadByte();
创建BinaryReader后,通过 ReadByte()方法读取文件第一个字节,获取到Dbf的文件类型,具体类型清单如下,通过对比可得知当前的DBF类型。
编码 | 说明 |
---|---|
0x02 | FoxBASE |
0x03 | FoxBASE+/dBASE III PLUS,无备注 |
0x30 | Visual FoxPro |
0x43 | dBASE IV SQL 表文件,无备注 |
0x63 | dBASE IV SQL 系统文件,无备注 |
0x83 | FoxBASE+/dBASE III PLUS,有备注 |
0x8B | dBASE IV 有备注 |
0xCB | dBASE IV SQL 表文件,有备注 |
0xF5 | FoxPro 2.x(或更早版本)有备注 |
0xFB | FoxBASE |
// 获取更新日期:年 1
int year = reader.ReadByte();
// 获取更新日期:月 2
int month = reader.ReadByte();
// 获取更新日期:日 3
int day = reader.ReadByte();
try
{
//转换为日期
UpdateDate = new DateTime(year + 1900, month, day);
}
catch
{
//获取文件的最后一次编辑时间
UpdateDate = new FileInfo(Filename).LastWriteTime;
}
通过三次的ReadByte()方法依次读取出年月日,并转换为对应的日期。当出错时就以文件的最后编辑时间作为参照。
// 读取数据总数,int 4字节长度 4-7
NumRecords = reader.ReadInt32();
通过ReadInt32()方法一次性读取4个字节,获得该DBF文件中现有的数据记录总数。
// 读取文件的头文件长度 short 2字节长度 8-9
HeaderLength = reader.ReadInt16();
通过ReadInt16()方法一次性读取2个字节,获得该DBF文件的头文件总字节长度。
// 读取每行记录的长度 short 2字节长度 10-11
RecordLength = reader.ReadInt16();
通过ReadInt16()方法一次性读取2个字节,获得该DBF文件中每一行数据记录的长度。
// 跳过系统保留区域 12-27 ;表的标记 28
reader.ReadBytes(17);
// 读取 Language Driver ID (LDID) byte 1字节长度 29
LanguageDriverId = reader.ReadByte();
通过ReadByte()方法读取代码页标记,用于设置编码。
// 跳过系统保留区 30-31
reader.ReadBytes(2);
字段描述区域包含了该DBF中的所有字段信息。每个字段的信息占用共计32字节长度,然后以0x0d结束。若该DBF包含后链信息,则后续是后链信息;若不包含后链信息,则后续是数据内容区域。
每个字段的信息32字节长度分布结构如下:
// 读取字段名称 :字段名,10字节,0-9;保留区,默认为0,1字节,10
string name = Encoding.GetString(reader.ReadBytes(11));
//获取保留区序号
int nullPoint = name.IndexOf((char)0);
//裁剪
if (nullPoint != -1)
{
name = name.Substring(0, nullPoint);
}
通过读取前面11个字节,然后获取第一个0的位置进行裁剪获得名称。
// 字段类型,1字节,11
char code = (char)reader.ReadByte();
获取字段类型后可得知其对应的数据类型,具体类型如下:
// 字段偏移量,int 4字节,12-15
int dataAddress = reader.ReadInt32();
// 字段长度,byte 1字节 16
byte tempLength = reader.ReadByte();
// 小数位数,byte 1字节 17
byte decimalcount = reader.ReadByte();
// 字段标记及保留区 ,字段标记 byte 1字节 18,;保留区 19-31
reader.ReadBytes(14);
Fields = new List();
bool readFieldFinish = false;
while (!readFieldFinish)
{
// 读取字段名称 :字段名,10字节,0-9;保留区,默认为0,1字节,10
string name = Encoding.GetString(reader.ReadBytes(11));
//获取保留区序号
int nullPoint = name.IndexOf((char)0);
//裁剪
if (nullPoint != -1)
{
name = name.Substring(0, nullPoint);
}
// 字段类型,1字节,11
char code = (char)reader.ReadByte();
// 字段偏移量,int 4字节,12-15
int dataAddress = reader.ReadInt32();
// 字段长度,byte 1字节 16
byte tempLength = reader.ReadByte();
// 小数位数,byte 1字节 17
byte decimalcount = reader.ReadByte();
// 字段标记及保留区 ,字段标记 byte 1字节 18,;保留区 19-31
reader.ReadBytes(14);
DbfField myField = new DbfField(name, code, tempLength, decimalcount)
{
DataAddress = dataAddress
};
Fields.Add(myField);
dt.Columns.Add(myField);
long pst = reader.BaseStream.Position;
//_numFields = (HeaderLength - FileDescriptorSize - 1) / FileDescriptorSize;
// 字段描述区的结束符 0x0d,有些版本后面有263个字节包含后链信息(相关数据库 (.dbc) 的相对路径)。
// 计算字段数量:【头长度-文件描述长度(32)-结束符长度(1)】/单字段描述区长度(32)进行计算会忽略263字符的情况
// 如果第一个字节为 0x00,则该文件不与数据库关联。因此数据库文件本身总是包含 0x00。
byte last = reader.ReadByte();
if (last == 0x0d)
{
readFieldFinish = true;
}
else
{
reader.BaseStream.Position = pst;
}
}
整体读取时需要注意的是每次读取完成后需要进行下一个字节的判断,当这个字节是0x0d时就代表字段描述区域已经完结了。这样的话可以避过后链信息区域的干扰,获取到正确的字段集合。
字段描述区域包含了该DBF中的所有数据记录,每条记录的字节长度固定为2.1.5中获取到的每行数据的字节长度,每个字段的长度为2.2.4.中获取到的字段长度。通过字节长度读取每个字段对应的数据内容,并根据在2.2.2.中获取的字段类型进行转换,便可以获取到对应的数据。
每条记录的第一个字节都是该记录的删除标记,当内容为空格 (0x20)时,表示该记录未标记删除,当内容为星号 (0x2A)时,表示该记录已标记为删除。