2 zghbssjzzy zghbssjzzy 于 2016.03.06 20:20 提问

求助,写一个VFP直接读写EXCEL二进制文件的程序,求教EXCEL文件格式。
在用VFP读写EXCEL文件数据时,当遇到非标准的EXCEL文件或高版本的EXCEL文件,VFP就会出错。这时通过调用第三方软件转换以后就可以使用了。但是在WINDOWS 7以上,系统会阻止VFP调用第三方软件。只有打开EXCEL文件直接读写数据了。
    在网上查了一些资料,但是对EXCEL文件的结构和读写的方法还不明白,求助高人指点一下。目前只能读取和解析文件头,试着读出了扇区列表,目录读取不正确,其他还不会……。

9个回答

caozhy
caozhy   Ds   Rxr 2016.03.06 20:27

excel文件的格式极其复杂,特别是通过ole方式嵌入别的文件格式。如果你希望自己从底层开始来解析excel,那么工作量之大超过你的想象。

zghbssjzzy
zghbssjzzy   2016.03.06 20:40

其实只需要基本的读写功能就行了。我从网上看了一个LAOLA FILE SYSTEM的介绍,看懂了一点点,现在正在解析目录。但是对目录的管理机制不清楚。全是外国人的资料,看不懂呀。希望已经走过来的高人指点一下呀。

zghbssjzzy
zghbssjzzy   2016.03.09 17:35

我自己摸索着,一点点地做吧。求路过的高人指点一二。

【火车文件系统】

真是可惜,微软不再升级VFP9软件了。这是一个很好用的数据库软件,我一直在用它写小程序,管理一些数据。平时经常遇到需要读写EXCEL文件的情况,但是因VFP9处理EXCEL文件能力不足,有时需要借助第三方软件,比如EXCELRW.DLL、LIBXL.DLL等,或者通过COM方式调用OFFICE或WPS来操作EXCEL文件,但是经常遇到问题。

有一天突发奇想,想直接用VFP读写EXCEL文件,那不就省事了吗。从网上查了一些资料,认真地学习,可是打击实在太大了。这时我认识到:知识是多么深广啊,自己是多么乏力呀!经过一番努力,从混乱中理出一点点头绪,总结一下,以资继续努力。

列举前辈的文章,以示尊敬:

1、马丁.施瓦兹的《劳拉文件系统》(德国)原文:《LAOLA file system》网址:
https://stuff.mit.edu/afs/athena/astaff/project/mimeutils/OldFiles/src/laola/guide.html
2、大魔王的《Office文件的奥秘——.NET平台下不借助Office实现Word、Powerpoint等文件的解析》(中国)网址:
http://www.cnblogs.com/mayswind/archive/2013/03/17/2962205.html
3、Agstick的《复合文档文件格式研究》(中国)网址:
http://club.excelhome.net/thread-227502-1-1.html
4、July的《教你透彻了解红黑树》(中国)网址:
http://blog.csdn.net/v_JULY_v/article/details/6105630

zghbssjzzy
zghbssjzzy   2016.03.09 17:39

一、什么是“火车文件系统”
我觉得把EXCEL比喻成一列火车很形象,用实物做比喻,更容易理解抽象概念。“火车文件系统”描述了一个文件内部如何组织管理嵌入文件的,与微软的OFFICE“复合文档”体系相似,跟《劳拉文件系统》差不多。其实都是对“复合文档”的管理,做的事情是一回事,管理方法、手段差不多,达到的结果相似,但是说法各异。

二、把EXCEL文件变成“一列火车”
(一)一列火车
把一个EXCEL文件,切成一段一段的,每段512字节,放到一个数据表中,该表名称叫做“车箱”。
所有“车箱”构成一列“火车”。第一条记录叫“车头”,剩下的记录都叫“车箱”。
为管理方便,每个车箱有个“车箱编号”,从0-N。车头也有“车箱编号”,是-1。
“车箱编号”和“车箱记录号”的关系是:车箱编号+2 = 车箱记录号
因此,用“火车文件系统”保存的文件,大小一定是512字节的整数倍。

(二)车箱用途
这列火车不是用来开的,我们把它摆在那,用来装“货物”。
车头,用来存放控制信息。
车箱,用来装货物。货物就是各种数据,可能是“工作簿”“工作表”“OLE物件”等等。
货物多时,就增加车箱,货物少时,就去掉车箱,总之车箱够用,但也不浪费(空着不用)。

