0

0

PHP也可以实现词法分析与自定义语言!

藏色散人

藏色散人

发布时间:2022-01-12 15:08:10

|

4939人浏览过

|

来源于learnku

转载

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

一、分析需求

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

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

二、实现需求

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

", "<", "!", "(", ")", "{", "}", ",", ";"
    ];
    // 源代码
    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;
        }
    }
}

三、如何使用

立即学习PHP免费学习笔记(深入)”;

require __DIR__ . "/vendor/autoload.php";
// 定义一段代码
$code = << 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));

生成词

Kuwebs企业网站管理系统3.1.5 UTF8
Kuwebs企业网站管理系统3.1.5 UTF8

酷纬企业网站管理系统Kuwebs是酷纬信息开发的为企业网站提供解决方案而开发的营销型网站系统。在线留言模块、常见问题模块、友情链接模块。前台采用DIV+CSS,遵循SEO标准。 1.支持中文、英文两种版本,后台可以在不同的环境下编辑中英文。 3.程序和界面分离,提供通用的PHP标准语法字段供前台调用,可以为不同的页面设置不同的风格。 5.支持google地图生成、自定义标题、自定义关键词、自定义描

下载
[{
    "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速学教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

php

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
2026赚钱平台入口大全
2026赚钱平台入口大全

2026年最新赚钱平台入口汇总,涵盖任务众包、内容创作、电商运营、技能变现等多类正规渠道,助你轻松开启副业增收之路。阅读专题下面的文章了解更多详细内容。

54

2026.01.31

高干文在线阅读网站大全
高干文在线阅读网站大全

汇集热门1v1高干文免费阅读资源,涵盖都市言情、京味大院、军旅高干等经典题材,情节紧凑、人物鲜明。阅读专题下面的文章了解更多详细内容。

40

2026.01.31

无需付费的漫画app大全
无需付费的漫画app大全

想找真正免费又无套路的漫画App?本合集精选多款永久免费、资源丰富、无广告干扰的优质漫画应用,涵盖国漫、日漫、韩漫及经典老番,满足各类阅读需求。阅读专题下面的文章了解更多详细内容。

50

2026.01.31

漫画免费在线观看地址大全
漫画免费在线观看地址大全

想找免费又资源丰富的漫画网站?本合集精选2025-2026年热门平台,涵盖国漫、日漫、韩漫等多类型作品,支持高清流畅阅读与离线缓存。阅读专题下面的文章了解更多详细内容。

12

2026.01.31

漫画防走失登陆入口大全
漫画防走失登陆入口大全

2026最新漫画防走失登录入口合集,汇总多个稳定可用网址,助你畅享高清无广告漫画阅读体验。阅读专题下面的文章了解更多详细内容。

13

2026.01.31

php多线程怎么实现
php多线程怎么实现

PHP本身不支持原生多线程,但可通过扩展如pthreads、Swoole或结合多进程、协程等方式实现并发处理。阅读专题下面的文章了解更多详细内容。

1

2026.01.31

php如何运行环境
php如何运行环境

本合集详细介绍PHP运行环境的搭建与配置方法,涵盖Windows、Linux及Mac系统下的安装步骤、常见问题及解决方案。阅读专题下面的文章了解更多详细内容。

0

2026.01.31

php环境变量如何设置
php环境变量如何设置

本合集详细讲解PHP环境变量的设置方法,涵盖Windows、Linux及常见服务器环境配置技巧,助你快速掌握环境变量的正确配置。阅读专题下面的文章了解更多详细内容。

0

2026.01.31

php图片如何上传
php图片如何上传

本合集涵盖PHP图片上传的核心方法、安全处理及常见问题解决方案,适合初学者与进阶开发者。阅读专题下面的文章了解更多详细内容。

2

2026.01.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PHP课程
PHP课程

共137课时 | 10.5万人学习

JavaScript ES5基础线上课程教学
JavaScript ES5基础线上课程教学

共6课时 | 11.2万人学习

PHP新手语法线上课程教学
PHP新手语法线上课程教学

共13课时 | 0.9万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号