├── .gitignore
├── DesignDecision.md
├── LICENSE
├── README.md
├── src
├── Sheet.py
├── SheetManager.py
└── excel_and_json.py
├── test1
├── mwb.xlsx
├── test_mainbook_model.sh
├── test_singlebook_model.sh
├── wb1.xlsx
└── wb2.xlsx
└── test2
├── clothes_level.xlsx
├── levelup.xlsx
├── mwb.xlsx
└── test_mainbook_model.sh
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 |
5 | # C extensions
6 | *.so
7 |
8 | # Distribution / packaging
9 | .Python
10 | env/
11 | bin/
12 | build/
13 | develop-eggs/
14 | dist/
15 | eggs/
16 | lib/
17 | lib64/
18 | parts/
19 | sdist/
20 | var/
21 | *.egg-info/
22 | .installed.cfg
23 | *.egg
24 |
25 | # Installer logs
26 | pip-log.txt
27 | pip-delete-this-directory.txt
28 |
29 | # Unit test / coverage reports
30 | htmlcov/
31 | .tox/
32 | .coverage
33 | .cache
34 | nosetests.xml
35 | coverage.xml
36 |
37 | # Translations
38 | *.mo
39 |
40 | # Mr Developer
41 | .mr.developer.cfg
42 | .project
43 | .pydevproject
44 |
45 | # Rope
46 | .ropeproject
47 |
48 | # Django stuff:
49 | *.log
50 | *.pot
51 |
52 | # Sphinx documentation
53 | docs/_build/
54 |
55 |
--------------------------------------------------------------------------------
/DesignDecision.md:
--------------------------------------------------------------------------------
1 | ExcelAndJSON的设计决策
2 | ============
3 |
4 | *很多人看到ExcelAndJSON的第一反映是,这东西我的公司里面也有,那么我为什么用呢?*
5 |
6 | *做为开发来说,每一个工具的存在,都是为了加快游戏开发的速度。那么从无到有,从有到精。有和没有,好用和不好用的差别,每一个都比前一个情况能提升50%的效率。(按IPD理论,极限速度是提升100%的效率,这里取保守数字)。*
7 |
8 | *ExcelAndJSON这个工具,前前后后设计思考大约有一年的时间。在之前的开发中,我们使用大量的类似工具,数量有四五个,如果考虑评估阶段的话,是十几个。*
9 |
10 | *这些工具或多或少都有这样那样的问题。而每一个问题,都是开发中的一个大坑。下面我们来看ExcelAndJSON是如何对这些问题提供解决方案的。*
11 |
12 | **Part1.为什么选择Python开发?**
13 | ============
14 |
15 | - 如果选择C++,那么是可以使用Qt的。但C++领域,一直没有好用的开源跨平台Excel解析库。要么是闭源的,要么是只支持老格式xls不支持新格式xlsx,还有就是不能够跨平台。而这些,恰恰都是ExcelAndJSON本身必须具备的特性。手游开发,决定了必须跨平台。开源项目决定了依赖库必须也是开源的。Office的不断更新,决定了必须支持新格式。所以,C++被淘汰出局。
16 |
17 | - 如果选择JS,因为我的方向是全栈式,目前来说在Node.js领域,npm中我没有找到非常好用的Excel解析库。很多库都是直接把Excel读成一个巨型JSON对象,这种写法是我所不能接受的,太SB了。还有一个原因在于,考虑未来扩展性,Node.js领域一直没有好用的UI库。另外,如果在web开发里面去找,我个人不是很喜欢BS架构的工具。所以,JS被淘汰出局。
18 |
19 | - 如果选择Java。Java目前在前端手机游戏开发领域,已经没落。在后端,快速开发方向面临新兴方案的冲击(RoR, Python,Node.js,Go),而且高性能方向又始终干不过C++。对于各个公司自行修改维护能否找到适合的人,是个问题(前端几乎没人做Java,后端可能有人做Java)。所以,Java也被淘汰出局。
20 |
21 | - 如果选择Python。首先,Python是跨平台的。其次,Python的学习速度很快,3~5年经验的人,上手时间顶多3~5天。再次,Python对于文件,文本,命令行处理,支持的非常之好。最后,Python里面也有方便的图形化工具,例如Qt就提供了Python版本。
22 |
23 | 所以,选择Python。
24 |
25 | **Part2.数组的作用**
26 | ============
27 |
28 | 如果没有数组,那么在遇到成序列的数据时候,比如设计怪物AI中的技能部分,表的结构就会是类似这个样子:
29 |
30 |
31 |
32 |
33 | length
34 | |
35 |
36 | skill1
37 | |
38 |
39 | skill2
40 | |
41 |
42 | skill3
43 | |
44 |
45 | skill4
46 | |
47 |
48 |
49 |
50 | 4
51 | |
52 |
53 | 火球
54 | |
55 |
56 | 冰箭
57 | |
58 |
59 | 魔法盾
60 | |
61 |
62 | 顺移
63 | |
64 |
65 |
66 |
67 | 3
68 | |
69 |
70 | 突刺
71 | |
72 |
73 | 半月
74 | |
75 |
76 | 重斩
77 | |
78 |
79 |
80 | |
81 |
82 |
83 |
84 | 1
85 | |
86 |
87 | 治疗
88 | |
89 |
90 |
91 | |
92 |
93 |
94 | |
95 |
96 |
97 | |
98 |
99 |
100 |
101 |
102 |
103 |
104 | 如果你使用过类似这样的JSON结构,那么你应该知道,在填写数据的时候,容易出错,输出数据的时候会很难看(不管是填充null作为空数据,还是不输出空白格,都一样难看。前者存在无用数据,后者丢失了表的结构,造成阅读困难),遍历代码写起来也很麻烦。
105 |
106 | 在JSON中,数组天生就可以获得其“元素个数”,并且可以方便的遍历。所以,我们要在工具层面支持数组,这样才能使用JSON的这个特性。
107 |
108 | **Part3.“引用”该怎么用?**
109 | ============
110 |
111 | 还是举一个例子,在经营建造游戏中,对于建筑物属性的定义,每个建筑的解锁等级这是一个固定值,该建筑占用的地块面积也是一个固定值。但是该建筑不同等级的属性,则是完全不相同的。如果是一个资源产生建筑,那么会有不同的资源生成速度和资源上限,如果是一个出兵建筑,会有可造兵种类别,出兵时间。如果是一个防御建筑,会有攻击半径,伤害力等。这些不同结构的字段,是没有可能放到一个二维表中的。
112 |
113 | 一般采用的方式是,会有几种方案:
114 | 1.会有一个主要的表来存放所有建筑包含的相同的字段,然后那些不相同的字段信息放到其他表中,然后通过主键跳转来访问。
115 | 2.直接拆成多个表来填数据
116 | 3.使用一些不同的开关字段或分类字段,让同一个字段在不同开关状态下有不同的含义。现在游戏越来越复杂,这是最不建议的一种方式。
117 |
118 | 上面的3种方案,维护和修改成本都很高。
119 |
120 | 采用引用实现就很简单,还是多个表,然后在主要表上,插入其他表的引用即可。
121 |
122 |
123 |
124 |
125 | s
126 | |
127 |
128 | i
129 | |
130 |
131 | i
132 | |
133 |
134 | r
135 | |
136 |
137 | r
138 | |
139 |
140 | r
141 | |
142 |
143 |
144 |
145 | name
146 | |
147 |
148 | unlock_lv
149 | |
150 |
151 | area
152 | |
153 |
154 | lv1
155 | |
156 |
157 | lv2
158 | |
159 |
160 | lv3
161 | |
162 |
163 |
164 |
165 | 基地
166 | |
167 |
168 | 1
169 | |
170 |
171 | 4
172 | |
173 |
174 | 基地.lv1
175 | |
176 |
177 | 基地.lv2
178 | |
179 |
180 | 基地.lv3
181 | |
182 |
183 |
184 |
185 | 铀矿
186 | |
187 |
188 | 3
189 | |
190 |
191 | 4
192 | |
193 |
194 | 铀矿.lv1
195 | |
196 |
197 | 铀矿.lv2
198 | |
199 |
200 | 铀矿.lv3
201 | |
202 |
203 |
204 |
205 | 兵营
206 | |
207 |
208 | 5
209 | |
210 |
211 | 1
212 | |
213 |
214 | 兵营.lv1
215 | |
216 |
217 | 兵营.lv2
218 | |
219 |
220 | 兵营.lv3
221 | |
222 |
223 |
224 |
225 |
226 | **Part4.主表模式的意义是什么?**
227 | ============
228 |
229 | 游戏开发中,前后端对于数据的需求是不一样的。前端需要的是一些显示数据,如资源名称,动作参数。后端需要的是一些计算数据,比如攻击力,防御力,伤害公式等。但是有一些数据,是前后端都需要的,比如:技能范围,技能类型等,这些数据既与前端的显示有关系也和后端的逻辑计算有关系。
230 |
231 | 那么这种情况下,按照传统方式,也会拆成若干表。一般是一张表前端用,一张表后端用。但问题在于,前后端都需要的数据该如何处理?在两个表之间同步是一个成本比较高的办法。
232 |
233 | 这就体现出主表模式的意义了。我们可以把这些数据都组织在一张表上:
234 |
235 |
236 |
237 |
238 | name
239 | |
240 |
241 | type
242 | |
243 |
244 | effect
245 | |
246 |
247 | atk
248 | |
249 |
250 |
251 |
252 | 平砍
253 | |
254 |
255 | 1
256 | |
257 |
258 | 平砍.png
259 | |
260 |
261 | 10
262 | |
263 |
264 |
265 |
266 | 横扫千军
267 | |
268 |
269 | 3
270 | |
271 |
272 | 横扫千军.png
273 | |
274 |
275 | 7
276 | |
277 |
278 |
279 |
280 | 暴风雪
281 | |
282 |
283 | 4
284 | |
285 |
286 | 暴风雪.png
287 | |
288 |
289 | 8
290 | |
291 |
292 |
293 |
294 |
295 | 然后在输出的时候,在主表模式中,分成两个来输出:
296 |
297 |
298 |
299 |
300 | skill->skill_fn
301 | |
302 |
303 | name
304 | |
305 |
306 | type
307 | |
308 |
309 | effect
310 | |
311 |
312 |
313 |
314 | skill->skill_bn
315 | |
316 |
317 | name
318 | |
319 |
320 | type
321 | |
322 |
323 | atk
324 | |
325 |
326 |
327 |
328 | **Finally**
329 | ============
330 |
331 | 需求一直在变,工具要提供的是应对不同需求的灵活性。
332 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 gdgoldlion
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ExcelAndJSON
2 | ============
3 |
4 |
5 |
6 | *by 老G (qq 233424570)*
7 |
8 |
9 |
10 | **Part0.缘起**
11 | ============
12 |
13 |
14 |
15 | Excel,是游戏开发中,策划最常用的数值编辑工具。它有着公式填充,数值曲线图等许多好用功能。作为Office办公套件的一部分,它的上手度,易用性也非常不错。
16 |
17 |
18 |
19 | JSON,是手机游戏开发中,最常用的数据交换格式。它的树形结构,让数据访问变得非常自然。并且,这种结构和脚本语言有着天然的兼容性(例如Python,JavaScript)。
20 |
21 |
22 |
23 | 基于上面的原因,在手机游戏开发过程中,很多公司都使用Excel编辑数值,然后导出JSON,最后加载到程序中。大家也都开发了许多通过Excel导出为JSON的工具。
24 |
25 | 但因为Excel是基于二维表的结构,最后导出为JSON时,JSON也是一个类似二维表的结构,这严重限制了JSON作为一种树形结构的数据交换格式的表现力和易用性。造成的结果就是,大家在开发中,不断的建表,因为二维表表现方式有限,所以只能通过当前数据获得一个主键,然后在另外的表上查找,甚至还可能出现在多个表间跳转等情况。很吃力。
26 |
27 | 除此之外,很多工具都存在着功能单一、流程复杂、配置不方便、设计不合理等问题。
28 |
29 |
30 |
31 | 为了解决上述问题,我尝试开发了这个工具。市面上大部分工具都起名为:ExcelToJSON。而我的想法是,结合Excel和JSON的优点,所以我的工具起名为:ExcelAndJSON。
32 |
33 |
34 |
35 | **Part1.运行环境**
36 | ==============
37 |
38 |
39 |
40 | - 支持xlsx格式的Office
41 |
42 | - Python 2.7
43 |
44 | - xlrd
45 |
46 |
47 |
48 | **Part2.特点**
49 | ============
50 |
51 |
52 |
53 | 易用
54 | --
55 |
56 | - 快速的上手速度
57 |
58 | - 配置简单,所见即所得
59 |
60 |
61 |
62 | 输出可定制
63 | -----
64 |
65 | - 支持各种输出格式的定制方式:数组与字典、折叠、引用、可选字段的输出
66 |
67 |
68 |
69 | 数据完整性
70 | -----
71 |
72 | - 单元格中的数据都会输出,不会允许跳过空数据,保证JSON结构的同一化
73 |
74 | - 所有的属性值,如果不填写,缺省值都是null,防止bool判断出错
75 |
76 | - 支持的字段类型较多,建议明确书写字段类型参数。如果当前单元格的数据没有填写对应的字段类型,程序会自动判断。
77 |
78 |
79 |
80 | 格式自携带
81 | -----
82 |
83 | - 数据与数据格式存储在同一张sheet上(主表模式除外),便于同步
84 |
85 |
86 |
87 | **Part3.初级功能**
88 | ==============
89 |
90 |
91 |
92 | *初级功能适用于简单的单机游戏,休闲益智类游戏,等没有太多复杂数值要处理的游戏类型。它的上手速度较快,而且也提供了足够的支持。*
93 |
94 |
95 |
96 | **3.1 单表模式**
97 | --------
98 |
99 | 顾名思义,单表模式就是只有一个workbook文件的模式。该模式下,我们只使用一个.xlsx文件。所有的sheet都在这个文件上。
100 |
101 |
102 |
103 | 单表模式的命令,举例如下:
104 |
105 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
106 | python excel_and_json.py singlebook -o ./ -i single.xlsx
107 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
108 |
109 | 命令说明:
110 |
111 | - singlebook:开启单表模式
112 |
113 | - -o:输出目录
114 |
115 | - -i:输入的.xlsx
116 |
117 |
118 |
119 | **3.2 表头**
120 | ------
121 |
122 | 表头决定了sheet的各种格式和内容信息
123 | __default__ | | | | | |
---|
__folding__ | | | | | |
__type__ | s | i | i | i | i |
__name__ | name | hp | mp | atk | def |
124 |
125 | 表头最左侧是一些约定标记,这些标记表示了该行是什么参数类型:
126 |
127 | - `__type__`:必选标记,表示该行是字段类型
128 |
129 | - `__name__`:必选标记,表示该行是字段名
130 |
131 | - `__default__`:可选标记,表示该行是缺省值
132 |
133 | - `__folding__`:可选标记,表示该行是折叠属性,在高级功能中详述
134 |
135 |
136 |
137 | ### 字段类型
138 |
139 | 字段类型是必选标记。ExcelAndJSON支持如下的字段类型:
140 |
141 | - s:字符串
142 |
143 | - i:整数
144 |
145 | - f:小数
146 |
147 | - b:布尔
148 |
149 | - as:字符串数组
150 |
151 | - ai:整数数组
152 |
153 | - af:小数数组
154 |
155 | - d:字典,若在字典中想使用数字字符串作为值,请在数字两端加""
156 |
157 | - r:引用,可以引用另一张sheet中的内容,在高级功能中详述
158 |
159 |
160 |
161 | ### 缺省值
162 |
163 | 缺省值是可选标记。如果该单元格是空白,程序会自动在该单元格上填写缺省值。
164 |
165 | - 为了保证布尔判断的正确性,以及表格结构的完整性,所有默认的缺省值都是null
166 |
167 | - 可以自定义缺省值,但与单元格一样,需要保证数据类型的正确性
168 |
169 |
170 |
171 | **3.3 数据区**
172 | -------
173 |
174 | 在表头下面的,就是数据区。数据区的大小是程序自行判断的,为了保证程序正确判断数据区的单元格位置,请在数据区下面一行和最右侧一列,保持空白。在数据区之外的单元格,程序不会读取,可自由编辑。
175 |
176 |
177 |
178 | ### null
179 |
180 | null为保留字,在单元格中可以直接填写null为占位符。程序最终会输出null到JSON结构中。
181 |
182 |
183 |
184 | ### 字段类型自动判断
185 |
186 | 如果当前单元格的数据没有填写对应的字段类型,程序将自动判断。只支持几种类型的自动判断:i,f,s。
187 |
188 |
189 |
190 |
191 | **Part4.高级功能**
192 | ==============
193 |
194 |
195 |
196 | *高级功能适用于比较复杂的大中型游戏,有大量数值要处理。它有一定的学习成本,但相应的提供了更强大的功能。*
197 |
198 |
199 |
200 | **4.1 引用**
201 | ------
202 |
203 | 引用是一个字段类型,其类型值为r。引用允许在一张sheet中,插入另一张sheet中的一行。
204 | __type__ | r |
---|
__name__ | lv1award |
zhangsan | lvAward.lv1 |
205 |
206 | 在引用属性的单元格中,填写的是lvAward.lv1。该格式为约定格式“表名.记录名”。程序会在名为“lvAward”的sheet中,查找到名为“lv1”的记录,并把该记录,作为属性值插入到当前sheet中。
207 |
208 | 注意,不允许各个sheet之间循环引用。在单表模式下,被引用的表也不会输出。
209 |
210 |
211 |
212 | **4.2 折叠**
213 | ------
214 |
215 | 折叠是一个可选字段属性。该功能允许把一张sheet中的字段,反复折叠,变成一个JSON,该JSON会填充到最后一次折叠对应的单元格中。
216 |
217 |
218 |
219 | 按JSON格式的特点,我们有两种折叠方式:
220 |
221 | - 按{}折叠:折叠后,会生成一个字典
222 |
223 | - 按[]折叠:折叠后,会生成一个数组
224 |
225 |
226 |
227 | 每一次折叠,不只会折叠数据。对应的字段类型,字段名,甚至折叠属性本身都会被折叠。我们约定,在折叠字段属性填写时,左侧括号后,必须紧接一个折叠后的字段名。折叠后的字段类型为d。
228 |
229 |
230 |
231 | 折叠本身的含义可能过于抽象,但其实质是非常简单的。下面演示了折叠的整个过程:
232 |
233 |
234 |
235 | 原始表
236 | __folding__ | {a{b | | } | | {c | }} |
---|
__type__ | s | i | i | i | s | b |
__name__ | name | hp | atk | def | description | leader |
237 |
238 |
239 | 第一次折叠
240 | __folding__ | {a | | {c | }} |
---|
__type__ | d | i | s | b |
__name__ | b | def | description | leader |
241 |
242 |
243 | 第二次折叠
244 | __folding__ | {a | | } |
---|
__type__ | d | i | d |
__name__ | b | def | c |
245 |
246 |
247 | 第三次折叠
248 | __folding__ | |
---|
__type__ | d |
__name__ | a |
249 |
250 |
251 | 折叠后的JSON结构形如:
252 |
253 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
254 | "a":{
255 | "b":{
256 | "name":"x x x",
257 | "hp":111,
258 | "atk":222
259 | },
260 | "def":333,
261 | "c":{
262 | "description":"x x x",
263 | "leader":false
264 | }
265 | }
266 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
267 |
268 |
269 | ### 引用与折叠的关系
270 |
271 | 特别注意,程序在进行数据输出时。先对每个sheet进行单独处理,此时会触发折叠操作。然后如果该sheet存在引用,再插入相应的数据。所以,引用插入的数据是不可以被折叠的。
272 |
273 |
274 |
275 |
276 | **4.3 主表模式**
277 | --------
278 |
279 | 顾名思义,主表模式是存在一个独立的主要workbook文件,该.xlsx内部存储了一些用于输出的配置信息。一个主表workbook可以读取和加载大量的数据workbook。并按照相应的定制信息,进行sheet的输出。
280 |
281 |
282 |
283 | 主表模式的命令,举例如下:
284 |
285 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
286 | python excel_and_json.py mainbook -o ./ -i main.xlsx
287 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
288 |
289 | 命令说明:
290 |
291 | - mainbook:开启主表模式
292 |
293 | - -o:输出目录
294 |
295 | - -i:输入的.xlsx
296 |
297 |
298 |
299 | ### 输入的workbook
300 |
301 | 使用`__workbook__`进行标记,在后面紧接要使用的workbook名。
302 | __workbook__ | workbook1 | workbook2 |
---|
303 |
304 |
305 | ### 输出的sheet
306 |
307 | 在`__workbook__`标记下面,每一行都是一个要输出sheet。每行开头为该sheet的名字。
308 | sheet1 | aaa | skill1 | skill2 | skill3 | option | lv1 | lv2 |
---|
sheet2 | name | atk | def | hp | | | |
sheet3 | lv1 | lv2 | lv3 | | | | |
sheet4 | | | | | | | |
sheet4->sheet5 | | | | | | | |
309 |
310 | - 选择输出的字段:如果需要字段的选择性输出,可以在sheet名后面接要输出的字段名。如果不写,则该sheet的所有字段都会被输出。
311 |
312 | - 表改名:支持在输出时修改表的名字,方便把一个表拆成多个。表的旧名和新名之间,用->连接。
313 |
314 |
315 |
316 | **Part5.输出BSON**
317 | ==============
318 |
319 |
320 |
321 | *有部分同行对JSON的读取性能表示担忧。使用BSON格式可以提高数据的读取速度,但也需要配置相应的读取库。*
322 |
323 | *实际上,在游戏开发中,JSON主要用来做一些配置信息和数值,并没有大规模的数据量,一般情况下不会成为性能瓶颈。而且因为Python不支持条件编译,增加BSON必然增加配置时间,或复杂度(通过替换文件方式)。*
324 |
325 | 可以手动添加BSON的输出支持,使用下面的库即可。
326 |
327 | [https://github.com/martinkou/bson][1]
328 |
329 |
330 | [1]: https://github.com/martinkou/bson
331 |
--------------------------------------------------------------------------------
/src/Sheet.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | __author__ = 'goldlion'
3 | __qq__ = 233424570
4 | __email__ = 'gdgoldlion@gmail.com'
5 |
6 | import xlrd
7 | import json
8 | import math
9 |
10 | from xlrd import XL_CELL_EMPTY, XL_CELL_TEXT, XL_CELL_NUMBER, XL_CELL_DATE, XL_CELL_BOOLEAN, XL_CELL_ERROR, \
11 | XL_CELL_BLANK
12 |
13 | import SheetManager
14 |
15 | class Field:
16 | def __init__(self):
17 | #字段名
18 | self.name = None
19 | #字段类型
20 | self.type = None
21 | #缺省值
22 | self.default = None
23 | #折叠属性
24 | self.folding = None
25 |
26 | def __str__(self):
27 | return "name:%r,type:%r,default:%r,folding:%r" % (self.name, self.type, self.default, self.folding)
28 |
29 | class Sheet:
30 | def __init__(self, sh):
31 | self.sh = sh
32 | self.name = sh.name
33 | #是否完全初始化完毕(最后一个步骤是插入表的引用)
34 | self.inited = False
35 | #字段属性列表
36 | self.fieldList = []
37 | #引用的其他sheet名
38 | self.referenceSheets = set()
39 | #解析的数据
40 | self.python_obj = {}
41 |
42 | self.__findRow()
43 | self.__findCol()
44 |
45 | self.__parseField()
46 | self.__parseReferenceSheet()
47 |
48 | self.__convertPython()
49 | self.__executeFolding()
50 |
51 | #查找数据起始行数,格式行,缺省值行,类型行,数据终止行数
52 | def __findRow(self):
53 | self.defaultRow = -1
54 | self.foldingRow = -1
55 |
56 | for i in range(0, 5):
57 | value = self.sh.cell(i, 0).value
58 | if value == '__default__':
59 | self.defaultRow = i
60 | elif value == '__folding__':
61 | self.foldingRow = i
62 | elif value == '__type__':
63 | self.typeRow = i
64 | elif value == '__name__':
65 | self.nameRow = i
66 | else:
67 | self.dataStartRow = i
68 | break
69 |
70 | for row in range(self.sh.nrows):
71 | if self.sh.cell(row, 0).ctype == XL_CELL_EMPTY:
72 | self.dataEndRow = row
73 | break
74 |
75 | if row == self.sh.nrows - 1:
76 | self.dataEndRow = self.sh.nrows
77 |
78 | #查找数据终止列数
79 | def __findCol(self):
80 | #遍历查找,如果在excel中存在多余的注释,列数为第一个空字符串出现的单元格下标#
81 | for col in range(self.sh.ncols):
82 | if self.sh.cell(self.nameRow, col).ctype == XL_CELL_EMPTY:
83 | self.dataEndCol = col
84 | break
85 |
86 | #若col未定义,则表示在excel中不存在多余的注释,则列数为整个表的列数#
87 | if col == self.sh.ncols - 1:
88 | self.dataEndCol = self.sh.ncols
89 |
90 | #解析字段属性
91 | def __parseField(self):
92 |
93 | for col in range(self.dataEndCol):
94 | field = Field()
95 | self.fieldList.append(field)
96 |
97 | #字段类型
98 | field.type = self.sh.cell(self.typeRow, col).value
99 |
100 | #字段名字
101 | field.name = self.sh.cell(self.nameRow, col).value
102 |
103 | #字段缺省值
104 | if self.defaultRow == -1:
105 | field.default = None
106 | else:
107 | type = field.type
108 | ctype = self.sh.cell(self.defaultRow, col).ctype
109 | value = self.sh.cell(self.defaultRow, col).value
110 |
111 | if col == 0: #第一位缺省值,占位符
112 | field.default = None
113 | elif ctype == XL_CELL_EMPTY: #空白格
114 | field.default = None
115 | elif value == 'null': #null格
116 | field.default = None
117 | elif type == 'i':
118 | field.default = int(value)
119 | elif type == 'f':
120 | field.default = value
121 | elif type == 's':
122 | field.default = value
123 | elif type == 'b':
124 | field.default = bool(value)
125 | elif type == 'as' or type == 'ai' or type == 'af': #数组
126 | field.default = self.__convertStrToList(value, type)
127 | elif type == 'd': #字典
128 | field.default = self.__convertStrToDict(value)
129 | elif type == 'r': #引用
130 | field.default = value
131 |
132 | #字段折叠
133 | if self.foldingRow == -1:
134 | field.folding = None
135 | else:
136 | ctype = self.sh.cell(self.foldingRow, col).ctype
137 | value = self.sh.cell(self.foldingRow, col).value
138 |
139 | if ctype == XL_CELL_EMPTY:
140 | field.folding = None
141 | else:
142 | field.folding = value
143 |
144 | def __parseReferenceSheet(self):
145 | for row in range(self.dataStartRow, self.dataEndRow):
146 | for col in range(1, self.dataEndCol):
147 | field = self.fieldList[col]
148 | fieldType = field.type
149 | value = self.sh.cell(row, col).value
150 |
151 | if fieldType == 'r': #引用,保存引用字符串,以备插入引用表
152 | sheetName = value.split(".")[0]
153 | self.referenceSheets.add(sheetName)
154 |
155 | #转换字符串为list
156 | def __convertStrToList(self, str, typeStr):
157 | type = typeStr[1]
158 | list = str.split(',')
159 | for i in range(len(list)):
160 | if type == 's':
161 | list[i] = list[i]
162 | elif type == 'i':
163 | list[i] = int(list[i])
164 | elif type == 'f':
165 | list[i] = float(list[i])
166 |
167 | return list
168 |
169 | #转换字符串为dict
170 | def __convertStrToDict(self, str):
171 | dict = {}
172 | list = str.split(',')
173 | for i in range(len(list)):
174 | kv = list[i].split(':')
175 | key = kv[0]
176 | value = kv[1]
177 |
178 | if value.isdigit() and '.' in value:
179 | dict[key] = float(value)
180 | elif value.isdigit():
181 | dict[key] = int(value)
182 | else:
183 | dict[key] = value
184 |
185 | return dict
186 |
187 | def log(self):
188 | print '缺省值行', self.defaultRow
189 | print '折叠行', self.foldingRow
190 | print '类型行', self.typeRow
191 | print '字段名行', self.nameRow
192 | print '数据起始行', self.dataStartRow
193 | print '数据终止行', self.dataEndRow
194 | print '数据终止列', self.dataEndCol
195 | print '字段属性'
196 | for field in self.fieldList:
197 | print field
198 | print '引用表', self.referenceSheets
199 |
200 | #获得当前行的recordId
201 | def __getRecordId(self, row):
202 | recordId = self.sh.cell(row, 0).value
203 | ctype = self.sh.cell(row, 0).ctype
204 | if ctype == XL_CELL_TEXT:
205 | pass
206 | elif ctype == XL_CELL_NUMBER:
207 | #处理为整数做主键
208 | recordId = int(recordId)
209 | #TODO 并不支持小数做主键
210 |
211 | return recordId
212 |
213 | #解析自身数据为python,并折叠。不包括引用数据。
214 | def __convertPython(self):
215 | #dump数据#
216 | for row in range(self.dataStartRow, self.dataEndRow):
217 | recordId = self.__getRecordId(row)
218 | record = self.python_obj[recordId] = {}
219 |
220 | for col in range(1, self.dataEndCol):
221 | field = self.fieldList[col]
222 |
223 | fieldName = field.name
224 | fieldType = field.type
225 |
226 | value = self.sh.cell(row, col).value
227 | ctype = self.sh.cell(row, col).ctype
228 |
229 | if ctype == XL_CELL_EMPTY: #如果是空的,就填入缺省值
230 | record[fieldName] = field.default
231 | elif value == 'null': #null为保留字
232 | record[fieldName] = None
233 | else:
234 | #如果没有类型字段,就自动判断类型,只支持i、f、s
235 | if fieldType == '' or fieldType == None:
236 | fieldType = self.__autoDecideType(value)
237 |
238 | if fieldType == 'i':
239 | record[fieldName] = int(value)
240 | elif fieldType == 'f':
241 | record[fieldName] = value
242 | elif fieldType == 's':
243 | record[fieldName] = value
244 | elif fieldType == 'b':
245 | record[fieldName] = bool(value)
246 | elif fieldType == 'as' or fieldType == 'ai' or fieldType == 'af':
247 | record[fieldName] = self.__convertStrToList(value, fieldType)
248 | elif fieldType == 'd':
249 | record[fieldName] = self.__convertStrToDict(value)
250 | elif fieldType == 'r': #引用,保存引用字符串,以备插入引用表
251 | record[fieldName] = value
252 |
253 | def __autoDecideType(self,value):
254 | if isinstance(value,float):
255 | if math.ceil(value) == value:
256 | return 'i'
257 | else:
258 | return 'f'
259 | else:
260 | return 's'
261 |
262 | def __executeFolding(self):
263 |
264 | while (True):
265 | foldingType = None
266 | #查找右侧括号#
267 | for i in range(len(self.fieldList)):
268 | field = self.fieldList[i]
269 | folding = field.folding
270 | if folding == None or folding == '':
271 | continue
272 |
273 | if folding[0] == '}':
274 | foldingType = "brace"
275 | elif folding[0] == ']':
276 | foldingType = "bracket"
277 | else: #未找到右括号,进入下一轮循环
278 | continue
279 |
280 | #记录折叠终止格
281 | endIndex = i
282 |
283 | #清除括号
284 | field.folding = folding[1:]
285 | break
286 |
287 | #未找到折叠字段类型,就跳出
288 | if foldingType == None:
289 | break
290 |
291 | #查找左侧括号#
292 | for i in range(endIndex - 1, -1, -1):
293 | field = self.fieldList[i]
294 | folding = field.folding
295 | if folding == None or folding == '':
296 | continue
297 |
298 | if foldingType == "brace":
299 | bracketIndex = folding.rfind('{')
300 | elif foldingType == "bracket":
301 | bracketIndex = folding.rfind('[')
302 |
303 | #未找到括号,跳过
304 | if bracketIndex == -1:
305 | continue
306 |
307 | #记录折叠起始格
308 | startIndex = i
309 |
310 | #取折叠后的名字
311 | foldingName = folding[bracketIndex + 1:]
312 |
313 | #清除括号和名字
314 | field.folding = folding[:bracketIndex]
315 | break
316 |
317 | #折叠数据#
318 | for row in range(self.dataStartRow, self.dataEndRow):
319 | #取记录
320 | recordId = self.__getRecordId(row)
321 | record = self.python_obj[recordId]
322 |
323 | #生成新对象
324 | if foldingType == "brace":
325 | foldingObj = {}
326 | elif foldingType == "bracket":
327 | foldingObj = []
328 |
329 | for col in range(startIndex, endIndex + 1):
330 | field = self.fieldList[col]
331 |
332 | #保存折叠后的数据
333 | if foldingType == "brace":
334 | foldingObj[field.name] = record[field.name]
335 | elif foldingType == "bracket":
336 | foldingObj.append(record[field.name])
337 |
338 | del record[field.name]
339 |
340 | #挂接新对象
341 | record[foldingName] = foldingObj
342 |
343 | #折叠字段#
344 | #需要清除的字段索引表
345 | delFieldList = []
346 | for col in range(startIndex + 1, endIndex + 1):
347 | field = self.fieldList[col]
348 | delFieldList.append(field)
349 |
350 | #如果最后一格有内容,则复制给合并后的格子
351 | if field.folding != None or field.folding != '':
352 | folding = field.folding
353 | else:
354 | folding = None
355 |
356 | #执行清除
357 | for field in delFieldList:
358 | self.fieldList.remove(field)
359 |
360 | #刷新折叠后的字段
361 | field = self.fieldList[startIndex]
362 | field.name = foldingName
363 | field.type = 'd' #折叠后变为字典类型
364 | if folding != None:
365 | field.folding = folding
366 |
367 | #刷新列表长度
368 | self.dataEndCol -= endIndex - startIndex
369 |
370 | def toPython(self, sheet_output_field=[]):
371 | #插入引用表
372 | if not self.inited:
373 | self.__mergePython()
374 | self.inited = True
375 |
376 | #选择性输出
377 | if sheet_output_field == []:
378 | return self.python_obj
379 | else:
380 | new_python_obj = self.python_obj.copy()
381 | for recordId in new_python_obj:
382 | delFieldNameList = []
383 |
384 | for fieldName in new_python_obj[recordId]:
385 | if fieldName in sheet_output_field:
386 | pass
387 | else:
388 | delFieldNameList.append(fieldName)
389 |
390 | for delFieldName in delFieldNameList:
391 | del new_python_obj[recordId][delFieldName]
392 |
393 | return new_python_obj
394 |
395 | #合并引用表到当前表
396 | def __mergePython(self):
397 | for row in range(self.dataStartRow, self.dataEndRow):
398 |
399 | recordId = self.__getRecordId(row)
400 | record = self.python_obj[recordId]
401 |
402 | for col in range(1, self.dataEndCol):
403 | field = self.fieldList[col]
404 | fieldName = field.name
405 | fieldType = field.type
406 |
407 | if fieldType == 'r': #引用
408 | value = record[fieldName]
409 | reference_sheetName = value.split('.')[0]
410 | reference_recordId = value.split('.')[1]
411 |
412 | if reference_recordId.isdigit():
413 | reference_recordId = int(reference_recordId)
414 | #TODO 并不支持小数做主键
415 |
416 | referenceSheet = SheetManager.getSheet(reference_sheetName)
417 | reference_python_obj = referenceSheet.toPython()
418 |
419 | record[fieldName] = reference_python_obj[reference_recordId]
420 |
421 | def toJSON(self,sheet_output_field=[]):
422 | json_obj = json.dumps(self.toPython(sheet_output_field), sort_keys=True, indent=2, ensure_ascii=False)
423 | return json_obj
424 |
425 | def openSheet(sh):
426 | return Sheet(sh)
--------------------------------------------------------------------------------
/src/SheetManager.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | __author__ = 'goldlion'
3 | __qq__ = 233424570
4 | __email__ = 'gdgoldlion@gmail.com'
5 |
6 | import xlrd
7 | import json
8 | import Sheet
9 |
10 | sheetDict = {}
11 | sheetNameList = []
12 |
13 | def addWorkBook(filepath):
14 | wb = xlrd.open_workbook(filepath)
15 |
16 | for sheet_index in range(wb.nsheets):
17 | sh = wb.sheet_by_index(sheet_index)
18 | sheet = Sheet.openSheet(sh)
19 | addSheet(sheet)
20 |
21 | def addSheet(sheet):
22 | sheetDict[sheet.name] = sheet
23 | sheetNameList.append(sheet.name)
24 |
25 | def getSheet(name):
26 | return sheetDict[name]
27 |
28 | def getSheetNameList():
29 | return sheetNameList
30 |
31 | def exportJSON(name,sheet_output_field = []):
32 | return sheetDict[name].toJSON(sheet_output_field)
33 |
34 | def isReferencedSheet(name):
35 | for sheetName in sheetDict:
36 | if name in sheetDict[sheetName].referenceSheets:
37 | return True
38 |
39 | return False
--------------------------------------------------------------------------------
/src/excel_and_json.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | __author__ = 'goldlion'
3 | __qq__ = 233424570
4 | __email__ = 'gdgoldlion@gmail.com'
5 |
6 | import xlrd
7 | import sys
8 | import getopt
9 |
10 | import SheetManager
11 |
12 | #单表模式
13 | def singlebook():
14 | opts, args = getopt.getopt(sys.argv[2:], "hi:o:")
15 |
16 | for op, value in opts:
17 | if op == "-i":
18 | file_path = value
19 | elif op == "-o":
20 | output_path = value
21 | elif op == "-h":
22 | #TODO 写说明文字
23 | # usage()
24 | sys.exit()
25 |
26 | if not "file_path" in locals().keys():
27 | # usage()
28 | sys.exit()
29 | elif not "output_path" in locals().keys():
30 | # usage()
31 | sys.exit()
32 |
33 | SheetManager.addWorkBook(file_path)
34 | sheetNameList = SheetManager.getSheetNameList()
35 |
36 | for sheet_name in sheetNameList:
37 | #单表模式下,被引用的表不会输出
38 | if SheetManager.isReferencedSheet(sheet_name):
39 | continue
40 |
41 | sheetJSON = SheetManager.exportJSON(sheet_name)
42 |
43 | f = file(output_path+sheet_name+'.json', 'w')
44 | f.write(sheetJSON.encode('UTF-8'))
45 | f.close()
46 |
47 | #主表模式
48 | def mainbook():
49 | opts, args = getopt.getopt(sys.argv[2:], "hi:o:")
50 |
51 | for op, value in opts:
52 | if op == "-i":
53 | file_path = value
54 | elif op == "-o":
55 | output_path = value
56 | elif op == "-h":
57 | #TODO 写说明文字
58 | # usage()
59 | sys.exit()
60 |
61 | if not "file_path" in locals().keys():
62 | # usage()
63 | sys.exit()
64 | elif not "output_path" in locals().keys():
65 | # usage()
66 | sys.exit()
67 |
68 | #获取主表各种参数#
69 | wb = xlrd.open_workbook(file_path)
70 | sh = wb.sheet_by_index(0)
71 |
72 | workbookPathList = []
73 | sheetList = []
74 | for row in range(sh.nrows):
75 | type = sh.cell(row,0).value
76 |
77 | if type == '__workbook__':
78 | pass
79 | else:
80 | sheetList.append([])
81 | sheet = sheetList[-1]
82 | sheet.append(type)
83 |
84 | for col in range(1,sh.ncols):
85 | value = sh.cell(row,col).value
86 |
87 | if type == '__workbook__' and value != '':
88 | workbookPathList.append(value)
89 | elif value != '':
90 | sheet.append(value)
91 |
92 | #加载所有xlsx文件#
93 | for workbookPath in workbookPathList:
94 | #读取所有sheet
95 | SheetManager.addWorkBook(workbookPath+".xlsx")
96 |
97 | #输出所有表#
98 | for sheet in sheetList:
99 |
100 | #表改名处理
101 | if '->' in sheet[0]:
102 | sheet_name = sheet[0].split('->')[0]
103 | sheet_output_name = sheet[0].split('->')[1]
104 | else:
105 | sheet_output_name = sheet_name = sheet[0]
106 |
107 | sheet_output_field = sheet[1:]
108 |
109 | sheetJSON = SheetManager.exportJSON(sheet_name,sheet_output_field)
110 |
111 | f = file(output_path+sheet_output_name+'.json', 'w')
112 | f.write(sheetJSON.encode('UTF-8'))
113 | f.close()
114 |
115 | if __name__ == '__main__':
116 | modelType = sys.argv[1]
117 |
118 | if modelType == "singlebook":
119 | singlebook()
120 | elif modelType == "mainbook":
121 | mainbook()
122 | else:
123 | # usage()
124 | sys.exit()
--------------------------------------------------------------------------------
/test1/mwb.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gdgoldlion/ExcelAndJSON/597718c05754292feda17498224a94251a76b2d7/test1/mwb.xlsx
--------------------------------------------------------------------------------
/test1/test_mainbook_model.sh:
--------------------------------------------------------------------------------
1 | python ../src/excel_and_json.py mainbook -o ./ -i mwb.xlsx
--------------------------------------------------------------------------------
/test1/test_singlebook_model.sh:
--------------------------------------------------------------------------------
1 | python ../src/excel_and_json.py singlebook -o ./ -i wb1.xlsx
--------------------------------------------------------------------------------
/test1/wb1.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gdgoldlion/ExcelAndJSON/597718c05754292feda17498224a94251a76b2d7/test1/wb1.xlsx
--------------------------------------------------------------------------------
/test1/wb2.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gdgoldlion/ExcelAndJSON/597718c05754292feda17498224a94251a76b2d7/test1/wb2.xlsx
--------------------------------------------------------------------------------
/test2/clothes_level.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gdgoldlion/ExcelAndJSON/597718c05754292feda17498224a94251a76b2d7/test2/clothes_level.xlsx
--------------------------------------------------------------------------------
/test2/levelup.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gdgoldlion/ExcelAndJSON/597718c05754292feda17498224a94251a76b2d7/test2/levelup.xlsx
--------------------------------------------------------------------------------
/test2/mwb.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gdgoldlion/ExcelAndJSON/597718c05754292feda17498224a94251a76b2d7/test2/mwb.xlsx
--------------------------------------------------------------------------------
/test2/test_mainbook_model.sh:
--------------------------------------------------------------------------------
1 | python ../src/excel_and_json.py mainbook -o ./ -i mwb.xlsx
--------------------------------------------------------------------------------