利用PHP实现词法分析器与自定义语言

目录
  • 一、分析需求
  • 二、实现需求
  • 三、如何使用
  • 四、使用场景

之前项目有一个需求,业务人员使用中文编写一些自定义公式,然后需要我们后台执行将结果返回到界面上,于是就基于有限状态机写了这个词法分析器,比较简单,希望能够抛砖引玉。

一、分析需求

输入中文公式,返回结果,比如:

现有薪资=10000;
个税起点=3000;
当前年份=2021;
如果(当前年份=2022){
    个税起点=5000;
}
返回 (现有薪资-个税起点) * 0.2;

二、实现需求

最初的想法是使用字符串替换的方式,将中文关键字替换成 php 的关键字,然后调用 eval 执行,这样确实也是可以的,但是总觉得不是很美丽,并且不能实现动态解析。就想着自己实现一个简单的词法分析,然后结合 ast 将词法转换成 php 代码执行,岂不快哉。当前版本没有用到抽象语法树来生成代码,全部使用字符串拼接。

<?php

/**

 * Class Lexer

 * @package Sett\OaLang

 * 词法分析器
   */
   class Lexer {
   // 内置关键字集合
   public $keywordList = [];
   // 内置操作符集合
   public $operatorList = [
       "+", "-", "*", "/", "=", ">", "<", "!", "(", ")", "{", "}", ",", ";"
   ];
   // 源代码
   private $input;
   // 当前的字符
   private $currChar;
   // 当前字符位置
   private $currCharPos = 0;
   // 结束符
   private $eof = "eof";
   // 当前编码
   private $currEncode  = "UTF-8";

   // 内置关键字
   public const VAR = "variable";
   public const STR = "string";
   public const KW  = "keyword";
   public const OPR = "operator";
   public const INT = "integer";
   public const NIL = "null";

    /**
     * Lexer constructor.
     * @param string $input
     */
    public function __construct(string $input) {
        $this->input    = $input;
        $this->currChar = mb_substr($this->input, $this->currCharPos, 1);
    }
    
    /**
     * @param array $keywordList
     */
    public function setKeywordList($keywordList) {
        $this->keywordList = $keywordList;
    }
    
    /**
     * @return array
     * @throws Exception
     */
    public function parseInput() {
        if ($this->input == "") {
            throw new Exception("code can not be empty");
        }
        $tokens = [];
        do {
            $token = $this->nextToken();
            if ($token["type"] != "eof") {
                $tokens[] = $token;
            }
            if ($token["type"] == self::KW) {
                $tokens[] = $this->makeToken(self::NIL, " ");
            }
        } while ($token["type"] != "eof");
        return $tokens;
    }
    
    /**
     * @return array
     */
    public function nextToken() {
        $this->skipBlankChar();
        $this->currChar == "" && $this->currChar = $this->eof;
        if ($this->isCnLetter()) {
            $word = $this->matchUntilNextCharIsNotCn();
            if ($this->isKeyword($word)) {
                $this->currCharPos -= 1;
                return $this->currToken(static::KW, $word);
            }
            // 不是关键字的全部归为变量
            return $this->makeToken(static::VAR, $word);
        }
        // 如果是操作符
        if ($this->isOperator()) {
            return $this->currToken(static::OPR, $this->currChar);
        }
        // 如果是数字
        if ($this->isNumber()) {
            return $this->currToken(static::INT, $this->currChar);
        }
        // 如果是字符串
        if ($str = $this->isStr()) {
            return $this->currToken(static::STR, $str);
        }
        // 如果是变量
        if ($this->isVar()) {
            $word = $this->matchVar();
            if ($this->isKeyword($word)) {
                return $this->currToken(static::KW, $word);
            }
            return $this->makeToken(static::VAR, $word);
        }
        if ($this->currChar == $this->eof) {
            return $this->currToken('eof', $this->currChar);
        }
        return $this->currToken(static::VAR, $this->currChar);
    }
    
    /**
     * @param string $input
     * @return string
     */
    private function matchVar(string $input = "") {
        $word = $input ?: '';
        while ($this->isVar()) {
            $word .= $this->currChar;
            $this->nextChar();
        }
        return $word;
    }
    
    /**
     * @return bool
     * 是否为普通变量
     */
    private function isVar() {
        return $this->isCnLetter() || $this->isEnLetter();
    }

    /**
     * 跳过空白字符
     */
    private function skipBlankChar() {
        while (ord($this->currChar) == 10 ||
            ord($this->currChar) == 13 ||
            ord($this->currChar) == 32) {
            $this->nextChar();
        }
    }
    
    /**
     * @param string $type
     * @param $word
     * @return array
     * 记录当前token和下一个字符
     */
    private function currToken(string $type, $word) {
        $token = $this->makeToken($type, $word);
        $this->nextChar();
        return $token;
    }
    
    /**
     * @param string $type
     * @param string $char
     * @return array
     */
    private function makeToken(string $type, string $char) {
        return ["type" => $type, "char" => $char, "pos" => $this->currCharPos];
    }

    /**
     * @return bool
     * 判断是否是英文字符
     */
    private function isEnLetter() {
        if ($this->currChar == "" || $this->currChar == $this->eof) {
            return false;
        }
        $ord = mb_ord($this->currChar, $this->currEncode);
        if ($ord > ord('a') && $ord < ord('z')) {
            return true;
        }
        return false;
    }
    
    /**
     * @return false|int
     * 是否中文字符
     */
    private function isCnLetter() {
        return preg_match("/^[\x{4e00}-\x{9fa5}]+$/u", $this->currChar);
    }
    
    /**
     * @return bool
     * 是否为数字
     */
    private function isNumber() {
        return is_numeric($this->currChar);
    }
    
    /**
     * @return bool
     * 是否是字符串
     */
    private function isStr() {
        return $this->matchCompleteStr();
    }
    
    /**
     * @return string
     * 匹配完整字符串
     */
    private function matchCompleteStr() {
        $char = "";
        if ($this->currChar == "\"") {
            $this->nextChar();
            while ($this->currChar != "\"") {
                if ($this->currChar != "\"") {
                    $char .= $this->currChar;
                }
                $this->nextChar();
            }
            return $char;
        }
        return $char;
    }
    
    /**
     * @return bool
     * 是否是操作符
     */
    private function isOperator() {
        return in_array($this->currChar, $this->operatorList);
    }
    
    /**
     * @return string
     * 匹配中文字符
     */
    private function matchUntilNextCharIsNotCn() {
        $char = "";
        while ($this->isCnLetter()) {
            $char .= $this->currChar;
            $this->nextChar();
        }
        return $char;
    }
    
    /**
     * @return void 获取下一个字符
     * 获取下一个字符
     */
    private function nextChar() {
        $this->currCharPos += 1;
        $this->currChar    = mb_substr($this->input, $this->currCharPos, 1);
        if ($this->currChar == "") {
            $this->currChar = $this->eof;
        }
    }
    
    /**
     * @param string $input
     * @return bool
     * 是否是关键字
     */
    private function isKeyword(string $input) {
        return ($this->keywordList[$input] ?? "") != "";
    }
    
    public function convert(array $tokens) {
        $code = "";
        foreach ($this->lexerIterator($tokens) as $generator) {
            switch ($generator["type"]) {
                case static::KW:
                    $code .= $this->keywordList[$generator["char"]];
                    break;
                case static::VAR:
                    $code .= sprintf("$%s", $generator["char"]);
                    break;
                case static::OPR:
                    $code .= $this->replace($generator["char"]);
                    break;
                case static::INT:
                    $code .= $generator["char"];
                    break;
                case static::STR:
                    $code .= sprintf("\"%s\"", $generator["char"]);
                    break;
                default:
                    $code .= $generator["char"];
            }
        }
        return $code;
    }
    
    private function replace(string $char) {
        return str_replace("+", ".", $char);
    }
    
    /**
     * @param array $tokens
     * @return \Generator
     */
    private function lexerIterator(array $tokens) {
        foreach ($tokens as $index => $token) {
            yield $token;
        }
    }

}

