この文書の内容は、検索のために Feishu 文書からコピーされたもので、内容のフォーマットが互換性がない場合があります。元のFeishu 文書を参照することをお勧めします。
背景#
前述の文書(Build A Parser By Rust(上))を通じて、パーサーに関する基本的な概念や Rust におけるパーサーライブラリの使用方法について理解しました。したがって、この記事では、前述の内容を具体的なケースに実践し、皆さんがその概念をよりよく理解し、実際の応用を習得できるようにします。また、一部のパーサー実装に関する設計も理解でき、今後のカスタムパーサーの実装に役立つアイデアを提供します。
以下では、nom と pest を使用して、一般的で比較的単純な Json パーサーを簡単に実装します。
注意:
- 以下で使用する nom または pest の依存バージョンは最新バージョンです。
[dependencies] pest = "2.6" pest_derive = "2.6" nom = "7.1.3"
- 実践の過程では、重要な API の具体的な意味や前述の内容に触れられていない部分のみを説明します。詳細を知りたい場合は、上文やライブラリのドキュメントを参照してください。
- この文書で構築されたパーサーは、解析の正確性のみを検証しており、関連する性能テストは行っていません。興味のある方は自分で試してみてください。
具体的なソースコードは以下で確認できます:
build parser by rust
Json 標準#
Json(JavaScript Object Notation)は、軽量なデータ交換フォーマットです。人間にとって読みやすく書きやすいテキスト形式でデータを表現します。Json フォーマットはキーと値のペアで構成され、JavaScript オブジェクトに似た構文を使用しています。そのため、JavaScript Object Notation と名付けられました。Json フォーマットは、ネットワーク上でデータを転送したり、設定情報を保存したり、異なるシステム間でデータを交換するために一般的に使用されます。Web 開発では非常に一般的で、API のデータ転送やフロントエンドとバックエンドのデータのやり取りなどに使用されます。Json は、モバイルアプリ開発やビッグデータ処理などの分野でも広く使用されています。Json フォーマットはシンプルで使いやすく、読みやすいため、非常に一般的であり、多くのアプリケーションでデータ交換の標準フォーマットの 1 つとなっています。
Json パーサーを実装するには、まずJson 標準プロトコルを理解する必要があります。標準は主に以下の 6 つの部分に分かれています:
Json 標注 | 説明 | 具体的定義 |
---|---|---|
空白 whitespace | 空白(whitespace)は任意の一対のマークの間に挿入できます | |
数値 number | 数値(number)は C または Java の数字に非常に似ていますが、8 進数と 16 進数の形式は使用しません | |
文字列 string | 文字列(string)は、ダブルクォーテーションで囲まれた 0 個以上の Unicode 文字のシーケンスであり、バックスラッシュでエスケープされます。1 文字(character)は、単独の文字列(character string)です | |
値 value | 値(value)は、ダブルクォーテーションで囲まれた文字列(string)、数値(number)、true、false、null、オブジェクト(object)、または配列(array)であることができます。これらの構造はネストできます | |
配列 array | 配列(array)は、値(value)の順序付きコレクションです。配列は左中括弧[ で始まり、右中括弧] で終わり、値はカンマ, で区切られます | |
オブジェクト object | オブジェクト(object)は、無秩序な名前 / 値ペアのコレクションです。オブジェクトは左括弧{ で始まり、右括弧} で終わります。各名前の後にはコロン: が続き、名前 / 値ペアはカンマ, で区切られます |
Json 標準プロトコルでは、データ型の定義と具体的な解析が非常に明確で簡単です。
以下では、その標準に基づいて、前述の nom と pest を使用して簡単に実装します。具体的なコードパスはnom/jsonとpest/jsonにあります。
nom を基にした実装#
Json モデル#
ここでは、空白を除く Json Valueを表すために列挙型を使用します:
#[derive(Debug, PartialEq)]
pub enum JsonValue {
Str(String),
Boolean(bool),
Num(f64),
Array(Vec<JsonValue>),
Object(HashMap<String, JsonValue>),
Null,
}
具体的な型解析#
- 空白
前述の内容から、空白要素は以下のいずれかの状況に分けられます。処理時には他の要素に出会うまで入力を消費し、最終的に whitespace を得ます:
- space->
" "
- linefeed->
"\n"
- carriage return->
"\r"
- horizontal tab->
"\t"
ここで、nom には 2 つの実装方法があります。1 つは組み込み関数multispace0
を直接使用する方法、もう 1 つはtake_while
を利用して解析関数を構築する方法です:
前文で
take_while
は述語パーサーとして入力を消費し続け、入力が述語を満たさなくなるまで続きます。
// whitespace Json 空白解析(nomの組み込み関数multispace0と同等)
fn whitespace(i: &str) -> IResult<&str, &str> {
let chars = " \t\r\n";
take_while(move |c| chars.contains(c))(i)
}
- 数値
前述の内容から、Json は正負数、小数、科学的記数法をサポートしています。alt
やbe_f64
などの解析器サブコンビネーターを使用して解析できますが、このシナリオではnom が提供する組み込み関数 **double
** を使用するのが一般的です。その使用方法は以下の例を参照してください:
use nom::number::complete::double;
let parser = |s| {
double(s)
};
assert_eq!(parser("1.1"), Ok(("", 1.1)));
assert_eq!(parser("123E-02"), Ok(("", 1.23)));
assert_eq!(parser("123K-01"), Ok(("K-01", 123.0)));
assert_eq!(parser("abc"), Err(Err::Error(("abc", ErrorKind::Float))));
- 文字列
ここでは、文字列と両端の引用符の中の文字列の状況をそれぞれ議論する必要があります:
- まず、文字列の左引用符の右側には 3 つの状況があります。引用符の間が空白の空文字の場合を除き、他の状況はコンビネーターを使用して両端の引用符を取り除き、引用符の間の文字列の内容を取得できます。コンビネーターの使用方法は多くありますが、ここでは一般的な 2 つの使用方法を示します:
alt
+delimited
:文字全体の構造の観点から解析preceded
+cut
+terminated
:文字の順序の観点から解析
// string 全体の文字列解析
fn string(i: &str) -> IResult<&str, &str> {
context(
"string",
preceded(char('\"'), cut(terminated(parse_str, char('\"')))))(i)
// parse_strの実装は後で説明します
}
fn string(i: &str) -> IResult<&str, &str> {
context(
"string",
alt((tag("\"\""), delimited(tag("\""), parse_str, tag("\"")))),
)(i)
}
ここで
cut
コンビネーターの役割はバックトラッキングを防ぐことです。解析に失敗した場合、他の可能な解析パスを試みることなく、即座に解析を停止します。これは不必要な性能コストや解析エラーを避けるのに非常に役立ちます。理解を助けるために公式の例を示します:use nom::combinator::cut; fn parser(input: &str) -> IResult<&str, &str> { alt(( preceded(one_of("+-"), cut(digit1)), rest ))(input) } assert_eq!(parser("+10 ab"), Ok((" ab", "10"))); assert_eq!(parser("ab"), Ok(("", "ab"))); assert_eq!(parser("+"), Err(Err::Failure(Error { input: "", code: ErrorKind::Digit })));
- 次に、引用符の中の文字列を取得した後、エスケープ文字を処理して実際の内容を取得する必要があります。現在、nom はエスケープ文字を処理するための
escaped
関数を提供しています。この関数の引数はescaped(normal, control, escapable)
であり、それぞれの引数は次のようになります:normal
:通常の文字をマッチするパーサーですが、制御文字を含む文字は受け付けませんcontrol
:制御文字(例えば、多くの言語で使用される\
)escapable
:マッチ可能なエスケープ文字
// 公式の例
use nom::bytes::complete::escaped;
use nom::character::complete::one_of;
fn esc(s: &str) -> IResult<&str, &str> {
// digit1:つまり、組み込みのパーサー関数で、少なくとも1つの数字をマッチします
// '\\':バックスラッシュ文字'\'
// r#""n\"#:「r#"{構造原始文字列リテラルの文字列内容}"#」、ここではマッチ可能なエスケープ文字が "、n、\
escaped(digit1, '\\', one_of(r#""n\"#))(s)
}
assert_eq!(esc("123;"), Ok((";", "123")));
assert_eq!(esc(r#"12\"34;"#), Ok((";", r#"12\"34"#)));
- 最後に、
escaped
関数と Json 標準に基づいてparse_str
関数を構築します。このシナリオで指定された 3 つの引数の意味は次のとおりです:normal
:マッチする「"Any codepoint except" or \ or control characters"」'\\'
:Json のエスケープ文字もバックスラッシュ文字ですescapable
:標準の説明にある",\,/,b
などをマッチします。16 進数の数字も別途処理する必要があります。- ここで特に注意すべきは、16 進数処理に使用される
peek
組み込み関数は、解析後に入力を消費しないため、後の解析が正常に行われます。
- ここで特に注意すべきは、16 進数処理に使用される
// parse_str 単独の文字列解析
fn parse_str(i: &str) -> IResult<&str, &str> {
escaped(normal, '\\', escapable)(i)
}
// normal 通常の文字解析
fn normal(i: &str) -> IResult<&str, &str> {
take_till1(|c: char| c == '\\' || c == '"' || c.is_ascii_control())(i)
}
// escapable エスケープ文字解析
fn escapable(i: &str) -> IResult<&str, &str> {
context(
"escaped",
alt((
tag("\""),
tag("\\"),
tag("/"),
tag("b"),
tag("f"),
tag("n"),
tag("r"),
tag("t"),
hex
)))(i)
}
// hex 16進数文字解析
fn hex(i: &str) -> IResult<&str, &str> {
context(
"hex",
preceded(
peek(tag("u")),
take_while_m_n(5, 5, |c: char| c.is_ascii_hexdigit() || c == 'u'),
))(i)
}
- 値
前述の空白、数値、文字列のパーサーを実装したので、次に基本型の boolean と null を完成させます:
// boolean ブールデータ型解析
fn boolean(i: &str) -> IResult<&str, bool> {
alt((
value(true, tag("true")),
value(false, tag("false"))
))(i)
}
// null Null解析
fn null(i: &str) -> IResult<&str, JsonValue> {
map(tag("null"), |_| JsonValue::Null)(i)
}
現在、実装した型パーサーに基づいて、値のパーサー(複合型は後で実装)を構築できます:
以下の実装には、少し理解しにくい構文が含まれています。ここで簡単に説明します:
map
関数の引数の型は、nom パーサートレイトとクロージャ関数 *FnMut
*(O1) -> O2
です。- ここでは、列挙型の要素の構造体が上記の匿名関数であるため、直接使用できます。
// json_value JsonValue 解析
fn json_value(i: &str) -> IResult<&str, JsonValue> {
context(
"json value",
delimited(
whitespace,
alt((
map(string, |s| JsonValue::Str(String::from(s))),
map(double, JsonValue::Num),
map(boolean, JsonValue::Boolean),
null,
map(array, JsonValue::Array),
map(object, JsonValue::Object)
)),
whitespace,
),
)(i)
}
- 配列
配列の標準記述に基づいて:
- まず、
delimited
を使用して左右の中括弧を取り除き、内容を解析しやすくします。 - 組み込み関数 **
separated_list0
を利用して、括弧内の内容を解析し、配列Vec<JsonValue>
** を得ます:
// array 配列解析
fn array(i: &str) -> IResult<&str, Vec<JsonValue>> {
context(
"array",
delimited(
tag("["),
separated_list0(tag(","), delimited(whitespace, json_value, whitespace)),
tag("]"),
),
)(i)
}
- オブジェクト
オブジェクトのような複雑なパーサーについては、コンビネーターの解析器の考え方を使用して、サブパーサーを分割して実装できます:
- まず、オブジェクト内の名前 / 値ペアの形式を解析するために、
separated_pair
+preceded
の組み合わせを使用します:
// key_value kv形式解析
fn key_value(i: &str) -> IResult<&str, (&str, JsonValue)> {
separated_pair(preceded(whitespace, string), cut(preceded(whitespace, char(':'))), json_value)(i)
}
- 次に、オブジェクト全体の構造を解析する考え方は次のとおりです:
- 左括弧 -> 括弧内の内容 ->(前述の)キーと値の形式で解析して配列を構築 -> 配列を HashMap の型に変換 -> 右括弧
// object オブジェクト形式解析
fn object(i: &str) -> IResult<&str, HashMap<String, JsonValue>> {
context(
"object",
preceded(
char('{'),
cut(terminated(
map(
separated_list0(preceded(whitespace, char(',')), key_value),
|tuple_vec| {
tuple_vec.into_iter().map(|(k, v)| (String::from(k), v)).collect()
},
),
preceded(whitespace, char('}')),
)),
),
)(i)
}
トップレベル解析関数#
前述の Json 標準のすべての注釈型を実装したので、最後にこのパーサーを使用するためのトップレベル関数を構築するだけです。
ここで、Json の最外層の結果はオブジェクトまたは配列のいずれかであるため、私たちのトップレベル関数は次のようになります:
fn root(i: &str) -> IResult<&str, JsonValue> {
delimited(
whitespace,
alt((
map(object, JsonValue::Object),
map(array, JsonValue::Array),
)),
opt(whitespace),
)(i)
}
最後に、以下のテスト関数を実行して、最終的な戻り値が正常であるかどうかを確認できます:
#[cfg(test)]
mod test_json {
use crate::nom::json::json::root;
#[test]
fn test_parse_json() {
let data = " { \"a\"\t: 42,
\"b\": [ \"x\", \"y\", 12 ] ,
\"c\": { \"hello\" : \"world\"}
} ";
println!("有効なJSONデータを解析しようとしています:\n\n**********\n{}\n**********\n", data);
//
// 有効なJSONデータを解析しようとしています:
//
// **********
// { "a" : 42,
// "b": [ "x", "y", 12 ] ,
// "c": { "hello" : "world"}
// }
// **********
println!(
"有効なファイルを解析中:\n{:#?}\n",
root(data)
);
// 有効なファイルを解析中:
// Ok(
// (
// "",
// Object(
// {
// "c": Object(
// {
// "hello": Str(
// "world",
// ),
// },
// ),
// "b": Array(
// [
// Str(
// "x",
// ),
// Str(
// "y",
// ),
// Num(
// 12.0,
// ),
// ],
// ),
// "a": Num(
// 42.0,
// ),
// },
// ),
// ),
// )
}
}
これで、nom を使用して実装された Json パーサーが完成しました。具体的な性能テストは行っていませんので、興味のある方は性能テストを行ってみてください。
pest を基にした実装#
Json モデル#
nom の前述の実装と似ており、ここでも空白を除く Json Valueを構築するために列挙型を使用します。
#[derive(Debug, PartialEq)]
pub enum JsonValue<'a> {
Number(f64),
String(&'a str),
Boolean(bool),
Array(Vec<JsonValue<'a>>),
Object(Vec<(&'a str, JsonValue<'a>)>),
Null,
}
実際にはライフタイムを宣言する必要はありません。String を直接使用できますが、ライフタイムを宣言する理由は&str
を導入したためで、これにより後の型変換処理を省略できます。
Json 解析後に得られる JsonValue をより良く表示し処理するために、JsonValue のシリアライザーを追加します:
pub fn serialize_json_value(val: &JsonValue) -> String {
use JsonValue::*; // 後の列挙型を便利にするため
match val {
Number(n) => format!("{}", n),
String(s) => format!("\"{}\"", s),
Boolean(b) => format!("{}", b),
Array(a) => {
let contents: Vec<_> = a.iter().map(serialize_json_value).collect();
format!("[{}]", contents.join(","))
}
Object(o) => {
let contents: Vec<_> = o
.iter()
.map(|(key, value)| format!("\"{}\":{}", key, serialize_json_value(value)))
.collect();
format!("{{{}}}", contents.join(","))
}
Null => "null".to_string(),
}
}
配列やオブジェクトの複合型を処理する際には、再帰的に処理する必要があることに注意してください。
Pest 文法解析#
ここで新たにjson.pest
を作成し、Pest 文法を使用して解析する Json 標準を記述します。
- 空白
標準の記述に基づき、選択演算子|
を使用して実装します:
WHITESPACE = _{ " " | "\t" | "\r" | "\n" }
ここで、いくつかの構文の特殊処理について前もって説明する必要があります:
- ルールにプレフィックス
_
を付けると、静黙ルールが作成されます。通常のルールとは異なり、解析中にトークンペアを生成せず、エラーを報告せず、最終的に最外層のトークンペアのみを取得します。 - pest では、単独で
WHITESPACE
を定義すると、すべてのシーケンスまたは繰り返しの間に暗黙的に挿入されます(原子ルールを除く)。- ここで言及されている「原子ルールを除く」という情報は、後でこの情報に関連するルールがあるため注意が必要です。
- 同様の暗黙の規約には
COMMENT
ルールがあり、これは文字内容に含まれる空白のシナリオを考慮した処理です。
以上のことから、今後のファイルでは原子ルールを除くすべてのシーケンスまたは繰り返しの間の解析で空白が無視されます。
- 数値
標準の記述に基づき、シーケンス演算子~
を使用して異なる解析条件を追加し、pest の数字関連の組み込みルールを利用します:
// 2. number
number = @{
"-"?
~ ("0" | ASCII_NONZERO_DIGIT ~ ASCII_DIGIT*)
~ ("." ~ ASCII_DIGIT*)?
~ (^"e" ~ ("+"|"-")? ~ ASCII_DIGIT+)?
}
ここでも、構文の特殊処理について説明する必要があります。理解を助けるために:
- ルールにプレフィックス
@
を付けると、原子ルールが作成され、以下の特性を持ちます:- 前述の
WHITESPACE
処理は無効になり、内部空白は隠されません。~
で構築されたシーケンス間の文字は無視されません。 - 原子ルール内で呼び出される他のルールも原子ルールと見なされます。
- 原子ルール内では、内部マッチのすべてのルールは静黙的と見なされます。つまり、最外層のルールの解析結果のみを取得できます。
- 前述の
- ルールの後ろに演算子
?
、*
、+
を付けると、それぞれ最大 1 つのマッチ、すべてのマッチ、少なくとも 1 つのマッチを表します。 - ルールの前に演算子
^
を付けると、大文字と小文字を区別しないことを示します。
- 文字列
標準の記述に基づき、文字列の解析を 3 つのルールに結合して、より明確に説明します:
// 3. string
string = ${ "\"" ~ inner ~ "\"" }
inner = @{ char* }
char = {
!( "\"" | "\\") ~ ANY
| "\\" ~ ("\"" | "\\" | "/" | "b" | "f" | "n" | "r" | "t")
| "\\" ~ ("u" ~ ASCII_HEX_DIGIT{4})
}
pest は nom のようにエスケープ文字を処理するための組み込み関数を提供していないため、解析時にエスケープ文字を手動で追加する必要があります。
ここで使用される構文の特殊処理について説明します:
- ルールにプレフィックス
$
を付けると、複合原子ルールが作成され、前述の原子ルールと似ていますが、異なる特性があります:- 前述の
WHITESPACE
処理は無効になり、内部マッチのすべてのルールは静黙的ではなく、通常のルールと同様に扱われます。
- 前述の
!(...) ~ ANY
は、括弧内で指定された文字以外の任意の文字をマッチすることを表します。
- 値
前述の実装に基づき、基本型の boolean と null を完成させます:
// 4. boolean
boolean = {"true" | "false"}
// 5. null
null = {"null"}
各データ型の解析ルールを組み合わせて値を構築します。異なるルールの解析過程を気にしないために、静黙ルール_
を使用してネストを減らします:
value = _{ number | string | boolean | array | object | null}
配列とオブジェクトについては、以下で説明します。
- 配列
標準の記述に基づき、空の配列と値のある配列を分け、**~
と\*
** を使用して複数の値が存在する可能性を示します:
// 6. array
array = {
"[" ~ "]"|
"[" ~ value ~ ("," ~ value)* ~ "]"
}
- オブジェクト
ここでは、オブジェクトの値をペアルールに分割し、前述の文字列と値のルールを利用します。空のオブジェクトと値のあるオブジェクトを区別します:
// 7. object
object = {
"{" ~ "}"|
"{" ~ pair ~ ("," ~ pair)* ~ "}"
}
pair = { string ~ ":" ~ value }
- 最終ルール
最後に、Json 全体を表す最終ルールが必要です。Json の内容は、オブジェクトまたは配列のみが合法です。
また、今後は解析された値自体と EOI ルールの 2 つのトークンペアのみが必要ですので、ルールを静黙的にマークします:
// 9. json
json = _{ SOI ~ (object | array) ~ EOI}
これで、必要な pest ルールの記述が完了しました。次に、ルールに基づいて AST を生成する構造を作成します。
AST 生成と解析#
- Pest ルールにバインドされた構造体を定義
pest はgrammarマクロを使用して Rust の構造体にバインドする必要があります:
use pest::Parser;
use pest_derive::Parser;
#[derive(Parser)]
#[grammar = "pest/json/json.pest"] // 自分のプロジェクトファイルに基づいて
pub struct JsonParser;
- AST 生成関数を構築
Pest ルールにバインドされた JsonParser を使用し、pest::Parser
の parse メソッドを使用して前述の json ルールに基づいて AST 結果を得ます:
pub fn root(content: &str) -> Result<JsonValue, Error<Rule>> {
let json = JsonParser::parse(Rule::json, content)?.next().unwrap();
// ......
}
Json は静黙的ルールで、最終的に生成されたトークンペアは Pair<Rule>
型であるため、next()
を 1 回呼び出すだけで済みます。
私たちの目標は、AST を解析して最終的な JsonValue を得ることですので、Pair<Rule>
を解析するメソッドが必要です。
- AST 解析関数
parse_json_value
関数を追加します:
- 得られた AST 結果を使用して、JsonValue の各型を前述のpest のルールに基づいて解析して値を割り当てます。
- 配列やオブジェクトの複合型の処理には、再帰関数を使用してネストされた値を検索します。
- マッチした Rule が JsonValue の型でない場合は、直接エラーを報告して終了します。
pub fn parse_json_value(pair: Pair<Rule>) -> JsonValue {
match pair.as_rule() {
Rule::number => JsonValue::Number(pair.as_str().parse().unwrap()),
Rule::string => JsonValue::String(pair.into_inner().next().unwrap().as_str()),
Rule::boolean => JsonValue::Boolean(pair.as_str().parse().unwrap()),
Rule::null => JsonValue::Null,
Rule::array => JsonValue::Array(pair.into_inner().map(parse_json_value).collect()),
Rule::object => JsonValue::Object(
pair.into_inner()
.map(|pair| {
let mut inner_rules = pair.into_inner();
let key = inner_rules
.next() // 得られたpairルール
.unwrap()
.into_inner()
.next() // 得られたpairルールの最初のトークンペア、すなわちkey
.unwrap()
.as_str();
let value = parse_json_value(inner_rules.next().unwrap());
(key, value)
})
.collect()
),
_ => unreachable!()
}
}
- 最終的なトップレベル関数と関数テスト
前の解析関数を基に、前述のトップレベル関数 **root
** を完成させて、最終的な解析結果を得ます:
pub fn root(content: &str) -> Result<JsonValue, Error<Rule>> {
let json = JsonParser::parse(Rule::json, content)?.next().unwrap();
Ok(parse_json_value(json))
}
最後に、以下のテスト関数を実行して解析結果を検証します:
#[cfg(test)]
mod test {
use crate::pest::json::json::{JsonValue, root, serialize_json_value};
#[test]
fn test_parse_json_by_pest() {
let data = " { \"a\"\t: 42,
\"b\": [ \"x\", \"y\", 12 ] ,
\"c\": { \"hello\" : \"world\"}
} ";
println!("有効なJSONデータを解析しようとしています:\n\n**********\n{}\n**********\n", data);
// 有効なJSONデータを解析しようとしています:
//
// **********
// { "a" : 42,
// "b": [ "x", "y", 12 ] ,
// "c": { "hello" : "world"}
// }
// **********
let json_result: JsonValue = root(data).expect("JSON解析に失敗しました");
println!("{}", serialize_json_value(&json_result))
// {"a":42,"b":["x","y",12],"c":{"hello":"world"}}
}
}
これで、pest を使用して実装された Json パーサーが完成しました。
まとめ#
私たちは、前述の nom と pest を使用して Json パーサーを構築しました。nom と pest はどちらも非常に古典的なパーサーライブラリであり、異なる優れた実装の考え方に基づいてパーサーを完成させ、大部分のパーサーライブラリの関連要求を満たすことができます。
これらの 2 つの文書を読むことで、皆さんは Rust のパーサーライブラリを使用して自分のカスタムパーサーを迅速に構築し、手動でパーサーを作成する苦痛から解放されることができると信じています。また、この過程で、Parser、Parser Combinator、Json 標準などの関連概念についても理解できたことでしょう。
最後までお読みいただき、ありがとうございました。パーサーや類似のパーサーのニーズについて興味がある方に役立つことを願っています。
今後、リポジトリには Redis プロトコルのパーサーも更新する予定です。篇幅の関係でここには掲載しません。
参考#
https://zhuanlan.zhihu.com/p/146455601