zghbssjzzy
zghbssjzzy   2016.03.09 17:41

(三)车箱位置
生活中,火车进站后,就停靠到站台上。每个“车箱”对应一个“车位”。
相应地,EXCEL文件被切段,存到“车箱”数据表中后。每条“记录”也对应一个“车位”。
所有“车位”的信息,保存到“车位”数据表中。“车位信息”就是“车箱状态”。
“车箱状态”,是一个数字,4字节长。可以是以下数值:
-1,空闲位置,没有车箱。
-2,车链结束
-3,车位专用
-4,车本专用
[0-N],货物使用,是下个车箱编号
“车位编号”和“车位记录号”的关系是:车位编号+1 = 车位记录号
“车位”个数可能多于“车箱”个数。

(四)车位管理
“车位信息”装在“车位专用”车箱中。一节车箱有128个“车位信息”。
随着“货物”的增多,“车箱”也增多。当“车位”不够时,就增加一节“车位专用”车箱,就会多出128个“车位”来。这样下去,“车位专用”车箱也会越来越多。
为管理“车位专用”车箱,用一个“账本”记录每个“车位专用”车箱的“车箱编号”,把它称作“车本”。存放在“车头”里。
由于容量限制,车头里的“车本”,只能记录109个“车位专用”车箱的“车箱编号”。
如果没有新的车位可能,则这个EXCEL文件最多只能保存:109 * 128=13952个车位,对应13952个车箱,
每个车箱512字节,EXCEL文件总容量=7143424字节,约6.8125M数据。
为了增加更多“车位”,就必须增加“车本”的容量。只能在“车头”后面,接上一个“车箱”,专门保存“车本”的内容。
我们把这种车箱称做“车本专用”车箱。其里面记录的是“车位专用”车箱的“车箱编号”。
并把其最后一个号码留下,用于保存下一个“车本专用”车箱的“车箱编号”。这样“车本专用”车箱就可以无限地增加了。
在一个“车本专用”就车箱中,能保存128-1=127个“车位专用”车箱的“车箱编号”。

zghbssjzzy
zghbssjzzy   2016.03.09 17:43

(五)车箱用法
使用原则:“先进先出”。
比如,有一批“货物”来了(对于货物是按批次管理的),需要存放到“车箱”里。
就从“车位”开头找起,看看哪节车箱空闲,找到后就把货物放到那里。
如果一个“车箱”放不下,就往后再找一个“空闲车箱”,直到货物放完。
如果找完“车位”,也没有“空闲车箱”可用,则先增加一个“车位专用”车箱,会多出128个空闲车位,
然后增加一个“车箱”,往里面放“货物”即可。
如果“车本”也用完了,则应该先增加一个“车本专用”车箱,然后再增加一个“车位专用”车箱,
然后再增加一个“货物使用”车箱。即可。

(六)货物管理
在管理“货物”时,我们把“货物”分成一批一批的。
每批货物自成一体,并且有一个不能重名的“名称”。
货物“顺序”地放到“车箱”中,“顺序”地取出来。
每批货物都用一个“虚拟专列”,来存放货物。

(七)虚拟专列
当给定一个“车箱编号”,我们就可以在“火车”中找到这个“车箱”:
车箱记录号 = 车箱编号+2
当把一个“车箱”和下个“车箱”用“车链”栓在一起,这样串下去,直到没有“车链”为止。这样就得到一串“车箱”,叫作“虚拟专列”。
“车链”的信息保存在“车位”数据表中。
“虚拟专列”是为了给货物管理提供方便的,所以“虚拟专列”主要是“货物专列”。
“货物专列”是用来存储货物的基本手段。目录、文件、属性、摘要、常量、垃圾等各种货物,在存储时,
都要先分得一个“货物专列”,然后将“货物”存到“货物专列”中。
“货物专列”的第一个“车箱编号”叫“专列入口”。
当给定一个“专列入口”后,根据上面方法,就可以找到一个“货物专列”。

有几个特殊专列,其管理方法与上不同:
“车位专用”车箱,形成一个“车位专列”,
“车本专用”车箱,形成一个“车本专列”,

zghbssjzzy
zghbssjzzy   2016.03.09 17:45

