3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

pythonで2つのjsonファイルを比較する

Last updated at Posted at 2019-07-18

diffコマンドだとjsonの比較には微妙

いろいろな設定情報をjsonで保管することが多くなったので、これらjsonファイルの差分を出したいのだが、pprint等で整形したものを比較しても、一番深い層の差分だけが検出されてしまい、構造がよく分からない。

以下、pprintで整形出力したjsonデータをdiff/sdiffした結果。
一応はdiffできるのだが、どのような構造なのか一見して分からない。
(sdiffの方を見れば分かるように「"key15-1":"testdata15-1abc"」は"key15"以下のデータなのだが、diff結果からは、その構造が判断できない。)

sdiffの方であればなんとか構造を把握できるが、大量のデータのうち一部が異なっている場合にsdiffでチェックするのは大変。
ついでに整形時のカンマ(,)位置の都合上、差分では無いはずの"testdata19-6"が差分として検出されてしまう。

[root]# diff test01.json test02.json
4c4
< "key03":"testdata03",
---
> "key03":"testdata03abc",
7c7
< "key06":3456789012,
---
> "key06":3456789012000,
10c10
< "key09":3.45678912,
---
> "key09":3.45678912001,
13c13
< "key12":true,
---
> "key12":false,
15c15
<   "testdata14-1",
---
>   "testdata14-1abc",
20c20
<     "key14-3a-3":"testdata14-3a-3"},
---
>     "key14-3a-3":"testdata14-3a-3abc"},
27c27
<   "key15-1":"testdata15-1",
---
>   "key15-1":"testdata15-1abc",
32c32
<     "key15-3a-3":"testdata15-3a-3"},
---
>     "key15-3a-3":"testdata15-3a-3abc"},
38,39c38
< "key17":1000000000,
< "key18":2000000000,
---
> "key16":"testdata16",
43,45d41
<   "testdata19-A01",
<   "testdata19-A02",
<   "testdata19-A03",
48c44,47
<   "testdata19-A04",
---
>   "testdata19-B01",
>   "testdata19-B02",
>   "testdata19-B03",
>   "testdata19-B04",
50,52c49
<   "testdata19-6",
<   "testdata19-A05",
<   "testdata19-A06"
---
>   "testdata19-6"
[root]# sdiff test01.json test02.json
{                                                               {
"key01":"testdata01",                                           "key01":"testdata01",
"key02":"testdata02",                                           "key02":"testdata02",
"key03":"testdata03",                                         | "key03":"testdata03abc",
"key04":1234567890,                                             "key04":1234567890,
"key05":2345678901,                                             "key05":2345678901,
"key06":3456789012,                                           | "key06":3456789012000,
"key07":1.23456789,                                             "key07":1.23456789,
"key08":2.34567891,                                             "key08":2.34567891,
"key09":3.45678912,                                           | "key09":3.45678912001,
"key10":true,                                                   "key10":true,
"key11":false,                                                  "key11":false,
"key12":true,                                                 | "key12":false,
"key14":[                                                       "key14":[
  "testdata14-1",                                             |   "testdata14-1abc",
  "testdata14-2",                                                 "testdata14-2",
  {"key14-3a":{                                                   {"key14-3a":{
    "key14-3a-1":"testdata14-3a-1",                                 "key14-3a-1":"testdata14-3a-1",
    "key14-3a-2":"testdata14-3a-2",                                 "key14-3a-2":"testdata14-3a-2",
    "key14-3a-3":"testdata14-3a-3"},                          |     "key14-3a-3":"testdata14-3a-3abc"},
  "key14-3b":{                                                    "key14-3b":{
    "key14-3b-1":"testdata14-3b-1",                                 "key14-3b-1":"testdata14-3b-1",
    "key14-3b-2":"testdata14-3b-2",                                 "key14-3b-2":"testdata14-3b-2",
    "key14-3b-3":"testdata14-3b-3"}}                                "key14-3b-3":"testdata14-3b-3"}}
],                                                              ],
"key15":{                                                       "key15":{
  "key15-1":"testdata15-1",                                   |   "key15-1":"testdata15-1abc",
  "key15-2":"testdata15-2",                                       "key15-2":"testdata15-2",
  "key15-3":{"key15-3a":{                                         "key15-3":{"key15-3a":{
    "key15-3a-1":"testdata15-3a-1",                                 "key15-3a-1":"testdata15-3a-1",
    "key15-3a-2":"testdata15-3a-2",                                 "key15-3a-2":"testdata15-3a-2",
    "key15-3a-3":"testdata15-3a-3"},                          |     "key15-3a-3":"testdata15-3a-3abc"},
   "key15-3b":{                                                    "key15-3b":{
    "key15-3b-1":"testdata15-3b-1",                                 "key15-3b-1":"testdata15-3b-1",
    "key15-3b-2":"testdata15-3b-2",                                 "key15-3b-2":"testdata15-3b-2",
    "key15-3b-3":"testdata15-3b-3"}}                                "key15-3b-3":"testdata15-3b-3"}}
},                                                              },
"key17":1000000000,                                           | "key16":"testdata16",
"key18":2000000000,                                           <
"key19":[                                                       "key19":[
  "testdata19-1",                                                 "testdata19-1",
  "testdata19-2",                                                 "testdata19-2",
  "testdata19-A01",                                           <
  "testdata19-A02",                                           <
  "testdata19-A03",                                           <
  "testdata19-3",                                                 "testdata19-3",
  "testdata19-4",                                                 "testdata19-4",
  "testdata19-A04",                                           |   "testdata19-B01",
                                                              >   "testdata19-B02",
                                                              >   "testdata19-B03",
                                                              >   "testdata19-B04",
  "testdata19-5",                                                 "testdata19-5",
  "testdata19-6",                                             |   "testdata19-6"
  "testdata19-A05",                                           <
  "testdata19-A06"                                            <
]                                                               ]
}                                                               }