三、如何使用

require __DIR__ . "/vendor/autoload.php";
// 定义一段代码
$code = <<<EOF
姓名="腕豪";
问候="你好啊";
地址=(1+2) * 3;
如果(地址 > 3){
    地址=1;
}否则{
    地址="艾欧尼亚"
}
说话 = ("我"+"爱")+"你";
返回 姓名+年龄;
EOF;
$lexer = new Lexer($code);
// 自定义你的关键字
$kwMap = [
    "如果" => "if", "否则" => "else", "返回" => "return", "否则如果" => "elseif"
];
$lexer->setKeywordList($kwMap);
// 这里是生成的词
$tokens = $lexer->parseInput();
// 将生成的词转成php,当然你也可以尝试用php-parse转ast再转成php,这里只是简单的拼接
var_dump($lexer->convert($tokens));

生成词

[{
    "type": "variable",
    "char": "姓名",
    "pos": 2
}, {
    "type": "operator",
    "char": "=",
    "pos": 2
}, {
    "type": "string",
    "char": "腕豪",
    "pos": 7
}, {
    "type": "operator",
    "char": ";",
    "pos": 8
}, {
    "type": "variable",
    "char": "问候",
    "pos": 13
}, {
    "type": "operator",
    "char": "=",
    "pos": 13
}, {
    "typ e": "string",
    "char": "你好啊",
    "pos": 17
}, {
    "type": "operator",
    "char": ";",
    "pos": 18
}, {
    "type": "variable",
    "char": "地址",
    "pos": 23
}, {
    "type": "operator",
    "char": "=",
    "pos": 23
}, {
    "type": "operator",
    "char": "(",
    "pos": 24
}, {
    "type": "integer",
    "char": "1",
    "pos": 25
}, {
    "type": "operator",
    "char": " +",
    "pos": 26
}, {
    "type": "integer",
    "char": "2",
    "pos": 27
}, {
    "type": "operator",
    "char": ")",
    "pos": 28
}, {
    "type": "operator",
    "char": "*",
    "pos": 30
}, {
    "type": "integer",
    "char": "3",
    "pos": 32
}, {
    "type": "operator",
    "char": ";",
    "pos": 33
}, {
    "type": "keyword",
    "char": "如果",
    "pos": 37
}, {
    "type": "nul l",
    "char": " ",
    "pos": 38
}, {
    "type": "operator",
    "char": "(",
    "pos": 38
}, {
    "type": "variable",
    "char": "地址",
    "pos": 41
}, {
    "type": "operator",
    "char": ">",
    "pos": 42
}, {
    "type": "integer",
    "char": "3",
    "pos": 44
}, {
    "type": "operator",
    "char": ")",
    "pos": 45
}, {
    "type": "operator",
    "char": "{",
    "pos": 46
}, {
    "type": "variable",
    "char": "地址",
    "pos": 55
}, {
    "type": "operator",
    "char": "=",
    "pos": 55
}, {
    "type": "integer",
    "char": "1",
    "pos": 56
}, {
    "type": "operator",
    "char": ";",
    "pos": 57
}, {
    "type": "operator",
    "char": "}",
    "pos": 60
}, {
    "type": "keyword",
    "char": "否则",
    "pos": 62
}, {
    "type": "null",
    "char ": " ",
    "pos": 63
}, {
    "type": "operator",
    "char": "{",
    "pos": 63
}, {
    "type": "variable",
    "char": "地址",
    "pos": 72
}, {
    "type": "operator",
    "char": "=",
    "pos": 72
}, {
    "type": "string",
    "char": "艾欧尼亚",
    "pos": 78
}, {
    "type": "operator",
    "char": ";",
    "pos": 79
}, {
    "type": "operator",
    "char": "}",
    "pos": 82
}, {
    "type": "variable",
    "char": "说话",
    "pos": 87
}, {
    "type": "operator",
    "char": "=",
    "pos": 88
}, {
    "type": "operator",
    "char": "(",
    "pos": 90
}, {
    "type": "string",
    "char": "我",
    "pos": 93
}, {
    "type": "operator",
    "char": "+",
    "pos": 94
}, {
    "type": "string",
    "char": "爱",
    "pos": 97
}, {
    "type": "operator",
    "char": ")",
    "pos": 98
}, {
    "type": "operator",
    "char": "+",
    "pos": 99
}, {
    "type": "string",
    "char": "你",
    "pos": 102
}, {
    "type": "operator",
    "char": ";",
    "pos": 103
}, {
    "type": "keyword",
    "char": "返回",
    "pos": 107
}, {
    "type": "null",
    "char": " ",
    "pos": 108
}, {
    "type": "variable",
    "char": "姓名",
    "pos": 111
}, {
    "typ e": "operator",
    "char": "+",
    "pos": 111
}, {
    "type": "variable",
    "char": "年龄",
    "pos": 114
}, {
    "type": "operator",
    "char": ";",
    "pos": 114
}]

