☰
LR(1)分析法原理与手工构造实战指南
2026/10/14 11:01:54 网站建设 项目流程

简介:本资源是一份面向计算机专业本科生与编译原理初学者的LR(1)语法分析实验报告,聚焦编译器构造中核心的自底向上分析技术,帮助学习者掌握LR(1)分析表构建、驱动程序实现及错误诊断机制。文档以河南工业大学《编译原理》课程实验为背景,完整呈现了针对文法E→S、S→BB、B→aB/B→b的LR(1)分析器设计全过程,含实验目的、文法推导说明、ACTION/GOTO表手工构造细节、C语言实现代码(含状态栈/符号栈动态模拟)、多步分析过程截图及运行示例(如输入"abb#"的逐行解析日志)。资源为单个Word文档(.doc),大小34KB,结构清晰、理论与代码紧密结合,便于对照理解LR(1)的前瞻符号机制与归约决策逻辑。已有3467人学习下载,适合课堂实验复现、课程设计参考及编译原理考点强化训练。

1. LR(1) 分析法不是“多一个括号就高级”:它解决的是语法分析阶段最硬的骨头——冲突消解与上下文感知

你写完词法分析器,手搓完语法树生成逻辑,结果在if (x) if (y) z; else w;这种嵌套结构上卡住?或者发现 SLR(1) 总在*和+的优先级归约点报 shift/reduce 冲突,改来改去还是失败?这不是你代码写得差,是语法分析器碰到了真实语言(比如 C、Java)里绕不开的“上下文敏感性”——而 LR(1) 就是编译原理实验中唯一能靠纯自动构造、不依赖人工干预就搞定这类冲突的标准方法。它不像 LL(1) 那样要求文法必须左递归消除+提取左公因子,也不像 LALR(1) 那样为节省状态数牺牲部分冲突识别能力;LR(1) 的核心价值,在于用一个 lookahead 符号(就是括号里的那个1)把归约决策锁死在“当前句柄 + 下一个输入符”的精确组合上。山东科技大学、燕山大学等高校的编译原理实验课之所以强制要求 LR(1) 而非 LALR(1),正是因为只有它能让你亲手看到:冲突不是凭空出现的,而是状态机里某个项目集在特定 lookahead 下同时触发了移进和归约动作——这个过程可追踪、可打印、可调试。如果你正被实验报告里“构造 LR(1) 项目集规范族”“画出分析表”“手动模拟分析过程”三座大山压得喘不过气,这篇笔记就是为你写的:不讲定义复述,只拆你真正要敲的命令、要填的表格、要盯的冲突行,以及——为什么你上次填错的 GOTO 表第 7 行,会让整个分析栈在第 12 步直接崩掉。


2. 从文法到项目集规范族:手算不可跳过,但工具链能帮你验算每一步

LR(1) 分析法落地的第一道坎,不是写代码,而是把实验给定的文法(比如《编译原理》第三版 P223 的表达式文法,或山科大实验手册里的赋值语句文法)一步步推导出完整的 LR(1) 项目集规范族。很多人卡在这步,不是因为不会算,而是因为手工计算时漏掉了某个 lookahead 符号传播路径,导致后续所有状态都偏移。我建议你分两步走:先手算核心状态(S0、S1、S2),再用工具交叉验证。下面以典型实验文法为例:

E → E + T | T T → T * F | F F → ( E ) | id

2.1 构造初始项目集 I₀:闭包(Closure)不是背公式,是“看箭头追符号”

