TinyDBF-用200行的DBF解析器来展示良好架构设计

序言

由于工作关系,需要工作当中,需要读取DBF文件,找了一些DBF读取开源软件,要么是太过庞大,动不动就上万行,要么是功能有问题,编码,长度,总之是没有找到一个非常爽的。在万般无奈之下,我老人家怒从心头起,恶向胆边生,决定自己写一下。结果只用了不到300行代码就搞定了,当然搞定不是唯一目标,还要优雅简洁的搞定,亲们跟随我的脚步一起感受一下简洁的设计与实现吧。

在开始编码之前,先介绍一下DBF,这个DBF可是个老东西,在DOS时代就已经出现,并且风骚了相当一段时间,后来随着大型数据库的应用,它逐步没落,但是由于其简洁易用的特点,还是应用在大量的数据交换当中。但是其发展过程中,也形成了许多种版本,不同版本的结构不一样,也就决定 了其解析程序也是不一样的。

今天我只实现了Foxbase/DBaseIII的解析,但是也为扩展各种其它版本做好了准备。

接口设计

上面一共就两个类,一个接口,Field和Header就是两个简单的POJO类,分别定义了文件头及字段相关的信息。

Reader接口是DBF文件读取的接口,主要定义了获取文件类型,编码,字段以及记录移动相关的方法。

代码实现

首先实现Reader的抽象类

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

public abstract class DbfReader implements Reader {

    protected String encode = "GBK";

    private FileChannel fileChannel;

    protected Header header;

    protected List<Field> fields;

    private boolean recordRemoved;

    int position = 0;

    static Map<Integer, Class> readerMap = new HashMap<Integer, Class>();

 

    static {

        addReader(3, FoxproDBase3Reader.class);

    }

 

    public static void addReader(int type, Class clazz) {

        readerMap.put(type, clazz);

    }

 

    public static void addReader(int type, String className) throws ClassNotFoundException {

        readerMap.put(type, Class.forName(className));

    }

 

    public byte getType() {

        return 3;

    }

 

    public String getEncode() {

        return encode;

    }

 

    public Header getHeader() {

        return header;

    }

 

    public List<Field> getFields() {

        return fields;

    }

 

 

    public boolean isRecordRemoved() {

        return recordRemoved;

    }

 

    public static Reader parse(String dbfFile, String encode) throws IOException, IllegalAccessException, InstantiationException {

        return parse(new File(dbfFile), encode);

    }

 

    public static Reader parse(String dbfFile) throws IOException, IllegalAccessException, InstantiationException {

        return parse(new File(dbfFile), "GBK");

    }

 

    public static Reader parse(File dbfFile) throws IOException, IllegalAccessException, InstantiationException {

        return parse(dbfFile, "GBK");

    }

 

    public static Reader parse(File dbfFile, String encode) throws IOException, IllegalAccessException, InstantiationException {

        RandomAccessFile aFile = new RandomAccessFile(dbfFile, "r");

        FileChannel fileChannel = aFile.getChannel();

        ByteBuffer byteBuffer = ByteBuffer.allocate(1);

        fileChannel.read(byteBuffer);

        byte type = byteBuffer.array()[0];

        Class<Reader> readerClass = readerMap.get((int) type);

        if (readerClass == null) {

            fileChannel.close();

            throw new IOException("不支持的文件类型[" + type + "]。");

        }

        DbfReader reader = (DbfReader) readerClass.newInstance();

        reader.setFileChannel(fileChannel);

        reader.readHeader();

        reader.readFields();

        return reader;

    }

 

    public void setFileChannel(FileChannel fileChannel) {

        this.fileChannel = fileChannel;

    }

 

 

    protected abstract void readFields() throws IOException;

 

    public void moveBeforeFirst() throws IOException {

        position = 0;

        fileChannel.position(header.getHeaderLength());

    }

 

    /**

     * @param position 从1开始

     * @throws java.io.IOException

     */

    public void absolute(int position) throws IOException {

        checkPosition(position);

        this.position = position;

        fileChannel.position(header.getHeaderLength() + (position - 1) * header.getRecordLength());

    }

 

    private void checkPosition(int position) throws IOException {

        if (position >= header.getRecordCount()) {

            throw new IOException("期望记录行数为" + (this.position + 1) + ",超过实际记录行数:" + header.getRecordCount() + "。");

        }

    }

 

    protected abstract Field readField() throws IOException;

 

    protected abstract void readHeader() throws IOException;

 

 

    private void skipHeaderTerminator() throws IOException {

        ByteBuffer byteBuffer = ByteBuffer.allocate(1);

        readByteBuffer(byteBuffer);

    }

 

    public void close() throws IOException {

        fileChannel.close();

    }

 

