☰
编译原理大作业:词法分析器与五种语法分析器实现解析
2026/10/3 3:06:08 网站建设 项目流程

简介:北京化工大学编译原理大作业完整实践包,围绕词法分析与语法分析两大编译前端核心主题,完整实现LL1、LR0、SLR1、LR1及LALR1等多种常用算法,适合计算机相关专业在校生完成课程设计、课程作业,也可作为毕业设计初期立项与算法对照的参考。压缩包为zip格式,共56个文件、约1.21MB,主体包含Python与Java源代码(11个py、7个java),配以XML工程配置、Markdown说明文档、JS/CSS等前端文件,其中词法分析部分同时提供控制台版与带动态交互页面的Web版,便于多角度理解实现思路。资源内附文档说明与运行截图,核心代码均经过测试,下载后可先打开README.md梳理目录结构;如遇环境或运行问题,还可联系作者远程指导,降低上手门槛。目前已有164人学习使用,对希望一次看懂词法分析到多种语法分析算法实现、并快速跑通演示的同学具有较高参考价值。

1. 编译原理大作业如何一次跑通:词法分析 + 五种语法分析器的资源结构

编译原理大作业最怕的不是文法定义不出来,而是词法 token 一进语法分析就爆栈,LL1、LR0、SLR1、LR1、LALR1 五个分析器各写各的,答辩时被老师问一句“LR1 和 LALR1 到底差在哪”就冷场。这份北京化工大学的编译原理大作业,把词法分析、五种语法分析算法、源代码、文档说明和运行截图打包在一起,目标很直接:让你先拿到一套能正常运行的基准实现,再在上面改文法、做对比,而不是从零啃算法。

适合计科、软工、自动化、电子信息方向做课程设计、结课作业或毕设演示的同学,也适合刚学完上下文无关文法、想用代码验证编译原理理论的人。下载后先打开 README.md 理顺启动顺序,再对照运行截图复现输出,比自己对着教材猜实现要快得多。如果你正卡在 SLR 冲突不知道是文法问题还是代码问题,这套资源里的五个分析器能直接把差异摆在眼前。

2. 词法分析双版本:控制台 DFA 与 Web 交互版的实现逻辑和取舍

2.1 控制台版的核心:一张状态转移表加一张接受态表

词法分析在编译前端里只负责正则层面的活:把源代码切成一串 Token。资源里词法分析有两个工程,一个叫 DFA,是控制台程序,另一个 DFAServer 是 Web 工程。先说控制台版。它的目录是标准 IntelliJ Java 工程,IDE 直接打开后,入口类运行起来就是从输入读一段源码,输出 token 列表,运行截图显示的就是这个结果。

做词法分析的经典路径是先写正则规则,再转 NFA,再确定化成 DFA。但看课设代码时,更常见的是手画 DFA 后直接把它翻译成一张状态转移表。核心数据结构可以这样组织:

