jsonもどきであればいい (LooseJSONDecoder事始め)

ConfigParser と json の MIXでは、よい塩梅の味付けだ、と言ったけれども。

ConfigParser と json の MIXの本題は「書きやすく、読みやすいユーザ設定ファイル」であって、決して「JSON を使うこと」ではないわけで。

とりわけ実際に使うとなった場合によくやらかすのが、ついつい Python の癖でこう書いてしまうこと:

1 [build]
2 
3 libraries: [
4     "advapi32",
5     "user32",
6     "shell32",
7   ]

最後のカンマが許容されるかされないかは言語によって違うし、同じ言語でも良い場合とダメな場合があったりもするけれど、json は常にこれを許容してくれない。

それが json なのだから、と、「jsonを使うこと」が目的なら別に気にならないのだけれど、「書きやすく、読みやすいユーザ設定ファイル」のためにはあまりよろしくない。

かといって、正規表現で「,]」「,}」を問答無用で置換してしまうのはさすがに気が引ける。(実は「使ってる場所では」今はそうしてるけど。)

なんかいい方法はないかなぁと思い、json モジュールの中をガン見してみたところ、parse_array 属性を差し替えてやれば対応出来そうだ、と思った。ただし、丸ごと差し替えるのは鬱陶しいので、「エラーになった場合だけ余分なカンマを置き換える」とすることにした。1時間ほどで出来た:

loose_json.py (未遂)
 1 # -*- coding: utf-8 -*-
 2 import json
 3 import re
 4 _RGX_EXPECT_OBJECT = re.compile(
 5     r'Expecting object: line ([0-9]+) column ([0-9]+) \(char ([0-9]+)\)')
 6 
 7 
 8 #
 9 # a JsonArray parser which allows extra comma.
10 #
11 class ParseArrayProxy(object):
12     def __init__(self, orig_parser):
13         self.orig_parser = orig_parser
14 
15     def __call__(
16         self,
17         s_and_end,
18         scan_once,
19         _w=json.decoder.WHITESPACE.match,
20         _ws=json.decoder.WHITESPACE_STR):
21 
22         try:
23             return self.orig_parser(s_and_end, scan_once, _w, _ws)
24         except ValueError as e:
25             m = _RGX_EXPECT_OBJECT.match(str(e))
26             if not m:
27                 raise
28             s, end = s_and_end
29             maybe_bracket = int(m.group(3))
30             if s[maybe_bracket] == ']':
31                 for i in range(maybe_bracket - 1, end, -1):
32                     if s[i].isspace():
33                         continue
34                     if s[i] == ',':
35                         s = "%s %s" % (s[:i], s[i+1:])
36                         return self.orig_parser(
37                             (s, end), scan_once, _w, _ws)
38                     raise
39             raise
40 
41 
42 #
43 # `json-like' decoder
44 #
45 class LooseJSONDecoder(json.JSONDecoder):
46     def __init__(self, encoding=None, object_hook=None, parse_float=None,
47             parse_int=None, parse_constant=None, strict=True,
48             object_pairs_hook=None):
49 
50         json.JSONDecoder.__init__(
51             self,
52             encoding, object_hook, parse_float,
53             parse_int, parse_constant, strict,
54             object_pairs_hook)
55 
56         orig = self.parse_array
57         self.parse_array = ParseArrayProxy(orig)
58         self.scan_once = json.scanner.py_make_scanner(self)
59 
60 
61 #
62 decoder2 = LooseJSONDecoder()
63 print(decoder2.decode("""
64 {"a": [1, 2, 3, ], "b": ["x", "y", "z",]}
65 """))
66 
67 #decoder = json.JSONDecoder()  # original
68 #print(decoder.decode("""
69 #{"a": [1, 2, 3, ], "b": ["x", "y", "z",]}
70 #""")) # raise ValueError

parse_object (JSONObject) も対応すれば、辞書の場合の余分なカンマにも対応出来るよ。

なお、実現上のいくつかのポイント:

  1. json.scanner.py_make_scanner は、json.scanner.make_scanner ではダメです。pure Python バージョンを使わないと、決して「ワタシの」プロキシは呼び出されません。
  2. if s[i] == ',':内で s の len が変わらないように注意な。つまりは、「スペースで差し替え」以外の方法はうまくいかんですよ