├── LICENSE ├── README.md ├── output.lua ├── test1.xlsx ├── test2.xlsx └── xls2lua.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 trumanzhao 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xls2lua 2 | 3 | 将excel文件(.xls, .xlsx)转换为lua代码的小工具. 4 | 5 | ## 使用方法 6 | 7 | 可以独立运行,也可以作为一个库导入. 8 | 9 | ```sh 10 | ./xls2lua.py *.xlsx 11 | ``` 12 | 13 | ## 主要特性: 14 | - 支持多级索引转换,生成层级table 15 | - 也支持无索引转换,按行生成数组 16 | - 支持差量转换,只转换有变更的文件(基于文件时间比较) 17 | - 支持把excel列标题直接做lua表名,也支持用额外metadata指定表名和字段类型(比如列标题可能是中文的) 18 | - 支持指定转换的数据类型,如string,number,bool,也可以不指定类型 19 | 20 | ## 环境需求 21 | 22 | python2或者python3均可,需要xlrd模块用于读取excel. 23 | 24 | ```sh 25 | pip install xlrd 26 | ``` 27 | 28 | ## 对excel表格的要求 29 | 30 | 可按两种模式来填写excel表格 31 | 32 | ### 模式1: 33 | 34 | 需要在excel中专门建一个名为xls2lua的Sheet,其中每一列对应一个需要转换的Sheet: 35 | 第一行指定了该Sheet对应的table名,形如: "SomeSheet=SomeTable" 36 | 其余行表示"数据列"到lua变量的映射,形如: "SomeColumn=SomeField" 37 | 38 | 注意SomeField支持一些符号标记: 39 | - '*'开头的映射名表示它在table中用作索引,索引可以有多个,但不能所有列都是索引. 40 | - '#'结尾的映射名表示映射为数字. 41 | - '$'结尾的映射名表示映射为字符串,指明了这种格式时,在excel中填写时无需加引号. 42 | - '?'结尾的映射名表示映射为布尔变量,如果填的是字符串,会自动处理常见的值,比如(0, 1, 是,否...). 43 | - 结尾不是"#$?"的,会尽可能将表格中的字面值照搬到lua中去,这时如果希望被处理为字符串的,需要在表格中自行添加引号. 44 | 45 | 示例参见test1.xls. 46 | 47 | ### 模式2: 48 | 49 | 无需在excel文件中加入额外的名为xls2lua的meta sheet,直接在列标题中标注即可; 50 | 标注格式与方式1类似,示例参见test2.xls. 51 | 52 | 53 | ## 可能的问题 54 | excel中的数字转换为代码文本时,是按6位小数精度处理的,如果有别的精度要求,可以修改代码中的定义. 55 | 注意,调整这个精度可能导致excel中填写的整数转换后变成了近似的小数. 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /output.lua: -------------------------------------------------------------------------------- 1 | --2017-08-13 09:09:02, https://github.com/trumanzhao/xls2lua 2 | 3 | --test1.xlsx: 带列标题示例 4 | table_with_header = 5 | { 6 | --大段位 7 | [1] = 8 | { 9 | --小段位 10 | [1] = {elo=0, name="青铜1", fight_count=3, reward=nil, protect=true}, 11 | [2] = {elo=100, name="青铜2", fight_count=3, reward={102, 1}, protect=false}, 12 | [3] = {elo=200, name="青铜3", fight_count=3, reward={103, 2}, protect=false}, 13 | }, 14 | [2] = 15 | { 16 | --小段位 17 | [1] = {elo=300, name="白银1", fight_count=3, reward={104, 3}, protect=true}, 18 | [2] = {elo=400, name="白银2", fight_count=3, reward={105, 4}, protect=false}, 19 | [3] = {elo=500, name="白银3", fight_count=3, reward={106, 5}, protect=false}, 20 | }, 21 | [3] = 22 | { 23 | --小段位 24 | [1] = {elo=600, name="黄金1", fight_count=5, reward={107, 6}, protect=true}, 25 | [2] = {elo=700, name="黄金2", fight_count=5, reward={108, 7}, protect=false}, 26 | [3] = {elo=800, name="黄金3", fight_count=5, reward={109, 8}, protect=false}, 27 | }, 28 | }; 29 | 30 | --test1.xlsx: 单列示例 31 | sigle_column = 32 | { 33 | "无类型指示的字符串必须加引号", 34 | "单列Sheet是无法做映射的,当做数组", 35 | "张三", 36 | "李四", 37 | "王麻子", 38 | "当然也可以是下面这样的数字", 39 | 12345, 40 | }; 41 | 42 | --test1.xlsx: 双列示例 43 | dictionary = 44 | { 45 | --名字 46 | ["张三"] = 10, --等级 47 | ["李四"] = 20, 48 | ["王麻子"] = 30, 49 | }; 50 | 51 | --test1.xlsx: 数组示例 52 | array_data = 53 | { 54 | {0, 1, 2, 3, 4, 5}, 55 | {1, 1, 2, 3, 4, 5}, 56 | {2, 2, 4, 6, 8, 10}, 57 | {3, 3, 6, 9, 12, 15}, 58 | {4, 4, 8, 12, 16, 20}, 59 | {5, 5, 10, 15, 20, 25}, 60 | }; 61 | 62 | --test2.xlsx: test2 63 | test2 = 64 | { 65 | --*dan# 66 | [1] = 67 | { 68 | --*step# 69 | [1] = {min_elo=0, fight=3, name="青铜1", protect=true}, 70 | [2] = {min_elo=100, fight=3, name="青铜2", protect=false}, 71 | [3] = {min_elo=200, fight=3, name="青铜3", protect=false}, 72 | }, 73 | [2] = 74 | { 75 | --*step# 76 | [1] = {min_elo=300, fight=3, name="白银1", protect=true}, 77 | [2] = {min_elo=400, fight=3, name="白银2", protect=false}, 78 | [3] = {min_elo=500, fight=3, name="白银3", protect=false}, 79 | }, 80 | [3] = 81 | { 82 | --*step# 83 | [1] = {min_elo=600, fight=5, name="黄金1", protect=true}, 84 | [2] = {min_elo=700, fight=5, name="黄金2", protect=false}, 85 | [3] = {min_elo=800, fight=5, name="黄金3", protect=false}, 86 | }, 87 | }; 88 | 89 | -------------------------------------------------------------------------------- /test1.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumanzhao/xls2lua/fd3247e13d0e2a257af02dc152c5ee47f4e6ee30/test1.xlsx -------------------------------------------------------------------------------- /test2.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumanzhao/xls2lua/fd3247e13d0e2a257af02dc152c5ee47f4e6ee30/test2.xlsx -------------------------------------------------------------------------------- /xls2lua.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | #repository: https://github.com/trumanzhao/xls2lua 4 | #trumanzhao, 2017/03/24, trumanzhao@foxmail.com 5 | 6 | import os, sys, argparse, time, datetime, hashlib, codecs, xlrd 7 | 8 | ''' 9 | 填表时一般无需刻意对字符串加引号,除非是raw模式. 10 | 在映射名前面加'*'表示主键,主键可以有多个;如果没有主键,则会按行处理为数组,那就不能指定映射了. 11 | 映射名结尾,'?'表示bool,'#'表示数字,'@'表示字符串,如果前面三者都不是,则是raw模式. 12 | bool类型会自动处理0,1到true,false的转换,也支持直接填true/false,是/否,有/无; EMPTY处理为false; 13 | 数字类型,如果没有填,则是0;如果填的不是数字,则抛出异常. 14 | 字符串模式,会自动加引号,EMPTY处理为""; 15 | raw模式,转换时照搬,即可能是字符串,也可能是代码,也可能是数字或布尔,转换时字符串不会额外加引号,EMPTY处理为0; 16 | 可以考虑加个参数,使得所有EMPTY都抛异常,或者指定EMPTY咋处理. 17 | ''' 18 | 19 | # 数字转字符串的格式,如果有不同的精度要求,可以调整这里 20 | number2string = "%.6f"; 21 | 22 | class _ColumnDesc(object): 23 | """列描述""" 24 | def __init__(self, column_name, field_name, column_idx): 25 | first_char = field_name[0]; 26 | last_char = field_name[-1]; 27 | map_table = {u"?":"bool", u"#":"number", u"$":"string"}; 28 | field_name = field_name if first_char != u"*" else field_name[1:]; 29 | field_name = field_name if last_char not in map_table else field_name[:-1]; 30 | self.column_name = column_name; 31 | self.column_idx = column_idx; 32 | self.is_key = (first_char == u"*"); 33 | self.field_name = field_name; 34 | self.map_type = map_table[last_char] if last_char in map_table else "raw"; 35 | 36 | class _SheetDesc(object): 37 | """sheet描述""" 38 | def __init__(self, sheet_name, table_name): 39 | self.sheet_name = sheet_name; 40 | self.table_name = table_name; 41 | self.columns = list(); 42 | self.maps = dict(); 43 | self.keys = list(); 44 | self.has_key = False; 45 | 46 | def map(self, column_name, field_name, column_idx): 47 | desc = _ColumnDesc(column_name, field_name, column_idx); 48 | self.columns.append(desc); 49 | self.maps[column_name] = desc; 50 | if desc.is_key: 51 | self.keys.append(desc); 52 | self.has_key = True; 53 | 54 | def _unicode_anyway(text): 55 | try: 56 | some_type = unicode; 57 | return text.decode("utf-8") if isinstance(text, str) else text; 58 | except NameError: 59 | return text.decode("utf-8") if isinstance(text, bytes) else text; 60 | 61 | class Converter(object): 62 | _scope = None; 63 | _indent = u"\t"; 64 | _meta = None; 65 | _lines = None; 66 | _tables = None; 67 | 68 | def __init__(self, scope, indent, meta): 69 | self._scope = scope; 70 | self._indent = indent == 0 and u"\t" or u" " * indent; 71 | self._meta = meta; 72 | self.reset(); 73 | 74 | def _get_signature(self): 75 | url = "https://github.com/trumanzhao/xls2lua"; 76 | now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'); 77 | return u"--%s, %s\n" % (now, url); 78 | 79 | def convert(self, xls_filename): 80 | xls_filename = _unicode_anyway(xls_filename); 81 | try: 82 | self._workbook = xlrd.open_workbook(xls_filename); 83 | self._xls_filetime = os.path.getmtime(xls_filename); 84 | self._xls_filename = xls_filename; 85 | except: 86 | raise Exception("Failed to load workbook: %s" % xls_filename); 87 | 88 | self._sheet_names = self._workbook.sheet_names(); 89 | self._meta_tables = list(); 90 | 91 | if self._meta in self._sheet_names: 92 | self._load_meta_sheet(); 93 | else: 94 | self._load_meta_header(); 95 | 96 | for sheet_desc in self._meta_tables: 97 | self._convert_sheet(sheet_desc); 98 | self._tables.append(sheet_desc.table_name); 99 | 100 | def save(self, filename): 101 | lua_dir = os.path.split(filename)[0]; 102 | if lua_dir != "" and not os.path.exists(lua_dir): 103 | os.makedirs(lua_dir); 104 | line = u""; 105 | if self._scope == u"local": 106 | for table_name in self._tables: 107 | if line == u"": 108 | line += u"return %s" % table_name; 109 | else: 110 | line += u", %s" % table_name; 111 | line += u";\n"; 112 | code = u"".join(self._lines) + line; 113 | open(filename, "wb").write(code.encode("utf-8")); 114 | 115 | def reset(self): 116 | self._lines = list(); 117 | self._lines.append(self._get_signature()); 118 | self._lines.append(u"\n"); 119 | self._tables = list(); 120 | 121 | #比较文件时间戳,如果input比较新或者output不存在,则返回True,否则False 122 | def compare_time(self, input_file, output_file): 123 | if not os.path.isfile(output_file): 124 | return True; 125 | input_time = os.path.getmtime(input_file); 126 | output_time = os.path.getmtime(output_file); 127 | return input_time >= output_time; 128 | 129 | #meta_tables: list of _SheetDesc 130 | #meta_tables之所以是一个list而不是dict,是因为允许对同一个sheet做多个映射转换 131 | def _load_meta_sheet(self): 132 | meta_sheet = self._workbook.sheet_by_name(self._meta); 133 | for column_idx in range(0, meta_sheet.ncols): 134 | self._load_meta_column(meta_sheet, column_idx); 135 | 136 | #meta_sheet中,每列定义了一个sheet的映射 137 | #本函数将每列数据load为一个meta_table: 138 | def _load_meta_column(self, meta_sheet, column_idx): 139 | text = meta_sheet.cell(0, column_idx).value; 140 | text_split = text.split("="); 141 | sheet_name = text_split[0]; 142 | table_name = text_split[1]; 143 | if sheet_name not in self._sheet_names: 144 | raise Exception("Meta error, sheet not exist: %s" % sheet_name); 145 | 146 | data_sheet = self._workbook.sheet_by_name(sheet_name); 147 | column_headers = dict(); 148 | for ncol in range(0, data_sheet.ncols): 149 | cell = data_sheet.cell(0, ncol); 150 | column_header = self._get_cell_raw(cell); 151 | column_headers[column_header] = ncol; 152 | 153 | sheet_desc = _SheetDesc(sheet_name, table_name); 154 | for row_idx in range(1, meta_sheet.nrows): 155 | cell = meta_sheet.cell(row_idx, column_idx); 156 | if cell.ctype != xlrd.XL_CELL_TEXT or cell.value == u"": 157 | continue; 158 | text_split = cell.value.split("="); 159 | column_name = text_split[0]; 160 | field_name = text_split[1]; 161 | if column_name not in column_headers: 162 | raise Exception("Meta data error, column(%s) not exist in sheet %s" % (column_name, sheet_name)); 163 | sheet_desc.map(column_name, field_name, column_headers[column_name]); 164 | #不能所有的列都是索引列 165 | if len(sheet_desc.keys) > 0 and len(sheet_desc.keys) == len(sheet_desc.columns): 166 | raise Exception("Meta data error, too many keys, sheet: %s" % sheet_name); 167 | self._meta_tables.append(sheet_desc); 168 | return True; 169 | 170 | def _load_meta_header(self): 171 | for sheet_name in self._sheet_names: 172 | data_sheet = self._workbook.sheet_by_name(sheet_name); 173 | sheet_desc = _SheetDesc(sheet_name, sheet_name); 174 | for column_idx in range(0, data_sheet.ncols): 175 | cell = data_sheet.cell(0, column_idx); 176 | column_header = self._get_cell_raw(cell); 177 | if column_header == u"": 178 | continue; 179 | sheet_desc.map(column_header, column_header, column_idx); 180 | #不能所有的列都是索引 181 | if len(sheet_desc.keys) == len(sheet_desc.columns): 182 | raise Exception("Meta data error, too many keys for columns, sheet: %s" % sheet_name); 183 | self._meta_tables.append(sheet_desc); 184 | 185 | def _convert_sheet(self, sheet_desc): 186 | if sheet_desc.has_key: 187 | self._gen_table_code(sheet_desc); 188 | return; 189 | self._gen_array_code(sheet_desc); 190 | 191 | #该函数尽可能返回xls看上去的字面值 192 | def _get_cell_raw(self, cell): 193 | if cell.ctype == xlrd.XL_CELL_TEXT: 194 | return cell.value; 195 | if cell.ctype == xlrd.XL_CELL_NUMBER: 196 | return (number2string % cell.value).rstrip('0').rstrip('.'); 197 | if cell.ctype == xlrd.XL_CELL_DATE: 198 | dt = xlrd.xldate.xldate_as_datetime(cell.value, self._workbook.datemode); 199 | return u"%s" % dt; 200 | if cell.ctype == xlrd.XL_CELL_BOOLEAN: 201 | return u"true" if cell.value else u"false"; 202 | return u""; 203 | 204 | def _get_cell_string(self, cell): 205 | cell_text = ""; 206 | if cell.ctype == xlrd.XL_CELL_TEXT: 207 | cell_text = cell.value; 208 | if cell.ctype == xlrd.XL_CELL_NUMBER: 209 | cell_text = (number2string % cell.value).rstrip('0').rstrip('.'); 210 | if cell.ctype == xlrd.XL_CELL_DATE: 211 | dt = xlrd.xldate.xldate_as_datetime(cell.value, self._workbook.datemode); 212 | cell_text = u"%s" % dt; 213 | if cell.ctype == xlrd.XL_CELL_BOOLEAN: 214 | cell_text = u"true" if cell.value else u"false"; 215 | return u'"%s"' % cell_text; 216 | 217 | def _get_cell_number(self, cell): 218 | if cell.ctype == xlrd.XL_CELL_TEXT: 219 | #这里认为用户填的是一个数字,可能是整数,也可能是小数,也可能是十六进制... 220 | return cell.value; 221 | if cell.ctype == xlrd.XL_CELL_NUMBER: 222 | return (number2string % cell.value).rstrip('0').rstrip('.'); 223 | if cell.ctype == xlrd.XL_CELL_DATE: 224 | dt = xlrd.xldate.xldate_as_datetime(cell.value, self._workbook.datemode); 225 | return u"%d" % time.mktime(dt.timetuple()); 226 | if cell.ctype == xlrd.XL_CELL_BOOLEAN: 227 | return u"1" if cell.value else u"0"; 228 | return u"0"; 229 | 230 | def _get_cell_bool(self, cell): 231 | text = self._get_cell_raw(cell); 232 | text = text.lower(); 233 | if text in [u"", u"nil", u"0", u"false", u"no", u"none", u"否", u"无"]: 234 | return u"false"; 235 | return u"true"; 236 | 237 | def _gen_array_code(self, sheet_desc): 238 | self._lines.append(u"--%s: %s\n" % (self._xls_filename, sheet_desc.sheet_name)); 239 | self._lines.append(u"%s%s =\n" % (self._scope == u"local" and u"local " or self._scope == u"global" and u"_G." or u"", sheet_desc.table_name)); 240 | self._lines.append(u"{\n"); 241 | sheet = self._workbook.sheet_by_name(sheet_desc.sheet_name); 242 | for row in sheet.get_rows(): 243 | line_code = u" " if len(row) <= 1 else u" {"; 244 | cell_idx = 1; 245 | for cell in row: 246 | if cell_idx != 1: 247 | line_code += u", "; 248 | line_code += self._get_cell_raw(cell); 249 | cell_idx = cell_idx + 1; 250 | line_code += u",\n" if len(row) <= 1 else u"},\n"; 251 | self._lines.append(line_code); 252 | self._lines.append(u"};\n"); 253 | self._lines.append(u"\n"); 254 | 255 | def _gen_table_code(self, sheet_desc): 256 | sheet = self._workbook.sheet_by_name(sheet_desc.sheet_name); 257 | root = list(); 258 | #生成层级数据结构 259 | for row_idx in range(1, sheet.nrows): 260 | row_content = dict(); 261 | for column_desc in sheet_desc.columns: 262 | row_content[column_desc.field_name] = self._get_cell_text(sheet, row_idx, column_desc); 263 | node = root; 264 | for key_idx in range(0, len(sheet_desc.keys)): 265 | key_desc = sheet_desc.keys[key_idx]; 266 | field_value = row_content[key_desc.field_name]; 267 | child = next((kv["v"] for kv in node if kv["k"] == field_value), None); 268 | if child == None: 269 | #这里用了list,而不是dict,是为了保持最终生成的行顺序尽可能跟填表顺序一致 270 | child = list(); 271 | comment = key_desc.column_name; 272 | node.append({"k":field_value, "v":child, "c":comment}); 273 | node = child; 274 | for column_desc in sheet_desc.columns: 275 | if not column_desc.is_key: 276 | field_name = column_desc.field_name; 277 | field_value = row_content[field_name]; 278 | comment = column_desc.column_name; 279 | node.append({"k":field_name, "v":field_value, "c":comment}); 280 | 281 | comment = u"%s: %s" % (self._xls_filename, sheet_desc.sheet_name); 282 | table_var = u"%s%s" % (self._scope == u"local" and u"local " or self._scope == "global" and "_G." or u"", sheet_desc.table_name); 283 | self._gen_tree_code(sheet_desc, root, 0, table_var, comment); 284 | self._lines.append(u"\n"); 285 | 286 | def _gen_tree_code(self, sheet_desc, node, step, key_name, comment): 287 | if comment != None: 288 | self._lines.append(self._indent * step + u"--" + comment + u"\n"); 289 | 290 | if step >= len(sheet_desc.keys): 291 | if len(node) == 1: 292 | child = node[0]; 293 | line = self._indent * step + key_name + u" = " + child["v"]; 294 | if comment != None: 295 | line += u", --%s\n" % child["c"]; 296 | else: 297 | line += u",\n"; 298 | self._lines.append(line); 299 | return; 300 | line = self._indent * step + key_name + u" = {"; 301 | first_item = True; 302 | for kv in node: 303 | lua_name = kv["k"]; 304 | if not first_item: 305 | line += u", "; 306 | line += u"%s=%s" % (lua_name, kv["v"]); 307 | first_item = False; 308 | line += u"},\n" 309 | self._lines.append(line); 310 | return; 311 | 312 | self._lines.append(self._indent * step + key_name + u" =\n"); 313 | self._lines.append(self._indent * step + u"{\n"); 314 | firstNode = True; 315 | for kv in node: 316 | comment = kv["c"] if firstNode else None; 317 | self._gen_tree_code(sheet_desc, kv["v"], step + 1, u"[%s]" % kv["k"], comment); 318 | firstNode = False; 319 | self._lines.append(self._indent * step + u"}" + (u";" if step == 0 else u",") + u"\n"); 320 | 321 | def _get_cell_text(self, sheet, row_idx, column_desc): 322 | cell = sheet.cell(row_idx, column_desc.column_idx); 323 | if column_desc.map_type == "number": 324 | return self._get_cell_number(cell); 325 | if column_desc.map_type == "bool": 326 | return self._get_cell_bool(cell); 327 | if column_desc.map_type == "string": 328 | return self._get_cell_string(cell); 329 | text = self._get_cell_raw(cell); 330 | return text if text != "" else "nil"; 331 | 332 | if __name__ == "__main__": 333 | parser = argparse.ArgumentParser("excel to lua convertor"); 334 | parser.add_argument("-s", "--scope", dest="scope", help="table scope,local,global", choices=["local", "global", "default"]); 335 | parser.add_argument("-i", "--indent", dest="indent", help="indent size, 0 for tab, default 4 (spaces)", type=int, default=4, choices=[0, 2, 4, 8]); 336 | parser.add_argument("-m", "--meta", dest="meta", help="meta sheet name, default 'xls2lua'", default="xls2lua"); 337 | parser.add_argument("-o", "--output", dest="output", help="output file", default="output.lua"); 338 | parser.add_argument("-f", "--force", dest="force", action="store_true", help="force convert"); 339 | parser.add_argument('inputs', nargs='+', help="input excel files"); 340 | args = parser.parse_args(); 341 | converter = Converter(args.scope, args.indent, args.meta); 342 | if args.force or any(converter.compare_time(filename, args.output) for filename in args.inputs): 343 | for filename in args.inputs: 344 | converter.convert(filename); 345 | converter.save(args.output); 346 | 347 | --------------------------------------------------------------------------------