public class DFA { private final int[][] transitionTable; // 状态编号 x 字符类别 -> 下一个状态 private final int[] acceptState; // 每个状态是否接受,0 表示不接受 private final TokenType[] acceptType; // 接受状态对应的 token 类型 public Token nextToken(String source, int start) { int state = 0; int pos = start; int lastAcceptPos = start; int lastAcceptState = 0; while (pos < source.length()) { int col = charClassIndex(source.charAt(pos)); state = transitionTable[state][col]; if (state < 0) break; pos++; if (acceptState[state] != 0) { lastAcceptPos = pos; lastAcceptState = state; } } if (lastAcceptState > 0) { return new Token(acceptType[lastAcceptState], source.substring(start, lastAcceptPos)); } throw new LexException("无法识别字符: " + source.charAt(start)); } }

这里面的参数值得逐个说清楚。transitionTable 的横轴是状态编号,纵轴是字符类别编号,比如 0 代表数字、1 代表字母、2 代表空格、3 代表运算符;charClassIndex 只做类别映射,不参与状态判断。token 必须切在 lastAcceptPos,这是教材里强调的最长匹配原则。如果没有 lastAcceptState 这个变量,很多实现会在碰到的第一个接受态就 return,导致int32被错切成int和32两个 token。

这个控制台工程按单字符分类切分,处理大部分课设文法都够用。如果你之后想支持字符串字面量里的转义符,就得在状态转移表里加一个 IN_STRING 状态,并给双引号和反斜杠分配单独的字符类别。课设阶段一般不需要,但做毕设扩展时一定要记得同步改 charClassIndex 和状态表。

2.2 Web 交互版:JSP、Servlet 和 fastjson 组成实时演示链路

DFAServer 工程保留了词法分析的核心逻辑,但换了一套展示方式。工程里有 web/WEB-INF、index.jsp、js/css/images 目录和 fastjson-1.2.9.jar,典型的 Java Web 项目。流程是:前端把源码文本通过表单 post 到 Servlet,Servlet 调用词法分析器得到 token 列表,再序列化成 JSON 返回,前端把 JSON 渲染成表格。

一个最常见的 Servlet 写法是:

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { String source = req.getParameter("source"); List<Token> tokens = new Lexer().analyze(source); JSONObject result = new JSONObject(); result.put("tokens", tokens.stream() .map(Token::toMap) .collect(Collectors.toList())); resp.setContentType("application/json;charset=UTF-8"); resp.getWriter().write(result.toJSONString()); }

这里选用 fastjson 而不是手动拼字符串,是因为 token 里的引号、反斜杠、换行都需要转义,手拼很容易出错。fastjson 的 JSONObject 直接处理了转义,而且序列化顺序稳定,方便前端统一渲染。注意 resp.setContentType 必须写在 getWriter 之前,否则浏览器会把 JSON 当成 text/html 处理,页面显示乱码或者直接弹下载框。

浏览器端拿到的 JSON 结构大概长这样:

{"tokens": [{"id": 2, "type": "KEYWORD", "value": "int"}, ...], "error": ""}

index.jsp 负责承载输入框和结果表格,js 文件负责发起 ajax 请求和渲染,css 只做布局。这层代码没有算法逻辑,改样式的空间很大。答辩前花半小时调一下页面布局,比在算法里硬扣用户体验划算得多。

2.3 两个版本的取舍:平时用控制台调 DFA,答辩用 Web 版演示

如果时间紧凑,只做控制台版也够交作业;但手上有两个版本时,分工就非常明确。控制台版适合单步调试,能看到每一个状态跳转;Web 版适合给老师现场演示,输入两行代码点一下按钮就出结果。我的习惯是先在控制台版里把词法规则调对,再放到 Web 版里跑一遍确认输入输出一致,然后才用 Web 版截图。

对比项控制台版Web 交互版
单步调试方便不方便
演示效果一般直观
依赖环境仅 JDKJDK + Tomcat
修改词法规则改 Java 后重跑改 Java 后还要重新部署
截图素材黑白文字可定制页面

要特别提醒的是两个版本的输入格式未必完全一样。控制台版可能直接读文本文件,Web 版走 HTTP 参数。如果发现两个版本对同一段代码输出的 token 不一致,优先检查词法规则是否真的共用了同一份配置。只把核心 DFA 留在公共代码里、把读取方式和输出格式放在外层,是这类双版本工程比较稳的做法。

3. 语法分析五件套:LL1、LR0、SLR1、LR1、LALR1 的代码结构和推导顺序

语法分析部分用 Python 实现。目录里有 LL1.py、LR0.py、SLR.py、LR1.py、LALR1.py,以及 public.py、struct.py、container.py。五个分析器能在同一套文法结构上共存,靠的是公共模块。读代码的顺序应该是 public.py → struct.py → container.py → 具体分析器,而不是一上来就打开 LL1.py。

3.1 public.py 和 struct.py:产生式对象、文法容器和 FIRST/FOLLOW 计算

拆这份源码时,第一个要看的就是 struct.py 里有没有定义 Production。大多数这类课设都会用一个类保存产生式左部和右部,直接存字符串虽然省事,但后面算集合时要反复做类型判断,非常难受。常见的定义是这样:

class Production: def __init__(self, lhs: str, rhs: list): self.lhs = lhs # 产生式左部,非终结符 self.rhs = rhs # 产生式右部,符号列表,例如 ["E", "+", "T"] def __repr__(self): return f"{self.lhs} -> {' '.join(self.rhs)}"

然后配套一个 Grammar 类把产生式按左部索引起来:

from collections import defaultdict class Grammar: def __init__(self, productions): self.productions = productions self.by_lhs = defaultdict(list) for p in productions: self.by_lhs[p.lhs].append(p) self.terminals = ... # 词法层定好的终结符集合 self.nonterminals = ... # 所有 lhs 的集合

这个数据结构的价值在于:计算 FIRST、FOLLOW 以及构造项目集闭包时,都要反复查找某个非终结符的所有产生式。如果你要改造这份代码,建议保留这套结构,否则五个分析器至少得改三处。

FIRST 集的计算是整份源码里最容易写错的部分。下面是课设比较常见的迭代式写法:

def compute_first(grammar: Grammar): first = {nt: set() for nt in grammar.nonterminals} changed = True while changed: changed = False for p in grammar.productions: before = len(first[p.lhs]) if not p.rhs: first[p.lhs].add('ε') else: for sym in p.rhs: if sym in grammar.terminals: first[p.lhs].add(sym) break else: first[p.lhs] |= (first[sym] - {'ε'}) if 'ε' not in first[sym]: break else: # for 循环没被 break,说明右部全部可空 first[p.lhs].add('ε') if len(first[p.lhs]) != before: changed = True return first

这段算法的三个关键点:终结符加入后立刻 break;非终结符并入它 FIRST 集的非 ε 部分,然后判断它是否可空,可空才继续看下一个符号;只有整条右部全可空,左部才加 ε。很多同学会漏掉最后的 for-else,导致含多个连续可空非终结符的文法算不出 ε。

FOLLOW 集则要在 FIRST 基础上做不动点迭代,规则是:对形如 A -> αBβ 的产生式,把 FIRST(β) 去掉 ε 并入 FOLLOW(B);如果 β 可空,把 FOLLOW(A) 全部并入 FOLLOW(B)。这个迭代至少要跑两轮,因为 FOLLOW(B) 的变化会继续影响别的符号。public.py 里如果有现成实现,先拿运行截图对一下再改。

3.2 LL1.py:预测分析表填表算法与冲突判定

LL1 分析器是五个里逻辑最朴素的:算完 FIRST/FOLLOW,把产生式填进二维表,再用栈驱动匹配。填表算法可以浓缩成这样:

def build_ll1_table(grammar, first, follow): table = {} for p in grammar.productions: first_rhs = first_of_rhs(p.rhs, first) for term in first_rhs - {'ε'}: table[(p.lhs, term)].append(p) if 'ε' in first_rhs: for term in follow[p.lhs]: table[(p.lhs, term)].append(p) return table

first_of_rhs 计算的是产生式右部整体能产生的终结符集合,逻辑和 FIRST 集类似,只是这次不把结果写回缓存。填表时,右部 FIRST 里的每个终结符都给产生式占一个格子;右部可空时,再用左部的 FOLLOW 集补格子。

判断 LL1 是否冲突就看同一格有没有两个产生式。常见的三种冲突来源是左递归、公共前缀、以及 ε-产生式和普通产生式在 FIRST/FOLLOW 上重叠。左递归直接用改写法处理,例如 E -> E + T | T 改写为 E -> T E' 和 E' -> + T E' | ε。公共前缀需要提取公因子,比左递归麻烦,课设很少深入。

LL1 的分析驱动部分是一个栈:初始栈底是 #,栈里压 S;读一个终结符就和栈顶匹配,读到 ε 产生式就弹栈。这段代码不长,但如果你的输入串末尾没有特殊结束符,很容易出现“栈空了字符串还没读完”的报错。注意 README 里对输入串格式的定义,一般需要追加一个结束标记。

3.3 LR 四兄弟的差异:项目集闭包、向前看符号和同心合并

LR0、SLR1、LR1、LALR1 共用文法结构,差别集中在项目集构造上。先看一张直观的对照表:

分析器项目形如归约判断依据状态数能力
LR0[A -> α·β]见到终结符一律归约最少最弱
SLR1同 LR0用 FOLLOW(A) 过滤少较强
LR1[A -> α·β, a]用向前看符号 a多最强
LALR1同 LR1 但合并同心项用向前看符号 a中等接近 LR1

LR0 的状态构造最简单,只做闭包和转移。闭包里的项目不带向前看,所以当某个状态下同时出现“可归约”和“可移进”就冲突,这种冲突在表达式文法里十分常见。SLR1 在冲突点上加了一道过滤:归约动作只在下一个符号属于 FOLLOW(A) 时才执行,能滤掉一部分冲突。LR1 更进一步,给每个项目额外记录一个向前看符号 a。

LR1 的闭包计算比 LR0 多一环。下面是一个简化的示意实现:

def closure(initial_items, grammar, first): items = list(initial_items) changed = True while changed: changed = False for item in items: dot = item.rhs.index('.') if dot + 1 >= len(item.rhs): continue sym = item.rhs[dot + 1] if sym not in grammar.nonterminals: continue lookaheads = lookahead_for_item(item, grammar, first) for prod in grammar.by_lhs[sym]: for la in lookaheads: new_item = Item(prod.lhs, ['.'] + prod.rhs, la) if new_item not in items: items.append(new_item) changed = True return items

要跟上注释:lookahead_for_item 算的是 FIRST(βa),其中 β 是圆点后面的剩余符号,a 是该项目的向前看符号。同样是 B -> ·γ,向前看是 # 和向前看是 '+',在 LR1 里是两个不同的项目,所以状态数膨胀很快。

LALR1 的思路是把这些“只有向前看差异、核相同”的项目合并。比如 [E -> T·, #] 和 [E -> T·, +] 合成一个状态,但因为向前看被合并,原本不冲突的格局偶尔会引入新冲突,这就是 LALR1 在某些场合反而比 LR1 容错差一点的典型场景。工程上 LALR1 用得多,是因为状态数小,查表快,主流编译器生成器大多基于 LALR 文法。

4. 避坑指南:复现源码包时最容易翻车的五个地方

以下是课设实战里最常被问到的问题,按现象、原因、解决三段记录,希望对你有用。

4.1 中文乱码,注释和 token 全变成问号

现象:词法分析输入文件里带中文注释,输出的字符串部分变成 ??,Web 页面返回的 JSON 也出现乱码。

原因:源码文件是 UTF-8 编码,但 Java 在 Windows 上默认按 GBK 读取,或者 IntelliJ 的全局编码没统一。Python 侧也可能因为 locale 默认编码不是 UTF-8,导致读文法文件出错。

解决:在代码里显式指定编码,不依赖系统默认值。Java 读文件用new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8),Python 用open(path, encoding='utf-8')。IntelliJ 里把 Settings → Editor → File Encodings 的 Global Encoding、Project Encoding 和 Properties Files 全部设为 UTF-8,然后重新构建。JSP 页面顶部加上<%@ page contentType="text/html; charset=UTF-8" %>,后端写 JSON 时同样声明application/json;charset=UTF-8。

4.2 LR0 一跑就报冲突,别急着重写代码

现象:同一个表达式文法,LR0.py 运行后出现 shift-reduce conflict,而 LR1.py 能正常结束。

原因:LR0 不区分向前看符号,在“既可能继续移进、又可能归约”的状态上必然产生冲突。表达式文法天然有这个特性,和代码质量无关。

解决:做课设演示时不要把 LR0 的冲突理解成“代码错了”。文档里写清楚“LR0 能力不足以处理该文法,因此 SLR1/LR1 引入向前看信息”,这反而是展示理论理解的机会。如果想让 LR0 也能通过,只能换更简单的文法,比如只含括号匹配的 S -> ( S ) S | ε。

4.3 LL1 报 FIRST/FOLLOW 冲突,先查左递归

现象:LL1.py 对原始表达式文法报预测表冲突,且报错位置集中在 E 的条目上。

原因:文法直接写了 E -> E + T,这是左递归。LL1 分析器无法处理左递归,因为计算 FIRST(E) 时需要先知道 FIRST(E),陷入循环或重复。

解决:先做左递归消除再算 FIRST/FOLLOW。一个已消除左递归的四则运算文法喂给 LL1.py 通常能直接通过。要是消除后依然冲突,再检查公共前缀,比如if_stmt -> if ( E ) S | if ( E ) S else S这种需要提取公因子。

4.4 SLR1 冲突、LR1 不冲突,不要急着改文法

现象:SLR1.py 报归约-归约或移进-归约冲突,但 LR1.py 对同样文法跑得很干净。

原因:SLR1 在归约时只看 FOLLOW(A),FOLLOW 集合混入了不该出现的符号;而 LR1 的向前看符号更精确,能区分哪些情形真正允许归约。

解决:先确认这是分析方法本身能力差异,不是编码问题。能跑通 LALR1,就用 LALR1 作为演示主体。答辩被问“为什么 SLR1 不行”,就回答 SLR1 是 LR0 的增强但向前看信息不完整,LALR1 合并了 LR1 状态,处理能力介于两者之间,这是很标准的考核点。

4.5 运行截图和当前代码对不上,答辩现场被质疑

现象:按 README 跑出来的结果,和资源包里运行截图的某几行对不上。

原因:多数情况是输入文件不同,或者截图来自初始版本,代码后来改过。极少数情况是运行环境差异导致 Unicode 显示不同。

解决:先把资源里的运行截图当验收基线。输入文件不一样就换回截图对应的输入;如果代码版本对不上,直接用当前版本重新生成一套截图,答辩时以 README 和当前输出为准。实时截图的技巧是只截关键分析步骤,比如 token 列表、预测分析表、项目集数量对比,不要截整屏。

5. 复现与验收流程:从解压、启动到答辩前对照运行截图

5.1 环境准备与文件划分

把 zip 解压后先不要急着打开 IDE,先按运行路径把内容分成三块。压缩包里有控制台词法分析、Web 词法分析和 Python 语法分析三套内容,混在一个目录里容易把依赖搞乱。建议建三个文件夹:

目录内容关键文件
lexer-console控制台词法分析DFA.iml、src/DFA
lexer-webWeb 交互词法分析DFAServer.iml、index.jsp、fastjson-1.2.9.jar
parser-pythonPython 语法分析LL1.py、LR0.py、SLR.py、LR1.py、LALR1.py、public.py

每套的 README.md 建议解压时就看一遍。资源描述里强调要先打开 README,这不是客套。课设工程的入口类和启动参数经常和目录结构不一致,README 是唯一靠谱的指引。

环境上,Java 工程需要 JDK 8 或 11,IntelliJ IDEA 直接打开 .iml 文件,Python 部分需要 Python 3.6 以上。Web 工程还要额外装 Tomcat 8.5 或 9。fastjson 的 jar 已经在 libs 目录里,不用再下载依赖。

5.2 先跑控制台词法分析

打开 DFA 工程,找到 main 方法运行。如果运行参数里要传文件路径,先把输入文件准备好。常见的成功标志是控制台顺序打印 token,每行包含类型、值和行列位置。

# 假设 IDE 编译输出目录是 out/production/DFA,入口类名以实际工程为准 java -cp out/production/DFA com.buct.dfa.Main test.c

如果类的路径不对,从 IDE 运行一次看控制台第一行输出即可确认。跑通后做三件事:看看关键字是否和代码保留字表一致;看看连续空格和换行有没有被跳过;再看看! =和!=这类双字符运算符是否被识别为一个 token。后两者是最容易翻车的地方。

5.3 启动 Web 版词法分析

Web 版在 IDEA 里按正常 Web 工程配置:Project Structure 里加 Artifact,类型选 war exploded,再配一个 Tomcat Server,Deployment 指向这个 Artifact。启动后访问 index.jsp,在文本框贴代码,点分析。页面能返回 token 表格就是通了。

如果页面 404,先确认 Artifact 名字和访问路径一致;如果 500,多数是 web.xml 的 schema 版本和 Tomcat 不匹配。JSP 页面第一行是<%@ page contentType="text/html; charset=UTF-8" %>,response 的 JSON 也要显式声明 UTF-8。中文界面出现乱码时,优先查这两个地方。

5.4 语法分析五件套的入口和输入格式

Python 分析器都是独立脚本,一般按文件输入或命令行传参运行:

python3 LL1.py input.txt python3 LR0.py input.txt python3 SLR.py input.txt python3 LR1.py input.txt python3 LALR1.py input.txt

input.txt 里装着文法规则和待分析的输入串。不同分析器的输入格式可能不一样,比如 LR1.py 可能要求文法带起始符号,LL1.py 可能要求额外给出终结符集合。打开 README.md 和对应 py 文件的 parse 函数,确认格式再执行。

读取文件时建议显式用 UTF-8:

with open("input.txt", "r", encoding="utf-8") as f: lines = [line.rstrip('\n') for line in f if line.strip()]

这段代码的作用是过滤空行和去掉换行符,避免空行干扰文法解析。如果你拿到的是 Windows 换行的文件,最好统一转成 \n,否则个别分析器在字符串匹配上会因为尾部多一个 \r 而出错。

提示:如果某个 py 文件运行时报 ModuleNotFoundError,先看是不是直接双击运行的,当前目录不对会找不到同目录模块,改成在工程根目录下用命令行运行。

5.5 对照运行截图验收三板斧

第一板斧是词法分析 token 的数量和类型名是否对齐;第二板斧是 LL1 预测分析表里除了空格外,冲突位置是否和截图一致;第三板斧是统计各 LR 分析器的状态数量,LR1 数量应该最多,LR0/LALR1 数量接近最少。我把这三项整理成验收清单,顺序执行:

