本文是『horseshoe·Regex專題』系列文章之一,後續會有更多專題推出
GitHub地址: https://github.com/veedrin/horseshoe
博客地址(文章排版真的很漂亮): https://veedrin.com
若是以爲對你有幫助,歡迎來GitHub點Star或者來個人博客親口告訴我
名餘曰正則兮,字餘曰靈均。
Regular Expressions
翻譯成中文叫正則表達式。也不知道是誰翻譯過來的,聽起來就很嚴肅。彷佛翻譯成通用表達式
更能傳達其精髓,若是你不怕夢見屈原的話。javascript
爲何叫通用表達式?由於它有一套和編程語言無關的文本匹配規則。不少語言都實現了正則表達式的文本匹配引擎,只不過在功能集合上略有不一樣。前端
咱們要記住的是三點:java
其一,正則表達式是用來提取文本的。git
其二,正則表達式的表達能力強大到使人髮指。github
其三,正則表達式的語法對初學者不友好。正則表達式
另外,本專題只涉及JavaScript語言的正則表達式,其餘語言的規則可能略有不一樣。express
我還爲各位讀者準備了一副宣傳語,應該能讓你心動(點贊)吧?編程
學一門前端工具,幾年就過期了。學正則表達式,受用一生。
什麼叫普通字符?編程語言
當咱們寫a
的時候,咱們指的就是a
;當咱們寫愛
的時候,咱們指的就是愛
。函數
'hello 😀 regex'.match(/😀/); // ["😀", index: 6, input: "hello 😀 regex", groups: undefined]
這就是普通字符,它在正則中的含義就是檢索它自己。除了正則規定的部分字符外,其他的都是普通字符,包括各類人類語言,包括emoji,只要可以表達爲字符串。
^
字符的英文是caret
,翻譯成中文是脫字符
。不要問我,又不是我翻譯的。它在正則中屬於元字符,一般表明的意義是文本的開始。說一般是由於當它在字符組中[^abc]
另有含義。
什麼叫文本的開始?就是若是它是正則主體的第一個符號,那緊跟着它的字符必須是被匹配文本的第一個字符。
'regex'.match(/^r/); // ["r", index: 0, input: "regex", groups: undefined]
問題來了,若是^
不是正則的第一個符號呢?
'regex'.match(/a^r/); // null
因此呀,關於它有三點須要注意:
$
字符與^
正好相反。它表明文本的結束,而且沒有其餘含義(實際上是有的,但不是在正則主體內)。一樣,它必須是正則主體的最後一個符號。
'regex'.match(/x$/); // ["x", index: 4, input: "regex", groups: undefined]
^
與$
特殊的地方在於它匹配的是一個位置。位置不像字符,它看不見,因此更不容易理解。
咱們如今已經知道$
匹配文本的結束位置,它是元字符。可是若是我想匹配$
自己呢?匹配一個美圓符號的需求再常見不過了吧。因此咱們得將它貶爲庶民。
\
反斜槓就是幹這個的。
'price: $3.6'.match(/\$[0-9]+\.[0-9]+$/); // ["$3.6", index: 7, input: "price: $3.6", groups: undefined]
上面的例子有點超綱了,超綱的部分先無論。
你能夠認爲\
也是一個元字符,它跟在另外一個元字符後面,就能還原它原本的含義。
若是有兩個\
呢?那就是轉義自身了。若是有三個\
呢?咱們得分紅兩段去理解。以此類推。
普通字符前面跟了一個\
是什麼效果?首先它們是一個總體,而後普通字符轉義後仍是普通字符。
通常來講,普通字符前面帶反斜槓仍是普通字符,可是有一些普通字符,帶反斜槓後反而變成了元字符。
要怪只能怪計算機領域的經常使用符號太少了。
元字符 | 含義 |
---|---|
b | 匹配一個單詞邊界(boundary) |
B | 匹配一個非單詞邊界 |
d | 匹配一個數字字符(digit) |
D | 匹配一個非數字字符 |
s | 匹配一個空白字符(space) |
S | 匹配一個非空白字符 |
w | 匹配一個字母或者一個數字或者一個下劃線(word) |
W | 匹配一個字母、數字和下劃線以外的字符 |
你這麼聰明,確定一眼就看出來,大寫表明反義。對,就是這麼好記。
\b
匹配的也是一個位置,而不是一個字符。單詞和空格之間的位置,就是所謂單詞邊界。
'hello regex'.match(/\bregex$/); // ["regex", index: 6, input: "hello regex", groups: undefined] 'hello regex'.match(/\Bregex$/); // null
所謂單詞邊界,對中文等其餘語言是無效的。
'jiangshuying gaoyuanyuan huosiyan'.match(/\bgaoyuanyuan\b/); // ["gaoyuanyuan", index: 13, input: "jiangshuying gaoyuanyuan huosiyan", groups: undefined] '江疏影 高圓圓 霍思燕'.match(/\b高圓圓\b/); // null
因此\b
翻譯一下就是^\w|\w$|\W\w|\w\W
。
\d
匹配一個數字,注意,這裏的數字不是指JavaScript中的數字類型,由於文本全是字符串。它指的是表明數字的字符。
'123'.match(/\d/); // ["1", index: 0, input: "123", groups: undefined]
\s
匹配一個空白字符。
這裏須要解釋一下什麼是空白字符。
空白字符不是空格,它是空格的超集。不少人說它是\f\n\r\t\v
的總和,其中\f
是換頁符,\n
是換行符,\r
是回車符,\t
是水平製表符,\v
是垂直製表符。是這樣麼?
'a b'.match(/\w\s\w/); // ["a b", index: 0, input: "a b", groups: undefined] 'a b'.match(/\w\f\w/); // null 'a b'.match(/\w\n\w/); // null 'a b'.match(/\w\r\w/); // null 'a b'.match(/\w\t\w/); // null 'a b'.match(/\w\v\w/); // null 'a b'.match(/\w \w/); // ["a b", index: 0, input: "a b", groups: undefined]
這樣說的人,明顯是沒有作過實驗。其實正確的寫法是空格\f\n\r\t\v
的總和,集合裏面包含一個空格,可千萬別忽略了。誒,難道空格在正則中的寫法就是空一格
麼,是的,就是這樣隨意。
這個集合中不少都是不可打印字符,估計只有\n
是咱們的老朋友。因此,若是不須要區分空格和換行的話,那就大膽的用\s
吧。
\w
匹配一個字母或者一個數字或者一個下劃線。爲何要將它們放一塊兒?想想JavaScript中的變量規則,包括不少應用的用戶名都只能是這三樣,因此把它們放一塊兒挺方便的。
不過要注意,字母指的是26個英文字母,其餘的不行。
'正則'.match(/\w/); // null
若是咱們將大寫和小寫的帶反斜槓的元字符組合在一塊兒,就能匹配任何字符。是的,不針對任何人。
'@regex'.match(/[\s\S]/); // ["@", index: 0, input: "@regex", groups: undefined]
方括號的含義咱們先按下不表。
.
在正則中的含義仙風道骨,它匹配換行符以外的任意單個字符。
若是文本不存在換行符,那麼.
和[\b\B]
和[\d\D]
和[\s\S]
和[\w\W]
是等價的。
若是文本存在換行符,那麼(.|\n)
和[\b\B]
和[\d\D]
和[\s\S]
和[\w\W]
是等價的。
'@regex'.match(/./); // ["@", index: 0, input: "@regex", groups: undefined]
前面咱們一直在強調,一個元字符只匹配一個字符。即使強大如.
它也只能匹配一個。
那匹配gooooogle
的正則是否是得寫成/gooooogle/
呢?
正則冷笑,並向你發射一個蔑視。
若是匹配的模式有重複,咱們能夠聲明它重複的次數。
量詞 | 含義 |
---|---|
? | 重複零次或者一次 |
+ | 重複一次或者屢次,也就是至少一次 |
* | 重複零次或者屢次,也就是任意次數 |
{n} | 重複n次 |
{n,} | 重複n次或者更屢次 |
{n,m} | 重複n次到m次之間的次數,包含n次和m次 |
有三點須要注意:
?
在諸如匹配http協議的時候很是有用,就像這樣:/http(s)?/
。它在正則中除了是量詞還有別的含義,後面會提到。/.*/
來匹配若干對咱們沒有價值的文本,它的含義是若干除換行符以外的字符
。好比咱們須要文本兩頭的格式化信息,中間是什麼無所謂,它就派上用場了。不過它的性能可很差。{n,m}
之間不能有空格,空格在正則中是有含義的。關於量詞最使人困惑的是:它重複什麼?
它重複緊貼在它前面的某個集合。第一點,必須是緊貼在它前面;第二點,重複一個集合。最多見的集合就是一個字符,固然正則中有一些元字符可以將若干字符變成一個集合,後面會講到。
'gooooogle'.match(/go{2,5}gle/); // ["gooooogle", index: 0, input: "gooooogle", groups: undefined]
若是一個量詞緊貼在另外一個量詞後面會怎樣?
'gooooogle'.match(/go{2,5}+gle/); // Uncaught SyntaxError: Invalid regular expression: /go{2,5}+gle/: Nothing to repeat
前面提到量詞不能緊跟在另外一個量詞後面,立刻要👋👋打臉了。
'https'.match(/http(s)?/); // ["https", "s", index: 0, input: "https", groups: undefined] 'https'.match(/http(s)??/); // ["http", undefined, index: 0, input: "https", groups: undefined]
然而,個人臉是這麼好打的?
緊跟在?
後面的?
它不是一個量詞,而是一個模式切換符,從貪婪模式切換到非貪婪模式。
貪婪模式在正則中是默認的模式,就是在既定規則之下匹配儘量多的文本。由於正則中有量詞,它的重複次數多是一個區間,這就有了取捨。
緊跟在量詞以後加上?
就能夠開啓非貪婪模式。怎麼省事怎麼來。
這裏的要點是,?
必須緊跟着量詞,不然的話它本身就變成量詞了。
正則中的普通字符只能匹配它本身。若是我要匹配一個普通字符,可是我不肯定它是什麼,怎麼辦?
'grey or gray'.match(/gr[ae]y/); // ["grey", index: 0, input: "grey or gray", groups: undefined]
方括號在正則中表示一個區間,咱們稱它爲字符組。
首先,字符組中的字符集合只是全部的可選項,最終它只能匹配一個字符。
而後,字符組是一個獨立的世界,元字符不須要轉義。
'$'.match(/[$&@]/); // ["$", index: 0, input: "$", groups: undefined]
最後,有兩個字符在字符組中有特殊含義。
^
在字符組中表示取反,再也不是文本開始的位置了。
'regex'.match(/[^abc]/); // ["r", index: 0, input: "regex", groups: undefined]
若是我就要^
呢?前面已經講過了,轉義。
-
原本是一個普通字符,在字符組中搖身一變成爲連字符。
'13'.match(/[1-9]3/); // ["13", index: 0, input: "13", groups: undefined]
連字符的意思是匹配範圍在它的左邊字符和右邊字符之間。
若是我這樣呢?
'abc-3'.match(/[0-z]/); // ["a", index: 0, input: "abc-3", groups: undefined]
'xyz-3'.match(/[0-c]/); // ["3", index: 4, input: "xyz-3", groups: undefined]
'xyz-3'.match(/[0-$]/); // Uncaught SyntaxError: Invalid regular expression: /[0-$]/: Range out of order in character class
發現什麼了沒有?只有兩種字符是能夠用連字符的:英文字母和數字。並且英文字母能夠和數字連起來,英文字母的順序在後面。這和撲克牌1 2 3 4 5 6 7 8 9 10 J Q K
是一個道理。
咱們已經知道量詞是怎麼回事了,咱們也知道量詞只能重複緊貼在它前面的字符。
若是我要重複的是一串字符呢?
'i love you very very very much'.match(/i love you very +much/); // null 'i love you very very very much'.match(/i love you v+e+r+y+ +much/); // null
這樣確定是不行的。是時候請圓括號出山了。
'i love you very very very much'.match(/i love you (very )+much/); // ["i love you very very very much", "very ", index: 0, input: "i love you very very very much", groups: undefined]
圓括號的意思是將它其中的字符集合打包成一個總體,而後量詞就能夠操做這個總體了。這和方括號的效果是徹底不同的。
並且默認的,圓括號的匹配結果是能夠捕獲的。
如今咱們有一個需求,匹配<div>
標籤。
'<div>hello regex</div>'.match(/<div>.*<\/div>/); // ["<div>hello regex</div>", index: 0, input: "<div>hello regex</div>", groups: undefined]
這很簡單。但若是我要匹配的是任意標籤,包括自定義的標籤呢?
'<App>hello regex</App>'.match(/<([a-zA-Z]+)>.*<\/\1>/); // ["<App>hello regex</App>", "App", index: 0, input: "<App>hello regex</App>", groups: undefined]
這時候就要用到正則的捕獲特性。正則內捕獲使用\數字
的形式,分別對應前面的圓括號捕獲的內容。這種捕獲的引用也叫反向引用。
咱們來看一個更復雜的狀況:
'<App>hello regex</App><p>A</p><p>hello regex</p>'.match(/<((A|a)pp)>(hello regex)+<\/\1><p>\2<\/p><p>\3<\/p>/); // ["<App>hello regex</App><p>A</p><p>hello regex</p>", "App", "A", "hello regex", index: 0, input: "<App>hello regex</App><p>A</p><p>hello regex</p>", groups: undefined]
若是有嵌套的圓括號,那麼捕獲的引用是先遞歸的,而後纔是下一個頂級捕獲。所謂深度優先。
'@abc'.match(/@(abc)/); // ["@abc", "abc", index: 0, input: "@abc", groups: undefined] RegExp.$1; // "abc"
沒錯,RegExp
就是構造正則的構造函數。若是有捕獲組,它的實例屬性$數字
會顯示對應的引用。
若是有多個正則呢?
'@abc'.match(/@(abc)/); // ["@abc", "abc", index: 0, input: "@abc", groups: undefined] '@xyz'.match(/@(xyz)/); // ["@xyz", "xyz", index: 0, input: "@xyz", groups: undefined] RegExp.$1; // "xyz"
RegExp
構造函數的引用只顯示最後一個正則的捕獲。
另外還有一個字符串實例方法也支持正則捕獲的引用,它就是replace
方法。
'hello **regex**'.replace(/\*{2}(.*)\*{2}/, '<strong>$1</strong>'); // "hello <strong>regex</strong>"
實際上它纔是最經常使用的引用捕獲的方式。
這是ES2018的新特性。
使用\數字
引用捕獲必須保證捕獲組的順序不變。如今開發者能夠給捕獲組命名了,有了名字之後,引用起來更加肯定。
'<App>hello regex</App>'.match(/<(?<tag>[a-zA-Z]+)>.*<\/\k<tag>>/); // ["<App>hello regex</App>", "App", index: 0, input: "<App>hello regex</App>", groups: {tag: "App"}]
在捕獲組內部最前面加上?<key>
,它就被命名了。使用\k<key>
語法就能夠引用已經命名的捕獲組。
是否是很簡單?
一般狀況下,開發者只是想在正則中將某些字符當成一個總體看待。捕獲組很棒,可是它作了額外的事情,確定須要額外的內存佔用和計算資源。因而正則又有了非捕獲組的概念。
'@abc'.match(/@(abc)/); // ["@abc", "abc", index: 0, input: "@abc", groups: undefined] '@abc'.match(/@(?:abc)/); // ["@abc", index: 0, input: "@abc", groups: undefined]
只要在圓括號內最前面加上?:
標識,就是告訴正則引擎:我只要這個總體,不須要它的引用,你就別費勁了。從上面的例子也能夠看出來,match
方法返回的結果有些許不同。
我的觀點:我以爲正則的捕獲設計應該反過來,默認不捕獲,加上?:
標識後才捕獲。由於大多數時候開發者是不須要捕獲的,可是它又懶得加?:
標識,會有些許性能浪費。
有時候開發者須要在正則中使用或者
。
'高圓圓'.match(/陳喬恩|高圓圓/); // ["高圓圓", index: 0, input: "高圓圓", groups: undefined]
|
就表明或者
。字符組其實也是一個多選結構,可是它們倆有本質區別。字符組最終只能匹配一個字符,而分支匹配的是左邊全部的字符或者右邊全部的字符。
咱們來看一個例子:
'我喜歡高圓圓'.match(/我喜歡陳喬恩|高圓圓/); // ["高圓圓", index: 3, input: "我喜歡高圓圓", groups: undefined]
由於|
是將左右兩邊一切兩半,而後匹配左邊或者右邊。因此上面的正則顯然達不到咱們想要的效果。這個時候就須要一個東西來縮小分支的範圍。誒,你可能已經想到了:
'我喜歡高圓圓'.match(/我喜歡(?:陳喬恩|高圓圓)/); // ["我喜歡高圓圓", index: 0, input: "我喜歡高圓圓", groups: undefined]
沒錯,就是圓括號。
正則中有一些元字符,它不匹配字符,而是匹配一個位置。好比以前提到的^
和$
。^
的意思是說這個位置應該是文本開始的位置。
正則還有一些比較高級的匹配位置的語法,它匹配的是:在這個位置以前或以後應該有什麼內容。
零寬(zero-width)是什麼意思?指的就是它匹配一個位置,自己沒有寬度。
斷言(assertion)是什麼意思?指的是一種判斷,斷言以前或以後應該有什麼或應該沒有什麼。
所謂的確定就是判斷有什麼,而不是判斷沒有什麼。
而先行指的是向前看(lookahead),斷言的這個位置是爲前面的規則服務的。
語法很簡單:圓括號內最左邊加上?=
標識。
'CoffeeScript JavaScript javascript'.match(/\b\w{4}(?=Script\b)/); // ["Java", index: 13, input: "CoffeeScript JavaScript javascript", groups: undefined]
上面匹配的是四個字母,這四個字母要知足如下條件:緊跟着的應該是Script
字符串,並且Script
字符串應該是單詞的結尾部分。
因此,零寬確定先行斷言的意思是:如今有一段正則語法,用這段語法去匹配給定的文本。可是,知足條件的文本不只要匹配這段語法,緊跟着它的必須是一個位置,這個位置又必須知足一段正則語法。
說的再直白點,我要匹配一段文本,可是這段文本後面必須緊跟着另外一段特定的文本。零寬確定先行斷言就是一個界碑,我要知足前面和後面全部的條件,可是我只要前面的文本。
咱們來看另外一種狀況:
'CoffeeScript JavaScript javascript'.match(/\b\w{4}(?=Script\b)\w+/); // ["JavaScript", index: 13, input: "CoffeeScript JavaScript javascript", groups: undefined]
上面的例子更加直觀,零寬確定先行斷言已經匹配過Script
一次了,後面的\w+
卻仍是能匹配Script
成功,足以說明它的零寬
特性。它爲緊貼在它前面的規則服務,而且不影響後面的匹配規則。
先行是向前看,那後行就是向後看(lookbehind)咯。
語法是圓括號內最左邊加上?<=
標識。
'演員高圓圓 將軍霍去病 演員霍思燕'.match(/(?<=演員)霍\S+/); // ["霍思燕", index: 14, input: "演員高圓圓 將軍霍去病 演員霍思燕", groups: undefined]
一個正則能夠有多個斷言:
'演員高圓圓 將軍霍去病 演員霍思燕'.match(/(?<=演員)霍.+?(?=\s|$)/); // ["霍思燕", index: 14, input: "演員高圓圓 將軍霍去病 演員霍思燕", groups: undefined]
確定是判斷有什麼,否認就是判斷沒有什麼咯。
語法是圓括號內最左邊加上?!
標識。
'TypeScript Perl JavaScript'.match(/\b\w{4}(?!Script\b)/); // ["Perl", index: 11, input: "TypeScript Perl JavaScript", groups: undefined]
語法是圓括號最左邊加上?<!
標識。
'演員高圓圓 將軍霍去病 演員霍思燕'.match(/(?<!演員)霍\S+/); // ["霍去病", index: 8, input: "演員高圓圓 將軍霍去病 演員霍思燕", groups: undefined]
正則表達式除了主體語法,還有若干可選的模式修飾符。
寫法就是將修飾符安插在正則主體的尾巴上。好比這樣:/abc/gi
。
g
是global
的縮寫。默認狀況下,正則從左向右匹配,只要匹配到告終果就會收工。g
修飾符會開啓全局匹配模式,找到全部匹配的結果。
'演員高圓圓 將軍霍去病 演員霍思燕'.match(/(?<=演員)\S+/); // ["高圓圓", index: 2, input: "演員高圓圓 將軍霍去病 演員霍思燕", groups: undefined] '演員高圓圓 將軍霍去病 演員霍思燕'.match(/(?<=演員)\S+/g); // ["高圓圓", "霍思燕"]
i
是ignoreCase
的縮寫。默認狀況下,/z/
是沒法匹配Z
的,因此咱們有時候不得不這樣寫:/[a-zA-Z]/
。i
修飾符能夠全局忽略大小寫。
不少時候咱們不在意文本是大寫、小寫仍是大小寫混寫,這個修飾符仍是頗有用的。
'javascript is great'.match(/JavaScript/); // null 'javascript is great'.match(/JavaScript/i); // ["javascript", index: 0, input: "javascript is great", groups: undefined]
m
是multiline
的縮寫。這個修飾符有特定起做用的場景:它要和^
和$
搭配起來使用。默認狀況下,^
和$
匹配的是文本的開始和結束,加上m
修飾符,它們的含義就變成了行的開始和結束。
` abc xyz `.match(/xyz/); // ["xyz", index: 5, input: "↵abc↵xyz↵", groups: undefined] ` abc xyz `.match(/^xyz$/); // null ` abc xyz `.match(/^xyz$/m); // ["xyz", index: 5, input: "↵abc↵xyz↵", groups: undefined]
這是ES2015的新特性。
y
是sticky
的縮寫。y
修飾符有和g
修飾符重合的功能,它們都是全局匹配。因此重點在sticky
上,怎麼理解這個粘連
呢?
g
修飾符不挑食,匹配完一個接着匹配下一個,對於文本的位置沒有要求。可是y
修飾符要求必須從文本的開始實施匹配,由於它會開啓全局匹配,匹配到的文本的下一個字符就是下一次文本的開始。這就是所謂的粘連。
'a bag with a tag has a mag'.match(/\wag/g); // ["bag", "tag", "mag"] 'a bag with a tag has a mag'.match(/\wag/y); // null 'bagtagmag'.match(/\wag/y); // ["bag", index: 0, input: "bagtagmag", groups: undefined] 'bagtagmag'.match(/\wag/gy); // ["bag", "tag", "mag"]
有人確定發現了貓膩:你不是說y
修飾符是全局匹配麼?看上面的例子,單獨一個y
修飾符用match方法怎麼並非全局匹配呢?
誒,這裏說來就話長了。
長話短說呢,就涉及到y
修飾符的本質是什麼。它的本質有二:
lastIndex
位置開始新的匹配。lastIndex是什麼?它是正則表達式的一個屬性,若是是全局匹配,它用來標註下一次匹配的起始點。這纔是粘連的本質所在。不知道大家發現什麼了沒有:lastIndex是正則表達式的一個屬性。而上面例子中的match方法是做用在字符串上的,都沒有lastIndex屬性,休怪人家工做不上心。
const reg = /\wag/y; reg.exec('bagtagmag'); // ["bag", index: 0, input: "bagtagmag", groups: undefined] reg.exec('bagtagmag'); // ["tag", index: 3, input: "bagtagmag", groups: undefined] reg.exec('bagtagmag'); // ["mag", index: 6, input: "bagtagmag", groups: undefined]
我們換成正則方法exec,屢次執行,正則的lastIndex在變,匹配的結果也在變。全局匹配無疑了吧。
這是ES2018的新特性。
s
不是dotAll
的縮寫。s
修飾符要和.
搭配使用,默認狀況下,.
匹配除了換行符以外的任意單個字符,然而它尚未強大到無所不能的地步,因此正則索性給它開個掛。
s
修飾符的做用就是讓.
能夠匹配任意單個字符。
s
是singleline
的縮寫。
` abc xyz `.match(/c.x/); // null ` abc xyz `.match(/c.x/s); // ["c↵x", index: 3, input: "↵abc↵xyz↵", groups: undefined]
這是ES2015的新特性。
u
是unicode
的縮寫。有一些Unicode字符超過一個字節,正則就沒法正確的識別它們。u
修飾符就是用來處理這些不常見的狀況的。
'𠮷'.match(/^.$/); // null '𠮷'.match(/^.$/u); // ["𠮷", index: 0, input: "𠮷", groups: undefined]
𠮷
念jí
,與吉
同義。
筆者對Unicode認識尚淺,這裏不過多展開。
本文是『horseshoe·Regex專題』系列文章之一,後續會有更多專題推出
GitHub地址: https://github.com/veedrin/horseshoe
博客地址(文章排版真的很漂亮): https://veedrin.com
若是以爲對你有幫助,歡迎來GitHub點Star或者來個人博客親口告訴我
👉 語法
👉 方法
👉 引擎