0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

オレオレデータファイルの作り方

0
Last updated at Posted at 2025-12-02

はじめに

これはAizu Advent Calendar 2025の二日目の記事です。良ければ他の方の記事も見て行ってください。

自己紹介

会津大学一年生の緑です。
最近温泉入るのにハマっていて、毎週末銭湯行ってます。炭酸良き。

色々あるデータファイル

皆さんは「データファイル」と言われたときに何の形式を思い浮かべるでしょうか?
恐らく多くの人はjsonファイルを思い浮かべると思います。まあ手軽なのでね。

json
{
  "key": "value"
}

あとはyamlでしょうか。僕はTaskfileを愛用しているので、意外と書く機会があります。

yaml
key: value
field:
  key1: value1
  key2: value2

tomlなんかも有名ですね。僕はpyproject.tomlhugo.tomlで見かけてます。

toml
[field]
Key = "Value"

ちなみに僕はtomlとiniの違いが分かりません

更に最近はTOONという、これまた謎なデータファイルを普及させようとしている勢力もいるようです。用途はAI向けだとか (csvで良くないか?)

TOON
users[2]{id,name,role}:
  1,Alice,admin
  2,Bob,user
csv
id,name,role
1,Alice,admin
2,Bob,user

なぜこんなにデータファイルがあるのか

ご覧いただいたように、データファイルの形式は様々あります。
ただデータを格納するだけなのに、なぜこんなにも種類があるのでしょうか?
なぜ2025年になってもなお、新しいフォーマットが生まれるのでしょうか?

それは人間だからです。
人間という傲慢な生き物は、少しでも己のニーズに満たない時、癇癪を起こします。愚かですね。

ということで

この記事を読んでいるあなたも人間だと思われるので、既存のデータファイルには満足していないと思います。しかし、癇癪を起す前にいったん落ち着いて欲しい。
そう、無い物は作ればいいのです。

では理想のデータファイルを作っていきましょう。
今回は Go言語 で実装していきます。なお、「解析対象の構文が正しい」という前提でやりますので、構文エラーとかは適当です。

理想の形式を作る

コードを書く前に、まずおのが理想をはっきりさせないことには何も始まりません。
適当な拡張子を創造し、そのファイルに自らの理想を綴ってください。大体ここまでできるようにしたいなーというのを全て詰め込みましょう。

今回はjsonから出発し、それを改変していこうと思います。

解析目標(以降target.json)
{
    "country": "JP",
    "age": 25,
    "favorites": ["beef", "rice", "egg"],
    "isStudent": true,
    "foodExpences": {
        "Mon": 1000,
        "Tue": 1000,
        "Wed": 1500,
        "Thu": 900,
        "Fri": 2000,
        "Sat": 1000,
        "Sun": 3000
    }
}

パーサーを作る

実はこういう解析の時って逐次解析はあまりよろしくなくて、「字句解析→構文解析→意味解析」 という手順を踏みます。
まあ簡単に言うと 「扱いやすいように加工→構文があってるかチェック→解析開始!」 です。こうすることで再帰処理とかがやりやすくなるんだとか。

しかし今回はオレオレデータファイル、読めればいいのだという精神で何となく作っていきましょう。バグがあろうと、仕様だと言えばそれは仕様なのです。

字句解析編