输出:

$姓名="腕豪";$问候="你好啊";$地址=(1.2)*3;if ($地址>3){$地址=1;}else {$地址="艾欧尼亚";}$说话=("我"."爱")."你";return $姓名.$年龄;

能执行吗?当然能。还存在一些小 bug,不想改了。

四、使用场景

什么,居然有人说没什么用?oa 系统总有用到的时候。

到此这篇关于利用PHP实现词法分析器与自定义语言的文章就介绍到这了,更多相关PHP词法分析器内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • php实现自动获取生成文章主题关键词功能的深入分析

    以前写程序一直在逃避这个问题,tag什么的都是要求使用程序的人自行输入,对于某些懒人及为了程序的体验,则是希望可以有自动生成文章关键词,自动获取文章tag的类似功能,这次为了迎接新的项目,所以捣鼓了一晚上,研究了一下这个功能.要实现自动获取关键词的功能,大概可以分成三步1,通过分词算法将标题和内容分别进行分割,提取出关键词和频度.当前主要的两个算法是中科院的ICTCLAS和隐马尔可夫模型.但这两个都太高端,有一定的门槛,且都是只支持C++/JAVA.基于PHP的当前有两个是值得推荐的PSCWS和

  • angularjs利用directive实现移动端自定义软键盘的示例

    最近公司项目的需求上要求我们iPad项目上一些需要输入数字的地方用我们自定义的软键盘而不是移动端设备自带的键盘,刚接到需求有点懵,因为之前没有做过,后来理了一下思路发现这东西也就那样.先看一下实现之后的效果: 实现的效果就是当点击页面中需要弹出软键盘的时候软键盘弹出,浮在页面的中间,和模态框一样的效果,可以在软键盘中输入任何数字,附带的功能有小数点.退格.清空.确定等功能.当在键盘上点击数字的时候页面中的表单中实时的添加对应的数字,上图中可以看到. 产品经理那边给的原因是iPad屏幕本来就小,如

  • 利用spring的拦截器自定义缓存的实现实例代码

    本文研究的主要是利用spring的拦截器自定义缓存的实现,具体实现代码如下所示. Memcached 是一个高性能的分布式内存对象缓存系统,用于动态Web应用以减轻数据库负载.它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高动态.数据库驱动网站的速度.本文利用Memcached 的实例和spring的拦截器实现缓存自定义的实现.利用拦截器读取自定义的缓存标签,key值的生成策略. 自定义的Cacheable package com.jeex.sci; @Target(ElementT

  • 利用Javascript实现一套自定义事件机制

    前言 事件机制为我们的web开发提供了极大的方便,使得我们能在任意时候指定在什么操作时做什么操作.执行什么样的代码. 如点击事件,用户点击时触发:keydown.keyup事件,键盘按下.键盘弹起时触发:还有上传控件中,文件加入前事件,上传完成后事件. 由于在恰当的时机会有相应的事件触发,我们能为这些事件指定相应的处理函数,就能在原本的流程中插入各种各样的个性化操作和处理,使得整个流程变得更加丰富. 诸如click.blur.focus等事件是原本的dom就直接提供的原生事件,而我们使用的一些其

  • 浅析如何利用angular结合translate为项目实现国际化

    前言 利用H5项目第一版本已经上线,话说有了第一期就有了第二期,这不要为第二期做准备了,老大发话第一件事就要利用Angular JS实现项目的国际化以及后续要借助这个框架来实现其他功能,好吧我表示没怎么接触过,这一个礼拜自己也对Angular基本的用法已经有了初步的了解以及熟悉,于是乎就有了这篇文章的产生.其实没我什么事,我也是主动请缨说交给我,因为年底了嘛,也没什么事,不急不忙的,一天也乐得清闲,还不给自己找点事做,而且还能在做的过程中能学到东西,何乐而不为呢!Angular已盛行一时,但请恕

  • iOS App开发中使用及自定义UITableViewCell的教程

    UITableView用来以表格的形式显示数据.关于UITableView,我们应该注意: (1)UITableView用来显示表格的可见部分,UITableViewCell用来显示表格的一行. (2)UITableView并不负责存储表格中的数据,而是仅仅存储足够的数据使得可以画出当前可见部分. (3)UITableView从UITableViewDelegate协议获取配置信息,从UITableViewDataSource协议获得数据信息. (4)所有的UITableView实现时实际上只有

  • ASP利用Google实现在线翻译功能

    有时候想为我们的网页提供多语言支持,如果一种语言用一张网页来做实在太麻烦了,幸好Google提供了语言工具功能,下面介绍如何利用它来实现网页多种语言之间的转换. 复制代码 代码如下: <form> <select name="lan"> <option value="en|de">英语 翻译成 德语</option> <option value="en|es">英语 翻译成 西班牙语&l

  • Django自定义分页效果

    分页功能在每个网站都是必要的,对于分页来说,其实就是根据用户的输入计算出应该显示在页面上的数据在数据库表中的起始位置. 确定分页需求: 1. 每页显示的数据条数 2. 每页显示页号链接数 3. 上一页和下一页 4. 首页和末页 效果图: 首先,利用django内置的分页功能,写分页类: from django.core.paginator import Paginator, Page # 导入django分页模块 class PageInfo(object): def __init__(self

  • A利用ASP小偷和Google实现在线翻译功能的代码

    复制代码 代码如下: 有时候想为我们的网页提供多语言支持,如果一种语言用一张网页来做实在太麻烦了,幸好Google提供了语言工具功能,下面介绍如何利用它来实现网页多种语言之间的转换. lan.htm <form> <select name="lan"> <option value="en|de">英语 翻译成 德语</option> <option value="en|es">英语 翻译

  • 如何使用JS在HTML中自定义字符串格式化

    Python中支持字符串格式化,其基本形式如下: str = "I'm %s, %s years old." % ('jack', 19) print(str) #结果: I'm jack, 19 years old. 在JavaScript中虽没有类似的方法,但我们可以利用字符串的replace方法自定义字符串的格式化方法: <script> $(function () { /*自定义字符串格式化*/ String.prototype.Format = function

  • Android自定义View实现多边形统计图示例代码

    前言   最近利用空闲时间学习了自定义View的一些知识,为了巩固,写了一个小东西,顺便分享出来,下面话不多说了,来一起看看详细的介绍吧. 简介   一个多边形统计图.边数,每个方向的值,每个点的文字等等都是可以设置的. 下面就来分析一下这个自定义View 这个view由以下几个部分组成 M层N边形 中心到各顶点的连线 填充区域 文字 @Override protected void onDraw(Canvas canvas) { if (!canDraw()) { return; } canv

随机推荐

其他