[root]#

上記のような差異から、構造が分かるように差分を抜き出す方法を考える。

調べてみたところjson patchを使って、一方のjsonからもう一方のjsonへの差分をpatchとして生成するようなものもあるが、もっとシンプルに変更差分を格納したlistで出力させたい。

jsonファイルの前提と比較方法

自分が作っているjsonファイルは以下のルールで作成しているので、ひとまずそれを前提に考える。
①json内には、辞書型(Object)、リスト(Array)、int、String、float、bool値のいずれか型のデータのみ収納している
②辞書型の特定のキー内には、必ず同じ型のデータを格納している
(同じキーに格納されているデータが、片方のファイルではint、もう片方ではstrということは無い)
③リストの中身は、その順番についても意味のあるデータとして扱う
(たとえば["sda","sdb","sdc","sdd"]と["sdc","sdb","sdd","sda"]の2つは、入っているデータは同じものだが順番が異なるため別のデータと考える)

これを以下の方法で比較してみる。
・辞書型のデータは再帰的に潜って比較。辞書型以外にぶつかるまで潜り続ける。
 変更差分が、どのようなキーを潜った先のデータなのか分かるように出力する
・int,str,float,bool値の場合は単純に比較
・リスト型は比較前にsortせず、再帰的にも潜らない。
 一番浅いリスト型の各要素を比較し、その差分を出力する
 (リスト内リストの一部に差異がある場合とか、何を元に「一部が異なるだけの同じデータ」と判断するか、汎用化しようとすると頭痛くなるので。)

差分を出力するスクリプト

2つのjsonファイルのファイル名を引数として、比較結果を出力するスクリプトを作成する。
スクリプトから実行する場合、3つ目の引数として「print」と入れると、標準出力に差分をpprintする

[root]# cat json_diff.py
import sys , json , pprint

