公司不是你家,領導不是你媽。本文已被 https://www.yourbatman.cn 收錄,裡面一併有Spring技術棧、MyBatis、JVM、中介軟體等小而美的專欄供以免費學習。關注公眾號【BAT的烏托邦】逐個擊破,深入掌握,拒絕淺嘗輒止。
- 前言
- 正文
- 最簡使用Demo
- JsonToken
- JsonParser的Feature
- 底層I/O流相關
- 支援非標準格式
- ALLOW_COMMENTS(false)
- ALLOW_YAML_COMMENTS(false)
- ALLOW_UNQUOTED_FIELD_NAMES(false)
- ALLOW_SINGLE_QUOTES(false)
- ALLOW_UNQUOTED_CONTROL_CHARS(false)
- ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER(false)
- ALLOW_NUMERIC_LEADING_ZEROS(false)
- ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS(false)
- ALLOW_NON_NUMERIC_NUMBERS(false)
- ALLOW_MISSING_VALUES(false)
- ALLOW_TRAILING_COMMA(false)
- 校驗相關
- 其它
- 總結
前言
各位好,我是A哥(YourBatman)。上篇文章:3. 懂了這些,方敢在簡歷上說會用Jackson寫JSON 聊完,流式API的寫部分可以認為你已完全掌握了,本文了解它讀的部分。
版本約定
- Jackson版本:
2.11.0
- Spring Framework版本:
5.2.6.RELEASE
- Spring Boot版本:
2.3.0.RELEASE
小貼士:截止到本文,本系列前面所有示例都只僅僅匯入
jackson-core
而已,後續若要新增jar包我會額外說明,否則相同
正文
什麼叫讀JSON?就是把一個JSON 字串 解析為物件or樹模型嘛,因此也稱作解析JSON串。Jackson底層流式API使用JsonParser
來完成JSON字串的解析。
最簡使用Demo
準備一個POJO:
@Data
public class Person {
private String name;
private Integer age;
}
測試用例:把一個JSON字串繫結(封裝)進一個POJO物件裡
@Test
public void test1() throws IOException {
String jsonStr = "{\"name\":\"YourBatman\",\"age\":18}";
Person person = new Person();
JsonFactory factory = new JsonFactory();
try (JsonParser jsonParser = factory.createParser(jsonStr)) {
// 只要還沒結束"}",就一直讀
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
String fieldname = jsonParser.getCurrentName();
if ("name".equals(fieldname)) {
jsonParser.nextToken();
person.setName(jsonParser.getText());
} else if ("age".equals(fieldname)) {
jsonParser.nextToken();
person.setAge(jsonParser.getIntValue());
}
}
System.out.println(person);
}
}
執行程式,輸出:
Person(name=YourBatman, age=18)
成功把一個JSON字串的值解析到Person物件。你可能會疑問,怎麼這麼麻煩?那當然,這是底層流式API,純手動檔嘛。你獲得了效能,可不要失去一些便捷性嘛。
小貼士:底層流式API一般面向“專業人士”,應用級開發使用高階API
ObjectMapper
即可。當然,讀完本系列就能讓你完全具備“專業人士”的實力?
JsonParser
針對不同的value型別,提供了非常多的方法用於實際值的獲取。
直接值獲取:
// 獲取字串型別
public abstract String getText() throws IOException;
// 數字Number型別值 標量值(支援的Number型別參照NumberType列舉)
public abstract Number getNumberValue() throws IOException;
public enum NumberType {
INT, LONG, BIG_INTEGER, FLOAT, DOUBLE, BIG_DECIMAL
};
public abstract int getIntValue() throws IOException;
public abstract long getLongValue() throws IOException;
...
public abstract byte[] getBinaryValue(Base64Variant bv) throws IOException;
這類方法可能會丟擲異常:比如value值本不是數字但你呼叫了getInValue()方法~
小貼士:如果value值是null,像getIntValue()、getBooleanValue()等這種直接獲取方法是會丟擲異常的,但getText()不會
帶預設值的值獲取,具有更好安全性:
public String getValueAsString() throws IOException {
return getValueAsString(null);
}
public abstract String getValueAsString(String def) throws IOException;
...
public long getValueAsLong() throws IOException {
return getValueAsLong(0);
}
public abstract long getValueAsLong(long def) throws IOException;
...
此類方法若碰到資料的轉換失敗時,不會丟擲異常,把def
作為預設值返回。
組合方法
同JsonGenerator
一樣,JsonParser也提供了高鈣片組合方法,讓你更加便捷的使用。
自動繫結
聽起來像高階功能,是的,它必須依賴於ObjectCodec
去實現,因為實際是全部委託給了它去完成的,也就是我們最為熟悉的readXXX系列方法:
我們知道,ObjectMapper就是一個ObjectCodec,它屬於高階API,本文顯然不會用到ObjectMapper它嘍,因此我們自己手敲一個實現來完成此功能。
自定義一個ObjectCodec,Person類專用:用於把JSON串自動繫結到例項屬性。
public class PersonObjectCodec extends ObjectCodec {
...
@SneakyThrows
@Override
public <T> T readValue(JsonParser jsonParser, Class<T> valueType) throws IOException {
Person person = (Person) valueType.newInstance();
// 只要還沒結束"}",就一直讀
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
String fieldname = jsonParser.getCurrentName();
if ("name".equals(fieldname)) {
jsonParser.nextToken();
person.setName(jsonParser.getText());
} else if ("age".equals(fieldname)) {
jsonParser.nextToken();
person.setAge(jsonParser.getIntValue());
}
}
return (T) person;
}
...
}
有了它,就可以實現我們的自動繫結了,書寫測試用例:
@Test
public void test3() throws IOException {
String jsonStr = "{\"name\":\"YourBatman\",\"age\":18, \"pickName\":null}";
JsonFactory factory = new JsonFactory();
try (JsonParser jsonParser = factory.createParser(jsonStr)) {
jsonParser.setCodec(new PersonObjectCodec());
System.out.println(jsonParser.readValueAs(Person.class));
}
}
執行程式,輸出:
Person(name=YourBatman, age=18)
這就是ObjectMapper自動繫結的核心原理所在,其它更為強大能力將在後續章節詳細展開。
JsonToken
在上例解析過程中,有一個非常重要的角色,那便是:JsonToken。它表示解析JSON內容時,用於返回結果的基本標記型別的列舉。
public enum JsonToken {
NOT_AVAILABLE(null, JsonTokenId.ID_NOT_AVAILABLE),
START_OBJECT("{", JsonTokenId.ID_START_OBJECT),
END_OBJECT("}", JsonTokenId.ID_END_OBJECT),
START_ARRAY("[", JsonTokenId.ID_START_ARRAY),
END_ARRAY("]", JsonTokenId.ID_END_ARRAY),
// 屬性名(key)
FIELD_NAME(null, JsonTokenId.ID_FIELD_NAME),
// 值(value)
VALUE_EMBEDDED_OBJECT(null, JsonTokenId.ID_EMBEDDED_OBJECT),
VALUE_STRING(null, JsonTokenId.ID_STRING),
VALUE_NUMBER_INT(null, JsonTokenId.ID_NUMBER_INT),
VALUE_NUMBER_FLOAT(null, JsonTokenId.ID_NUMBER_FLOAT),
VALUE_TRUE("true", JsonTokenId.ID_TRUE),
VALUE_FALSE("false", JsonTokenId.ID_FALSE),
VALUE_NULL("null", JsonTokenId.ID_NULL),
}
為了輔助理解,A哥用一個例子,輸出各個部分一目瞭然:
@Test
public void test2() throws IOException {
String jsonStr = "{\"name\":\"YourBatman\",\"age\":18, \"pickName\":null}";
System.out.println(jsonStr);
JsonFactory factory = new JsonFactory();
try (JsonParser jsonParser = factory.createParser(jsonStr)) {
while (true) {
JsonToken token = jsonParser.nextToken();
System.out.println(token + " -> 值為:" + jsonParser.getValueAsString());
if (token == JsonToken.END_OBJECT) {
break;
}
}
}
}
執行程式,輸出:
{"name":"YourBatman","age":18, "pickName":null}
START_OBJECT -> 值為:null
FIELD_NAME -> 值為:name
VALUE_STRING -> 值為:YourBatman
FIELD_NAME -> 值為:age
VALUE_NUMBER_INT -> 值為:18
FIELD_NAME -> 值為:pickName
VALUE_NULL -> 值為:null
END_OBJECT -> 值為:null
從左至右解析,一一對應。各個部分用下面這張圖可以簡略表示出來:
小貼士:解析時請確保你的的JSON串是合法的,否則丟擲
JsonParseException
異常
JsonParser的Feature
它是JsonParser的一個內部列舉類,共15個列舉值:
public enum Feature {
AUTO_CLOSE_SOURCE(true),
ALLOW_COMMENTS(false),
ALLOW_YAML_COMMENTS(false),
ALLOW_UNQUOTED_FIELD_NAMES(false),
ALLOW_SINGLE_QUOTES(false),
@Deprecated
ALLOW_UNQUOTED_CONTROL_CHARS(false),
@Deprecated
ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER(false),
@Deprecated
ALLOW_NUMERIC_LEADING_ZEROS(false),
@Deprecated
ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS(false),
@Deprecated
ALLOW_NON_NUMERIC_NUMBERS(false),
@Deprecated
ALLOW_MISSING_VALUES(false),
@Deprecated
ALLOW_TRAILING_COMMA(false),
STRICT_DUPLICATE_DETECTION(false),
IGNORE_UNDEFINED(false),
INCLUDE_SOURCE_IN_LOCATION(true);
}
小貼士:列舉值均為bool型別,括號內為預設值
每個列舉值都控制著JsonParser
不同的行為。下面分類進行解釋
底層I/O流相關
自2.10版本後,使用
StreamReadFeature#AUTO_CLOSE_SOURCE
代替
Jackson的流式API指的是I/O流,所以即使是讀,底層也是用I/O流(Reader)去讀取然後解析的。
AUTO_CLOSE_SOURCE(true)
原理和JsonGenerator的AUTO_CLOSE_TARGET(true)
一樣,不再解釋,詳見上篇文章對應部分。
支援非標準格式
JSON是有規範的,在它的規範裡並沒有描述到對註釋的規定、對控制字元的處理等等,也就是說這些均屬於非標準行為。比如這個JSON串:
{
"name" : "YourBarman", // 名字
"age" : 18 // 年齡
}
你看,若你這麼寫IDEA都會飄紅提示你:
但是,在很多使用場景(特別是JavaScript)裡,我們會在JSON串裡寫註釋(屬性多時尤甚)那麼對於這種串,JsonParser如何控制處理呢?它提供了對非標準JSON格式的相容,通過下面這些特徵值來控制。
ALLOW_COMMENTS(false)
自2.10版本後,使用
JsonReadFeature#ALLOW_JAVA_COMMENTS
代替
是否允許/* */
或者//
這種型別的註釋出現。
@Test
public void test4() throws IOException {
String jsonStr = "{\n" +
"\t\"name\" : \"YourBarman\", // 名字\n" +
"\t\"age\" : 18 // 年齡\n" +
"}";
JsonFactory factory = new JsonFactory();
try (JsonParser jsonParser = factory.createParser(jsonStr)) {
// 開啟註釋支援
// jsonParser.enable(JsonParser.Feature.ALLOW_COMMENTS);
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
String fieldname = jsonParser.getCurrentName();
if ("name".equals(fieldname)) {
jsonParser.nextToken();
System.out.println(jsonParser.getText());
} else if ("age".equals(fieldname)) {
jsonParser.nextToken();
System.out.println(jsonParser.getIntValue());
}
}
}
}
執行程式,丟擲異常:
com.fasterxml.jackson.core.JsonParseException: Unexpected character ('/' (code 47)): maybe a (non-standard) comment? (not recognized as one since Feature 'ALLOW_COMMENTS' not enabled for parser)
at [Source: (String)"{
"name" : "YourBarman", // 名字
"age" : 18 // 年齡
}"; line: 2, column: 26]
放開註釋的程式碼,再次執行程式,正常work。
ALLOW_YAML_COMMENTS(false)
自2.10版本後,使用
JsonReadFeature#ALLOW_YAML_COMMENTS
代替
顧名思義,開啟後將支援Yaml格式的的註釋,也就是#
形式的註釋語法。
ALLOW_UNQUOTED_FIELD_NAMES(false)
自2.10版本後,使用
JsonReadFeature#ALLOW_UNQUOTED_FIELD_NAMES
代替
是否允許屬性名不帶雙引號"",比較簡單,示例略。
ALLOW_SINGLE_QUOTES(false)
自2.10版本後,使用
JsonReadFeature#ALLOW_SINGLE_QUOTES
代替
是否允許屬性名支援單引號,也就是使用''
包裹,形如這樣:
{
'age' : 18
}
ALLOW_UNQUOTED_CONTROL_CHARS(false)
自2.10版本後,使用
JsonReadFeature#ALLOW_UNESCAPED_CONTROL_CHARS
代替
是否允許JSON字串包含非引號控制字元(值小於32的ASCII字元,包含製表符和換行符)。 由於JSON規範要求對所有控制字元使用引號,這是一個非標準的特性,因此預設禁用。
那麼,哪些字元屬於控制字元呢?做個簡單科普:我們一般說的ASCII碼共128個字元(7bit),共分為兩大類
控制字元
控制字元,也叫不可列印字元。第0~32號及第127號(共34個)是控制字元,例如常見的:LF(換行)、CR(回車)、FF(換頁)、DEL(刪除)、BS(退格)等都屬於此類。
控制字元大部分已經廢棄不用了,它們的用途主要是用來操控已經處理過的文字,ASCII值為8、9、10 和13 分別轉換為退格、製表、換行和回車字元。它們並沒有特定的圖形顯示,但會依不同的應用程式,而對文字顯示有不同的影響。
話外音:你看不見我,但我對你影響還蠻大
非控制字元
也叫可顯示字元,或者可列印字元,能從鍵盤直接輸入的字元。比如0-9數字,逗號、分號這些等等。
話外音:你肉眼能看到的字元就屬於非控制字元
ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER(false)
自2.10版本後,使用
JsonReadFeature#ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER
代替
是否允許**反斜槓**轉義任何字元。這句話不是非常好理解,看下面這個例子:
@Test
public void test4() throws IOException {
String jsonStr = "{\"name\" : \"YourB\\'atman\" }";
JsonFactory factory = new JsonFactory();
try (JsonParser jsonParser = factory.createParser(jsonStr)) {
// jsonParser.enable(JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER);
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
String fieldname = jsonParser.getCurrentName();
if ("name".equals(fieldname)) {
jsonParser.nextToken();
System.out.println(jsonParser.getText());
}
}
}
}
執行程式,報錯:
com.fasterxml.jackson.core.JsonParseException: Unrecognized character escape ''' (code 39)
at [Source: (String)"{"name" : "YourB\'atman" }"; line: 1, column: 19]
...
放開註釋掉的程式碼,再次執行程式,一切正常,輸出:YourB'atman
。
ALLOW_NUMERIC_LEADING_ZEROS(false)
自2.10版本後,使用
JsonReadFeature#ALLOW_LEADING_ZEROS_FOR_NUMBERS
代替
是否允許像00001
這樣的“數字”出現(而不報錯)。看例子:
@Test
public void test5() throws IOException {
String jsonStr = "{\"age\" : 00018 }";
JsonFactory factory = new JsonFactory();
try (JsonParser jsonParser = factory.createParser(jsonStr)) {
// jsonParser.enable(JsonParser.Feature.ALLOW_NUMERIC_LEADING_ZEROS);
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
String fieldname = jsonParser.getCurrentName();
if ("age".equals(fieldname)) {
jsonParser.nextToken();
System.out.println(jsonParser.getIntValue());
}
}
}
}
執行程式,輸出:
com.fasterxml.jackson.core.JsonParseException: Invalid numeric value: Leading zeroes not allowed
at [Source: (String)"{"age" : 00018 }"; line: 1, column: 11]
...
放開注掉的程式碼,再次執行程式,一切正常。輸出18
。
ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS(false)
自2.10版本後,使用
JsonReadFeature#ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS
代替
是否允許小數點.
打頭,也就是說.1
這種小數格式是否合法。預設是不合法的,需要開啟此特徵才能支援,例子就略了,基本同上。
ALLOW_NON_NUMERIC_NUMBERS(false)
自2.10版本後,使用
JsonReadFeature#ALLOW_NON_NUMERIC_NUMBERS
代替
是否允許一些解析器識別一組“非數字”(如NaN)作為合法的浮點數值。這個屬性和上篇文章的JsonGenerator#QUOTE_NON_NUMERIC_NUMBERS
特徵值是遙相呼應的。
@Test
public void test5() throws IOException {
String jsonStr = "{\"percent\" : NaN }";
JsonFactory factory = new JsonFactory();
try (JsonParser jsonParser = factory.createParser(jsonStr)) {
// jsonParser.enable(JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS);
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
String fieldname = jsonParser.getCurrentName();
if ("percent".equals(fieldname)) {
jsonParser.nextToken();
System.out.println(jsonParser.getFloatValue());
}
}
}
}
執行程式,拋錯:
com.fasterxml.jackson.core.JsonParseException: Non-standard token 'NaN': enable JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS to allow
at [Source: (String)"{"percent" : NaN }"; line: 1, column: 17]
放開註釋掉的程式碼,再次執行,一切正常。輸出:
NaN
小貼士:NaN也可以表示一個Float物件,是的你沒聽錯,即使它不是數字但它也是Float型別。具體你可以看看Float原始碼裡的那幾個常量
ALLOW_MISSING_VALUES(false)
自2.10版本後,使用
JsonReadFeature#ALLOW_MISSING_VALUES
代替
是否允許支援JSON陣列中“缺失”值。怎麼理解:陣列中缺失了值表示兩個逗號之間,啥都沒有,形如這樣[value1, , value3]
。
@Test
public void test6() throws IOException {
String jsonStr = "{\"names\" : [\"YourBatman\",,\"A哥\",,] }";
JsonFactory factory = new JsonFactory();
try (JsonParser jsonParser = factory.createParser(jsonStr)) {
// jsonParser.enable(JsonParser.Feature.ALLOW_MISSING_VALUES);
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
String fieldname = jsonParser.getCurrentName();
if ("names".equals(fieldname)) {
jsonParser.nextToken();
while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
System.out.println(jsonParser.getText());
}
}
}
}
}
執行程式,拋錯:
YourBatman // 能輸出一個,畢竟第一個part(JsonToken)是正常的嘛
com.fasterxml.jackson.core.JsonParseException: Unexpected character (',' (code 44)): expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false')
at [Source: (String)"{"names" : ["YourBatman",,"A哥",,] }"; line: 1, column: 27]
放開註釋掉的程式碼,再次執行,一切正常,結果為:
YourBatman
null
A哥
null
null
請注意:此時陣列的長度是5哦。
小貼士:此處用的String型別展示結果,是因為null可以作為String型別(
jsonParser.getText()
得到null是合法的)。但如果你使用的int型別(或者bool型別),那麼如果是null的話就報錯嘍Current token (VALUE_NULL) not of boolean type
,有興趣的親可自行嘗試,鞏固下理解的效果。報錯原因文上已有說明~
ALLOW_TRAILING_COMMA(false)
自2.10版本後,使用
JsonReadFeature#ALLOW_TRAILING_COMMA
代替
是否允許最後一個多餘的逗號(一定是最後一個)。這個特徵是非常重要的,若開關開啟,有如下效果:
- [true,true,]等價於[true, true]
- {"a": true,}等價於{"a": true}
當這個特徵和上面的ALLOW_MISSING_VALUES
特徵同時使用時,本特徵優先順序更高。也就是說:會先去除掉最後一個逗號後,再進行陣列長度的計算。
舉個例子:當然這兩個特徵開關都開啟時,[true,true,]等價於[true, true]好理解;並且呢,[true,true,,]
是等價於[true, true, null]
的哦,可千萬別忽略最後的這個null。
@Test
public void test7() throws IOException {
String jsonStr = "{\"results\" : [true,true,,] }";
JsonFactory factory = new JsonFactory();
try (JsonParser jsonParser = factory.createParser(jsonStr)) {
jsonParser.enable(JsonParser.Feature.ALLOW_MISSING_VALUES);
// jsonParser.enable(JsonParser.Feature.ALLOW_TRAILING_COMMA);
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
String fieldname = jsonParser.getCurrentName();
if ("results".equals(fieldname)) {
jsonParser.nextToken();
while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
System.out.println(jsonParser.getBooleanValue());
}
}
}
}
}
執行程式,輸出:
YourBatman
null
A哥
null
null
這完全就是上例的效果嘛。現在我放開註釋掉的程式碼,再次執行,結果為:
YourBatman
null
A哥
null
請注意對比前後的結果差異,並自己能能自己合理解釋。
校驗相關
Jackson在JSON標準之外,給出了兩個校驗相關的特徵。
STRICT_DUPLICATE_DETECTION(false)
自2.10版本後,使用
StreamReadFeature#STRICT_DUPLICATE_DETECTION
代替
是否允許JSON串有兩個相同的屬性key,預設是允許的。
@Test
public void test8() throws IOException {
String jsonStr = "{\"age\":18, \"age\": 28 }";
JsonFactory factory = new JsonFactory();
try (JsonParser jsonParser = factory.createParser(jsonStr)) {
// jsonParser.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION);
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
String fieldname = jsonParser.getCurrentName();
if ("age".equals(fieldname)) {
jsonParser.nextToken();
System.out.println(jsonParser.getIntValue());
}
}
}
}
執行程式,正常輸出:
18
28
若放開註釋程式碼,再次執行,則拋錯:
18 // 第一個數字還是能正常輸出的喲
com.fasterxml.jackson.core.JsonParseException: Duplicate field 'age'
at [Source: (String)"{"age":18, "age": 28 }"; line: 1, column: 17]
IGNORE_UNDEFINED(false)
自2.10版本後,使用
StreamReadFeature#IGNORE_UNDEFINED
代替
是否忽略沒有定義的屬性key。和JsonGenerator.Feature#IGNORE_UNKNOWN
的這個特徵一樣,它作用於預先定義了格式的資料型別,如Avro、protobuf
等等,JSON是不需要預先定義的哦~
同樣的,你可以通過這個API預先設定格式:
JsonParser:
public void setSchema(FormatSchema schema) {
...
}
其它
INCLUDE_SOURCE_IN_LOCATION(true)
自2.10版本後,使用
StreamReadFeature#INCLUDE_SOURCE_IN_LOCATION
代替
是否構建JsonLocation
物件來表示每個part的來源,你可以通過JsonParser#getCurrentLocation()
來訪問。作用不大,就此略過。
總結
本文介紹了底層流式API JsonParser讀JSON的方式,它不僅僅能夠處理標準JSON,也能通過Feature特徵值來控制,開啟對一些非標準但又比較常用的JSON串的支援,這不正式一個優秀框架/庫應有的態度麼:相容性。
結合上篇文章對寫JSON時JsonGenerator
的描述,能夠總結出兩點原則:
- 寫:100%遵循規範
- 讀:最大程度相容幷包
寫代表你的輸出,遵循規範的輸出能確保第三方在用你輸出的資料時不至於對你破口大罵,所以這是你應該做好的本分。讀代表你的輸入,能夠處理規範的格式是你的職責,但我若還能額外的處理一些非標準格式(一般為常用的),那絕對是閃耀點,也就是你給的情分。本分是你應該做的,而情分就是你的加分項。