天天看點

手寫一個詞法分析器前言intellij plugin詞法解析總結

手寫一個詞法分析器前言intellij plugin詞法解析總結

前言

最近大部分時間都在撸

Python

,其中也會涉及到将資料庫表轉換為

Python

ORM

架構的

Model

,但我們并沒有找到一個合适的工具來做這個意義不大的”體力活“,是以每次建立表後大家都是根據自己的表結構手寫一遍

Model

一兩張表還好,一旦 10 幾張表都要寫一遍時那痛苦隻有自己知道;這時程式員的 slogan 再次印證:一切毫無意義的體力勞動終将被計算機取代。

intellij plugin

既然沒有現成的工具那就自己寫一個吧,示範效果如下:

手寫一個詞法分析器前言intellij plugin詞法解析總結

考慮到我們主要是用

PyCharm

開發,正好

jetbrains

也提供了

SDK

用于開發插件,是以

UI

層面可以不用額外考慮了。

使用流程很簡單,隻需要導入

DDL

語句就可以生成

Python

所需要的

Model

代碼。

例如導入以下 DDL:

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `userName` varchar(20) DEFAULT NULL COMMENT '使用者名',
  `password` varchar(100) DEFAULT NULL COMMENT '密碼',
  `roleId` int(11) DEFAULT NULL COMMENT '角色ID',
  PRIMARY KEY (`id`),  
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8
           

便會生成對應的 Python 代碼:

class User(db.Model):
    __tablename__ = 'user'
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    userName = db.Column(db.String)  # 使用者名
    password = db.Column(db.String)  # 密碼
    roleId = db.Column(db.Integer)  # 角色ID
           

詞法解析

仔細對比源檔案及目标代碼會很容易找出規律,無非就是解析出表名、字段、及字段的屬性(是否為主鍵、類型、長度),最後再轉換為

Python

所需要的模闆即可。

在我動手之前我認為是非常簡單的,無非就是解析字元串,但實際上手後發現不是那麼回事;主要是有以下幾個問題:

  1. 如何識别出表名稱?
  2. 同樣的如何識别出字段名稱,同時還得關聯上該字段的類型、長度、注釋。
  3. 如何識别出主鍵?

總結一句話,如何通過一系列規則識别出一段字元串中的關鍵資訊,這同樣也是 MySQL Server 所做的事情。

在開始真正解析 DDL 之前,先來看下一段簡單的腳本如何解析:

x = 20
           

按照我們平時開發的經驗,這條語句分為以下幾部分:

  • x

     表示變量
  • =

     表示指派符号
  • 20

     表示指派結果

是以我們對這段腳本的解析結果應當為:

VAR      x
GE         =
VAL      100
           

這個解析過程在編譯原理中稱為”詞法解析“,可能大家聽到

編譯原理

這幾個字就頭大(我也是);對于剛才那段腳本我們可以編寫一個非常簡單的詞法解析器生成這樣的結果。

狀态遷移

再開始之前先捋一下思路,可以看到上文的結果中通過

VAR

表示變量、

GE

表示指派符号 ”=“、

VAL

表示指派結果,現在需要重點記住這三個狀态。

在依次讀取字元解析時,程式就是在這幾個狀态中來回切換,如下圖:

手寫一個詞法分析器前言intellij plugin詞法解析總結
  1. 預設為初始狀态。
  2. 當字元為字母時進入 

    VAR

     狀态。
  3. 當字元為 ”=“ 符号時進入 

    GE

     狀态。
手寫一個詞法分析器前言intellij plugin詞法解析總結

同理,當不滿足這幾個狀态時候又會回到初始進而再次确認新的狀态。

光看圖有點抽象,直接來看核心代碼:

public class Result{
        public TokenType tokenType ;
        public StringBuilder text = new StringBuilder();
    }
           

首先定義了一個結果類,收集最終的解析結果;其中的

TokenType

就對應了圖中的三種狀态,簡單的用枚舉值來表示。

public enum TokenType {
    INIT,
    VAR,
    GE,
    VAL
}
           

首先對應到第一張圖:初始化狀态。

需要對目前解析的字元定義一個

TokenType

手寫一個詞法分析器前言intellij plugin詞法解析總結

和圖中描述的流程一緻,判斷目前字元給定一個狀态即可。

接着對應到第二張圖:狀态之間的轉換。

手寫一個詞法分析器前言intellij plugin詞法解析總結

會根據不同的狀态進入不同的

case

,在不同的

case

中判斷是否應當跳轉到其他狀态(進入

INIT

狀态後會重新生成狀态)。

舉個例子:

x=20

:

首選會進入

VAR

狀态,接着下一個字元為空格,自然在 38 行中重新進入初始狀态,導緻再次确定下一個字元

=

進入

GE

狀态。

當腳本為

ab=30

: 第一個字元為 a 也是進入

VAR

狀态,第二個字元為 b,依然為字母,是以進入 36 行,狀态不會改變,同時将 b 這個字元追加進來;後續步驟就和上一個例子一緻了。

多說無益,建議大家自己跑一下單測就會明白:https://github.com/crossoverJie/sqlalchemy-transfer/blob/master/src/test/java/top/crossoverjie/plugin/core/lab/TestLexerTest.java

手寫一個詞法分析器前言intellij plugin詞法解析總結
手寫一個詞法分析器前言intellij plugin詞法解析總結

DDL 解析

簡單的解析完成後來看看

DDL