def list_diff(list1,list2):
  offset=0 #比較している場所のズレ。片方の途中にデータ挿入されていると、比較先のアドレスがズレるので
  skip=0 #list2をforで回すが、list2の次の行がすでに差分と分かっている場合はskipする
  diff_list1=[]  #list1側のみに存在する行、list2側のみに存在する行を差分としてそれぞれ格納
  diff_list2=[]
  #print(list1,list2)
  for count in range(len(list2)):
    #print("count=",count,"  offset=",offset,"   skip=",skip)
    if skip > 0:
      skip=skip-1
      continue
    if len(list1)-1 < count+offset:  #list2は要素が残っているがlist1が終了
      diff_list2=diff_list2+list2[count:]  #list2の残りの要素を差分とする
      break
    if list2[count] == list1[count+offset]:
      continue
    for check_offset in range(1,max(len(list1),len(list2))-count-offset):  # 差異がある場合、それぞれの値と、相方のより後ろの値とを比較
      try:
        if list2[count+check_offset] == list1[count+offset]:   # list2のcheck_offset後と一致した場合
          diff_list2=diff_list2+list2[count:count+check_offset]
          offset=offset-check_offset
          skip=check_offset
          break
      except:pass
      try:
        if list2[count] == list1[count+offset+check_offset]:   # list1のcheck_offset後と一致した場合
          diff_list1=diff_list1+list1[count+offset:count+offset+check_offset]
          offset=offset+check_offset
          break
      except:pass
    else: #breakされなかった場合:list1[count+offset],list2[count]のいずれも、ほかのlist1,list2の値と一致しなかった場合
      diff_list1=diff_list1+[list1[count+offset]]
      diff_list2=diff_list2+[list2[count]]

  return {">":diff_list1+list1[len(list2)+offset:],"<":diff_list2}


def dict_diff(dict1,dict2):
  outdata={}
  for ele1 in dict2:
    if ele1 not in dict1:  #dict2側に存在するがdict1側に存在しないデータ(key)
      if "<" not in outdata : outdata["<"]={}
      outdata["<"].update({ele1:dict2[ele1]})
    elif dict2[ele1] == dict1[ele1]:
      pass
    elif type(dict2[ele1]) is str or type(dict2[ele1]) is int or type(dict2[ele1]) is float or type(dict2[ele1]) is bool:
      outdata[ele1]={"<":dict2[ele1],">":dict1[ele1]}
    elif type(dict2[ele1]) is list:
      outdata[ele1]=list_diff(dict1[ele1],dict2[ele1])
    elif (type(dict2[ele1]) is dict) and (dict_diff(dict1[ele1],dict2[ele1]) != []): #dict_diffを2回実行は無駄かな?
      outdata[ele1]=dict_diff(dict1[ele1],dict2[ele1])
    else:
      outdata[ele1]="#####type error"

  for ele2 in dict1:  #dict1側に存在するがdict2側に存在しないデータ(key)を抽出
    if ele2 not in dict2:
      if ">" not in outdata : outdata[">"]={}
      outdata[">"].update({ele2:dict1[ele2]})

  return outdata


def json_diff(file1,file2):

  jsondata1 = json.load(open(file1, 'r'))
  jsondata2 = json.load(open(file2, 'r'))

  return dict_diff(jsondata1,jsondata2)


def main(file1="",file2=""):
  args = sys.argv
  if len(args) != 1 and len(args) != 3 and len(args) != 4: print("引数エラー")  #コマンドから実行した場合
  elif len(args) == 3 or len(args) == 4 :
    file1=args[1]
    file2=args[2]

  elif file1=="" or file2=="":
    print("2つの引数が必要です")  #モジュールとして呼び出した場合

  if len(args) == 4 and args[3]=="print":  #3番目の引数に"print"を指定すると、returnする前に標準出力にpprintする
    pprint.pprint(json_diff(file1,file2))
  return json_diff(file1,file2)

if __name__=='__main__':
    main()
[root]#

※2019/7/23 片方のjsonにしか存在しないキーに対する差分情報が、紛らわしい出力だったため修正

main()は、下記例のようにインタプリタから直接呼び出す場合用。
importして外部から呼び出す場合は、json_diff.json_diff(file1,file2)の方を使った方が良い。(使い方次第ではsys.argvが悪さするので)

上記スクリプトにより、先のjsonを比較してみた結果は以下。

