json เป็นรูปแบบการเก็บข้อมูลที่นิยมใช้ใน javascript มีลักษณะคล้ายคลึงกับดิกชันนารีและลิสต์ในภาษาไพธอน
ในไพธอนมีมอดูลชื่อ json เอาไว้สำหรับอ่านเขียนไฟล์ชนิด json โดยเฉพาะ
มอดูลนี้ใช้ไม่ยาก หลักๆก็มีอยู่ ๔ คำสั่งคือ dump, dumps, load, loads
json.dump(obj,fp) |
เขียน obj เป็นข้อความ json ลงไฟล์ fp |
s = json.dumps(obj) |
แปลง obj เป็นข้อความ json |
obj = json.load(fp) |
อ่านไฟล์ fp เป็น json |
obj = json.loads(s) |
แปลงข้อความ json เป็นออบเจ็กต์ |
ในที่นี้ fp คือตัวไฟล์ json ที่เปิดขึ้นมาด้วยคำสั่ง open
ส่วน s เป็นสายอักขระที่เก็บข้อความ json ไว้
ส่วน obj คือออบเจ็กต์ของไพธอนที่ต้องการแปลงหรือแปลงมาจาก json
โดยรวมแล้วคือที่มี s ต่อท้ายจะเอาไว้แปลงระหว่างออบเจ็กต์กับสายอักขระ แต่ที่ไม่มี s จะไว้แปลงระหว่างออบเจ็กต์กับไฟล์
load ตรงข้ามกับ dump โดย load เป็นการแปลงจาก json เป็นออบเจ็กต์ในไพธอน ส่วน dump จะแปลงออบเจ็กต์เป็น json
การแปลงออบเจ็กต์เป็น json ถ้ามีดิกชันนารีของลิสต์และทูเพิลอยู่ ถ้าใช้คำสั่ง dumps ก็จะได้เป็นสายอักขระ
import json
ab = {'a':[0,1],"b":(2,3)}
print(ab) # ได้ {'a': [0, 1], 'b': (2, 3)}
print(json.dumps(ab)) # ได้ {"a": [0, 1], "b": [2, 3]}
แต่ถ้าต้องการจะเขียนลงในไฟล์เลยก็ใช้ dump
with open('ab.json','w') as f:
json.dump(ab,f)
แบบนี้ก็จะได้ไฟล์ ab.json เป็นไฟล์ json ที่เก็บข้อมูลออบเจ็กต์นั้นบันทึกลงในเครื่อง
จะเห็นว่าข้อความ json ที่ได้นั้นเป็นลักษณะโครงสร้างที่มีลักษณะคล้ายในไพธอน พอแปลงไปแล้วจึงไม่ได้เห็นว่าจะมีอะไรต่างไปจากเดิมนัก แต่ก็มีความแตกต่างให้เห็นได้บางส่วน
เช่น json จะใช้เครื่องหมายคำพูดคร่อมสายอักขระเป็น
" "
แต่ไพธอนจะใช้
' '
เป็นหลัก ถึงเวลาเขียนจริงๆจะใช้
' '
หรือ
" "
ก็ได้เหมือนกัน แต่เวลาแสดงผลในไพธอนโดยมาตรฐานแล้วจะใช้
' '
และ json ไม่มีการแยกลิสต์กับทูเพิลเหมือนอย่างในไพธอน ดังนั้นจะถูกแปลงเป็นลิสต์ (ใน json เรียกว่า อาเรย์)
ออบเจ็กต์ชนิดต่างๆในไพธอนจะถูกแปลงเป็น json ในลักษณะดังนี้
dict > object
list,tuple > array
str > string
int,float > number
True > true
False > false
None > null
เพียงแต่ว่าในส่วนของคีย์ของดิกชันนารีนั้นใน json จะเป็นสายอักขระเสมอ ดังนั้นแม้จะใส่เป็นตัวเลขไปก็จะถูกแปลงเป็นชนิดสายอักขระ (คือมีเครื่องหมายคำพูดคร่อม)
ตัวอย่าง
ab = [{1:2.3,'a':('b','c')},None,False,True]
print(json.dumps(ab)) # ได้ [{"1": 2.3, "a": ["b", "c"]}, null, false, true]
ถ้าหากใส่ออบเจ็กต์ชนิดนอกเหนือจากที่กำหนดมานี้ลงไปก็จะแปลงไม่ได้
เช่น เซ็ต
print(json.dumps({2,1})) # ได้ TypeError: Object of type set is not JSON serializable
สามารถแก้ได้โดยเติมตัวเลือกเสริม default ลงไป เพื่อบอกว่าจะใช้ฟังก์ชันอะไรมาจัดการกับตัวแปรที่ไม่สามารถแปลงได้ปกติ
เช่นถ้าต้องการให้เมื่อเจอเซ็ตก็จะถูกเปลี่ยนเป็นลิสต์หรือสายอักขระหรือเป็นอะไรบางอย่างที่จะเลี่ยงข้อผิดพลาดได้ก่อนที่จะแปลงเป็น json อีกที
print(json.dumps([{1,2},2],default=list)) # ได้ [[1, 2], 2]
print(json.dumps([{1,2},2],default=str)) # ได้ ["{1, 2}", 2]
print(json.dumps([{1,2},2],default=sum)) # ได้ [3, 2]
print(json.dumps([{1,2},2],default=lambda x: '@'.join([str(s) for s in x]))) # ได้ ["1@2", 2]
นอกจากนี้ คีย์ของดิกชันนารีในไพธอนสามารถใช้เป็นทูเพิลได้ แต่สำหรับ json จะต้องเป็นพวกตัวแปรเดี่ยว จะเป็นทูเพิลไม่ได้
ถ้าใช้คีย์เป็นทูเพิลแล้วพอแปลงเป็น json จะเกิดข้อผิดพลาดขึ้น เช่น
print(json.dumps({(1,2):1})) # ได้ TypeError: keys must be str, int, float, bool or None, not tuple
เพียงแต่ว่าหากใส่ตัวเลือกเสริม skipkeys=True ลงไปจะไม่เกิดข้อผิดพลาด แต่คีย์เวิร์ดที่เป็นทูเพิลจะถูกข้ามไป เช่น
print(json.dumps({(1,2):1,3:4},skipkeys=True)) # ได้ {"3": 4}
นอกจากนี้ ในไพธอนมีจำนวนที่เป็น nan, inf หรือ -inf พวกนี้ถ้าใส่ใน json จะกลายเป็น NaN, Infinity, -Infinity
a = float('-inf')
b = float('nan')
c = float('inf')
print([a,b,c]) # ได้ [-inf, nan, inf]
print(json.dumps([a,b,c])) # ได้ [-Infinity, NaN, Infinity]
แต่ถ้าหากใส่ตัวเลือก allow_nan=False ลงไปเมื่อมีจำนวนพวกนี้จะเกิดข้อผิดพลาดขึ้นมา
print(json.dumps([a,b,c],allow_nan=False)) # ได้ ValueError: Out of range float values are not JSON compliant
ดิกชันนารีในไพธอนก่อนเวอร์ชัน 3.6 จะไม่สนลำดับของข้อมูลที่ใส่เข้าไป ดังนั้นเวลาที่แปลงเป็น json จะได้ลำดับแบบไหนก็อาจคาดเดายาก
dic = {}
dic['d'] = 1
dic['i'] = 2
dic['c'] = 3
print(dic) # ได้ {'i': 2, 'c': 3, 'd': 1}
print(json.dumps(dic)) # ได้ {"i": 2, "c": 3, "d": 1}
เพื่อที่จะให้เรียงลำดับตามที่ต้องการอาจต้องใช้ OrderedDict แทน dict ธรรมดา
from collections import OrderedDict
dic = OrderedDict()
dic['d'] = 1
dic['i'] = 2
dic['c'] = 3
print(dic) # ได้ OrderedDict([('d', 1), ('i', 2), ('c', 3)])
print(json.dumps(dic)) # ได้ {"d": 1, "i": 2, "c": 3}
เพียงแต่ว่าในไพธอนตั้งแต่ 3.6 เป็นต้นไปดิกชันนารีจะมีการจำลำดับ จึงไม่จำเป็นต้องใช้ OrderedDict แล้ว
หากต้องการให้มีการจัดลำดับคีย์ใหม่ให้เรียงกันเป็นระเบียบก็สามารถใส่ตัวเลือกเสริม sort_keys=True
dic = {'d':1,'i':2,'c':3}
print(json.dumps(dic,sort_keys=True)) # ได้ {"c": 3, "d": 1, "i": 2}
หากต้องการให้ข้อความ json ที่ได้นั้นแบ่งเป็นหลายบรรทัดแทนที่จะเป็นบรรทัดเดียวต่อกันยาวก็ให้เติมตัวเลือกเสริม indent ลงไป โดยใส่จำนวนวรรคในการร่น
เช่น
print(json.dumps({1:2,3:[4,[5,{6:7,8:9}]]},indent=4))
ได้
{
"1": 2,
"3": [
4,
[
5,
{
"6": 7,
"8": 9
}
]
]
}
ข้อมูลในดิกชันนารีปกติจะกั้นด้วย : ส่วนในลิสต์จะกั้นด้วย , โดยมีเว้นวรรคตามหนึ่งช่อง แต่หากอยากเปลี่ยนให้วรรคมากขึ้นหรือน้อยลง หรือเปลี่ยนเป็นตัวอื่นก็กำหนดได้โดยตั้งที่ตัวเลือกเสริม separators
ตัวอย่าง
print(json.dumps({1:2,3:[4,[5,{6:7,8:9}]]}))
# ได้ {"1": 2, "3": [4, [5, {"6": 7, "8": 9}]]}
print(json.dumps({1:2,3:[4,[5,{6:7,8:9}]]},separators=[',',':']))
# ได้ {"1":2,"3":[4,[5,{"6":7,"8":9}]]}
print(json.dumps({1:2,3:[4,[5,{6:7,8:9}]]},separators=[', ',': ']))
# ได้ {"1": 2, "3": [4, [5, {"6": 7, "8": 9}]]}
print(json.dumps({1:2,3:[4,[5,{6:7,8:9}]]},separators=['+ ','| ']))
# ได้ {"1"| 2+ "3"| [4+ [5+ {"6"| 7+ "8"| 9}]]}
ปัญหาอีกอย่างของคำสั่ง json คือหากเจออักษรที่ไม่ใช่ ascii จะถูกแปลงเป็นรหัส สามารถแก้ปัญหานี้ได้โดยใส่ตัวเลือกเสริม ensure_ascii=False
print(json.dumps(['ก','๑'])) # ได้ ["\u0e01", "\u0e51"]
print(json.dumps(['ก','๑'], ensure_ascii=False)) # ได้ ["ก", "๑"]
สรุปตัวเลือกทั้งหมดในการแปลงออบเจ็กต์ไปเป็น json ด้วยคำสั่ง dump หรือ dumps
ตัวเลือกเสริม |
คำอธิบาย |
ค่าตั้งต้น |
skipkeys |
เมื่อเจอคีย์ที่เป็นทูเพิลถ้าเป็น True จะข้าม ถ้าเป็น False จะขึ้น TypeError |
False |
ensure_ascii |
ถ้าเป็น True จะแปลงอักษรที่ไม่ใช่ ascii ให้เป็นรหัส |
True |
check_circular |
ถ้า True จะป้องกันการวนซ้ำไม่รู้จบที่อาจเกิดขึ้นขณะแปลง เช่น OverflowError |
True |
allow_nan |
เมื่อเจอ nan, inf, inf ถ้าเป็น True จะถูกแปลงเป็น NaN, Infinity, -Infinity ถ้า False จะขึ้น ValueError |
True |
indent |
ถ้าใส่ตัวเลขจะจัดแบ่งข้อมูลเป็นบรรทัดโดยจำนวนเคาะวรรคที่จะร่นตามที่ใส่ ถ้าใส่ None จะรวมกันอยู่บรรทัดเดียว |
None |
separators |
ตัวที่ใช้ในการคั่นลิสต์และดิกชันนารี |
[', ', ': '] |
default |
ฟังก์ชันที่จะจัดการเมื่อเจอตัวแปรที่โดยมาตรฐานแล้วไม่สามารถแปลงเป็น json ได้ |
None |
sort_keys |
จัดเรียงข้อมูลในดิกชันนารีตามคีย์ |
False |
การอ่านไฟล์ json หากมีไฟล์ json อยู่แล้วต้องการอ่านขึ้นมาเป็นออบเจ็กต์ในไพธอนก็ใช้คำสั่ง load
with open('obj.json') as f:
obj = json.load(f)
แต่ถ้ามีข้อความ json เก็บไว้ในตัวแปรอยู่แล้วหรือจะเขียนข้อความ json ลงไปโดยตรงก็ใช้คำสั่ง loads แปลงเป็นออบเจ็กต์
obj = json.loads('["a","b"]')
print(obj) # ได้ ['a', 'b']
เมื่อทกำการแปลง ข้อมูลจาก json จะถูกแปลงเป็นออบเจ็กต์ในไพธอนดังนี้
object > dict
array > list
string > str
number (int) > int
number (real) > float
true > True
false > False
null > None
ตัวอย่าง
print(json.loads('["ก",1,1.2,null,false,true,{"3":5}]')) # ได้ ['ก', 1, 1.2, None, False, True, {'3': 5}]
หากมีข้อมูลที่เป็นรหัส ซึ่งอาจเกิดจากการที่ตอนใช้คำสั่ง dumps ไม่ได้ใส่ ensure_ascii=False เวลาใช้ loads ก็จะถูกแปลงกลับเป็นตัวอักษรด้วย
print(json.dumps(['ฒ'])) # ได้ ["\u0e12"]
print(json.loads('["\u0e12"]')) # ได้ ['ฒ']
ปกติแล้วตัวเลขจำนวนเต็มจะถูกตีความแปลงเป็น int แต่สามารถใส่ตัวเลือกเสริม parse_int ลงไปเพื่อให้มีการจัดการเป็นอย่างอื่นได้
เช่นสามารถทำให้เลขจำนวนเต็มทั้งหมดเป็นเลขทศนิยมได้
print(json.loads('[1,2.1]',parse_int=float)) # ได้ [1.0, 2.1]
ในทำนองเดียวกัน ตัวเลือกเสริม parse_float เอาไว้จัดการกับ float
เช่นหากต้องการทำให้เลขทศนิยมทั้งหมดกลายเป็นจำนวนเต็ม
print(json.loads('[1,2.1]',parse_float=lambda x:int(float(x)))) # ได้ [1, 2]
ปกติ NaN, Infinity, -Infinity ก็จะถูกแปลงเป็น nan, inf, -inf
print(json.loads('[-Infinity,NaN,Infinity]')) # ได้ [-inf, nan, inf]
แต่ถ้าต้องการให้ถูกตีความต่างออกไปสามารถกำหนดตัวเลือกเสริม parse_constant ได้ เช่น
print(json.loads('[-Infinity,NaN,Infinity]',parse_constant=str)) # ได้ ['-Infinity', 'NaN', 'Infinity']
ตัวเลือกเสริม object_hook มีไว้กำหนดว่าหากเจอข้อมูลที่เป็นดิกชันนารีจะให้ใช้ฟังก์ชันอะไรจัดการเป็นพิเศษหรือเปล่า
นอกจากนี้ยังมีตัวเลือกเสริม object_pairs_hook จะคล้ายกับ object_hook แต่ต่างกันตรงที่จะเริ่มอ่านดิกชันนานีนั้นในรูปของลิสต์ของทูเพิล
object_hook กับ object_pairs_hook จะไม่ถูกใช้พร้อมกัน ถ้าใส่ทั้งคู่ object_pairs_hook จะถูกใช้ ส่วน object_hook จะถูกมองข้ามไป
ตัวอย่าง
ab = '[{"a":1,"b":2},3]'
print(json.loads(ab,object_hook=lambda x: tuple(x.items()))) # ได้ [(('a', 1), ('b', 2)), 3]
print(json.loads(ab,object_hook=str)) # ได้ ["{'a': 1, 'b': 2}", 3]
print(json.loads(ab,object_pairs_hook=str)) # ได้ ["[('a', 1), ('b', 2)]", 3]
print(json.loads(ab,object_pairs_hook=lambda x:['%s~%s'%s for s in x])) # ได้ [['a~1', 'b~2'], 3]
JSONDecoder และ JSONEncoder เมื่อใช้ dumps หรือ loads แล้วต้องการใส่ตัวเลือกเสริมจะต้องใส่ทุกครั้งที่ต้องการแปลง แต่ถ้าหากต้องการให้เวลาแปลงทุกครั้งใช้ตัวเลือกเสริมนั้นตลอดโดยที่ไม่ต้องมาคอยใส่ทุกครั้งก็อาจใช้ JSONDecoder และ JSONEncoder ช่วยแทน
สร้างออบเจ็กต์ JSONEncoder ขึ้นมาโดยใส่ตัวเลิกเสริมลงไปตอนสร้าง จากนั้นใช้คำสั่ง encode เพื่อทำงานแทน dumps ได้
je = json.JSONEncoder(ensure_ascii=False)
print(je.encode(['ก','ข'])) # ได้ ["ก", "ข"]
print(je.encode(['あ','の'])) # ได้ ["あ", "の"]
ในทำนองเดียวกัน สร้างออบเจ็กต์ JSONDecoder ขึ้นมาแล้วใช้คำสั่ง decode เพื่อทำงานแทน loads ได้
jd = json.JSONDecoder(parse_int=float)
print(jd.decode('[1,2]')) # ได้ [1.0, 2.0]
print(jd.decode('[3,4,5]')) # ได้ [3.0, 4.0, 5.0]
หรือจะใช้วิธีสร้างคลาสย่อยขึ้นแล้วป้อนลงไปในตัวเลือกเสริม cls ตอนใช้ dumps หรือ loads ก็ได้
class Je(json.JSONEncoder):
def __init__(self,**kw):
super(Je,self).__init__(ensure_ascii=False)
class Jd(json.JSONDecoder):
def __init__(self,**kw):
super(Jd,self).__init__(parse_int=float)
print(json.dumps(['ฮ'],cls=Je)) # ได้ ["ฮ"]
print(json.loads('[0]',cls=Jd)) # ได้ [0.0]
อ้างอิง