  1. 确定输入文件内容与截图一致;
  2. 记录每个分析器的输出行数和关键报错;
  3. 比对状态数或冲突位置;
  4. 差异出现时,先怀疑输入,再怀疑编码,最后看代码改动。

如果资源里没给原始输入文件,就自己构造一份能复现截图结果的输入。四则运算表达式是最稳妥的选择,因为它在词法和语法两层都有明确输出。

5.6 替换文法:把自己设计的语法安全塞进去

课设一般不允许原样交资源里的文法,至少要改成自己的语言子集或者加上扩展功能。替换文法的常规步骤是:

  • 先看 README 里的文法符号说明,比如终结符用什么表示,ε 怎么写;
  • 仿照现有文法文件格式,加一条产生式就运行一遍,避免一次改太多定位不了问题;
  • 每加一组产生式,就依次跑 LL1 和 LR1,确认两种分析器都接受。

一个可以直接用的四则运算文法(已消除左递归)长这样:

S -> E E -> T E' E' -> + T E' | ε T -> F T' T' -> * F T' | ε F -> ( E ) | id

如果你的语言里要加赋值语句,就围绕 identifier 做扩展,比如加S -> id = E。注意 LL1 在加入新产生式后要立刻重算 FIRST/FOLLOW,不要手填预测表,因为 ε 带来的影响很隐蔽。

6. 进阶验证:用四则运算文法把五种分析器的边界全压一遍

拿到这份源码后,除了完成大作业,我比较推荐再做一步交叉验证:用同一份无二义算术文法,依次跑 LL1、LR0、SLR1、LR1、LALR1,记录四个维度——接受还是拒绝、状态数、冲突位置、分析表规模。这一步对理解几个分析方法的能力边界非常有价值。

文法用上一节那份已消除左递归的版本,输入串选id + id * id。按顺序运行后,我自己的预期结果是这样:

分析器状态数是否接受冲突位置特点
LL1非状态模型,表驱动接受无表规模取决于非终结符 × 终结符
LR0最少可能拒绝移进/归约表达式文法的常态
SLR1少看 FOLLOW可能在归约处向前看不完整
LR1最多接受几乎无向前看精确
LALR1中等接受几乎无状态数和 SLR1 接近

这个测试做完,你会发现“LR 系比 LL 系强”不能简单总结。LL1 在文法是 LL(1) 时处理得又简单又直白;LR 系在同样文法上牺牲状态数,换来对更多文法的包容。答辩时如果老师问“为什么五个分析器输出不太一样”,你可以直接拿着这张表讲。

验证时还有个值得记录的细节:如果 SLR1 冲突而 LALR1 不冲突,把这个文法单独存成一个文件,作为“能力差异演示用例”。这是答辩现场很有区分度的材料,比口头讲理论更有说服力。状态数对比也一样,LR1 通常比 LR0 多不少状态,这个差距能直观解释向前看符号的代价。

从那以后,我每次复现编译原理分析器,都会强制自己把五个分析器全部跑一遍再录截图。不是为了把流程做满,而是不同分析器在同一文法上的差异,往往就是期末答辩最有区分度的考点。如果你需要这套包含词法分析双工程、五个语法分析 py 文件、文档说明和运行截图的完整资源包,下载后从 README.md 开始读,按第 5 章的流程走一遍,两小时之内应该能复现出和截图一致的输出。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询