(八)虚拟小火车
有些“货物”“容量”很小,如果给它分配“专列”,就连一个“车箱”都很浪费。为了节省空间,
我们把一个“车箱”切成8段,每一段当作一个“小车箱”用,把它称作“小车箱”。
这样以来,我们就可以虚拟出一列“小火车”了。

但是,并非每个EXCEL文件必须有“小火车”,需要的时修改才会有。
我们规定,当“货物容量”小于4096字节时,才会在“小火车”中,生成一个“小专列”,用来保存货物。
当“货物容量”大于等于4096字节时,直接保存在“货物专列”中。

“小火车”的管理,类似“火车”的管理。
首先虚拟一列“小火车”,
然后把“小火车”切成“小车箱”,
每个“小车箱”有一个“小车箱编号”从0-N,
每个“小车箱”对应一个“小车位”,
“小车位”放到“小车位车箱”中,然后形成“小车位专列”,
“小车位车箱”由“小车本”管理,
“小车本”放到“小车本车箱”中,然后形成“小车本专列”。

(九)特殊专列的生成(正在学习中……)
1“车本专列”的生成
2“车位专列”的生成
3“小车本专列”的生成
4“小车位专列”的生成
5“小火车”的生成
6“虚拟小专列”的生成

(十)车头结构

(十一)目录专列和目录树

(十二)垃圾数据

三、EXCEL文件解析

四、测试程序

zghbssjzzy
zghbssjzzy   2016.03.10 22:38

今天重新修正了一下,下图是“火车文件系统”数据结构:
图片说明

zghbssjzzy
zghbssjzzy   2016.03.10 22:39

*火车文件系统
*2016-3-10
*zhangyi
*测试通过
*说明:目录没有按树形排序,今天累了,明天再说吧。


RETURN 火车()
PROCEDURE 火车
PRIVATE ALL
?PROG()

系统设置()
读入表结构()
生成表名称()
新建空表()

初始文件([1.XLS])
检测文件()
打开文件()
是否火车()
读入文件()
解析车头()
解析柜车()
解析卡车()
解析车位()
解析小车专列()
解析小车()
解析目录专列()
解析目录()
关闭文件()

ENDPROC


*


PROCEDURE 系统设置
PRIVATE ALL
?PROG()

ON SHUTDOWN QUIT

SET TALK OFF
SET SAFE OFF

SET ANSI ON 
SET EXACT ON 
SET ENGINEBEHAVIOR 70

CLOSE ALL
CLEAR ALL
CLEAR

ENDPROC


*


PROCEDURE 读入表结构
PRIVATE ALL
?PROG()

CREATE CURSOR 表结构 (;
    结构名称 V (20),;
    字段名称 V (40),;
    字段类型 V (1) ,;
    字段长度 I ,;
    字段小数 I ,;
    取数方法 V (10) ,;
    开始位置 I ,;
    数据长度 I ,;
    计算公式 V (40) ,;
    字段顺序 I ;
)

ERR=.F.
TRY
    APPEND FROM 表结构 XLS ;
    FOR 结构名称#[结构名称] ;
    AND NOT EMPTY(结构名称)
CATCH
    \ERROF:打开“表结构.XLS”时出错,
    \请查检文件是否正在打开,
    \或者已经删除。
    ERR=.T.
ENDTRY
IF ERR
    RETURN TO MASTER
ENDIF

UPDATE 表结构 SET 字段顺序=RECNO()

ENDPROC


*


PROCEDURE 生成表名称
PRIVATE ALL
?PROG()

CREATE CURSOR 表名称 (结构名称 V (20))

INSERT INTO 表名称 ;
SELECT DIST 结构名称 ;
FROM 表结构 ;
ORDER BY 字段顺序

ENDPROC


*


PROCEDURE 新建空表
PRIVATE ALL
?PROG()

SELECT 表名称
SCAN
REC=RECNO()
    当前表名=结构名称
    新表(当前表名)
SELECT 表名称
GO REC
ENDSCAN

ENDPROC


*


PROCEDURE 新表(当前表名)
PRIVATE ALL
SELECT ;
字段名称,;
字段类型,;
字段长度,;
字段小数 ;
FROM 表结构 ;
WHERE 结构名称=当前表名 ;
INTO ARRAY AAA