[root]# python3.6 ./json_diff.py ./test01.json ./test02.json print
{'<': {'key16': 'testdata16'},
 '>': {'key17': 1000000000, 'key18': 2000000000},
 'key03': {'<': 'testdata03abc', '>': 'testdata03'},
 'key06': {'<': 3456789012000, '>': 3456789012},
 'key09': {'<': 3.45678912001, '>': 3.45678912},
 'key12': {'<': False, '>': True},
 'key14': {'<': ['testdata14-1abc',
                 {'key14-3a': {'key14-3a-1': 'testdata14-3a-1',
                               'key14-3a-2': 'testdata14-3a-2',
                               'key14-3a-3': 'testdata14-3a-3abc'},
                  'key14-3b': {'key14-3b-1': 'testdata14-3b-1',
                               'key14-3b-2': 'testdata14-3b-2',
                               'key14-3b-3': 'testdata14-3b-3'}}],
           '>': ['testdata14-1',
                 {'key14-3a': {'key14-3a-1': 'testdata14-3a-1',
                               'key14-3a-2': 'testdata14-3a-2',
                               'key14-3a-3': 'testdata14-3a-3'},
                  'key14-3b': {'key14-3b-1': 'testdata14-3b-1',
                               'key14-3b-2': 'testdata14-3b-2',
                               'key14-3b-3': 'testdata14-3b-3'}}]},
 'key15': {'key15-1': {'<': 'testdata15-1abc', '>': 'testdata15-1'},
           'key15-3': {'key15-3a': {'key15-3a-3': {'<': 'testdata15-3a-3abc',
                                                   '>': 'testdata15-3a-3'}}}},
 'key19': {'<': ['testdata19-B01',
                 'testdata19-B02',
                 'testdata19-B03',
                 'testdata19-B04'],
           '>': ['testdata19-A01',
                 'testdata19-A02',
                 'testdata19-A03',
                 'testdata19-A04',
                 'testdata19-A05',
                 'testdata19-A06']}}
[root]#

変更差分がどのキーを潜った場所なのか、なんとなく分かるようになった。
「<」と「>」とで、どちらのjsonファイルにのみ存在するデータ差分なのか分かるようにした。
同じキーの中身が違う場合は{'キー': {'<': 'file1のデータ', '>': 'file2のデータ'}}のように出力
片方のファイルにしか存在しないキーについては{'キー': {'<': 'file1のみ存在するデータ'}}のように出力
リスト型を再帰的に潜らない仕様にしたため、key14のデータなどが、最上位のlistの要素単位でまとめて差分になっているが妥協する。

仕様外動作確認

以下の前提を外したデータに対して動作を確認する

②辞書型の特定のキー内には、必ず同じ型のデータを格納している
(同じキーに格納されているデータが、片方のファイルではint、もう片方ではstrということは無い)
③リストの中身は、その順番についても意味のあるデータとして扱う。
(たとえば["sda","sdb","sdc","sdd"]と["sdc","sdb","sdd","sda"]の2つは、入っているデータは同じものだが順番が異なるため別のデータと考える)

以下、の2つのjsonを比較してみる。
"key03"の中身はデータ型をflotとstrとで変えている。
"key04"の中身のリストは全く同じ要素だが、順番を並び替えている。ついでに重複データも含まれる。

[root]# sdiff test03.json test04.json
{                                                               {
"key01":"testdata01",                                           "key01":"testdata01",
"key02":1234567890,                                             "key02":1234567890,
"key03":1.23456789,                                           | "key03":"hogemoga",
"key04":[                                                       "key04":[
  1024,                                                       <
  "testdata4-1",                                              <
  "testdata4-1",                                                  "testdata4-1",
                                                              >   ["testdat4-3-1","testdat4-3-2"],
  "testdata4-2",                                                  "testdata4-2",
                                                              >   1024,
                                                              >   ["testdat4-4-1","testdat4-4-2"],
  "testdata4-2",                                                  "testdata4-2",
  ["testdat4-3-1","testdat4-3-2"],                            |   "testdata4-1"
  ["testdat4-4-1","testdat4-4-2"]                             <
]                                                               ]
}                                                               }

[root]#

これを先のスクリプトで比較すると

[root]# python3.6 ./json_diff.py ./test03.json ./test04.json print
{'key03': {'<': 'hogemoga', '>': 1.23456789},
 'key04': {'<': ['testdata4-2',
                 1024,
                 ['testdat4-4-1', 'testdat4-4-2'],
                 'testdata4-2',
                 'testdata4-1'],
           '>': [1024,
                 'testdata4-1',
                 'testdata4-2',
                 'testdata4-2',
                 ['testdat4-4-1', 'testdat4-4-2']]}}
[root]#

"key03"は別の型でも比較できている様子。値の比較前にtype()=type()で型チェックする必要があるかと思ったが、別の型のデータを==で比較してもエラーにはならないのか。