ここでやることは、「解析対象の文字列をトークンという意味のある単語ごとに分解」することです。
例えば先ほどの解析目標では、{[といったデータ型に関するもの、"country""age"といったキー、"JP"20trueといった値は価値のある情報ですが、タブ(\t)や改行(\n)、 (空白)などの文字は人間が見やすいように置かれているだけで、情報としては価値がありません。
そういった価値のない情報を捨てます。

main.go
package main

import (
	"fmt"
	"log"
	"os"
	"slices"
	"strconv"
)

type TokenType int

type Token struct {
	Type  TokenType
	Value string
}

var (
	IGNORES = []rune{' ', '\n', '\t', '\r'}
)

const (
	BLOCK_BEGIN TokenType = iota
	BLOCK_END
	ARRAY_BEGIN
	ARRAY_END
	COLON
	STRING
	INT
	TRUE
	FALSE
	NULL
)

func debug(tokens []Token) {
	for _, t := range tokens {
		fmt.Printf("value: %s\n", t.Value)
	}
}

func parse(file string) {
	b, err := os.ReadFile(file)
	if err != nil {
		log.Fatal(err)
	}
	debug(tokenize(string(b))) // テスト
}

func tokenize(text string) []Token {
	// 字句解析
	res := []Token{}
	var (
		s           rune
		tmp         string
		stringValue string
		isStore     bool
		isBackSlash bool
	)
	for i := 0; i < len(text); i++ {
		s = rune(text[i])
		if slices.Contains(IGNORES, s) {
			continue // 空白文字は無視
		}
		if isStore {
			if s == ',' || s == '}' {
				isStore = false
				res = append(res, askwd(tmp))
				if s == '}' {
					res = append(res, Token{
						BLOCK_END,
						"}",
					})
				}
				tmp = ""
			} else {
				tmp += string(s)
			}
			continue
		}
		switch s {
		case ',':
			continue
		case '{':
			res = append(res, Token{
				BLOCK_BEGIN,
				"{",
			})
		case '}':
			res = append(res, Token{
				BLOCK_END,
				"}",
			})
		case '[':
			res = append(res, Token{
				ARRAY_BEGIN,
				"[",
			})
		case ']':
			res = append(res, Token{
				ARRAY_END,
				"]",
			})
		case ':':
			res = append(res, Token{
				COLON,
				":",
			})
		case '"':
			stringValue = ""
			for i < len(text) {
				i++
				if isBackSlash {
					isBackSlash = false
					switch rune(text[i]) {
					case 'n':
						stringValue += "\n"
					case 'r':
						stringValue += "\r"
					case 't':
						stringValue += "\t"
					case '"':
						stringValue += "\""
					case '\\':
						stringValue += "\\"
					}
				} else if rune(text[i]) == '"' {
					res = append(res, Token{
						STRING,
						stringValue,
					})
					break
				} else if rune(text[i]) == '\\' {
					isBackSlash = true
				} else {
					stringValue += string(text[i])
				}
			}
		default:
			isStore = true
			tmp += string(s)
		}
	}
	return res
}

func askwd(kwd string) Token {
	// true や nullといったキーワードや数字の判別
	switch kwd {
	case "true":
		return Token{
			TRUE,
			"true",
		}
	case "false":
		return Token{
			FALSE,
			"false",
		}
	case "null":
		return Token{
			NULL,
			"null",
		}
	default:
		// 数字じゃないなら不明な文字
		_, err := strconv.Atoi(kwd)
		if err != nil {
			log.Fatalf("不明な文字列: %s", kwd)
		}
		return Token{
			INT,
			kwd,
		}
	}
}

func main() {
	parse("./target.json")
}

うわ、泥くさ! と思ったそこのあなた! 正解です。

ざっくり解説すると

type TokenType int

type Token struct {
	Type  TokenType
	Value string
}

var (
	IGNORES = []rune{' ', '\n', '\t', '\r'}
)

const (
	BLOCK_BEGIN TokenType = iota
	BLOCK_END
	ARRAY_BEGIN
	ARRAY_END
	COLON
	STRING
	INT
	TRUE
	FALSE
	NULL
)

では無視する文字をIGNORES配列に入れ、次にトークン、つまりデータの最小単位の種類の登録を行っています。
続いてtokenize関数では

		switch s {
		case ',':
			continue
		case '{':
			res = append(res, Token{
				BLOCK_BEGIN,
				"{",
			})
		case '}':
			res = append(res, Token{
				BLOCK_END,
				"}",
			})
		case '[':
			res = append(res, Token{
				ARRAY_BEGIN,
				"[",
			})
		case ']':
			res = append(res, Token{
				ARRAY_END,
				"]",
			})
		case ':':
			res = append(res, Token{
				COLON,
				":",
			})
		case '"':
			stringValue = ""
			for i < len(text) {
				i++
				if isBackSlash {
					isBackSlash = false
					switch rune(text[i]) {
					case 'n':
						stringValue += "\n"
					case 'r':
						stringValue += "\r"
					case 't':
						stringValue += "\t"
					case '"':
						stringValue += "\""
					case '\\':
						stringValue += "\\"
					}
				} else if rune(text[i]) == '"' {
					res = append(res, Token{
						STRING,
						stringValue,
					})
					break
				} else if rune(text[i]) == '\\' {
					isBackSlash = true
				} else {
					stringValue += string(text[i])
				}
			}
		default:
			isStore = true
			tmp += string(s)
		}

というように、トークンごとに判定して分解をします。
"の場合は、次の"が現れるまで読み、文字列として追加します。
\を読んだら次の文字に応じて適切に加工します。
何のトークンの判定にも引っかからなかった場合、いったんisStoreフラグを立て

		if isStore {
			if s == ',' || s == '}' {
				isStore = false
				res = append(res, askwd(tmp))
				if s == '}' {
					res = append(res, Token{
						BLOCK_END,
						"}",
					})
				}
				tmp = ""
			} else {
				tmp += string(s)
			}
			continue
		}

にて、,}(最後の要素は,が不要なため)を読むまでtmpに文字を保持し、その文字列を以下のaskwd関数で予約語(truenullなど)かどうか、数字かどうかの判定をします。そのいずれにも該当しない場合はエラーを投げます。

func askwd(kwd string) Token {
	// true や nullといったキーワードや数字の判別
	switch kwd {
	case "true":
		return Token{
			TRUE,
			"true",
		}
	case "false":
		return Token{
			FALSE,
			"false",
		}
	case "null":
		return Token{
			NULL,
			"null",
		}
	default:
		// 数字じゃないなら不明な文字
		_, err := strconv.Atoi(kwd)
		if err != nil {
			log.Fatalf("不明な文字列: %s", kwd)
		}
		return Token{
			INT,
			kwd,
		}
	}
}

なお、これを実行するとこんな感じになります。

value: {
value: country
value: :
value: JP
value: age
value: :
value: 25
value: favorites
value: :
value: [
value: beef
value: rice
value: egg
value: ]
value: isStudent
value: :
value: true
value: foodExpences
value: :
value: {
value: Mon
value: :
value: 1000
value: Tue
value: :
value: 1000
value: Wed
value: :
value: 1500
value: Thu
value: :
value: 900
value: Fri
value: :
value: 2000
value: Sat
value: :
value: 1000
value: Sun
value: :
value: 3000
value: }
value: }

興奮してきましたね。

構文解析編 & 意味解析編

構文解析は文法チェックなどをするんですが、しょせんはオレオレデータファイルなのでエラー処理は適当でいいでしょう。動けばいいんです動けば。
あと構文解析木と呼ばれるものを作るのですが、データファイルなのでなんとここがゴールです。プログラミング言語とか作る際はここから意味解析をしなければいけません。大変ですね。

main.go
package main

import (
	"encoding/json" // 結果表示のため
	"fmt"
	"log"
	"os"
	"slices"
	"strconv"
)

var (
	IGNORES = []rune{' ', '\n', '\t', '\r'}
)

type TokenType int

type Token struct {
	Type  TokenType
	Value string
}

func (t *Token) GetValue() any {
	switch t.Type {
	case INT:
		n, _ := strconv.Atoi(t.Value)
		return n
	case TRUE:
		return true
	case FALSE:
		return false
	case NULL:
		return nil
	default:
		return t.Value
	}
}

const (
	BLOCK_BEGIN TokenType = iota
	BLOCK_END
	ARRAY_BEGIN
	ARRAY_END
	COLON
	STRING
	INT
	TRUE
	FALSE
	NULL
)

func debug(tokens []Token) {
	for i, t := range tokens {
		fmt.Printf("%d value: %s %d\n", i, t.Value, t.Type)
	}
}

func tokenize(text string) []Token {
	// 字句解析
	res := []Token{}
	var (
		s           rune
		tmp         string
		stringValue string
		isStore     bool
		isBackSlash bool
	)
	for i := 0; i < len(text); i++ {
		s = rune(text[i])
		if slices.Contains(IGNORES, s) {
			continue // 空白文字は無視
		}
		if isStore {
			if s == ',' || s == '}' {
				isStore = false
				res = append(res, askwd(tmp))
				if s == '}' {
					res = append(res, Token{
						BLOCK_END,
						"}",
					})
				}
				tmp = ""
			} else {
				tmp += string(s)
			}
			continue
		}
		switch s {
		case ',':
			continue
		case '{':
			res = append(res, Token{
				BLOCK_BEGIN,
				"{",
			})
		case '}':
			res = append(res, Token{
				BLOCK_END,
				"}",
			})
		case '[':
			res = append(res, Token{
				ARRAY_BEGIN,
				"[",
			})
		case ']':
			res = append(res, Token{
				ARRAY_END,
				"]",
			})
		case ':':
			res = append(res, Token{
				COLON,
				":",
			})
		case '"':
			stringValue = ""
			for i < len(text) {
				i++
				if isBackSlash {
					isBackSlash = false
					switch rune(text[i]) {
					case 'n':
						stringValue += "\n"
					case 'r':
						stringValue += "\r"
					case 't':
						stringValue += "\t"
					case '"':
						stringValue += "\""
					case '\\':
						stringValue += "\\"
					}
				} else if rune(text[i]) == '"' {
					res = append(res, Token{
						STRING,
						stringValue,
					})
					break
				} else if rune(text[i]) == '\\' {
					isBackSlash = true
				} else {
					stringValue += string(text[i])
				}
			}
		default:
			isStore = true
			tmp += string(s)
		}
	}
	return res
}

func askwd(kwd string) Token {
	// true や nullといったキーワードや数字の判別
	switch kwd {
	case "true":
		return Token{
			TRUE,
			"true",
		}
	case "false":
		return Token{
			FALSE,
			"false",
		}
	case "null":
		return Token{
			NULL,
			"null",
		}
	default:
		// 数字じゃないなら不明な文字
		_, err := strconv.Atoi(kwd)
		if err != nil {
			log.Fatalf("不明な文字列: %s", kwd)
		}
		return Token{
			INT,
			kwd,
		}
	}
}

func parse(file string) map[string]any {
	b, err := os.ReadFile(file)
	if err != nil {
		log.Fatal(err)
	}
	tokens := tokenize(string(b))
	if len(tokens) == 0 {
		return map[string]any{}
	}
	if tokens[0].Type != BLOCK_BEGIN {
		log.Fatal("構文エラー: 文頭が'{'で始まっていません")
	}
	if tokens[len(tokens)-1].Type != BLOCK_END {
		log.Fatal("構文エラー: 文末が'}'で終わっていません")
	}
	idx := 1
	return _parse(tokens, &idx)
}

func _parse(tokens []Token, idx *int) map[string]any {
	var (
		res map[string]any = map[string]any{}
		t   Token
		tmp Token
	)
	for ; *idx < len(tokens); (*idx)++ {
		t = tokens[*idx]
		switch t.Type {
		case COLON:
			if tmp.Type != STRING {
				log.Fatal("キーに出来るのは数字か文字のみです")
			}
			(*idx)++
			t = tokens[*idx]
			switch t.Type {
			case BLOCK_BEGIN:
				(*idx)++
				res[tmp.Value] = _parse(tokens, idx) // 辞書は再帰で
			case ARRAY_BEGIN:
				(*idx)++
				t = tokens[*idx]
				arr := []any{}
				if t.Type == BLOCK_BEGIN {
					for tokens[*idx].Type == BLOCK_BEGIN {
						(*idx)++
						arr = append(arr, _parse(tokens, idx))
						(*idx)++
					}
					(*idx)--
					res[tmp.Value] = arr
				} else {
					for t.Type != ARRAY_END {
						arr = append(arr, t.GetValue())
						(*idx)++
						t = tokens[*idx]
					}
					res[tmp.Value] = arr
				}
			default:
				res[tmp.Value] = t.GetValue()
			}
			tmp = Token{}
		case BLOCK_END:
			return res
		default:
			tmp = t
		}
	}
	log.Fatal("構文エラー") // } で返ってないとおかしい
	return res
}

func main() {
	data := parse(`./target.json`)
	d, _ := json.MarshalIndent(data, "", "  ")
	fmt.Println(string(d))
}

字句解析して得られたトークンたちを一つずつ読んで、その種類ごとに処理を分岐するだけです。
読む位置の指定にポインタを使うことで現在位置の進捗を共有でき、再帰処理が可能になります。

出力
{
  "age": 25,
  "country": "JP",
  "favorites": [
    "beef",
    "rice",
    "egg"
  ],
  "foodExpences": {
    "Fri": 2000,
    "Mon": 1000,
    "Sat": 1000,
    "Sun": 3000,
    "Thu": 900,
    "Tue": 1000,
    "Wed": 1500
  },
  "isStudent": true
}

json.MarshalIndentで問題なくシリアライズできているので、解析には成功したことが分かります。

実はここまで作ってしまえば結構実用的で、例えばVSCodeの混沌としたsettings.jsonを(コメントを除けば)パース出来てしまいます。

settings.json
{
    "editor.fontSize": 24,
    "editor.unicodeHighlight.invisibleCharacters": false,
    "editor.wordSeparators": "./\\()\"'-:,.;<>~!@#$%^&*|+=[]{}`~? 、。「」【】『』()!?てにをはがのともへでや",
    "latex-workshop.latex.tools": [
        {
          "name": "Latexmk (LuaLaTeX)",
          "command": "latexmk",
          "args": [
            "-f", "-gg", "-lualatex", "-synctex=1", "-interaction=nonstopmode", "-file-line-error", "%DOC%"
          ]
        },
        {
          "name": "Latexmk (XeLaTeX)",
          "command": "latexmk",
          "args": [
            "-f", "-gg", "-xelatex", "-synctex=1", "-interaction=nonstopmode", "-file-line-error", "%DOC%"
          ]
        },
        {
          "name": "Latexmk (upLaTeX)",
          "command": "latexmk",
          "args": [
            "-f", "-gg", "-synctex=1", "-interaction=nonstopmode", "-file-line-error", "%DOC%"
          ]
        },
        {
          "name": "Latexmk (pLaTeX)",
          "command": "latexmk",
          "args": [
            "-f", "-gg", "-latex='platex'", "-latexoption='-kanji=utf8 -no-guess-input-env'", "-synctex=1", "-interaction=nonstopmode", "-file-line-error", "%DOC%"
          ]
        }
    ],
    "latex-workshop.latex.recipes": [
        {
          "name": "LuaLaTeX",
          "tools": [
            "Latexmk (LuaLaTeX)"
          ]
        },
        {
          
          "name": "XeLaTeX",
          "tools": [
            "Latexmk (XeLaTeX)"
          ]
        },
        {
          "name": "upLaTeX",
          "tools": [
            "Latexmk (upLaTeX)"
          ]
        },
        {
          "name": "pLaTeX",
          "tools": [
            "Latexmk (pLaTeX)"
          ]
        },
    ],
    "latex-workshop.latex.magic.args": [
      "-f", "-gg", "-synctex=1", "-interaction=nonstopmode", "-file-line-error", "%DOC%"
    ],
    "latex-workshop.latex.clean.fileTypes": [
        "*.aux", "*.bbl", "*.blg", "*.idx", "*.ind", "*.lof", "*.lot", "*.out", "*.toc", "*.acn", "*.acr", "*.alg", "*.glg", "*.glo", "*.gls", "*.ist", "*.fls", "*.log", "*.fdb_latexmk", "*.synctex.gz",
        "_minted*", "*.nav", "*.snm", "*.vrb",
    ],

    "latex-workshop.latex.autoClean.run": "onBuilt",
    "latex-workshop.view.pdf.viewer": "tab",
    "latex-workshop.latex.autoBuild.run": "never",

    "[tex]": {
        "editor.suggest.snippetsPreventQuickSuggestions": false,
        "editor.tabSize": 2
    },

    "[latex]": {
        "editor.suggest.snippetsPreventQuickSuggestions": false,
        "editor.tabSize": 2
    },

    "[bibtex]": {
        "editor.tabSize": 2
    },

    "latex-workshop.intellisense.package.enabled": true,
    "security.workspace.trust.enabled": false,
    "explorer.confirmDelete": false,
    "explorer.confirmDragAndDrop": false,
    "go.formatTool": "gofmt",
    "workbench.editor.empty.hint": "hidden",
}

出力
{
  "[bibtex]": {
    "editor.tabSize": 2
  },
  "[latex]": {
    "editor.suggest.snippetsPreventQuickSuggestions": false,
    "editor.tabSize": 2
  },
  "[tex]": {
    "editor.suggest.snippetsPreventQuickSuggestions": false,
    "editor.tabSize": 2
  },
  "editor.fontSize": 24,
  "editor.unicodeHighlight.invisibleCharacters": false,
  "editor.wordSeparators": "./\\()\"'-:,.;\u003c\u003e~!@#$%^\u0026*|+=[]{}`~?ãããããããããï¼ï¼ï¼ï¼ã¦ã«ãã¯ãã®ã¨ãã¸ã§ã",
  "explorer.confirmDelete": false,
  "explorer.confirmDragAndDrop": false,
  "go.formatTool": "gofmt",
  "latex-workshop.intellisense.package.enabled": true,
  "latex-workshop.latex.autoBuild.run": "never",
  "latex-workshop.latex.autoClean.run": "onBuilt",
  "latex-workshop.latex.clean.fileTypes": [
    "*.aux",
    "*.bbl",
    "*.blg",
    "*.idx",
    "*.ind",
    "*.lof",
    "*.lot",
    "*.out",
    "*.toc",
    "*.acn",
    "*.acr",
    "*.alg",
    "*.glg",
    "*.glo",
    "*.gls",
    "*.ist",
    "*.fls",
    "*.log",
    "*.fdb_latexmk",
    "*.synctex.gz",
    "_minted*",
    "*.nav",
    "*.snm",
    "*.vrb"
  ],
  "latex-workshop.latex.magic.args": [
    "-f",
    "-gg",
    "-synctex=1",
    "-interaction=nonstopmode",
    "-file-line-error",
    "%DOC%"
  ],
  "latex-workshop.latex.recipes": [
    {
      "name": "LuaLaTeX",
      "tools": [
        "Latexmk (LuaLaTeX)"
      ]
    },
    {
      "name": "XeLaTeX",
      "tools": [
        "Latexmk (XeLaTeX)"
      ]
    },
    {
      "name": "upLaTeX",
      "tools": [
        "Latexmk (upLaTeX)"
      ]
    },
    {
      "name": "pLaTeX",
      "tools": [
        "Latexmk (pLaTeX)"
      ]
    }
  ],
  "latex-workshop.latex.tools": [
    {
      "args": [
        "-f",
        "-gg",
        "-lualatex",
        "-synctex=1",
        "-interaction=nonstopmode",
        "-file-line-error",
        "%DOC%"
      ],
      "command": "latexmk",
      "name": "Latexmk (LuaLaTeX)"
    },
    {
      "args": [
        "-f",
        "-gg",
        "-xelatex",
        "-synctex=1",
        "-interaction=nonstopmode",
        "-file-line-error",
        "%DOC%"
      ],
      "command": "latexmk",
      "name": "Latexmk (XeLaTeX)"
    },
    {
      "args": [
        "-f",
        "-gg",
        "-synctex=1",
        "-interaction=nonstopmode",
        "-file-line-error",
        "%DOC%"
      ],
      "command": "latexmk",
      "name": "Latexmk (upLaTeX)"
    },
    {
      "args": [
        "-f",
        "-gg",
        "-latex='platex'",
        "-latexoption='-kanji=utf8 -no-guess-input-env'",
        "-synctex=1",
        "-interaction=nonstopmode",
        "-file-line-error",
        "%DOC%"
      ],
      "command": "latexmk",
      "name": "Latexmk (pLaTeX)"
    }
  ],
  "latex-workshop.view.pdf.viewer": "tab",
  "security.workspace.trust.enabled": false,
  "workbench.editor.empty.hint": "hidden"
}

マルチバイトが悪さしているせいで文字化けしてますが、面倒なので見なかったことにします。

改変

さて、このままだとただjsonパーサーを作っただけになってしまうので、何かしら改変します。
例えば:だけじゃなく=にも反応するようにするとか...

target.json
{
    "country" = "JP",
    "age" = 25,
    "favorites" = ["beef", "rice", "egg"],
    "isStudent" = true,
    "foodExpences" = {
        "Mon" = 1000,
        "Tue" = 1000,
        "Wed" = 1500,
        "Thu" = 900,
        "Fri" = 2000,
        "Sat" = 1000,
        "Sun" = 3000
    }
}
tokenize関数
		switch s {
		()
+		case ':', '=':
			res = append(res, Token{
				COLON,
				":",
			})

いっそのこと最初と最後の{}を消してしまうとか...

target.json
"country" = "JP",
"age" = 25,
"favorites" = ["beef", "rice", "egg"],
"isStudent" = true,
"foodExpences" = {
    "Mon" = 1000,
    "Tue" = 1000,
    "Wed" = 1500,
    "Thu" = 900,
    "Fri" = 2000,
    "Sat" = 1000,
    "Sun" = 3000,
}
parse関数
func parse(file string) map[string]any {
	b, err := os.ReadFile(file)
	if err != nil {
		log.Fatal(err)
	}
	tokens := tokenize(string(b))
	if len(tokens) == 0 {
		return map[string]any{}
	}
-	/*if tokens[0].Type != BLOCK_BEGIN {
-		log.Fatal("構文エラー: 文頭が'{'で始まっていません")
-	}
-	if tokens[len(tokens)-1].Type != BLOCK_END {
-		log.Fatal("構文エラー: 文末が'}'で終わっていません")
-	}*/
	idx := 0
	return _parse(tokens, &idx)
}
_parse関数
    ()
-	//log.Fatal("構文エラー") // } で返ってないとおかしい
	return res

ここまで来たら,も消してしまいましょう。どうせ複数行にまたがることなんて無いのでね。

"country" = "JP"
"age" = 25
"favorites" = ["beef", "rice", "egg"]
"isStudent" = true
"foodExpences" = {
    "Mon" = 1000
    "Tue" = 1000
    "Wed" = 1500
    "Thu" = 900
    "Fri" = 2000
    "Sat" = 1000
    "Sun" = 3000
}
var (
+	IGNORES = []rune{' ', '\t', '\r'} // \nを読むようにする
)
tokenize関数
        ()
		if isStore {
+			if s == '\n' || s == ',' || s == '}' {
				isStore = false
				res = append(res, askwd(tmp))
				if s == '}' {
					res = append(res, Token{
						BLOCK_END,
						"}",
					})
				}
				tmp = ""
			} else {
				tmp += string(s)
			}
			continue
		}
        switch s {
+		case ',', '\n':
			continue
        ()

,か担っていた役割を\nにも持たせるだけです。

キーは文字列で固定なので""要らなくない? ということで無くします。

country = "JP"
age = 25
favorites = ["beef", "rice", "egg"]
isStudent = true
foodExpences = {
    Mon = 1000
    Tue = 1000
    Wed = 1500
    Thu = 900
    Fri = 2000
    Sat = 1000
    Sun = 3000
}
tokenize関数
		if isStore {
+			if s == '=' || s == ':' || s == '\n' || s == ',' || s == '}' {
				isStore = false
+				switch s {
+				case '=', ':':
+					res = append(res, Token{
+						STRING,
+						tmp,
+					})
+					res = append(res, Token{
+						COLON,
+						":",
+					})
+				case '}':
+					res = append(res, askwd(tmp))
+					res = append(res, Token{
+						BLOCK_END,
+						"}",
+					})
+				default:
+					res = append(res, askwd(tmp))
+				}
+				tmp = ""
			} else {
				tmp += string(s)
			}
			continue
		}
tokenize関数
		case ':', '=':
+			if tmp == "" {
+				log.Fatal("構文エラー")
+			}
+			res = append(res, Token{
+				STRING,
+				tmp,
+			})
+			tmp = ""
			res = append(res, Token{
				COLON,
				":",
			})

完成

jsonから出発して、最終的に

country = "JP"
age = 25
favorites = ["beef", "rice", "egg"]
isStudent = true
foodExpences = {
    Mon = 1000
    Tue = 1000
    Wed = 1500
    Thu = 900
    Fri = 2000
    Sat = 1000
    Sun = 3000
}

という謎のフォーマットを誕生させることができました。おめでとうございます。

最終的なコードはこちらです。

main.go
package main

import (
	"encoding/json" // 結果表示のため
	"fmt"
	"log"
	"os"
	"slices"
	"strconv"
)

var (
	IGNORES = []rune{' ', '\t', '\r'}
)

type TokenType int

type Token struct {
	Type  TokenType
	Value string
}

func (t *Token) GetValue() any {
	switch t.Type {
	case INT:
		n, _ := strconv.Atoi(t.Value)
		return n
	case TRUE:
		return true
	case FALSE:
		return false
	case NULL:
		return nil
	default:
		return t.Value
	}
}

const (
	BLOCK_BEGIN TokenType = iota
	BLOCK_END
	ARRAY_BEGIN
	ARRAY_END
	COLON
	STRING
	INT
	TRUE
	FALSE
	NULL
)

func debug(tokens []Token) {
	for i, t := range tokens {
		fmt.Printf("%d value: %s %d\n", i, t.Value, t.Type)
	}
}

func tokenize(text string) []Token {
	// 字句解析
	res := []Token{}
	var (
		s           rune
		tmp         string
		stringValue string
		isStore     bool
		isBackSlash bool
	)
	for i := 0; i < len(text); i++ {
		s = rune(text[i])
		if slices.Contains(IGNORES, s) {
			continue // 空白文字は無視
		}
		if isStore {
			if s == '=' || s == ':' || s == '\n' || s == ',' || s == '}' {
				isStore = false
				switch s {
				case '=', ':':
					res = append(res, Token{
						STRING,
						tmp,
					})
					res = append(res, Token{
						COLON,
						":",
					})
				case '}':
					res = append(res, askwd(tmp))
					res = append(res, Token{
						BLOCK_END,
						"}",
					})
				default:
					res = append(res, askwd(tmp))
				}
				tmp = ""
			} else {
				tmp += string(s)
			}
			continue
		}
		switch s {
		case ',', '\n':
			continue
		case '{':
			res = append(res, Token{
				BLOCK_BEGIN,
				"{",
			})
		case '}':
			res = append(res, Token{
				BLOCK_END,
				"}",
			})
		case '[':
			res = append(res, Token{
				ARRAY_BEGIN,
				"[",
			})
		case ']':
			res = append(res, Token{
				ARRAY_END,
				"]",
			})
		case ':', '=':
			if tmp == "" {
				log.Fatal("構文エラー")
			}
			res = append(res, Token{
				STRING,
				tmp,
			})
			tmp = ""
			res = append(res, Token{
				COLON,
				":",
			})
		case '"':
			stringValue = ""
			for i < len(text) {
				i++
				if isBackSlash {
					isBackSlash = false
					switch rune(text[i]) {
					case 'n':
						stringValue += "\n"
					case 'r':
						stringValue += "\r"
					case 't':
						stringValue += "\t"
					case '"':
						stringValue += "\""
					case '\\':
						stringValue += "\\"
					}
				} else if rune(text[i]) == '"' {
					res = append(res, Token{
						STRING,
						stringValue,
					})
					break
				} else if rune(text[i]) == '\\' {
					isBackSlash = true
				} else {
					stringValue += string(text[i])
				}
			}
		default:
			isStore = true
			tmp += string(s)
		}
	}
	return res
}

func askwd(kwd string) Token {
	// true や nullといったキーワードや数字の判別
	switch kwd {
	case "true":
		return Token{
			TRUE,
			"true",
		}
	case "false":
		return Token{
			FALSE,
			"false",
		}
	case "null":
		return Token{
			NULL,
			"null",
		}
	default:
		_, err := strconv.Atoi(kwd)
		if err != nil {
			log.Fatalf("不明な文字列: %s", kwd)
		}
		return Token{
			INT,
			kwd,
		}
	}
}

func parse(file string) map[string]any {
	b, err := os.ReadFile(file)
	if err != nil {
		log.Fatal(err)
	}
	tokens := tokenize(string(b))
	if len(tokens) == 0 {
		return map[string]any{}
	}
	/*if tokens[0].Type != BLOCK_BEGIN {
		log.Fatal("構文エラー: 文頭が'{'で始まっていません")
	}
	if tokens[len(tokens)-1].Type != BLOCK_END {
		log.Fatal("構文エラー: 文末が'}'で終わっていません")
	}*/
	idx := 0
	return _parse(tokens, &idx)
}

func _parse(tokens []Token, idx *int) map[string]any {
	var (
		res map[string]any = map[string]any{}
		t   Token
		tmp Token
	)
	for ; *idx < len(tokens); (*idx)++ {
		t = tokens[*idx]
		switch t.Type {
		case COLON:
			if tmp.Type != STRING {
				log.Fatal("キーに出来るのは数字か文字のみです")
			}
			(*idx)++
			t = tokens[*idx]
			switch t.Type {
			case BLOCK_BEGIN:
				(*idx)++
				res[tmp.Value] = _parse(tokens, idx) // 辞書は再帰で
			case ARRAY_BEGIN:
				(*idx)++
				t = tokens[*idx]
				arr := []any{}
				if t.Type == BLOCK_BEGIN {
					for tokens[*idx].Type == BLOCK_BEGIN {
						(*idx)++
						arr = append(arr, _parse(tokens, idx))
						(*idx)++
					}
					(*idx)--
					res[tmp.Value] = arr
				} else {
					for t.Type != ARRAY_END {
						arr = append(arr, t.GetValue())
						(*idx)++
						t = tokens[*idx]
					}
					res[tmp.Value] = arr
				}
			default:
				res[tmp.Value] = t.GetValue()
			}
			tmp = Token{}
		case BLOCK_END:
			return res
		default:
			tmp = t
		}
	}
	//log.Fatal("構文エラー") // } で返ってないとおかしい
	return res
}

func main() {
	data := parse(`target.json`)
	d, _ := json.MarshalIndent(data, "", "  ")
	fmt.Println(string(d))
}

終わりに

構文解析はこのように非常に泥臭いコードになりがちです。皆さんも、標準ライブラリへの感謝を忘れずに、一日一日を生きていきましょう。

現場からは以上です。

おまけ

今回僕が書いたコードを C言語 に書き替えてと Gemini に丸投げしたところ、普通に一発で通るコード出してきました。シンギュラリティですね...

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

// --- 定数・Enum定義 ---

typedef enum {
    BLOCK_BEGIN,
    BLOCK_END,
    ARRAY_BEGIN,
    ARRAY_END,
    COLON,
    STRING_TOKEN, // STRINGはCの予約語と被る可能性があるため変更
    INT_TOKEN,
    TRUE_TOKEN,
    FALSE_TOKEN,
    NULL_TOKEN
} TokenType;

typedef struct {
    TokenType type;
    char *value;
} Token;

// --- 値の型定義 (Goの any / interface{} に相当) ---

typedef enum {
    V_INT,
    V_STRING,
    V_BOOL,
    V_NULL,
    V_OBJECT,
    V_ARRAY
} ValueType;

struct Value; // 前方宣言

typedef struct KeyValue {
    char *key;
    struct Value *value;
} KeyValue;

typedef struct {
    KeyValue **pairs;
    int count;
    int capacity;
} Object;

typedef struct {
    struct Value **items;
    int count;
    int capacity;
} Array;

typedef struct Value {
    ValueType type;
    union {
        int i_val;
        char *s_val;
        int b_val; // 0 or 1
        Object *obj_val;
        Array *arr_val;
    };
} Value;

// --- グローバル変数 (GoのIGNORES相当は関数内で処理) ---

// --- プロトタイプ宣言 ---
void print_value(Value *v, int indent_level);
Value *parse_recursive(Token *tokens, int num_tokens, int *idx);

// --- ヘルパー関数: メモリ管理と文字列 ---

// 文字列を結合して新しい文字列を返す (tmp += string(s) の代わり)
char *append_char(char *str, char c) {
    size_t len = (str == NULL) ? 0 : strlen(str);
    char *new_str = realloc(str, len + 2);
    if (!new_str) { perror("malloc"); exit(1); }
    new_str[len] = c;
    new_str[len + 1] = '\0';
    return new_str;
}

// 動的配列にトークンを追加
Token *append_token(Token *tokens, int *count, Token t) {
    tokens = realloc(tokens, sizeof(Token) * (*count + 1));
    tokens[*count] = t;
    (*count)++;
    return tokens;
}

// --- 字句解析 (Tokenize) ---

Token askwd(char *kwd) {
    Token t;
    t.value = strdup(kwd);
    if (strcmp(kwd, "true") == 0) {
        t.type = TRUE_TOKEN;
    } else if (strcmp(kwd, "false") == 0) {
        t.type = FALSE_TOKEN;
    } else if (strcmp(kwd, "null") == 0) {
        t.type = NULL_TOKEN;
    } else {
        // 数字かどうかの簡易チェック (厳密にはstrtolのエラーチェックが必要だがGoコードに合わせる)
        char *endptr;
        strtol(kwd, &endptr, 10);
        if (*endptr != '\0') {
            fprintf(stderr, "不明な文字列: %s\n", kwd);
            exit(1);
        }
        t.type = INT_TOKEN;
    }
    return t;
}

Token *tokenize(const char *text, int *out_count) {
    Token *res = NULL;
    int count = 0;
    char *tmp = NULL;
    char *stringValue = NULL;
    int isStore = 0;
    int isBackSlash = 0;
    size_t len = strlen(text);

    for (size_t i = 0; i < len; i++) {
        char s = text[i];

        // IGNORES check
        if (s == ' ' || s == '\t' || s == '\r') {
            continue;
        }

        if (isStore) {
            if (s == '=' || s == ':' || s == '\n' || s == ',' || s == '}') {
                isStore = 0;
                if (s == '=' || s == ':') {
                    res = append_token(res, &count, (Token){STRING_TOKEN, tmp ? tmp : strdup("")});
                    tmp = NULL; // 所有権移動とみなす
                    res = append_token(res, &count, (Token){COLON, strdup(":")});
                } else if (s == '}') {
                    if (tmp) {
                        res = append_token(res, &count, askwd(tmp));
                        free(tmp); tmp = NULL;
                    }
                    res = append_token(res, &count, (Token){BLOCK_END, strdup("}")});
                } else {
                    // default case in Go
                    if (tmp) {
                        res = append_token(res, &count, askwd(tmp));
                        free(tmp); tmp = NULL;
                    }
                }
                tmp = NULL;
            } else {
                tmp = append_char(tmp, s);
            }
            continue;
        }

        switch (s) {
        case ',':
        case '\n':
            continue;
        case '{':
            res = append_token(res, &count, (Token){BLOCK_BEGIN, strdup("{")});
            break;
        case '}':
            res = append_token(res, &count, (Token){BLOCK_END, strdup("}")});
            break;
        case '[':
            res = append_token(res, &count, (Token){ARRAY_BEGIN, strdup("[")});
            break;
        case ']':
            res = append_token(res, &count, (Token){ARRAY_END, strdup("]")});
            break;
        case ':':
        case '=':
            if (tmp == NULL || strlen(tmp) == 0) {
                fprintf(stderr, "構文エラー: キーが空です\n");
                exit(1);
            }
            res = append_token(res, &count, (Token){STRING_TOKEN, tmp}); // tmp所有権移動
            tmp = NULL;
            res = append_token(res, &count, (Token){COLON, strdup(":")});
            break;
        case '"':
            stringValue = strdup(""); // 空文字列で初期化
            while (i + 1 < len) {
                i++;
                char c = text[i];
                if (isBackSlash) {
                    isBackSlash = 0;
                    switch (c) {
                        case 'n': stringValue = append_char(stringValue, '\n'); break;
                        case 'r': stringValue = append_char(stringValue, '\r'); break;
                        case 't': stringValue = append_char(stringValue, '\t'); break;
                        case '"': stringValue = append_char(stringValue, '"'); break;
                        case '\\': stringValue = append_char(stringValue, '\\'); break;
                        default:  stringValue = append_char(stringValue, c); break; // その他はそのまま
                    }
                } else if (c == '"') {
                    res = append_token(res, &count, (Token){STRING_TOKEN, stringValue});
                    stringValue = NULL; // 所有権移動
                    break;
                } else if (c == '\\') {
                    isBackSlash = 1;
                } else {
                    stringValue = append_char(stringValue, c);
                }
            }
            break;
        default:
            isStore = 1;
            tmp = append_char(tmp, s);
            break;
        }
    }
    
    // cleanup leftovers if loop finishes inside isStore (though unexpected in valid json)
    if (tmp) free(tmp);
    if (stringValue) free(stringValue);

    *out_count = count;
    return res;
}

// --- 構文解析 (Parse) ---

// Value生成ヘルパー
Value *create_value_int(int v) {
    Value *val = malloc(sizeof(Value));
    val->type = V_INT;
    val->i_val = v;
    return val;
}
Value *create_value_bool(int v) {
    Value *val = malloc(sizeof(Value));
    val->type = V_BOOL;
    val->b_val = v;
    return val;
}
Value *create_value_null() {
    Value *val = malloc(sizeof(Value));
    val->type = V_NULL;
    return val;
}
Value *create_value_string(char *s) {
    Value *val = malloc(sizeof(Value));
    val->type = V_STRING;
    val->s_val = strdup(s);
    return val;
}

Value *token_get_value(Token *t) {
    switch (t->type) {
    case INT_TOKEN: return create_value_int(atoi(t->value));
    case TRUE_TOKEN: return create_value_bool(1);
    case FALSE_TOKEN: return create_value_bool(0);
    case NULL_TOKEN: return create_value_null();
    default: return create_value_string(t->value);
    }
}

// Object(Map)操作
void object_add(Object *obj, char *key, Value *val) {
    if (obj->count >= obj->capacity) {
        obj->capacity = (obj->capacity == 0) ? 8 : obj->capacity * 2;
        obj->pairs = realloc(obj->pairs, sizeof(KeyValue*) * obj->capacity);
    }
    KeyValue *kv = malloc(sizeof(KeyValue));
    kv->key = strdup(key);
    kv->value = val;
    obj->pairs[obj->count++] = kv;
}

// Array操作
void array_add(Array *arr, Value *val) {
    if (arr->count >= arr->capacity) {
        arr->capacity = (arr->capacity == 0) ? 8 : arr->capacity * 2;
        arr->items = realloc(arr->items, sizeof(Value*) * arr->capacity);
    }
    arr->items[arr->count++] = val;
}

Value *parse_recursive(Token *tokens, int num_tokens, int *idx) {
    Value *resVal = malloc(sizeof(Value));
    resVal->type = V_OBJECT;
    resVal->obj_val = calloc(1, sizeof(Object));

    Token tmp = {0, NULL};
    int has_tmp = 0;

    for (; *idx < num_tokens; (*idx)++) {
        Token t = tokens[*idx];

        switch (t.type) {
        case COLON:
            if (!has_tmp || tmp.type != STRING_TOKEN) {
                fprintf(stderr, "キーに出来るのは数字か文字のみです\n");
                exit(1);
            }
            (*idx)++;
            if (*idx >= num_tokens) break;
            t = tokens[*idx];

            switch (t.type) {
            case BLOCK_BEGIN:
                (*idx)++;
                object_add(resVal->obj_val, tmp.value, parse_recursive(tokens, num_tokens, idx));
                break;
            case ARRAY_BEGIN:
                (*idx)++;
                if (*idx >= num_tokens) break;
                t = tokens[*idx];
                
                Value *arrVal = malloc(sizeof(Value));
                arrVal->type = V_ARRAY;
                arrVal->arr_val = calloc(1, sizeof(Array));

                if (t.type == BLOCK_BEGIN) {
                    while (*idx < num_tokens && tokens[*idx].type == BLOCK_BEGIN) {
                        (*idx)++;
                        array_add(arrVal->arr_val, parse_recursive(tokens, num_tokens, idx));
                        (*idx)++;
                    }
                    (*idx)--;
                } else {
                    while (*idx < num_tokens && t.type != ARRAY_END) {
                        array_add(arrVal->arr_val, token_get_value(&t));
                        (*idx)++;
                        if (*idx < num_tokens) t = tokens[*idx];
                    }
                }
                object_add(resVal->obj_val, tmp.value, arrVal);
                break;
            default:
                object_add(resVal->obj_val, tmp.value, token_get_value(&t));
                break;
            }
            has_tmp = 0;
            break;
        case BLOCK_END:
            return resVal;
        default:
            tmp = t;
            has_tmp = 1;
            break;
        }
    }
    return resVal;
}

Value *parse(const char *filename) {
    FILE *fp = fopen(filename, "rb");
    if (!fp) { perror("fopen"); exit(1); }

    fseek(fp, 0, SEEK_END);
    long fsize = ftell(fp);
    fseek(fp, 0, SEEK_SET);

    char *buffer = malloc(fsize + 1);
    fread(buffer, 1, fsize, fp);
    buffer[fsize] = 0;
    fclose(fp);

    int token_count = 0;
    Token *tokens = tokenize(buffer, &token_count);
    free(buffer);

    if (token_count == 0) {
        Value *empty = malloc(sizeof(Value));
        empty->type = V_OBJECT;
        empty->obj_val = calloc(1, sizeof(Object));
        return empty;
    }

    int idx = 0;
    Value *result = parse_recursive(tokens, token_count, &idx);

    // トークンの後始末 (値はValue構造体にコピーされている前提)
    for(int i=0; i<token_count; i++) {
        free(tokens[i].value);
    }
    free(tokens);

    return result;
}

// --- JSON出力 (json.MarshalIndent相当) ---

void print_indent(int level) {
    for (int i = 0; i < level; i++) printf("  ");
}

void print_value(Value *v, int indent) {
    switch (v->type) {
    case V_INT: printf("%d", v->i_val); break;
    case V_BOOL: printf(v->b_val ? "true" : "false"); break;
    case V_NULL: printf("null"); break;
    case V_STRING: 
        // 簡易的なエスケープ処理が必要だが、ここではそのまま出力
        printf("\"%s\"", v->s_val); 
        break;
    case V_ARRAY:
        printf("[\n");
        for (int i = 0; i < v->arr_val->count; i++) {
            print_indent(indent + 1);
            print_value(v->arr_val->items[i], indent + 1);
            if (i < v->arr_val->count - 1) printf(",");
            printf("\n");
        }
        print_indent(indent);
        printf("]");
        break;
    case V_OBJECT:
        printf("{\n");
        for (int i = 0; i < v->obj_val->count; i++) {
            print_indent(indent + 1);
            printf("\"%s\": ", v->obj_val->pairs[i]->key);
            print_value(v->obj_val->pairs[i]->value, indent + 1);
            if (i < v->obj_val->count - 1) printf(",");
            printf("\n");
        }
        print_indent(indent);
        printf("}");
        break;
    }
}

// --- メイン関数 ---

int main() {
    Value *data = parse("target.json");
    print_value(data, 0);
    printf("\n");
    
    // 注意: 本来はここで data 内の再帰的な free 処理が必要
    return 0;
}

...と思ったら、 Python への書き換えにはほんのちょっとだけ失敗しました。凡ミスも愛らしいですね。暇な方、良ければどこが問題か考えてみてください。

import sys
import json
from enum import Enum, auto

# --- 定数・Enum定義 ---

class TokenType(Enum):
    BLOCK_BEGIN = auto()
    BLOCK_END = auto()
    ARRAY_BEGIN = auto()
    ARRAY_END = auto()
    COLON = auto()
    STRING = auto()
    INT = auto()
    TRUE = auto()
    FALSE = auto()
    NULL = auto()

IGNORES = {' ', '\t', '\r'}

class Token:
    def __init__(self, t_type, value):
        self.type = t_type
        self.value = value

    def get_value(self):
        if self.type == TokenType.INT:
            return int(self.value)
        elif self.type == TokenType.TRUE:
            return True
        elif self.type == TokenType.FALSE:
            return False
        elif self.type == TokenType.NULL:
            return None
        else:
            return self.value

    def __repr__(self):
        return f"Token({self.type.name}, {self.value})"

# --- ヘルパー関数 ---

def askwd(kwd):
    """キーワード判定 (true, false, null, number)"""
    if kwd == "true":
        return Token(TokenType.TRUE, "true")
    elif kwd == "false":
        return Token(TokenType.FALSE, "false")
    elif kwd == "null":
        return Token(TokenType.NULL, "null")
    else:
        try:
            int(kwd) # 数字チェック
            return Token(TokenType.INT, kwd)
        except ValueError:
            sys.exit(f"不明な文字列: {kwd}")

# --- 字句解析 (Tokenize) ---

def tokenize(text):
    res = []
    tmp = ""
    is_store = False
    is_back_slash = False
    
    i = 0
    n = len(text)
    
    while i < n:
        s = text[i]
        
        if s in IGNORES:
            i += 1
            continue

        if is_store:
            if s in ('=', ':', '\n', ',', '}'):
                is_store = False
                if s in ('=', ':'):
                    res.append(Token(TokenType.STRING, tmp))
                    res.append(Token(TokenType.COLON, ":"))
                elif s == '}':
                    res.append(askwd(tmp))
                    res.append(Token(TokenType.BLOCK_END, "}"))
                else:
                    res.append(askwd(tmp))
                tmp = ""
            else:
                tmp += s
            # is_storeの場合は continue せずに次の文字へ(Goのロジック準拠のためループ制御を変則的にしない)
            if not is_store and s != '}': # } の場合は下のswitchに入らないようにする
                pass 
            else:
                i += 1
                continue

        # switch文相当
        if s in (',', '\n'):
            pass
        elif s == '{':
            res.append(Token(TokenType.BLOCK_BEGIN, "{"))
        elif s == '}':
            res.append(Token(TokenType.BLOCK_END, "}"))
        elif s == '[':
            res.append(Token(TokenType.ARRAY_BEGIN, "["))
        elif s == ']':
            res.append(Token(TokenType.ARRAY_END, "]"))
        elif s in (':', '='):
            if tmp == "":
                sys.exit("構文エラー")
            res.append(Token(TokenType.STRING, tmp))
            tmp = ""
            res.append(Token(TokenType.COLON, ":"))
        elif s == '"':
            string_value = ""
            while i + 1 < n:
                i += 1
                char = text[i]
                if is_back_slash:
                    is_back_slash = False
                    if char == 'n': string_value += "\n"
                    elif char == 'r': string_value += "\r"
                    elif char == 't': string_value += "\t"
                    elif char == '"': string_value += "\""
                    elif char == '\\': string_value += "\\"
                    else: string_value += char
                elif char == '"':
                    res.append(Token(TokenType.STRING, string_value))
                    break
                elif char == '\\':
                    is_back_slash = True
                else:
                    string_value += char
        else:
            is_store = True
            tmp += s
        
        i += 1
            
    return res

# --- 構文解析 (Parse) ---

def parse(file_path):
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            text = f.read()
    except FileNotFoundError:
        sys.exit(f"File not found: {file_path}")

    tokens = tokenize(text)
    if not tokens:
        return {}
    
    # Pythonではintポインタの代わりにリストでインデックスをラップして参照渡しを擬似的に再現
    idx = [0] 
    return _parse(tokens, idx)

def _parse(tokens, idx_wrapper):
    res = {}
    tmp = None # 直前のトークン(キー候補)

    while idx_wrapper[0] < len(tokens):
        t = tokens[idx_wrapper[0]]
        
        if t.type == TokenType.COLON:
            if not tmp or tmp.type != TokenType.STRING:
                sys.exit("キーに出来るのは数字か文字のみです")
            
            idx_wrapper[0] += 1 # COLONの次へ
            if idx_wrapper[0] >= len(tokens): break
            t = tokens[idx_wrapper[0]]

            if t.type == TokenType.BLOCK_BEGIN:
                idx_wrapper[0] += 1
                res[tmp.value] = _parse(tokens, idx_wrapper)
            
            elif t.type == TokenType.ARRAY_BEGIN:
                idx_wrapper[0] += 1
                arr = []
                if idx_wrapper[0] < len(tokens):
                    t_next = tokens[idx_wrapper[0]]
                    
                    if t_next.type == TokenType.BLOCK_BEGIN:
                        while idx_wrapper[0] < len(tokens) and tokens[idx_wrapper[0]].type == TokenType.BLOCK_BEGIN:
                            idx_wrapper[0] += 1
                            arr.append(_parse(tokens, idx_wrapper))
                            idx_wrapper[0] += 1
                        idx_wrapper[0] -= 1 # adjust for outer loop inc
                        res[tmp.value] = arr
                    else:
                        while idx_wrapper[0] < len(tokens) and t_next.type != TokenType.ARRAY_END:
                            arr.append(t_next.get_value())
                            idx_wrapper[0] += 1
                            t_next = tokens[idx_wrapper[0]]
                        res[tmp.value] = arr
            else:
                res[tmp.value] = t.get_value()
            
            tmp = None # リセット

        elif t.type == TokenType.BLOCK_END:
            return res
        
        else:
            tmp = t
        
        idx_wrapper[0] += 1

    return res

# --- Main ---

def main():
    data = parse('target.json')
    # ensure_ascii=False で日本語などもそのまま表示可能
    print(json.dumps(data, indent=2, ensure_ascii=False))

if __name__ == "__main__":
    main()
Geminiの凡ミスの答え
tokenize関数
            # is_storeの場合は continue せずに次の文字へ(Goのロジック準拠のためループ制御を変則的にしない)
+           if not is_store and (s not in {'}', ':', '='}): # } の場合は下のswitchに入らないようにする
                pass 
            else:
                i += 1
                continue

ちなみに、Geminiさんはin演算にtupleを渡してますが、普通setの方が早いです。計算量は脅威の $O(1)$

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?