CREATE CURSOR (当前表名) FROM ARRAY AAA

ENDPROC


*


PROCEDURE 复制(当前表样,当前表名)
PRIVATE ALL
AFIELD(AAA,当前表样)
CREATE CURSOR (当前表名) FROM ARRAY AAA
ENDPROC


*


PROCEDURE 清空(当前表名)
PRIVATE ALL
ZAP IN (当前表名)
ENDPROC


*


PROCEDURE 关闭(当前表名)
PRIVATE ALL
USE IN (当前表名)
ENDPROC


*


PROCEDURE 新行(当前表名)
PRIVATE ALL
APPEND BLANK IN (当前表名)
ENDPROC


*


PROCEDURE UInt8(SS,NN)
PRIVATE ALL
S1=SUBSTR(SS,NN,1)
RETURN CTOBIN(S1,[1RS])
ENDPROC


*


PROCEDURE UInt16(SS,NN)
PRIVATE ALL
S1=SUBSTR(SS,NN,2)
RETURN CTOBIN(S1,[2RS])
ENDPROC


*


PROCEDURE UInt32(SS,NN)
PRIVATE ALL
S1=SUBSTR(SS,NN,4)
RETURN CTOBIN(S1,[4RS])
ENDPROC


*


PROCEDURE UCHAR(SS,NN,LL)
PRIVATE ALL
RETURN SUBSTR(SS,NN,LL)
ENDPROC


*


PROCEDURE WCHAR(SS,NN,LL)
PRIVATE ALL
RETURN STRCONV(SUBSTR(SS,NN,LL),6)
ENDPROC


*


PROCEDURE 初始文件(当前文件)
PRIVATE ALL
?PROG()
INSERT INTO 文件 (文件名) VALUE(当前文件)
ENDPROC


*


PROCEDURE 检测文件
PRIVATE ALL
?PROG()
UPDATE 文件 SET 发现文件=FILE(文件名)
ENDPROC


*


PROCEDURE 打开文件
PRIVATE ALL
?PROG()
UPDATE 文件 SET 文件号=-1
UPDATE 文件 SET 文件号=FOPEN(文件名,12) WHERE 发现文件
UPDATE 文件 SET 打开文件=(文件号>-1)
ENDPROC


*


PROCEDURE 关闭文件
PRIVATE ALL
?PROG()
UPDATE 文件 SET 关闭文件=FCLOSE(文件号) WHERE 打开文件
ENDPROC


*


PROCEDURE 是否火车
PRIVATE ALL
?PROG()
UPDATE 文件 SET 指针位置=FSEEK(文件号, 0, 0)
UPDATE 文件 SET 火车标志=FREAD(文件.文件号,8)
UPDATE 文件 SET 指针位置=FSEEK(文件号, 0, 0)
UPDATE 文件 SET 是火车=STRCONV(火车标志,15)=[D0CF11E0A1B11AE1]
IF NOT 文件.是火车
\不是火车文件
关闭文件()
RETURN TO MASTER
ENDIF
ENDPROC


*


PROCEDURE 读入文件
PRIVATE ALL
?PROG()
当前表名=[车箱]
清空(当前表名)
FSEEK(文件.文件号, 0, 0)
DO WHILE NOT FEOF(文件.文件号)
INSERT INTO (当前表名) VALUE (FREAD(文件.文件号,512))
ENDDO
FSEEK(文件.文件号, 0, 0)
ENDPROC


*


PROCEDURE 定位车箱(当前编号)
PRIVATE ALL
GO 当前编号+2 IN 车箱
ENDPROC


*


PROCEDURE 定位车位(当前编号)
PRIVATE ALL
GO 当前编号+1 IN 车位
ENDPROC


*