"key04"は、test04.jsonの1つ目の'testdata4-1'が、test03.jsonの2つ目の値と一致。
次にtest04.jsonの2つ目の["testdat4-3-1","testdat4-3-2"]が、test03.jsonの6番目の値と一致。
まで見た時点で全データ比較が完了し、残りが差分として出力された。
仕様どおりではあるが、これは分かりづらい。
今回の例のような、listの中身の順番が整列していないデータには、この比較方法は向いていない。
リストの中身の順番が意味を持たないなら、list_diff()を呼び出す前にlistをsortしておけば良さそう。

判明している問題点

先のlistの問題以外に思いつくところとして、

キー"<",">"の辞書型として差異を出力しているが、仮に"<",">"といったキーの辞書データを含むjsonファイルの場合、出力が読み取りづらくなってしまう。

今回のスクリプトを使う場合、"<",">"キーが現れた時点で、そこから下は差分を表すものとして解釈することになるため、辞書を潜る"<",">"キーが存在すると読み込み時にバグを引き起こすことになる。

差分の出力を表形式に整形するスクリプト

先のスクリプトの出力を使って、さらに以下の形式で整形する。

[
[差分位置(辞書を潜ったキーのリスト) , jsonfile1側のデータ , jsonfile2側のデータ ],
[差分位置(辞書を潜ったキーのリスト) , jsonfile1側のデータ , jsonfile2側のデータ ],
[差分位置(辞書を潜ったキーのリスト) , jsonfile1側のデータ , jsonfile2側のデータ ],



]

後でexcel等に整理するなら、この方が便利そうなので。

[root]# cat create_diff_table.py
import sys
import json_diff
import pprint

def create_diff_table(file1="",file2=""):

  difflist=[]
  def dict_check(diffs,keylist):
    #print("debug:",diffs,"  keylist:",keylist)
    if "<" in diffs and ">" in diffs:
      difflist.append([keylist,diffs[">"],diffs["<"]])
    elif "<" in diffs:
      difflist.append([keylist,"",diffs["<"]])
    elif ">" in diffs:
      difflist.append([keylist,diffs[">"],""])
    for diff in diffs:
      if diff == "<" or diff == ">": continue
      if type(diffs[diff]) is dict: dict_check(diffs[diff],keylist+[diff])

  #pprint.pprint(json_diff.main(file1,file2))
  dict_check(json_diff.json_diff(file1,file2),[])

  return difflist

def main(file1="",file2=""):

  args = sys.argv
  if len(args) != 1 and len(args) != 3 and len(args) != 4: print("引数エラー")  #コマンドから実行した場合
  elif len(args) == 3 or len(args) == 4 :
    file1=args[1]
    file2=args[2]
  elif file1=="" or file2=="":
    print("2つの引数が必要です")  #モジュールとして呼び出した場合

  if len(args) == 4 and args[3]=="print":  #3番目の引数に"print"を指定すると、returnする前に標準出力にpprintする
    pprint.pprint(create_diff_table(file1,file2))

  return create_diff_table(file1,file2)

if __name__=='__main__':
    main()
[root]#

これで、最初にsdiffした test01.jsonとtest02.jsonを比較すると

[root]# python3.6 ./create_diff_table.py ./test01.json ./test02.json print
[[[], {'key17': 1000000000, 'key18': 2000000000}, {'key16': 'testdata16'}],
 [['key03'], 'testdata03', 'testdata03abc'],
 [['key06'], 3456789012, 3456789012000],
 [['key09'], 3.45678912, 3.45678912001],
 [['key12'], True, False],
 [['key14'],
  ['testdata14-1',
   {'key14-3a': {'key14-3a-1': 'testdata14-3a-1',
                 'key14-3a-2': 'testdata14-3a-2',
                 'key14-3a-3': 'testdata14-3a-3'},
    'key14-3b': {'key14-3b-1': 'testdata14-3b-1',
                 'key14-3b-2': 'testdata14-3b-2',
                 'key14-3b-3': 'testdata14-3b-3'}}],
  ['testdata14-1abc',
   {'key14-3a': {'key14-3a-1': 'testdata14-3a-1',
                 'key14-3a-2': 'testdata14-3a-2',
                 'key14-3a-3': 'testdata14-3a-3abc'},
    'key14-3b': {'key14-3b-1': 'testdata14-3b-1',
                 'key14-3b-2': 'testdata14-3b-2',
                 'key14-3b-3': 'testdata14-3b-3'}}]],
 [['key15', 'key15-1'], 'testdata15-1', 'testdata15-1abc'],
 [['key15', 'key15-3', 'key15-3a', 'key15-3a-3'],
  'testdata15-3a-3',
  'testdata15-3a-3abc'],
 [['key19'],
  ['testdata19-A01',
   'testdata19-A02',
   'testdata19-A03',
   'testdata19-A04',
   'testdata19-A05',
   'testdata19-A06'],
  ['testdata19-B01', 'testdata19-B02', 'testdata19-B03', 'testdata19-B04']]]