這樣的腳本應當如何解析:

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `userName` varchar(20) DEFAULT NULL COMMENT '使用者名',
  `password` varchar(100) DEFAULT NULL COMMENT '密碼',
  `roleId` int(11) DEFAULT NULL COMMENT '角色ID',
  PRIMARY KEY (`id`),  
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8
           

原理類似,首先還是要看出規律(也就是文法):

  • 表名是第一行語句,同時以 

    CREATE TABLE

     開頭。
  • 每一個字段的資訊(名稱、類型、長度、備注)都是以 "`" 符号開頭 "," 結尾。
  • 主鍵是以 PRIMART 字元串開頭的字段,以 

    )

     結尾。

根據我們需要解析的資料種類,我這裡定義了這個枚舉:

手寫一個詞法分析器前言intellij plugin詞法解析總結

然後在初始化類型時進行判斷指派:

手寫一個詞法分析器前言intellij plugin詞法解析總結

由于需要解析的資料不少,是以這裡的判斷條件自然也就多了。

遞歸解析

針對于

DDL

的文法規則,我們這裡還有需要有特殊處理的地方;比如解析具體字段資訊時如何關聯起來?

舉個例子:

`userName` varchar(20) DEFAULT NULL COMMENT '使用者名',
`password` varchar(100) DEFAULT NULL COMMENT '密碼',
           

這裡我們解析出來的資料得有一個映射關系:

手寫一個詞法分析器前言intellij plugin詞法解析總結

是以我們隻能一個字段的全部資訊解析完成并且關聯好之後才能解析下一個字段。

于是這裡我采用了遞歸的方式進行解析(不一定是最好的,歡迎大家提出更優的方案)。

} else if (value == '`' && pStatus == Status.BASE_INIT) {
    result.tokenType = DDLTokenType.FI;
    result.text.append(value);
} 
           

當目前字元為 ”`“ 符号時,将狀态置為 "FI"(FieldInfo),同時當解析到為 "," 符号時便進入遞歸處理。

手寫一個詞法分析器前言intellij plugin詞法解析總結

可以了解為将這一段字元串單獨提取出來處理:

`userName` varchar(20) DEFAULT NULL COMMENT '使用者名',
           

接着再将這段字元遞歸調用目前方法再次進行解析,這時便按照字段名稱、類型、長度、注釋的規則解析即可。

同時既然存在遞歸,還需要将子遞歸的資料關聯起來,是以我在傳回結果中新增了一個

pid

的字段,這個也容易了解。

預設值為 0,一旦遞歸後便自增 +1,保證每次遞歸的資料都是唯一的。

用同樣的方法在解析主鍵時也是先将整個字元串提取出來:

PRIMARY KEY (`id`)
           

隻不過是 "P" 打頭 ")" 結尾。

} else if (value == 'P' && pStatus == Status.BASE_INIT) {
    result.tokenType = DDLTokenType.P_K;
    result.text.append(value);
} 
           
手寫一個詞法分析器前言intellij plugin詞法解析總結

也是将整段字元串遞歸解析,再遞歸的過程中進行狀态切換

P_K--->P_K_V

最終擷取到主鍵。

手寫一個詞法分析器前言intellij plugin詞法解析總結

是以通過對剛才那段

DDL

解析得到的結果如下:

手寫一個詞法分析器前言intellij plugin詞法解析總結

這樣每個字段也通過了

pid

進行了區分關聯。

是以現在隻需要對這個詞法解析器進行封裝,便可以提供一個簡單的

API

來擷取表中的資料了。

手寫一個詞法分析器前言intellij plugin詞法解析總結

總結

到此整個詞法解析器的全部内容都已經完成了,雖然實作的是一個小功能,但我自己花的時間可不少,其中光複習編譯原理就讓人頭疼。

但這還隻是整個編譯語言知識點的冰山一角,後續還有文法、語義、中間、目标代碼等一系列内容,都是一個比一個難啃。

其實我相信大多數人和我想法一樣,這個東西太底層而且枯燥,真正從事這方面工作的也都是鳳毛麟角,是以花這時間幹啥呢?

是以我也決定這個弄完後就棄坑啦。

手寫一個詞法分析器前言intellij plugin詞法解析總結

哈哈,開個玩笑,或許有生之年自己也能實作一門程式設計語言,當老了和兒子吹牛時也能有點資本。

本文所有源碼及插件位址:

https://github.com/crossoverJie/sqlalchemy-transfer

大家看完記得點贊分享一鍵三連哦

手寫一個詞法分析器前言intellij plugin詞法解析總結
手寫一個詞法分析器前言intellij plugin詞法解析總結

更多推薦内容

↓↓↓

《手把手實作延時消息》

《如何參與一個頂級的開源項目》

《也許是東半球直接地氣的分庫分表實踐了》

《What?一個 Dubbo 服務啟動要兩個小時!》

《又一次生産 CPU 高負載的排查實踐》

《沒那麼簡單的線程池》

《一次分表踩坑的探讨》

《『并發包入坑指北』之阻塞隊列》

《一緻性 Hash 算法的實際應用》

《利用政策模式優化過多 if else》

《長連接配接的心跳及重連設計》

《為自己搭建一個分布式 IM(即時通訊) 系統》

《一次生産 CPU 100% 排查優化實踐》

《沒錯,老闆讓我寫個 BUG!》

《判斷一個元素在億級資料中是否不存在》

《設計一個可插拔的 IOC 容器》

《一次 HashSet 所引起的并發問題》

《一次記憶體溢出排查實踐》

《如何優雅的使用和了解線程池》