LoginSignup
1
0

JSONをツリー形式にしてほしい。型ごとにまとめて。

Last updated at Posted at 2022-12-02

Screenshot from 2022-11-19 08-35-20.png

はじめに

この記事は自作のJSON解析CLIツールjtrの紹介です。

開発経緯

近年GoやNim, TypeScriptといった静的型付け言語を扱うようになって、PythonやJavaScriptと違って型が強く意識した開発が求められるようになりました。
提供されているWebAPIからJSONを抜き出したり、逆に個人制作のWebアプリからJSONを提供する側に回るとき、typeを定義して各言語のオブジェクトとして扱う機会が多くあります。
特に、別のWebAPIからJSONを利用する際に、長ったらしいJSONを眺めて概要把握し、typeを定義してJSONを各言語のオブジェクトに落とし込む作業に疲れたので、もっと効率化できないかと考えました。

JSONをパースする際に型が意識されるケース

JSONをパースする際に型が意識されるケースをコード例を示しながら説明します。

JSONパース@Go

例として、Goはtypeを定義するとき、タグというメタ情報をtypeに定義しておいて、JSONパースする作業を楽にしてくれます。

引用: u1and0/pnsearch

Go言語で型の定義
// Option : ソート列の選択、昇順/降順、AND検索OR検索切り替え
Option struct {
  SortOrder string `form:"orderby"`
  SortAsc   bool   `form:"asc"`
  OR        bool   `form:"or"`
}
// Filter : 発注日、納入日フィルター
Filter struct {
  Order    string `form:"発注"`
  Delivery string `form:"納入"`
}
Go言語でJSONのマッピング
import "github.com/gin-gonic/gin"

