├── .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 | 35 | 38 | 41 | 44 | 47 | 48 | 49 | 52 | 55 | 58 | 61 | 64 | 65 | 66 | 69 | 72 | 75 | 78 | 81 | 82 | 83 | 86 | 89 | 92 | 95 | 98 | 99 | 100 |
33 | length 34 | 36 | skill1 37 | 39 | skill2 40 | 42 | skill3 43 | 45 | skill4 46 |
50 | 4 51 | 53 | 火球 54 | 56 | 冰箭 57 | 59 | 魔法盾 60 | 62 | 顺移 63 |
67 | 3 68 | 70 | 突刺 71 | 73 | 半月 74 | 76 | 重斩 77 | 79 |
80 |
84 | 1 85 | 87 | 治疗 88 | 90 |
91 |
93 |
94 |
96 |
97 |
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 | 127 | 130 | 133 | 136 | 139 | 142 | 143 | 144 | 147 | 150 | 153 | 156 | 159 | 162 | 163 | 164 | 167 | 170 | 173 | 176 | 179 | 182 | 183 | 184 | 187 | 190 | 193 | 196 | 199 | 202 | 203 | 204 | 207 | 210 | 213 | 216 | 219 | 222 | 223 | 224 |
125 | s 126 | 128 | i 129 | 131 | i 132 | 134 | r 135 | 137 | r 138 | 140 | r 141 |
145 | name 146 | 148 | unlock_lv 149 | 151 | area 152 | 154 | lv1 155 | 157 | lv2 158 | 160 | lv3 161 |
165 | 基地 166 | 168 | 1 169 | 171 | 4 172 | 174 | 基地.lv1 175 | 177 | 基地.lv2 178 | 180 | 基地.lv3 181 |
185 | 铀矿 186 | 188 | 3 189 | 191 | 4 192 | 194 | 铀矿.lv1 195 | 197 | 铀矿.lv2 198 | 200 | 铀矿.lv3 201 |
205 | 兵营 206 | 208 | 5 209 | 211 | 1 212 | 214 | 兵营.lv1 215 | 217 | 兵营.lv2 218 | 220 | 兵营.lv3 221 |
225 | 226 | **Part4.主表模式的意义是什么?** 227 | ============ 228 | 229 | 游戏开发中,前后端对于数据的需求是不一样的。前端需要的是一些显示数据,如资源名称,动作参数。后端需要的是一些计算数据,比如攻击力,防御力,伤害公式等。但是有一些数据,是前后端都需要的,比如:技能范围,技能类型等,这些数据既与前端的显示有关系也和后端的逻辑计算有关系。 230 | 231 | 那么这种情况下,按照传统方式,也会拆成若干表。一般是一张表前端用,一张表后端用。但问题在于,前后端都需要的数据该如何处理?在两个表之间同步是一个成本比较高的办法。 232 | 233 | 这就体现出主表模式的意义了。我们可以把这些数据都组织在一张表上: 234 | 235 | 236 | 237 | 240 | 243 | 246 | 249 | 250 | 251 | 254 | 257 | 260 | 263 | 264 | 265 | 268 | 271 | 274 | 277 | 278 | 279 | 282 | 285 | 288 | 291 | 292 | 293 |
238 | name 239 | 241 | type 242 | 244 | effect 245 | 247 | atk 248 |
252 | 平砍 253 | 255 | 1 256 | 258 | 平砍.png 259 | 261 | 10 262 |
266 | 横扫千军 267 | 269 | 3 270 | 272 | 横扫千军.png 273 | 275 | 7 276 |
280 | 暴风雪 281 | 283 | 4 284 | 286 | 暴风雪.png 287 | 289 | 8 290 |
294 | 295 | 然后在输出的时候,在主表模式中,分成两个来输出: 296 | 297 | 298 | 299 | 302 | 305 | 308 | 311 | 312 | 313 | 316 | 319 | 322 | 325 | 326 | 327 |
300 | skill->skill_fn 301 | 303 | name 304 | 306 | type 307 | 309 | effect 310 |
314 | skill->skill_bn 315 | 317 | name 318 | 320 | type 321 | 323 | atk 324 |
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__siiii
__name__namehpmpatkdef
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
zhangsanlvAward.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__siiisb
__name__namehpatkdefdescriptionleader
237 | 238 | 239 | 第一次折叠 240 |
__folding__{a{c}}
__type__disb
__name__bdefdescriptionleader
241 | 242 | 243 | 第二次折叠 244 |
__folding__{a}
__type__did
__name__bdefc
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__workbook1workbook2
303 | 304 | 305 | ### 输出的sheet 306 | 307 | 在`__workbook__`标记下面,每一行都是一个要输出sheet。每行开头为该sheet的名字。 308 |
sheet1aaaskill1skill2skill3optionlv1lv2
sheet2nameatkdefhp
sheet3lv1lv2lv3
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 --------------------------------------------------------------------------------