PROCEDURE 解析车箱(当前偏移,当前结构,当前表名)
PRIVATE ALL
SELECT 表结构
SCAN FOR 结构名称=当前结构
REC=RECNO()

    当前字段=字段名称
    当前方法=取数方法
    当前位置=开始位置
    当前长度=数据长度
    当前公式=计算公式

    DO CASE
    CASE 当前方法=[UCHAR]
        当前数据 = UCHAR(车箱.W,当前偏移+当前位置,当前长度) 
    CASE 当前方法=[WCHAR]
        当前数据 = WCHAR(车箱.W,当前偏移+当前位置,当前长度) 
    CASE 当前方法=[UINT8]
        当前数据 = UINT8(车箱.W,当前偏移+当前位置) 
    CASE 当前方法=[UINT16]
        当前数据 = UINT16(车箱.W,当前偏移+当前位置) 
    CASE 当前方法=[UINT32]
        当前数据 = UINT32(车箱.W,当前偏移+当前位置) 
    CASE NOT EMPTY(当前公式)
        SELECT (当前表名)
        当前数据 = EVAL(当前公式)
    OTHERWISE 
        LOOP
    ENDCASE

    TRY
        REPLACE IN (当前表名) &当前字段 WITH 当前数据
    CATCH
        \ERROR
    ENDTRY

SELECT 表结构
GO REC
ENDSCAN

ENDPROC


*


PROCEDURE 解析车头
PRIVATE ALL
?PROG()

**********
*当前变量
**********
当前车箱=-1
当前偏移=0
当前表名=[车头]

**********
*清空数据
**********
清空(当前表名)

**********
*新建记录
**********
新行(当前表名)

**********
*定位车箱
**********
定位车箱(当前车箱)

**********
*解析车头
**********
解析车箱(当前偏移,当前表名,当前表名)

ENDPROC


*


PROCEDURE 解析柜车
PRIVATE ALL
?PROG()

**********
*当前变量
**********
当前补车=车头.补车入口
当前个数=车头.补车个数
当前表名=[柜车]
当前偏移=509

**********
*初始化
**********
清空(当前表名)

**********
*车头
**********
APPEND BLANK IN (当前表名)
REPL IN (当前表名) 入口 WITH -1
REPL IN (当前表名) 偏移 WITH 77
REPL IN (当前表名) 个数 WITH 109
REPL IN (当前表名) 车链 WITH 当前补车

**********
*补车
**********
FOR 当前循环=1 TO 当前个数

    当前车箱=当前补车

    IF 当前车箱<0
        EXIT
    ENDIF

    定位车箱(当前车箱)

    当前补车=UINT32(车箱.W,当前偏移)

    APPEND BLANK IN (当前表名)
    REPL IN (当前表名) 入口 WITH 当前车箱
    REPL IN (当前表名) 偏移 WITH 1
    REPL IN (当前表名) 个数 WITH 127
    REPL IN (当前表名) 车链 WITH 当前补车
ENDFOR

ENDPROC


*


PROCEDURE 解析卡车
PRIVATE ALL
?PROG()

**********
*当前变量
**********
当前表名=[卡车]

**********
*初始化
**********
清空(当前表名)

**********
*扫描柜车
**********
SELECT 柜车
SCAN
REC=RECNO()

    **********
    *当前变量
    **********
    当前车箱=入口
    当前偏移=偏移
    当前个数=个数

    **********
    *无效车箱
    **********
    IF 当前车箱<-1
        LOOP
    ENDIF

    **********
    *定位柜车
    **********
    定位车箱(当前车箱)

    **********
    *读出卡车
    **********
    FOR 当前循环=1 TO 当前个数

        当前位置=(当前循环-1)*4
        当前卡车=UINT32(车箱.W,当前偏移+当前位置)
        APPEND BLANK IN (当前表名)
        REPL IN (当前表名) 入口 WITH 当前卡车

    ENDFOR

SELECT 柜车
GO REC
ENDSCAN

ENDPROC


*车位状态:
*-1 空闲车位
*-2 车链结束
*-3 卡车专用
*-4 柜车专用

  • 0 无效数据
    *>0 车链使用,是下个车箱编号


    PROCEDURE 解析车位
    PRIVATE ALL
    ?PROG()


    *当前变量


    当前表名=[车位]


    *初始化


    清空(当前表名)


    *扫描卡车


    SELECT 卡车
    SCAN
    REC=RECNO()

    **********
    *当前变量
    **********
    当前车箱=入口
    当前偏移=1
    当前个数=128
    
    **********
    *无效车箱
    **********
    IF 当前车箱<0
        LOOP
    ENDIF
    
    **********
    *定位卡车
    **********
    定位车箱(当前车箱)
    
    **********
    *读出车位
    **********
    FOR 当前循环=1 TO 当前个数
    
        当前位置=(当前循环-1)*4
        当前状态=UINT32(车箱.W,当前偏移+当前位置)
        APPEND BLANK IN (当前表名)
        REPL IN (当前表名) 状态 WITH 当前状态
    
    ENDFOR
    

    SELECT 卡车
    GO REC
    ENDSCAN