[root]#

'testdata15-3a-3'と'testdata15-3a-3abc'との差異が、'key15'→'key15-3'→'key15-3a'→'key15-3a-3'の順に深く潜った先の差分であることが分かる。

単純なリスト形式になったので、たとえば以下のようなスクリプトで簡単にhtmlに整形できる

[root]# cat ./create_diff_html.py
import sys
import create_diff_table


def main(file1="",file2="",file1_title="",file2_title=""):

  args = sys.argv
  file1=args[1]
  file2=args[2]
  file1_title=args[3]
  file2_title=args[4]

  with open('diff_test.html',mode='w') as f:
    f.write("<html><head><title>テスト08</title></head><body bgcolor=white>")

    f.write("<table border>")
    f.write("<tr><th>item</th><th>"+file1_title+"</th><th>"+file2_title+"</th></tr>")
    for diff in create_diff_table.create_diff_table(file1,file2):
      f.write("<tr><td>"+str(diff[0]).replace("'","").replace("[","").replace("]","").replace(","," -> ")+"</td><td>"+str(diff[1])+"</td><td>"+str(diff[2])+"</td></tr>")

    f.write("</table>")

    f.write("<br></body></html>")

if __name__=='__main__':
    main()
[root]#
[root]# python3.6 ./create_diff_html.py ./test01.json ./test02.json new old
[root]# cat diff_test.html
<html><head><title>テスト08</title></head><body bgcolor=white><table border><tr><th>item</th><th>new</th><th>old</th></tr><tr><td></td><td>{'key17': 1000000000, 'key18': 2000000000}</td><td>{'key16': 'testdata16'}</td></tr><tr><td>key03</td><td>testdata03</td><td>testdata03abc</td></tr><tr><td>key06</td><td>3456789012</td><td>3456789012000</td></tr><tr><td>key09</td><td>3.45678912</td><td>3.45678912001</td></tr><tr><td>key12</td><td>True</td><td>False</td></tr><tr><td>key14</td><td>['testdata14-1', {'key14-3a': {'key14-3a-1': 'testdata14-3a-1', 'key14-3a-2': 'testdata14-3a-2', 'key14-3a-3': 'testdata14-3a-3'}, 'key14-3b': {'key14-3b-1': 'testdata14-3b-1', 'key14-3b-2': 'testdata14-3b-2', 'key14-3b-3': 'testdata14-3b-3'}}]</td><td>['testdata14-1abc', {'key14-3a': {'key14-3a-1': 'testdata14-3a-1', 'key14-3a-2': 'testdata14-3a-2', 'key14-3a-3': 'testdata14-3a-3abc'}, 'key14-3b': {'key14-3b-1': 'testdata14-3b-1', 'key14-3b-2': 'testdata14-3b-2', 'key14-3b-3': 'testdata14-3b-3'}}]</td></tr><tr><td>key15 ->  key15-1</td><td>testdata15-1</td><td>testdata15-1abc</td></tr><tr><td>key15 ->  key15-3 ->  key15-3a ->  key15-3a-3</td><td>testdata15-3a-3</td><td>testdata15-3a-3abc</td></tr><tr><td>key19</td><td>['testdata19-A01', 'testdata19-A02', 'testdata19-A03', 'testdata19-A04', 'testdata19-A05', 'testdata19-A06']</td><td>['testdata19-B01', 'testdata19-B02', 'testdata19-B03', 'testdata19-B04']</td></tr></table><br></body></html>[root]#
3
2
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
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?