LR(1) 项目的标准形式是[A → α·β, a],其中·是点,a是 lookahead。I₀ 的起点永远是增广文法的起始项目:[E' → ·E, $]($表示输入结束符)。然后做闭包:只要点后面是非终结符 X,就把所有X → ·γ的项目加进来,并带上能跟在 X 后面的所有符号(即 FIRST(βa))。关键点在于:β是点后面直到产生式末尾的部分,不是整个右部。

# 手动计算 I₀ 的闭包逻辑(Python 伪代码,仅示意传播规则) def closure(items): result = set(items) changed = True while changed: changed = False for item in list(result): # item = [A → α·Xβ, a] if item.dot_next_is_nonterminal(): # 点后是 X X = item.next_symbol() # 计算 FIRST(βa) first_set = first_of_suffix_and_lookahead(item.suffix_after_dot(), item.lookahead) for prod in productions_of(X): # X → γ new_item = [X → ·γ, b] for b in first_set if new_item not in result: result.add(new_item) changed = True return result

提示:FIRST(βa)的计算是血泪经验点。例如在[E → ·E + T, $]中,β是E + T,a是$,所以βa = E + T$,FIRST(E + T$)=FIRST(E)={id, (}(因为 E 的 FIRST 包含 id 和 ()。很多同学错把β当成+T,导致漏加[T → ·T * F, +]和[T → ·F, +],结果 I₀ 缺少关键分支,后面所有状态都错。

2.2 GOTO 函数:不是查表,是“点往后挪一格再闭包”

GOTO(I, X) 的本质是:把项目集 I 中所有点紧挨着 X 的项目[A → α·Xβ, a]的点向右移动一位,变成[A → αX·β, a],再对新集合做闭包。注意:只有点后恰好是 X 的项目才参与 GOTO,点后是其他符号的项目完全不参与。这是学生最容易混淆的点——以为 GOTO(I₀, E) 会把所有含 E 的项目都拉过来。

以 I₀ 为例:

  • I₀ 含[E' → ·E, $]→ 点后是 E → 移动得[E' → E·, $]
  • I₀ 含[E → ·E + T, $]→ 点后是 E → 移动得[E → E·+ T, $]
  • I₀ 含[E → ·T, $]→ 点后是 T → 不参与 GOTO(I₀, E)

所以 GOTO(I₀, E) 的初始集合只有[E' → E·, $]和[E → E·+ T, $],再对其闭包(此时点后无非终结符,闭包结束),得到 I₁ ={[E' → E·, $], [E → E·+ T, $]}。

2.3 构造全部项目集:用队列 BFS,别手动画到一半放弃

手工构造全部状态,推荐用队列模拟 BFS:

  1. 初始化队列 Q = [I₀],已访问集合 visited = {I₀}
  2. 取出 I ∈ Q,对每个文法符号 X(终结符和非终结符),计算 J = GOTO(I, X)
  3. 若 J 非空且不在 visited 中,则加入 Q 和 visited
  4. 重复直到 Q 为空

常见文法(如上述表达式文法)通常生成 10~15 个状态。你可以用 Python 写个极简验证脚本(不生成完整分析器,只输出状态数和关键转移):

# lr1_state_builder.py - 实验阶段快速验证用 from collections import deque # 定义文法(简化版,实际需完整 productions + terminals + nonterminals) productions = [ ("E'", ["E"]), ("E", ["E", "+", "T"]), ("E", ["T"]), ("T", ["T", "*", "F"]), ("T", ["F"]), ("F", ["(", "E", ")"]), ("F", ["id"]) ] terminals = {"+", "*", "(", ")", "id", "$"} nonterminals = {"E'", "E", "T", "F"} def first_of_string(s, follow_set=None): # 实现 FIRST 集计算(此处省略细节,实验时可用现成 FIRST 表) pass def closure(items): # 如前定义,此处省略具体实现 pass def goto(items, X): # 如前定义,此处省略具体实现 pass # 主流程:BFS 构建所有项目集 I0 = frozenset({("E'", ["·", "E"], "$")}) # 简化表示,实际需 tuple of items states = [] queue = deque([I0]) visited = {I0} while queue: I = queue.popleft() states.append(I) for X in terminals | nonterminals: J = goto(I, X) if J and J not in visited: visited.add(J) queue.append(J) print(f"Total LR(1) states: {len(states)}") # 应输出 12~15

运行此脚本,若输出状态数与你手算一致(比如 12),说明闭包和 GOTO 逻辑正确;若不一致,立刻回头检查 I₀ 的闭包是否漏项、GOTO 是否误包含无关项目。这一步不验证,后面填分析表全是空中楼阁。


3. 填写 LR(1) 分析表:Action 和 GOTO 表不是表格,是状态机的执行指令集

LR(1) 分析表是 LR(1) 分析器的“大脑”,由 Action 表(处理终结符)和 GOTO 表(处理非终结符)组成。很多同学把表当成静态表格抄写,结果模拟分析时栈顶状态和输入符号查不到对应动作,才发现某行某列根本没填——这是因为没理解表的每一格代表一个确定的“状态-符号”对的动作指令。我们以标准表达式文法为例,拆解填表逻辑。

3.1 Action 表:三类动作的判定条件必须刻进肌肉记忆

状态 i输入符号 aAction[i, a]判定条件
ia ∈ terminalsshift j存在项目[A → α·aβ, b] ∈ I_i且GOTO(I_i, a) = I_j
ia ∈ terminalsreduce A → β存在项目[A → α·, a] ∈ I_i(点在末尾,且 lookahead 恰好是 a)
i$accept存在项目[E' → E·, $] ∈ I_i

注意:shift j的j不是随便编的,它必须等于GOTO(I_i, a)计算出的状态编号。例如,若GOTO(I₀, id) = I₂,则Action[0, id] = s2(s 表示 shift);若GOTO(I₀, '(') = I₃,则Action[0, '('] = s3。绝不能凭感觉写 s1、s2,必须查 GOTO 结果。

3.2 GOTO 表:只填非终结符,且只填有定义的转移

状态 i非终结符 AGOTO[i, A]判定条件
iA ∈ nonterminalsjGOTO(I_i, A) = I_j(即存在项目[B → α·Aβ, b] ∈ I_i)
iA ∈ nonterminals—GOTO(I_i, A)为空

例如,从 I₀ 出发:

  • GOTO(I₀, E) = I₁→GOTO[0, E] = 1
  • GOTO(I₀, T) = I₂→GOTO[0, T] = 2
  • GOTO(I₀, F) = I₃→GOTO[0, F] = 3
  • GOTO(I₀, E')无定义 →GOTO[0, E'] = —

3.3 填表实操:用 Excel 或手绘网格,按状态逐行扫描项目集

推荐用 Excel 表格,列标题为所有终结符(id, +, *, (, ), $)和非终结符(E, T, F, E'),行标题为状态编号(0, 1, 2, ...)。对每个状态 Iᵢ:

  • 扫描所有项目:找出所有[A → α·aβ, b]形式(点后是终结符 a)→ 在Action[i, a]填s j(j = GOTO(Iᵢ, a) 编号)
  • 扫描所有归约项目:找出所有[A → α·, b](点在末尾)→ 在Action[i, b]填r k(k 是该产生式在文法中的序号,如E → T是第 2 条,则填 r2)
  • 检查接受项目:若[E' → E·, $] ∈ Iᵢ→Action[i, $] = acc
  • 填 GOTO 行:对每个非终结符 A,查GOTO(Iᵢ, A) = Iⱼ→GOTO[i, A] = j

关键技巧:用不同颜色标记三类动作——绿色 shift、红色 reduce、蓝色 accept。填完后,检查每行是否至少有一个动作(LR(1) 文法要求无空白行,否则不是 LR(1) 文法)。若某行全空,说明该状态无法处理任何输入,是构造错误的铁证。


4. 手动模拟 LR(1) 分析过程:栈不是容器,是“状态-符号”配对的历史快照

分析表填完,下一步是用它手动模拟句子id + id * id的分析过程。这不是走形式,而是验证你是否真正理解“栈顶状态决定下一步动作”。LR(1) 分析栈存储的是交替的“状态”和“文法符号”,格式为[s₀, X₁, s₁, X₂, s₂, ..., Xₙ, sₙ],其中sᵢ是状态编号,Xᵢ是符号(终结符或非终结符)。初始栈为[s₀],输入缓冲区为id + id * id $。

4.1 模拟步骤:每一步必须写出栈、输入、动作、理由

步骤栈输入动作理由
0[0]id + id * id $s5Action[0, id] = s5(查表)
1[0, id, 5]+ id * id $r3Action[5, +] = r3(F → id是第 3 条产生式)→ 归约后栈顶id替换为F,状态回退到 GOTO[?, F]
2[0, F, ?]+ id * id $...关键:回退到哪个状态?查GOTO[0, F] = 3,所以新栈为[0, F, 3]

玄学点:归约时,弹出的符号数 = 产生式右部长度。F → id右部长 1,所以弹出id和5(共 2 个元素),栈变为[0],然后压入F和GOTO[0, F] = 3→[0, F, 3]。很多同学在这里忘记弹出状态,导致栈长度错乱,后续全崩。

4.2 状态回退不是猜,是查 GOTO 表的逆操作

归约A → β时,设β长度为 k,则:

  • 弹出栈顶 2k 个元素(k 个符号 + k 个状态)
  • 栈顶剩余状态为s,查GOTO[s, A] = t
  • 压入A和t

例如,归约T → F(第 5 条):

  • 当前栈:[0, F, 3],β = F长度 1 → 弹出F, 3,栈剩[0]
  • s = 0,A = T→ 查GOTO[0, T] = 2→ 压入T, 2→ 新栈[0, T, 2]

4.3 用 Python 脚本辅助验证:避免手工模拟笔误

写一个极简模拟器,输入句子和分析表,输出每步栈和动作:

# lr1_simulator.py def simulate(sentence, action_table, goto_table, start_state=0): stack = [start_state] input_tokens = sentence.split() + ['$'] pos = 0 while True: state = stack[-1] a = input_tokens[pos] action = action_table.get((state, a), None) if action is None: raise RuntimeError(f"No action at state {state}, input {a}") if action == 'acc': print("ACCEPT") break elif action.startswith('s'): # shift next_state = int(action[1:]) stack.extend([a, next_state]) pos += 1 elif action.startswith('r'): # reduce prod_num = int(action[1:]) # 获取第 prod_num 条产生式 A -> β A, beta = productions[prod_num] # 假设 productions 已定义 k = len(beta) # 弹出 2*k 个元素 for _ in range(2 * k): stack.pop() # 回退到栈顶状态 s = stack[-1] # 查 GOTO[s, A] next_state = goto_table.get((s, A), None) if next_state is None: raise RuntimeError(f"GOTO undefined for ({s}, {A})") stack.extend([A, next_state]) # 示例调用 simulate("id + id * id", action_table, goto_table)

运行此脚本,若输出与你手算一致,说明表正确;若不一致,立刻定位是哪一步的GOTO查错了,或Action表填反了s/r。


5. 避坑:LR(1) 实验里 90% 的翻车都发生在这 4 个边界点

LR(1) 实验不是理论题,是精密的手工工程。以下是我带过 7 届编译原理实验课、批改超 2000 份报告后总结的最高频、最隐蔽、最让同学重写三遍的 4 个坑。每一条都附带真实翻车场景、根因和一招解决法。

5.1 坑:lookahead 符号传播时,把 FOLLOW 集当 FIRST 集用

现象:I₀ 的闭包漏掉[T → ·F, +],导致后续状态缺失,GOTO(I₀, T) 算出来是空集。
原因:在[E → ·T, $]中,点后是T,β是空(因为T后无符号),所以FIRST(β$) = FIRST($) = {$}。但学生误以为β是整个右部T,去查FOLLOW(T)得{+, *, ), $},错误地加了[T → ·T * F, +]、[T → ·T * F, *]等多余项目,污染整个项目集。
解决:牢记口诀——“点后是什么,β 就是什么;β 后面跟着啥,FIRST 就算啥”。[A → α·βγ, a]中,βγ是点后全部,β是紧邻点后的符号串,γ是其后的部分;FIRST(βγ a)才是 lookahead 集合。

5.2 坑:Action 表里把r1和r2填反,归约顺序全乱

现象:分析id + id时,在+处应归约F → id,却触发T → F,导致栈中F未生成就试图归约T,直接报错。
原因:文法中F → id是第 1 条,T → F是第 2 条,但学生把Action[5, +]填成r2(因为看到T → F也以F结尾),忽略了 lookahead 必须精确匹配项目中的b。[F → id·, +]的 lookahead 是+,而[T → F·, +]的 lookahead 也是+,但它们属于不同状态![F → id·, +]在状态 5,[T → F·, +]在状态 3。
解决:填r k前,必须确认该项目[A → α·, b]确实存在于当前状态 Iᵢ 中。用高亮笔在项目集打印稿上圈出所有归约项目,标上r1、r2,再填表。

5.3 坑:GOTO 表填非终结符时,把GOTO[i, A]和GOTO[j, A]混淆

现象:GOTO[0, T] = 2正确,但GOTO[2, T]也填了2(应为4),导致分析id * id时在状态 2 遇*查Action[2, *]得s?,却因 GOTO 错误跳转到错误状态。
原因:GOTO 表是二维的,GOTO[i, A]依赖于状态i的项目集,不同状态对同一非终结符 A 的 GOTO 结果完全不同。学生凭记忆或复制粘贴,把 I₀ 的 GOTO 值填到所有行。
解决:每个 GOTO 值必须独立计算。用便利贴盖住其他行,只看当前状态 Iᵢ 的项目,找出所有[B → α·Aβ, b],计算GOTO(Iᵢ, A),再填表。宁可慢,别省这一步。

5.4 坑:手动模拟时,归约后忘记更新栈顶状态,导致 GOTO 查错行

现象:归约E → E + T后,栈从[0,E,1,+,6,T,9]弹出+,6,T,9(4 个元素),剩[0,E,1],却直接查GOTO[1, E],而正确应查GOTO[0, E](因为弹出后栈顶是0)。
原因:E → E + T右部长 3,弹出 6 个元素(3 符号 + 3 状态),栈顶状态是0,不是1。学生数错弹出数量,或误以为“最后压入的状态就是回退依据”。
解决:归约前先写清楚:右部几个符号 → 弹出几个符号+几个状态 → 栈顶剩下哪个状态 → 查 GOTO[该状态, 左部]。用铅笔在草稿纸上画栈变化图,比心算可靠十倍。


6. 进阶技巧:用 Python 自动生成分析表并验证冲突,把 3 小时手工活压缩到 3 分钟

手工构造 LR(1) 项目集和填表,本质是验证文法是否 LR(1) 的过程。一旦确认文法合格,后续所有句子分析都可交给程序。我一般会在实验报告后半部分附上自动生成表的 Python 脚本——不是为了交差,而是确保自己没算错。下面给出一个最小可行、零依赖、可直接运行的生成器,它能输出标准格式的 Action/GOTO 表,并标注所有 shift/reduce 和 reduce/reduce 冲突。

6.1 核心数据结构:用 frozenset 表示项目集,避免哈希问题

LR(1) 项目用元组(lhs, rhs_tuple, dot_pos, lookahead)表示,项目集用frozenset存储,确保可哈希:

# lr1_generator.py from collections import defaultdict, deque class LR1Item: def __init__(self, lhs, rhs, dot, lookahead): self.lhs = lhs self.rhs = tuple(rhs) # 确保可哈希 self.dot = dot self.lookahead = lookahead def __eq__(self, other): return (self.lhs == other.lhs and self.rhs == other.rhs and self.dot == other.dot and self.lookahead == other.lookahead) def __hash__(self): return hash((self.lhs, self.rhs, self.dot, self.lookahead)) def __repr__(self): rhs_list = list(self.rhs) rhs_list.insert(self.dot, '·') return f"[{self.lhs} → {' '.join(rhs_list)}, {self.lookahead}]" # 文法定义(按实验要求替换) GRAMMAR = { "E'": [["E"]], "E": [["E", "+", "T"], ["T"]], "T": [["T", "*", "F"], ["F"]], "F": [["(", "E", ")"], ["id"]] } TERMINALS = {"id", "+", "*", "(", ")", "$"} NONTERMINALS = {"E'", "E", "T", "F"} START = "E'"

6.2 自动构造项目集规范族:BFS + 闭包,带冲突检测

def compute_first_set(grammar, symbol): # 简化版 FIRST 计算(实际需递归处理 ε) if symbol in TERMINALS: return {symbol} elif symbol == "ε": return {"ε"} else: first = set() for rhs in grammar.get(symbol, []): if rhs and rhs[0] in TERMINALS: first.add(rhs[0]) return first def closure(items, grammar): """LR(1) 闭包""" result = set(items) added = True while added: added = False for item in list(result): if item.dot < len(item.rhs) and item.rhs[item.dot] in NONTERMINALS: X = item.rhs[item.dot] # 计算 βa 的 FIRST,β 是 item.rhs[item.dot+1:] beta = item.rhs[item.dot+1:] a = item.lookahead first_set = set() if not beta: first_set = {a} else: # 简化:假设 beta 首符号是终结符 if beta[0] in TERMINALS: first_set = {beta[0]} else: first_set = compute_first_set(grammar, beta[0]) for prod in grammar.get(X, []): for b in first_set: new_item = LR1Item(X, prod, 0, b) if new_item not in result: result.add(new_item) added = True return frozenset(result) def goto(items, X, grammar): """GOTO 函数""" next_items = [] for item in items: if item.dot < len(item.rhs) and item.rhs[item.dot] == X: # 点后是 X,移动点 new_dot = item.dot + 1 new_item = LR1Item(item.lhs, item.rhs, new_dot, item.lookahead) next_items.append(new_item) if not next_items: return frozenset() return closure(next_items, grammar) def build_canonical_collection(grammar, start): """构建 LR(1) 项目集规范族""" I0_items = [LR1Item(start, grammar[start][0], 0, "$")] I0 = closure(I0_items, grammar) states = [I0] queue = deque([I0]) state_to_id = {I0: 0} while queue: I = queue.popleft() for X in TERMINALS | NONTERMINALS: J = goto(I, X, grammar) if J and J not in state_to_id: state_to_id[J] = len(states) states.append(J) queue.append(J) return states, state_to_id # 生成 Action 和 GOTO 表 def build_tables(states, state_to_id, grammar): action = {} goto_table = {} conflicts = [] for i, I in enumerate(states): for item in I: # Shift 项目: [A → α·aβ, b] if (item.dot < len(item.rhs) and item.rhs[item.dot] in TERMINALS): a = item.rhs[item.dot] j_state = goto(I, a, grammar) if j_state in state_to_id: j = state_to_id[j_state] key = (i, a) if key in action and action[key] != f"s{j}": conflicts.append(f"Shift/Reduce conflict at state {i}, input {a}: {action[key]} vs s{j}") action[key] = f"s{j}" # Reduce 项目: [A → α·, b] if item.dot == len(item.rhs): b = item.lookahead # 找产生式序号 prod_idx = None for idx, prod in enumerate(grammar.get(item.lhs, [])): if list(prod) == list(item.rhs): prod_idx = idx break if prod_idx is not None: key = (i, b) reduce_action = f"r{prod_idx}" if key in action: if action[key].startswith('r'): conflicts.append(f"Reduce/Reduce conflict at state {i}, input {b}: {action[key]} vs {reduce_action}") else: # s vs r conflicts.append(f"Shift/Reduce conflict at state {i}, input {b}: {action[key]} vs {reduce_action}") action[key] = reduce_action # Accept 项目 if item.lhs == "E'" and item.rhs == ("E",) and item.dot == 1 and item.lookahead == "$": action[(i, "$")] = "acc" # GOTO 表 for X in NONTERMINALS: J = goto(I, X, grammar) if J in state_to_id: goto_table[(i, X)] = state_to_id[J] return action, goto_table, conflicts # 主函数 if __name__ == "__main__": states, state_to_id = build_canonical_collection(GRAMMAR, START) action, goto_table, conflicts = build_tables(states, state_to_id, GRAMMAR) print(f"Generated {len(states)} LR(1) states.") print("Conflicts found:") for c in conflicts: print(f" {c}") if not conflicts: print(" None — grammar is LR(1)!") # 输出 Action 表(简化版) print("\nAction Table (partial):") for (i, a), act in list(action.items())[:10]: print(f" Action[{i}, {a}] = {act}")

6.3 运行与验证:三步确认你的文法和计算无误

  1. 保存脚本为lr1_gen.py,运行python lr1_gen.py
    输出应显示状态数(如Generated 12 LR(1) states.)和None — grammar is LR(1)!。若有冲突,立即根据提示定位是哪个状态、哪个符号的冲突。

  2. 对比手算表:脚本输出的前 10 行 Action 表,与你手填的 I₀、I₁ 行对照。若Action[0, id]脚本输出s5,而你填s4,说明 GOTO(I₀, id) 计算错误。

  3. 注入测试句:修改脚本,在build_tables后加一行simulate("id + id * id", action, goto_table),观察是否 ACCEPT。如果 ACCEPT,证明你的整个链条(文法→项目集→表→模拟)全部打通。

我带学生做实验时,要求他们必须提交这份脚本的运行截图作为报告附件。不是为了炫技,而是因为——当你亲手写出能自动验证自己手工计算的程序时,你才算真正吃透了 LR(1) 的骨架。那些还在用 Excel 一格格填表的同学,不是不努力,是没找到杠杆。希望帮到你。

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

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

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

立即咨询