ENDPROC


*


PROCEDURE 解析小车专列
PRIVATE ALL
?PROG()

**********
*当前变量
**********
当前车箱=车头.小车入口
当前个数=车头.小车个数
当前表名=[小车专列]

**********
*初始化
**********
清空(当前表名)

**********
*扫描车位
**********
FOR 当前循环=1 TO 当前个数

    **********
    *无效车箱
    **********
    IF 当前车箱<0
        LOOP
    ENDIF

    **********
    *保存数据
    **********
    APPEND BLANK IN (当前表名)
    REPL IN (当前表名) 入口 WITH 当前车箱

    **********
    *定位车位
    **********
    定位车位(当前车箱)

    **********
    *读出状态
    **********
    当前状态=车位.状态

    **********
    *车位状态
    **********
    DO CASE
    CASE 当前状态=-1 &&空闲车位
    CASE 当前状态=-2 &&车链结束
    CASE 当前状态=-3 &&卡车专用
    CASE 当前状态=-4 &&柜车专用
    CASE 当前状态=0  &&无效数据
    CASE 当前状态>0  &&车链使用,是下个车箱编号
        当前车箱=当前状态
    ENDCASE

ENDFOR

ENDPROC


*1分8


PROCEDURE 解析小车
PRIVATE ALL
?PROG()

**********
*当前变量
**********
当前表名=[小车]
当前编号=0

**********
*初始化
**********
清空(当前表名)

**********
*扫描小车专列
**********
SELECT 小车专列
SCAN
REC=RECNO()

    当前入口=入口

    FOR 当前位置=0 TO 7

        APPEND BLANK IN (当前表名)
        REPL IN (当前表名) 入口 WITH 当前入口
        REPL IN (当前表名) 位置 WITH 当前位置
        REPL IN (当前表名) 编号 WITH 当前编号

        当前编号=当前编号+1
    ENDFOR

SELECT 小车专列
GO REC
ENDSCAN

ENDPROC


*


PROCEDURE 解析目录专列
PRIVATE ALL
?PROG()

**********
*当前变量
**********
当前车箱=车头.目录入口
当前表名=[目录专列]

**********
*初始化
**********
清空(当前表名)

**********
*扫描车位
**********
DO WHILE 当前车箱>0 AND NOT EOF([车位])

    **********
    *保存数据
    **********
    APPEND BLANK IN (当前表名)
    REPL IN (当前表名) 入口 WITH 当前车箱

    **********
    *定位车位
    **********
    定位车位(当前车箱)

    **********
    *车位状态
    **********
    当前车箱=车位.状态

ENDDO

ENDPROC


*目录类型:

  • 0 Invalid ,无效数据
  • 1 Storage ,目录
  • 2 Stream ,文件
  • 3 LockBytes,锁定字节
  • 4 Property ,属性
  • 5 Root ,根目录


    PROCEDURE 解析目录
    PRIVATE ALL
    ?PROG()


    *当前变量


    当前结构=[目录]
    当前表名=[目录]
    当前编号=0


    *扫描目录专列


    SELECT 目录专列
    SCAN
    REC=RECNO()

    **********
    *当前变量
    **********
    当前车箱=入口
    
    **********
    *定位目录车箱
    **********
    定位车箱(当前车箱)
    
    **********
    *1分4
    **********
    FOR 当前位置=0 TO 3
    
        当前偏移=当前位置*128
    
        APPEND BLANK IN (当前表名)
        REPL IN (当前表名) 车箱 WITH 当前车箱
        REPL IN (当前表名) 位置 WITH 当前位置
        REPL IN (当前表名) 编号 WITH 当前编号
    
        解析车箱(当前偏移,当前结构,当前表名)
    
        当前编号=当前编号+1
    
    ENDFOR
    

    SELECT 目录专列
    GO REC
    ENDSCAN

ENDPROC


*END OF ALL


Csdn user default icon
上传中...
上传图片
插入图片
准确详细的回答,更有利于被提问者采纳,从而获得C币。复制、灌水、广告等回答会被删除,是时候展现真正的技术了!