    public void next() throws IOException {

        checkPosition(position);

        ByteBuffer byteBuffer = ByteBuffer.allocate(1);

        readByteBuffer(byteBuffer);

        this.recordRemoved = (byteBuffer.array()[0] == '*');

        for (Field field : fields) {

            read(field);

        }

        position++;

    }

 

    public boolean hasNext() {

        return position < header.getRecordCount();

    }

 

    private void read(Field field) throws IOException {

        ByteBuffer buffer = ByteBuffer.allocate(field.getLength());

        readByteBuffer(buffer);

        field.setStringValue(new String(buffer.array(), encode).trim());

        field.setBuffer(buffer);

    }

 

    protected void readByteBuffer(ByteBuffer byteBuffer) throws IOException {

        fileChannel.read(byteBuffer);

    }

}

这个类是最大的一个类,值得注意的是几个静态方法: addReader和parse,

addReader用于增加新的类型的Reader,parse用于解析文件。

parse的执行过程是首先读取第一个字节,判断是否有对应的解析实现类,如果有,就有对应的解析实现类去解析,如果没有,则抛出错误声明不支持。

下面写实现类就简单了,下面是FoxproDBase3的解析器:

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

public class FoxproDBase3Reader extends DbfReader {

    protected void readFields() throws IOException {

        fields = new ArrayList<Field>();

        for (int i = 0; i < (header.getHeaderLength() - 32 - 1) / 32; i++) {

            fields.add(readField());

        }

    }

 

    public byte getType() {

        return 3;

    }

 

    protected Field readField() throws IOException {

        Field field = new Field();

        ByteBuffer byteBuffer = ByteBuffer.allocate(32);

        readByteBuffer(byteBuffer);

        byte[] bytes = byteBuffer.array();

        field.setName(new String(bytes, 0, 11, encode).trim().split("\0")[0]);

        field.setType((char) bytes[11]);

        field.setDisplacement(Util.getUnsignedInt(bytes, 12, 4));

        field.setLength(Util.getUnsignedInt(bytes, 16, 1));

        field.setDecimal(Util.getUnsignedInt(bytes, 17, 1));

        field.setFlag(bytes[18]);

        return field;

    }

 

    protected void readHeader() throws IOException {

        header = new Header();

        ByteBuffer byteBuffer = ByteBuffer.allocate(31);

        readByteBuffer(byteBuffer);

        byte[] bytes = byteBuffer.array();

        header.setLastUpdate((Util.getUnsignedInt(bytes, 0, 1) + 1900) * 10000 + Util.getUnsignedInt(bytes, 1, 1) * 100 + Util.getUnsignedInt(bytes, 2, 1));

        header.setRecordCount(Util.getUnsignedInt(bytes, 3, 4));

        header.setHeaderLength(Util.getUnsignedInt(bytes, 7, 2));

        header.setRecordLength(Util.getUnsignedInt(bytes, 9, 2));

    }

}

测试用例

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

public class DbfReaderTest {

    static String[] files = {"BESTIMATE20140401", "BHDQUOTE20140401"};

 

    public static void main(String[] args) throws IOException, IllegalAccessException, InstantiationException {

        for (String file : files) {

            printFile(file);

        }

    }

 

    public static void printFile(String fileName) throws IOException, InstantiationException, IllegalAccessException {

        Reader dbfReader = DbfReader.parse("E:\\20140401\\" + fileName + ".DBF");

        for (Field field : dbfReader.getFields()) {

            System.out.printf("name:%s %s(%d,%d)\n", field.getName(), field.getType(), field.getLength(), field.getDecimal());

        }

        System.out.println();

        for (int i = 0; i < dbfReader.getHeader().getRecordCount(); i++) {

            dbfReader.next();

            for (Field field : dbfReader.getFields()) {

                System.out.printf("%" + field.getLength() + "s", field.getStringValue());

            }

            System.out.println();

        }

        dbfReader.close();

 

    }

}

可以看到最后的使用也是非常简洁的。

代码统计

总共的代码行数是282行,去掉import和接口声明之类的,真正干活的代码大概就200行了:

总结

上面不仅展示了如何实现DBF文件的解析,同时还展示了如何在现在面临的需求与未来的扩展进行合理均衡的设计方式。

比如:要实现另外一个标准的DBF文件支持,只要类似上面FoxproDBase3Reader类一样,简单实现之后,再调用DbfParser.addReader(xxxReader);

好的设计需要即避免过度设计,搞得太复杂,同时也要对未来的变化与扩展做适当考虑,避免新的需求来的时候需要这里动动,那里改改导致结构上的调整与变化,同时要注意遵守DRY原则,可以这样说如果程序中有必要的大量的重复,就说明一定存在结构设计上的问题。

所有的代码都可以在下面的连接看到:

https://git.oschina.net/tinyframework/tiny/tree/master/framework/org.tinygroup.dbf/src/main/java/org/tinygroup/dbf

时间: 2024-12-21 21:23:55