func ReturnTempl(c *gin.Context) {
  //
  // --snip--
  //
	// Extract query
	q := newQuery()
	if err := c.ShouldBind(q); err != nil {
		msg := fmt.Sprintf("%#v Bad Query", q)
    c.JSON(http.StatusBadRequest, gin.H{"msg": msg, "query": q})
		return
	}

フォームの文字列をc.ShouldBind(q)でtypeで定義したQuery構造体に変換します。
このとき、form:"orderby"のようなメタ情報を手がかりにフレームワーク側がいい感じに割り当ててくれます。

JSONパース@Nim

次の例では、配信されたJSONをNim言語のオブジェクトとして扱います。

引用: u1and0/growiapi

Nim言語で型の定義
  ## _api/v3/page で取得できるJSONオブジェクトとページの存在、エラーメッセージ
  MetaPage* = object
    page*: Page
    limit: int
    exist: bool
    error: string

Nim言語でJSONのマッピング
import json

proc initMetaPage*(path: string, limit = 50): MetaPage =
  # --snip--
  let res: Response = result.get()
  case res.status:
    of $Http200:
      # --snip--
      result.page = to(parseJson(jsonStr)["page"], Page)

parseJson(jsonStr)で文字列をJSON(Nimの型定義としてはJsonNode型)に変換してくれます。
更に、to(JsonNode, T)テンプレートでJsonNodeを型Tにいい感じに割り当ててくれます。

以上の例から見られるような いい感じに割当ててくれる ようにするためには、事前にtypeで構造体、オブジェクト、そして各プロパティの型を決めておいてあげないといけないわけです。

jqというコマンドがある

短いJSONならいいけど
長いJSONは構造を把握するだけで疲れます。

解読に疲れるJSONの例
$ curl -fsSL 'https://raw.githubusercontent.com/nim-lang/packages/master/packages.json' | jq
[
  {
    "name": "passy",
    "url": "https://github.com/infinitybeond1/passy",
    "method": "git",
    "tags": [
      "password",
      "generator",
      "cryptography",
      "security"
    ],
    "description": "A fast little password generator",
    "license": "GPL3",
    "web": "https://github.com/infinitybeond1/passy"
  },
  {
    "name": "entgrep",
    "url": "https://github.com/srozb/entgrep",
    "method": "git",
    "tags": [
      "command-line",
      "crypto",
      "cryptography",
      "security"
    ],
    "description": "A grep but for secrets (based on entropy).",
    "license": "MIT",
    "web": "https://github.com/srozb/entgrep"
  },
  {
    ...他 27000行ほど
]

jtrの紹介

ここからようやくjtrコマンドの説明です。
使い方はシンプルで、jqコマンドの使い方をリスペクトしています。

名前の由来がjq + tree1 => jtr ですので、使い方もjqに似せています。

使用例

標準入力をJSONとしてパースし、型をオブジェクト名の右に添えて標準出力に出力します。
オブジェクトに階層があれば

$ echo {"foo":5.0,"baz":[{"foo":{"bar":100,"baz":"click","cat":null}},],"login":true} | jtr
.
├── foo <float>
├── baz [].
│     └── foo
│         ├── bar <int>
│         ├── baz <string>
│         └── cat <null>
└── login <bool>

プロパティアクセス

jqと同様に、プロパティを下ってそこからツリー表示します。
下記の例ではbazオブジェクト以下をツリー表示しています。

$ echo '{"foo":5.0,"baz":[{"foo":{"bar":100,"baz":"click","cat":null}}],"login":true}' | jtr '.baz'
[].
  └── foo
      ├── bar <int>
      ├── baz <string>
      └── cat <null>

オプション

今のところ、使えるオプションは--jqだけ。
jq表示を同時に出力します。

options:
  --jq, -q          Display jq view
  --help, -h        Show help message
  --version, -v     Show version

巨大なJSONに使ってみた

使用例で見る感じ、どんな結果になるかは想像つくかと思いますが、実際に取り扱う巨大なJSONファイルに対して実行してみた結果を書きます。

test case 1

NimのパッケージリポジトリのJSONです。

実行コマンドをjq, jtr の順に実行します。

packages.jsonのjq出力(省略あり)
$ curl -fsSL 'https://raw.githubusercontent.com/nim-lang/packages/master/packages.json' | jq
[
  {
    "name": "passy",
    "url": "https://github.com/infinitybeond1/passy",
    "method": "git",
    "tags": [
      "password",
      "generator",
      "cryptography",
      "security"
    ],
    "description": "A fast little password generator",
    "license": "GPL3",
    "web": "https://github.com/infinitybeond1/passy"
  },
  {
    "name": "entgrep",
    "url": "https://github.com/srozb/entgrep",
    "method": "git",
    "tags": [
      "command-line",
      "crypto",
      "cryptography",
      "security"
    ],
    "description": "A grep but for secrets (based on entropy).",
    "license": "MIT",
    "web": "https://github.com/srozb/entgrep"
  },
  {
    ...他 27000行ほど
]
packages.jsonのjtr出力(省略なし)
$ curl -fsSL 'https://raw.githubusercontent.com/nim-lang/packages/master/packages.json' | jtr
[].
  ├── name <string>
  ├── url <string>
  ├── method <string>
  ├── tags []string
  ├── description <string>
  ├── license <string>
  ├── web <string>
  └── doc <string>

まだこれならarrayの各要素が12個なので、jqでもいけるか。
しかし、jqjtrの出力をよく見比べてください。3要素目以降で"web"要素の次に"doc"という要素があることは、このjqの2要素だけではわからなかったでしょう。
jtrならarrayをすべて走査して、最も大きい要素の解析結果を返してくれます。そのため、すべてのJSONを読み込まなくても、ここでは出てきていない3要素目以降に"doc"というオブジェクトが登場することに気づくことができます。

細かいこと言うと、最初に出てきた要素数最大のオブジェクトを解析します。そのため、例えば3要素目に"doc"を持つオブジェクトが登場して、4要素目以降に"doc"の代わりに"misc"という要素に置き換わっていても、"misc"というプロパティが出てきたことには気づくことがありません。
なお、"doc"オブジェクトが出てくるのは、調べた時点で全2055要素中の147要素だけでした。これは見逃してしまいそうですね。

test case 2

24.9MBの巨大なJSONを扱ってみます。

large-file.jsonのjq出力(省略あり)
$ curl -fsSL 'https://raw.githubusercontent.com/json-iterator/test-data/master/large-file.json' | jq
[
  {
    "id": "2489651045",
    "type": "CreateEvent",
    "actor": {
      "id": 665991,
      "login": "petroav",
      "gravatar_id": "",
      "url": "https://api.github.com/users/petroav",
      "avatar_url": "https://avatars.githubusercontent.com/u/665991?"
    },
    "repo": {
      "id": 28688495,
      "name": "petroav/6.828",
      "url": "https://api.github.com/repos/petroav/6.828"
    },
    "payload": {
      "ref": "master",
      "ref_type": "branch",
      "master_branch": "master",
      "description": "Solution to homework and assignments from MIT's 6.828 (Operating Systems Engineering). Done in my spare time.",
      "pusher_type": "user"
    },
    "public": true,
    "created_at": "2015-01-01T15:00:00Z"
  },
  {
    "id": "2489651051",
    "type": "PushEvent",
    "actor": {
      "id": 3854017,
      "login": "rspt",
      "gravatar_id": "",
      "url": "https://api.github.com/users/rspt",
      "avatar_url": "https://avatars.githubusercontent.com/u/3854017?"
    },
    "repo": {
      "id": 28671719,
      "name": "rspt/rspt-theme",
      "url": "https://api.github.com/repos/rspt/rspt-theme"
    },
    "payload": {
      "push_id": 536863970,
      "size": 1,
      "distinct_size": 1,
      "ref": "refs/heads/master",
      "head": "6b089eb4a43f728f0a594388092f480f2ecacfcd",
      "before": "437c03652caa0bc4a7554b18d5c0a394c2f3d326",
      "commits": [
        {
          "sha": "6b089eb4a43f728f0a594388092f480f2ecacfcd",
          "author": {
            "email": "5c682c2d1ec4073e277f9ba9f4bdf07e5794dabe@rspt.ch",
            "name": "rspt"
          },
          "message": "Fix main header height on mobile",
          "distinct": true,
          "url": "https://api.github.com/repos/rspt/rspt-theme/commits/6b089eb4a43f728f0a594388092f480f2ecacfcd"
        }
      ]
    },
    "public": true,
    "created_at": "2015-01-01T15:00:01Z"
  },
  {
    "id": "2489651053",
    "type": "PushEvent",
    "actor": {
      "id": 6339799,
      "login": "izuzero",
      "gravatar_id": "",
      "url": "https://api.github.com/users/izuzero",
      "avatar_url": "https://avatars.githubusercontent.com/u/6339799?"
    },
    "repo": {
      "id": 28270952,
      "name": "izuzero/xe-module-ajaxboard",
      "url": "https://api.github.com/repos/izuzero/xe-module-ajaxboard"
    },
    "payload": {
      "push_id": 536863972,
      "size": 1,
      "distinct_size": 1,
      "ref": "refs/heads/develop",
      "head": "ec819b9df4fe612bb35bf562f96810bf991f9975",
      "before": "590433109f221a96cf19ea7a7d9a43ca333e3b3e",
      "commits": [
        {
          "sha": "ec819b9df4fe612bb35bf562f96810bf991f9975",
          "author": {
            "email": "df05f55543db3c62cf64f7438018ec37f3605d3c@gmail.com",
            "name": "Eunsoo Lee"
          },
          "message": "#20 게시글 및 댓글 삭제 시 새로고침이 되는 문제 해결\n\n원래 의도는 새로고침이 되지 않고 확인창만으로 해결되어야 함.\n기본 게시판 대응 플러그인에서 발생한 이슈.",
          "distinct": true,
          "url": "https://api.github.com/repos/izuzero/xe-module-ajaxboard/commits/ec819b9df4fe612bb35bf562f96810bf991f9975"
        }
      ]
    },
    "public": true,
    ...他に 11251
large-file.jsonのjtr出力(省略なし)
$ curl -fsSL 'https://raw.githubusercontent.com/json-iterator/test-data/master/large-file.json' | jtr
[].
  ├── id <string>
  ├── type <string>
  ├── actor
  │   ├── id <int>
  │   ├── login <string>
  │   ├── gravatar_id <string>
  │   ├── url <string>
  │   └── avatar_url <string>
  ├── repo
  │   ├── id <int>
  │   ├── name <string>
  │   └── url <string>
  ├── payload
  │   ├── action <string>
  │   ├── comment
  │   │   ├── url <string>
  │   │   ├── id <int>
  │   │   ├── diff_hunk <string>
  │   │   ├── path <string>
  │   │   ├── position <int>
  │   │   ├── original_position <int>
  │   │   ├── commit_id <string>
  │   │   ├── original_commit_id <string>
  │   │   ├── user
  │   │   │   ├── login <string>
  │   │   │   ├── id <int>
  │   │   │   ├── avatar_url <string>
  │   │   │   ├── gravatar_id <string>
  │   │   │   ├── url <string>
  │   │   │   ├── html_url <string>
  │   │   │   ├── followers_url <string>
  │   │   │   ├── following_url <string>
  │   │   │   ├── gists_url <string>
  │   │   │   ├── starred_url <string>
  │   │   │   ├── subscriptions_url <string>
  │   │   │   ├── organizations_url <string>
  │   │   │   ├── repos_url <string>
  │   │   │   ├── events_url <string>
  │   │   │   ├── received_events_url <string>
  │   │   │   ├── type <string>
  │   │   │   └── site_admin <bool>
  │   │   ├── body <string>
  │   │   ├── created_at <string>
  │   │   ├── updated_at <string>
  │   │   ├── html_url <string>
  │   │   ├── pull_request_url <string>
  │   │   └── _links
  │   │       ├── self
  │   │       │   └── href <string>
  │   │       ├── html
  │   │       │   └── href <string>
  │   │       └── pull_request
  │   │           └── href <string>
  │   └── pull_request
  │       ├── url <string>
  │       ├── id <int>
  │       ├── html_url <string>
  │       ├── diff_url <string>
  │       ├── patch_url <string>
  │       ├── issue_url <string>
  │       ├── number <int>
  │       ├── state <string>
  │       ├── locked <bool>
  │       ├── title <string>
  │       ├── user
  │       │   ├── login <string>
  │       │   ├── id <int>
  │       │   ├── avatar_url <string>
  │       │   ├── gravatar_id <string>
  │       │   ├── url <string>
  │       │   ├── html_url <string>
  │       │   ├── followers_url <string>
  │       │   ├── following_url <string>
  │       │   ├── gists_url <string>
  │       │   ├── starred_url <string>
  │       │   ├── subscriptions_url <string>
  │       │   ├── organizations_url <string>
  │       │   ├── repos_url <string>
  │       │   ├── events_url <string>
  │       │   ├── received_events_url <string>
  │       │   ├── type <string>
  │       │   └── site_admin <bool>
  │       ├── body <string>
  │       ├── created_at <string>
  │       ├── updated_at <string>
  │       ├── closed_at <null>
  │       ├── merged_at <null>
  │       ├── merge_commit_sha <string>
  │       ├── assignee
  │       │   ├── login <string>
  │       │   ├── id <int>
  │       │   ├── avatar_url <string>
  │       │   ├── gravatar_id <string>
  │       │   ├── url <string>
  │       │   ├── html_url <string>
  │       │   ├── followers_url <string>
  │       │   ├── following_url <string>
  │       │   ├── gists_url <string>
  │       │   ├── starred_url <string>
  │       │   ├── subscriptions_url <string>
  │       │   ├── organizations_url <string>
  │       │   ├── repos_url <string>
  │       │   ├── events_url <string>
  │       │   ├── received_events_url <string>
  │       │   ├── type <string>
  │       │   └── site_admin <bool>
  │       ├── milestone
  │       │   ├── url <string>
  │       │   ├── labels_url <string>
  │       │   ├── id <int>
  │       │   ├── number <int>
  │       │   ├── title <string>
  │       │   ├── description <null>
  │       │   ├── creator
  │       │   │   ├── login <string>
  │       │   │   ├── id <int>
  │       │   │   ├── avatar_url <string>
  │       │   │   ├── gravatar_id <string>
  │       │   │   ├── url <string>
  │       │   │   ├── html_url <string>
  │       │   │   ├── followers_url <string>
  │       │   │   ├── following_url <string>
  │       │   │   ├── gists_url <string>
  │       │   │   ├── starred_url <string>
  │       │   │   ├── subscriptions_url <string>
  │       │   │   ├── organizations_url <string>
  │       │   │   ├── repos_url <string>
  │       │   │   ├── events_url <string>
  │       │   │   ├── received_events_url <string>
  │       │   │   ├── type <string>
  │       │   │   └── site_admin <bool>
  │       │   ├── open_issues <int>
  │       │   ├── closed_issues <int>
  │       │   ├── state <string>
  │       │   ├── created_at <string>
  │       │   ├── updated_at <string>
  │       │   ├── due_on <null>
  │       │   └── closed_at <null>
  │       ├── commits_url <string>
  │       ├── review_comments_url <string>
  │       ├── review_comment_url <string>
  │       ├── comments_url <string>
  │       ├── statuses_url <string>
  │       ├── head
  │       │   ├── label <string>
  │       │   ├── ref <string>
  │       │   ├── sha <string>
  │       │   ├── user
  │       │   │   ├── login <string>
  │       │   │   ├── id <int>
  │       │   │   ├── avatar_url <string>
  │       │   │   ├── gravatar_id <string>
  │       │   │   ├── url <string>
  │       │   │   ├── html_url <string>
  │       │   │   ├── followers_url <string>
  │       │   │   ├── following_url <string>
  │       │   │   ├── gists_url <string>
  │       │   │   ├── starred_url <string>
  │       │   │   ├── subscriptions_url <string>
  │       │   │   ├── organizations_url <string>
  │       │   │   ├── repos_url <string>
  │       │   │   ├── events_url <string>
  │       │   │   ├── received_events_url <string>
  │       │   │   ├── type <string>
  │       │   │   └── site_admin <bool>
  │       │   └── repo
  │       │       ├── id <int>
  │       │       ├── name <string>
  │       │       ├── full_name <string>
  │       │       ├── owner
  │       │       │   ├── login <string>
  │       │       │   ├── id <int>
  │       │       │   ├── avatar_url <string>
  │       │       │   ├── gravatar_id <string>
  │       │       │   ├── url <string>
  │       │       │   ├── html_url <string>
  │       │       │   ├── followers_url <string>
  │       │       │   ├── following_url <string>
  │       │       │   ├── gists_url <string>
  │       │       │   ├── starred_url <string>
  │       │       │   ├── subscriptions_url <string>
  │       │       │   ├── organizations_url <string>
  │       │       │   ├── repos_url <string>
  │       │       │   ├── events_url <string>
  │       │       │   ├── received_events_url <string>
  │       │       │   ├── type <string>
  │       │       │   └── site_admin <bool>
  │       │       ├── private <bool>
  │       │       ├── html_url <string>
  │       │       ├── description <string>
  │       │       ├── fork <bool>
  │       │       ├── url <string>
  │       │       ├── forks_url <string>
  │       │       ├── keys_url <string>
  │       │       ├── collaborators_url <string>
  │       │       ├── teams_url <string>
  │       │       ├── hooks_url <string>
  │       │       ├── issue_events_url <string>
  │       │       ├── events_url <string>
  │       │       ├── assignees_url <string>
  │       │       ├── branches_url <string>
  │       │       ├── tags_url <string>
  │       │       ├── blobs_url <string>
  │       │       ├── git_tags_url <string>
  │       │       ├── git_refs_url <string>
  │       │       ├── trees_url <string>
  │       │       ├── statuses_url <string>
  │       │       ├── languages_url <string>
  │       │       ├── stargazers_url <string>
  │       │       ├── contributors_url <string>
  │       │       ├── subscribers_url <string>
  │       │       ├── subscription_url <string>
  │       │       ├── commits_url <string>
  │       │       ├── git_commits_url <string>
  │       │       ├── comments_url <string>
  │       │       ├── issue_comment_url <string>
  │       │       ├── contents_url <string>
  │       │       ├── compare_url <string>
  │       │       ├── merges_url <string>
  │       │       ├── archive_url <string>
  │       │       ├── downloads_url <string>
  │       │       ├── issues_url <string>
  │       │       ├── pulls_url <string>
  │       │       ├── milestones_url <string>
  │       │       ├── notifications_url <string>
  │       │       ├── labels_url <string>
  │       │       ├── releases_url <string>
  │       │       ├── created_at <string>
  │       │       ├── updated_at <string>
  │       │       ├── pushed_at <string>
  │       │       ├── git_url <string>
  │       │       ├── ssh_url <string>
  │       │       ├── clone_url <string>
  │       │       ├── svn_url <string>
  │       │       ├── homepage <string>
  │       │       ├── size <int>
  │       │       ├── stargazers_count <int>
  │       │       ├── watchers_count <int>
  │       │       ├── language <string>
  │       │       ├── has_issues <bool>
  │       │       ├── has_downloads <bool>
  │       │       ├── has_wiki <bool>
  │       │       ├── has_pages <bool>
  │       │       ├── forks_count <int>
  │       │       ├── mirror_url <null>
  │       │       ├── open_issues_count <int>
  │       │       ├── forks <int>
  │       │       ├── open_issues <int>
  │       │       ├── watchers <int>
  │       │       └── default_branch <string>
  │       ├── base
  │       │   ├── label <string>
  │       │   ├── ref <string>
  │       │   ├── sha <string>
  │       │   ├── user
  │       │   │   ├── login <string>
  │       │   │   ├── id <int>
  │       │   │   ├── avatar_url <string>
  │       │   │   ├── gravatar_id <string>
  │       │   │   ├── url <string>
  │       │   │   ├── html_url <string>
  │       │   │   ├── followers_url <string>
  │       │   │   ├── following_url <string>
  │       │   │   ├── gists_url <string>
  │       │   │   ├── starred_url <string>
  │       │   │   ├── subscriptions_url <string>
  │       │   │   ├── organizations_url <string>
  │       │   │   ├── repos_url <string>
  │       │   │   ├── events_url <string>
  │       │   │   ├── received_events_url <string>
  │       │   │   ├── type <string>
  │       │   │   └── site_admin <bool>
  │       │   └── repo
  │       │       ├── id <int>
  │       │       ├── name <string>
  │       │       ├── full_name <string>
  │       │       ├── owner
  │       │       │   ├── login <string>
  │       │       │   ├── id <int>
  │       │       │   ├── avatar_url <string>
  │       │       │   ├── gravatar_id <string>
  │       │       │   ├── url <string>
  │       │       │   ├── html_url <string>
  │       │       │   ├── followers_url <string>
  │       │       │   ├── following_url <string>
  │       │       │   ├── gists_url <string>
  │       │       │   ├── starred_url <string>
  │       │       │   ├── subscriptions_url <string>
  │       │       │   ├── organizations_url <string>
  │       │       │   ├── repos_url <string>
  │       │       │   ├── events_url <string>
  │       │       │   ├── received_events_url <string>
  │       │       │   ├── type <string>
  │       │       │   └── site_admin <bool>
  │       │       ├── private <bool>
  │       │       ├── html_url <string>
  │       │       ├── description <string>
  │       │       ├── fork <bool>
  │       │       ├── url <string>
  │       │       ├── forks_url <string>
  │       │       ├── keys_url <string>
  │       │       ├── collaborators_url <string>
  │       │       ├── teams_url <string>
  │       │       ├── hooks_url <string>
  │       │       ├── issue_events_url <string>
  │       │       ├── events_url <string>
  │       │       ├── assignees_url <string>
  │       │       ├── branches_url <string>
  │       │       ├── tags_url <string>
  │       │       ├── blobs_url <string>
  │       │       ├── git_tags_url <string>
  │       │       ├── git_refs_url <string>
  │       │       ├── trees_url <string>
  │       │       ├── statuses_url <string>
  │       │       ├── languages_url <string>
  │       │       ├── stargazers_url <string>
  │       │       ├── contributors_url <string>
  │       │       ├── subscribers_url <string>
  │       │       ├── subscription_url <string>
  │       │       ├── commits_url <string>
  │       │       ├── git_commits_url <string>
  │       │       ├── comments_url <string>
  │       │       ├── issue_comment_url <string>
  │       │       ├── contents_url <string>
  │       │       ├── compare_url <string>
  │       │       ├── merges_url <string>
  │       │       ├── archive_url <string>
  │       │       ├── downloads_url <string>
  │       │       ├── issues_url <string>
  │       │       ├── pulls_url <string>
  │       │       ├── milestones_url <string>
  │       │       ├── notifications_url <string>
  │       │       ├── labels_url <string>
  │       │       ├── releases_url <string>
  │       │       ├── created_at <string>
  │       │       ├── updated_at <string>
  │       │       ├── pushed_at <string>
  │       │       ├── git_url <string>
  │       │       ├── ssh_url <string>
  │       │       ├── clone_url <string>
  │       │       ├── svn_url <string>
  │       │       ├── homepage <string>
  │       │       ├── size <int>
  │       │       ├── stargazers_count <int>
  │       │       ├── watchers_count <int>
  │       │       ├── language <string>
  │       │       ├── has_issues <bool>
  │       │       ├── has_downloads <bool>
  │       │       ├── has_wiki <bool>
  │       │       ├── has_pages <bool>
  │       │       ├── forks_count <int>
  │       │       ├── mirror_url <null>
  │       │       ├── open_issues_count <int>
  │       │       ├── forks <int>
  │       │       ├── open_issues <int>
  │       │       ├── watchers <int>
  │       │       └── default_branch <string>
  │       └── _links
  │           ├── self
  │           │   └── href <string>
  │           ├── html
  │           │   └── href <string>
  │           ├── issue
  │           │   └── href <string>
  │           ├── comments
  │           │   └── href <string>
  │           ├── review_comments
  │           │   └── href <string>
  │           ├── review_comment
  │           │   └── href <string>
  │           ├── commits
  │           │   └── href <string>
  │           └── statuses
  │               └── href <string>
  ├── public <bool>
  ├── created_at <string>
  └── org
      ├── id <int>
      ├── login <string>
      ├── gravatar_id <string>
      ├── url <string>
      └── avatar_url <string>

ものすごい量の要素数だ...。
しかし、jqの10000行も見なくても、jtrの383行を見れば、どんな感じの構造のJSONなのかはわかりますね。

パフォーマンス

ネットワーク速度に依存しないようにlarge-file.jsonのrawcontentをローカルに保存してから実行しました。

行数はwcコマンドで数えます。
実行コマンドは下記です。

$ cat large-file.json | wc -l
$ cat large-file.json | jq | wc -l
$ cat large-file.json | jtr | wc -l

実行速度は、コマンド実行から標準出力に表示され終わるまでの時間です。
実行コマンドは下記です。

$ time cat large-file.json
$ time cat large-file.json | jq
$ time cat large-file.json | jtr

結果を示します。

cat cat+jq cat+jtr
行数(行) 11351 732457 383
実行速度(秒) 3.273 11.216 2.882

出力行が少なければ実行速度が早くなるのはまぁ当然な結果なわけですが、実行速度も申し分なく、型まで判別してくれるのは便利なコマンドではないかな、と思う次第です。

インストール

github releaseからダウンロードしてください。

$ curl -fLO https://github.com/u1and0/jtr/releases/download/v0.2.8/jtr-linux.zip
$ unzip jtr-linux.zip
$ chmod 755 ./jtr
$ ./jtr -v

または、Nimbleを使ってください。
https://nimble.directory/pkg/jtr

$ nimble install jtr

ビルド

Githubからフォークしてください。
u1and0/jtr

実装

実装のコアのコードを示します。
詳細が知りたければGithub上のソースを見てください。

概要

実装はNimの標準パッケージstd/jsonが強力ですのでそこまで苦労しませんでした。

parseJson()を使うとJsonNodeKindという型でそのオブジェクトの型を判別できるので、switch-case分岐してタイプを表示するか、Objectを再帰的に探索するか、Arrayを再帰的に探索します。

探索の結果はすべてstring型でつなげていって、最終的にstringを標準出力にecho()するだけです。

プリミティブ型の探索

要はnullかboolかstringかfloatかintかを判定してくれます。

プリミティブ型解析rootTree()の実装
func rootTree*(jnode: JsonNode): string =
  ## Tree view for type of plain JSON recursively
  ##
  ## ```nim
  ## doAssert rootTree(parseJson("null")) == "<null>"
  ## doAssert rootTree(parseJson("true")) == "<bool>"
  ## doAssert rootTree(parseJson("\"5\"")) == "<string>"
  ## doAssert rootTree(parseJson($5.0)) == "<float>"
  ## doAssert rootTree(parseJson($5)) == "<int>"
  ## ```
  case jnode.kind
    of JNull: "<null>"
    of JBool: "<bool>"
    of JString: "<string>"
    of JInt: "<int>"
    of JFloat: "<float>"
    of JArray: arrayTree(jnode)
    of JObject: ".\n" & objectTree(jnode)

Objectの探索

Objectを再帰的に潜って解析します。

オブジェクト型の解析objectTree()の実装
func objectTree*(jobj: JsonNode, indent = ""): string =
  ## Tree view for type of object elemnts recursively
  ##
  ## ```nim
  ## let obj = """
  ## {"foo": "0", "obj": {"bar":1, "baz":"2"}, "name": "ken"}
  ## """.parseJson
  ## doAssert objectTree(obj) == """├── foo <string>
  ## ├── obj
  ## │   ├── bar <int>
  ## │   └── baz <string>
  ## └── name <string>"""
  ## ```

  var
    res: seq[string]
    i: int
    branch, nextIndent: string
  let isLastOne = func(i: int): bool = i >= len(jobj)-1
  for key, val in jobj.pairs:
    if isLastOne(i):
      branch = "└── "
      nextIndent = indent & " ".repeat(4)
    else:
      branch = "├── "
      nextIndent = indent & "│" & " ".repeat(3)
    let types: string = case val.kind
      of JNull: " <null>"
      of JBool: " <bool>"
      of JString: " <string>"
      of JInt: " <int>"
      of JFloat: " <float>"
      of JArray: " " & arrayTree(val, nextIndent)
      of JObject: "\n" & objectTree(val, nextIndent)
    res.add(&"{indent}{branch}{key}{types}")
    i += 1
  return res.join("\n")

Arrayの探索

Array型のすべての要素をまとめて表示します。

Array型解析arrayTree()の実装
func arrayTree*(jarray: JsonNode, indent = ""): string =
  ## Tree view for type of array elemnts recursively
  ##
  ## ```nim
  ## doAssert arrayTree(parseJson("[null,null,null]")) == "[]null"
  ## doAssert arrayTree(parseJson("[true,false,false]")) == "[]bool"
  ## doAssert arrayTree(parseJson("[\"cat\", \"dog\"]")) == "[]string"
  ## doAssert arrayTree(parseJson("[1,2,3]")) == "[]int"
  ## doAssert arrayTree(parseJson("[1.0,10.0,100.0]")) == "[]float"
  ## doAssert arrayTree(parseJson("[1,\"2\",3]")) == "[]any"
  ## doAssert arrayTree(parseJson("[[1,2,3]]")) == "[][]int"
  ## doAssert arrayTree(parseJson("""
  ## [
  ##   {
  ##     "foo":{
  ##       "bar": 1,
  ##       "baz": "key"
  ##     }
  ##   },
  ##   {
  ##     "foo":{
  ##       "bar": 2,
  ##       "baz": "signal",
  ##       "cat": null
  ##     }
  ##   }
  ## ]
  ## """)) == """[].
  ##   └── foo
  ##       ├── bar <int>
  ##       ├── baz <string>
  ##       └── cat <null>"""
  ## ```

  let typesArr: seq[JsonNodeKind] = collect:
    for val in jarray:
      val.kind
  if all(typesArr, func(x: JsonNodeKind): bool = x == JNull):
    return "[]null"
  elif all(typesArr, func(x: JsonNodeKind): bool = x == JBool):
    return "[]bool"
  elif all(typesArr, func(x: JsonNodeKind): bool = x == JString):
    return "[]string"
  elif all(typesArr, func(x: JsonNodeKind): bool = x == JInt):
    return "[]int"
  elif all(typesArr, func(x: JsonNodeKind): bool = x == JFloat):
    return "[]float"
  elif all(typesArr, func(x: JsonNodeKind): bool = x == JArray):
    return "[]" & arrayTree(jarray[0]) # TODO 本当にjarray[0]だけでいいのか?seekLargestObject通すべきでは
  elif all(typesArr, func(x: JsonNodeKind): bool = x == JObject):
    let nextIndent = indent & " ".repeat(2)
    let largestObj = seekLargestObject(jarray)
    return "[].\n" & objectTree(largestObj, nextIndent)
  else: # 全ての型が一致しない場合
    return "[]any"
  1. treeコマンドの実装例としてDenoでCLIツールを作ってみる:treeコマンドの実装を参考にさせていただきました。コードができあがっていく過程が見えて大変参考になりました。ここでも再帰処理を使って実装しています。

1
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
1
0