问题描述
- 求助,写一个VFP直接读写EXCEL二进制文件的程序,求教EXCEL文件格式。
-
在用VFP读写EXCEL文件数据时,当遇到非标准的EXCEL文件或高版本的EXCEL文件,VFP就会出错。这时通过调用第三方软件转换以后就可以使用了。但是在WINDOWS 7以上,系统会阻止VFP调用第三方软件。只有打开EXCEL文件直接读写数据了。 在网上查了一些资料,但是对EXCEL文件的结构和读写的方法还不明白,求助高人指点一下。目前只能读取和解析文件头,试着读出了扇区列表,目录读取不正确,其他还不会……。
解决方案
excel文件的格式极其复杂,特别是通过ole方式嵌入别的文件格式。如果你希望自己从底层开始来解析excel,那么工作量之大超过你的想象。
解决方案二:
其实只需要基本的读写功能就行了。我从网上看了一个LAOLA FILE SYSTEM的介绍,看懂了一点点,现在正在解析目录。但是对目录的管理机制不清楚。全是外国人的资料,看不懂呀。希望已经走过来的高人指点一下呀。
解决方案三:
我自己摸索着,一点点地做吧。求路过的高人指点一二。
【火车文件系统】
真是可惜,微软不再升级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
解决方案四:
一、什么是“火车文件系统”
我觉得把EXCEL比喻成一列火车很形象,用实物做比喻,更容易理解抽象概念。“火车文件系统”描述了一个文件内部如何组织管理嵌入文件的,与微软的OFFICE“复合文档”体系相似,跟《劳拉文件系统》差不多。其实都是对“复合文档”的管理,做的事情是一回事,管理方法、手段差不多,达到的结果相似,但是说法各异。
二、把EXCEL文件变成“一列火车”
(一)一列火车
把一个EXCEL文件,切成一段一段的,每段512字节,放到一个数据表中,该表名称叫做“车箱”。
所有“车箱”构成一列“火车”。第一条记录叫“车头”,剩下的记录都叫“车箱”。
为管理方便,每个车箱有个“车箱编号”,从0-N。车头也有“车箱编号”,是-1。
“车箱编号”和“车箱记录号”的关系是:车箱编号+2 = 车箱记录号
因此,用“火车文件系统”保存的文件,大小一定是512字节的整数倍。
(二)车箱用途
这列火车不是用来开的,我们把它摆在那,用来装“货物”。
车头,用来存放控制信息。
车箱,用来装货物。货物就是各种数据,可能是“工作簿”“工作表”“OLE物件”等等。
货物多时,就增加车箱,货物少时,就去掉车箱,总之车箱够用,但也不浪费(空着不用)。
解决方案五:
(三)车箱位置
生活中,火车进站后,就停靠到站台上。每个“车箱”对应一个“车位”。
相应地,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个“车位专用”车箱的“车箱编号”。
解决方案六:
(五)车箱用法
使用原则:“先进先出”。
比如,有一批“货物”来了(对于货物是按批次管理的),需要存放到“车箱”里。
就从“车位”开头找起,看看哪节车箱空闲,找到后就把货物放到那里。
如果一个“车箱”放不下,就往后再找一个“空闲车箱”,直到货物放完。
如果找完“车位”,也没有“空闲车箱”可用,则先增加一个“车位专用”车箱,会多出128个空闲车位,
然后增加一个“车箱”,往里面放“货物”即可。
如果“车本”也用完了,则应该先增加一个“车本专用”车箱,然后再增加一个“车位专用”车箱,
然后再增加一个“货物使用”车箱。即可。
(六)货物管理
在管理“货物”时,我们把“货物”分成一批一批的。
每批货物自成一体,并且有一个不能重名的“名称”。
货物“顺序”地放到“车箱”中,“顺序”地取出来。
每批货物都用一个“虚拟专列”,来存放货物。
(七)虚拟专列
当给定一个“车箱编号”,我们就可以在“火车”中找到这个“车箱”:
车箱记录号 = 车箱编号+2
当把一个“车箱”和下个“车箱”用“车链”栓在一起,这样串下去,直到没有“车链”为止。这样就得到一串“车箱”,叫作“虚拟专列”。
“车链”的信息保存在“车位”数据表中。
“虚拟专列”是为了给货物管理提供方便的,所以“虚拟专列”主要是“货物专列”。
“货物专列”是用来存储货物的基本手段。目录、文件、属性、摘要、常量、垃圾等各种货物,在存储时,
都要先分得一个“货物专列”,然后将“货物”存到“货物专列”中。
“货物专列”的第一个“车箱编号”叫“专列入口”。
当给定一个“专列入口”后,根据上面方法,就可以找到一个“货物专列”。
有几个特殊专列,其管理方法与上不同:
“车位专用”车箱,形成一个“车位专列”,
“车本专用”车箱,形成一个“车本专列”,
解决方案七:
(八)虚拟小火车
有些“货物”“容量”很小,如果给它分配“专列”,就连一个“车箱”都很浪费。为了节省空间,
我们把一个“车箱”切成8段,每一段当作一个“小车箱”用,把它称作“小车箱”。
这样以来,我们就可以虚拟出一列“小火车”了。
但是,并非每个EXCEL文件必须有“小火车”,需要的时修改才会有。
我们规定,当“货物容量”小于4096字节时,才会在“小火车”中,生成一个“小专列”,用来保存货物。
当“货物容量”大于等于4096字节时,直接保存在“货物专列”中。
“小火车”的管理,类似“火车”的管理。
首先虚拟一列“小火车”,
然后把“小火车”切成“小车箱”,
每个“小车箱”有一个“小车箱编号”从0-N,
每个“小车箱”对应一个“小车位”,
“小车位”放到“小车位车箱”中,然后形成“小车位专列”,
“小车位车箱”由“小车本”管理,
“小车本”放到“小车本车箱”中,然后形成“小车本专列”。
(九)特殊专列的生成(正在学习中……)
1“车本专列”的生成
2“车位专列”的生成
3“小车本专列”的生成
4“小车位专列”的生成
5“小火车”的生成
6“虚拟小专列”的生成
(十)车头结构
(十一)目录专列和目录树
(十二)垃圾数据
三、EXCEL文件解析
四、测试程序
解决方案八:
今天重新修正了一下,下图是“火车文件系统”数据结构:
解决方案九:
*火车文件系统
*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