TinyDBF-用200行的DBF解析器来展示良好架构设计的相关文章

仅用500行Python代码实现一个英文解析器的教程_python

语法分析器描述了一个句子的语法结构,用来帮助其他的应用进行推理.自然语言引入了很多意外的歧义,以我们对世界的了解可以迅速地发现这些歧义.举一个我很喜欢的例子: 正确的解析是连接"with"和"pizza",而错误的解析将"with"和"eat"联系在了一起: 过去的一些年,自然语言处理(NLP)社区在语法分析方面取得了很大的进展.现在,小小的 Python 实现可能比广泛应用的 Stanford 解析器表现得更出色. 文章剩下

使用70行Python代码实现一个递归下降解析器的教程_python

 第一步:标记化 处理表达式的第一步就是将其转化为包含一个个独立符号的列表.这一步很简单,且不是本文的重点,因此在此处我省略了很多. 首先,我定义了一些标记(数字不在此中,它们是默认的标记)和一个标记类型:   token_map = {'+':'ADD', '-':'ADD', '*':'MUL', '/':'MUL', '(':'LPAR', ')':'RPAR'} Token = namedtuple('Token', ['name', 'value']) 下面就是我用来标记 `expr`

JOpt Simple 4.0发布 测试驱动的命令行解析器

JOpt Simple是一个用于Java程序的测试驱动的简单命令行解析器.它支持POSIX getopt() 和 GNU getopt_long() . JOpt Simple 4.0版本更新日志: 1.增加了选项解析的方法,formatHelpWith(HelpFormatter) 允许程序员改变printHelpOn() 打印帮助.2.通过选项文,处理一个HelpFormatter,其值OptionDescriptors分析器已配置.3.添加OptionSet.hasOptions() 选项

JOpt Simple 4.1发布 测试驱动的命令行解析器

JOpt Simple 4.1此版本允许短选项集群包含可以接受的参数.当遇到这样的选择是,在集群中的其余字符被视为该选项的参数. JOpt Simple是一个用于Java程序的测试驱动的简单命令行解析器.它支持POSIX getopt() 和 GNU getopt_long() . 示例:http://pholser.github.com/jopt-simple/examples.html 下载地址:http://pholser.github.com/jopt-simple/download.h

parseConf(配置文件解析器)

1 /****************************************************************************** 2 * 3 * parseConf(配置文件解析器) 4 * 5 * 1. 很多时候,我们安装一些软件,都可以通过改一些软件的配置文件来修改程序的 6 * 运行性能,如Tomcat修改端口号,访问数据库时一些固定的参数等等; 7 * 2. 本Demo就是干着这么一件事,从properties.conf文件中取出键值对(keyvalue

XML入门教程:XML 解析器

xml|教程|入门教程 如需读取.更新.创建或者操作某个XML文档,则需要XML解析器. 实例 解析XML文件 - 跨浏览器的实例 本例是一个跨浏览器的实例,把某个XML文档("note.xml")载入XML解析器. <html><body><script type="text/vbscript">set xmlDoc=CreateObject("Microsoft.XMLDOM")xmlDoc.async=&

UglifyJS有个超赞的JavaScript解析器

我一直在为Jscex寻找好用的JavaScript解析器,之前我用的是Narcissus,也写过相关文章.不过可惜的是,Narcissus使用了SpiderMonkey的扩展,因此它并不是用ECMAScript 3实现的,无法在IE 8等浏览器中使用.目前Jscex使用的是NarrativeJS中旧版的Narcissus,但是我并不喜欢它输出的AST结构,使用中也发现高级功能里的一些bug,有些食之无味弃之可惜的感觉,而改写新版Narcissus又必须大动干戈.最近我接触到了UglifyJS,发

C语言实现的XML解析器

最近做嵌入式开发,板子上面需要有解析XML的功能,理所当然地我就去网上找开源的来用.结果找来的要不是C++的,要不就是超级复杂的.像libxml,我统计了下解析一个40几KB的XML文件,居然动态申请内存100多次,对于没有mmu功能的arm7,真是无福消受了. 所以,我只能自己写一个来用了. 我写的这个xml解析器,非常简单,核心代码只有600多行.当然,功能也相对弱些,只支持ansi编码的xml文件,只能解析,不能生成. 整个解析器只用到了 若干条 EBNF文法 和 一个DFA状态机 (用来

微软xml解析器

XML解析器可以读取.更新.创建.操作一个XML文档. -------------------------------------------------------------------------------- 使用XML解析器 微软的XML解析器是和IE5.0+浏览器捆绑在一起的. 一旦你安装了IE5.0,那么就获得了XML解析器.这个浏览器除了被浏览器内部调用外,还可以在脚本中或者程序中调用.这个解析器的特点是支持与程序设计语言无关的编程模型,他支持以下技术: JavaScript,