找回密码
 注册
搜索
[新手上路]批处理新手入门导读[视频教程]批处理基础视频教程[视频教程]VBS基础视频教程[批处理精品]批处理版照片整理器
[批处理精品]纯批处理备份&还原驱动[批处理精品]CMD命令50条不能说的秘密[在线下载]第三方命令行工具[在线帮助]VBScript / JScript 在线参考
查看: 823|回复: 6

[原创教程] [Perl]捕获平衡组(配对括号、多层镶嵌)结构化的歪招

[复制链接]
发表于 2026-3-25 22:03:53 | 显示全部楼层 |阅读模式
本帖最后由 523066680 于 2026-3-26 09:21 编辑

我看到之前论坛有不少相关的帖子和讨论。但是本人没有参与,主要是自己几乎没有遇到类似需求。
不过最近遇到了 :-)

首先考虑偷懒的做法,比如交给 DOM 解析器处理

  1. # 523066680@163.com
  2. use Modern::Perl;
  3. use Mojo::DOM;
  4. STDOUT->autoflush(1);

  5. my $text =<<CODE;
  6. lv0:
  7. line1 (lv1:a (lv2:test(lv3:part1(lv4:deep)),turn(lv3:part2)) string);
  8. line2 (lv1:b (lv2:I) (lv2:II));
  9. CODE

  10. # 把括号转成标签,标签名自定
  11. $text =~ s/\(/<b>/g;
  12. $text =~ s/\)/<\/b>/g;

  13. # 交给 DOM 解析器
  14. my $dom = Mojo::DOM->new->xml(1)->parse( $text );
  15. print_st($dom, 0);

  16. sub print_st
  17. {
  18.     my ( $node, $lv ) = @_;
  19.     for my $e ( $node->children->each() )
  20.     {
  21.         my $s = $e;
  22.         # 还原括号
  23.         $s =~ s/<b>/\(/g;
  24.         $s =~ s/<\/b>/\)/g;
  25.         printf "%s%s\n", "  "x$lv, $s;
  26.         print_st( $e, $lv+1 );
  27.     }
  28. }
复制代码


输出结果:
  1. (lv1:a (lv2:test(lv3:part1(lv4:deep)),turn(lv3:part2)) string)
  2.   (lv2:test(lv3:part1(lv4:deep)),turn(lv3:part2))
  3.     (lv3:part1(lv4:deep))
  4.       (lv4:deep)
  5.     (lv3:part2)
  6. (lv1:b (lv2:I) (lv2:II))
  7.   (lv2:I)
  8.   (lv2:II)
复制代码


性能差了点,耗时 600ms,不过比起写正则应该节省了不少调试的时间。

评分

参与人数 1技术 +1 收起 理由
zzz19760225 + 1 不明觉厉!

查看全部评分

发表于 2026-4-3 15:51:32 | 显示全部楼层
本帖最后由 523066680 于 2026-4-3 17:13 编辑

请问版主,选择圆括号作为字符处理对象,而不是大括号{}或尖括号<>,有什么原因吗?
 楼主| 发表于 2026-3-25 22:49:14 | 显示全部楼层
搞完野路子了,看看有没有正道。相信这种问题肯定有成熟的模块,AI 推的第一个模块:
Text::Balanced - Extract delimited text sequences from strings.

先看第一个极简示例
  1. use Text::Balanced qw(extract_bracketed);

  2. my $text = "(inside(lv2:(lv3:deep)))right";
  3. my ($extracted, $remainder) = extract_bracketed($text, '()');

  4. printf "%s\n", $extracted;
  5. printf "%s\n", $remainder;
复制代码

结果
  1. (inside(lv2:(lv3:deep)))
  2. right
复制代码


这个extract_bracketed 只负责提取从(括号起到右侧配对的)括号范围的内容,它不会返回带层次的结构体。
如果左侧带有其他字符例如 "abc(括号内的)..." 那么需要手动去掉左侧内容或者提取从左括号( 开始的子串交给 extract_bracketed 处理
接着让ai写了一个递归的版本,像剥洋葱一样处理
  1. #!/usr/bin/env perl
  2. use strict;
  3. use warnings;
  4. use Text::Balanced qw(extract_bracketed);

  5. my $text = "left (inside(lv2:(lv3:deep))) def(inside2(lv2:(lv3:deep1)(lv3:deep2))) right";

  6. print "orig: $text\n";
  7. print "-" x 50 . "\n";

  8. # 第一步:处理顶层的所有独立括号组
  9. # 因为字符串开头可能不是括号,或者中间有 "def" 隔开,我们需要循环查找
  10. process_level($text, 0);

  11. sub process_level
  12. {
  13.     my ($str, $level) = @_;
  14.    
  15.     # 缩进显示
  16.     my $indent = "  " x $level;
  17.    
  18.     # 只要字符串里还有左括号,就继续尝试提取
  19.     while ($str =~ /\(/)
  20.     {
  21.         # 找到第一个左括号的位置,截取子串供 extract_bracketed 使用
  22.         # 因为 extract_bracketed 必须从字符串开头就是括号才能工作
  23.         my $start_pos = index($str, '(');
  24.         my $substring = substr($str, $start_pos);
  25.         
  26.         # 执行提取
  27.         my ($extracted, $remainder) = extract_bracketed($substring, '()');
  28.         
  29.         if (defined $extracted)
  30.         {
  31.             # 1. 打印当前层级的完整括号内容
  32.             print "${indent}Lv$level: $extracted\n";
  33.             
  34.             # 2. 准备下一层级的数据:去掉最外层的括号
  35.             # 提取的内容格式是 "(...)", 我们取中间部分 [1 .. length-2]
  36.             my $inner_content = substr($extracted, 1, -1);
  37.             
  38.             # 3. 递归处理内部内容(进入下一层)
  39.             process_level($inner_content, $level + 1);
  40.             
  41.             # 4. 更新 $str,跳过刚才处理过的这部分,继续寻找同级的下一个括号
  42.             # 原字符串 = (括号前的垃圾) + (提取的部分) + (剩余部分)
  43.             # 我们需要保留:(括号前的垃圾) + (extract_bracketed 返回的 remainder)
  44.             # 注意:$substring 是从 $start_pos 开始的,所以 $start_pos 之前的部分要拼回去
  45.             my $prefix = substr($str, 0, $start_pos);
  46.             $str = $prefix . $remainder;
  47.             
  48.         }
  49.         else
  50.         {
  51.             # 理论上如果 =~ /\(/ 成立,extract_bracketed 应该能提取到,除非括号不匹配
  52.             warn "检测到左括号但无法提取完整结构,可能存在语法错误。\n";
  53.             last;
  54.         }
  55.     }
  56. }
复制代码


打印结果
  1. orig: left (inside(lv2:(lv3:deep))) def(inside2(lv2:(lv3:deep1)(lv3:deep2))) right
  2. --------------------------------------------------
  3. Lv0: (inside(lv2:(lv3:deep)))
  4.   Lv1: (lv2:(lv3:deep))
  5.     Lv2: (lv3:deep)
  6. Lv0: (inside2(lv2:(lv3:deep1)(lv3:deep2)))
  7.   Lv1: (lv2:(lv3:deep1)(lv3:deep2))
  8.     Lv2: (lv3:deep1)
  9.     Lv2: (lv3:deep2)
复制代码


耗时 200 ms
 楼主| 发表于 2026-3-26 17:08:39 | 显示全部楼层
这些模块太强大了!还要啥自行车……   直接用。

  1. #!/usr/bin/env perl
  2. use strict;
  3. use warnings;
  4. use Text::Delimited::Marpa ':constants';

  5. my($parser) = Text::Delimited::Marpa -> new
  6. (
  7.         open    => '<:',
  8.         close   => ':>',
  9.         options => print_errors | print_warnings | mismatch_is_fatal,
  10. );
  11. my($text) = q|a <:b <:c:> d:> e <:f <: g <:h:> i:> j:> k|;
  12. my($span) = 0;

  13. my($result);

  14. print '        | ';
  15. printf '%10d', $_ for (1 .. 9);
  16. print "\n";
  17. print '        |';
  18. print '0123456789' for (0 .. 8);
  19. print "0\n";
  20. print "Parsing |$text|. \n";
  21. print "Span  Start  End  Length  Text\n";

  22. if ($parser -> parse(text => \$text) == 0)
  23. {
  24.         my($attributes);
  25.         my($indent);
  26.         my($text);

  27.         for my $node ($parser -> tree -> traverse)
  28.         {
  29.                 next if ($node -> is_root);

  30.                 $span++;

  31.                 $attributes = $node -> meta;
  32.                 $text       = $$attributes{text};
  33.                 $indent     = $node -> depth - 1;

  34.                 print sprintf("%4d  %5d  %3d  %6d  %-s\n", $span, $$attributes{start}, $$attributes{end}, $$attributes{length}, '    ' x $indent . "|$text|") if (length($text) );
  35.         }
  36. }
复制代码


输出结果
  1.         |          1         2         3         4         5         6         7         8         9
  2.         |0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
  3. Parsing |a <:b <:c:> d:> e <:f <: g <:h:> i:> j:> k|.
  4. Span  Start  End  Length  Text
  5.    1      4   12       9  |b <:c:> d|
  6.    2      8    8       1      |c|
  7.    3     20   37      18  |f <: g <:h:> i:> j|
  8.    4     24   33      10      | g <:h:> i|
  9.    5     29   29       1          |h|
复制代码


 楼主| 发表于 2026-3-31 22:22:49 | 显示全部楼层

使用 "占位符" 方案

本帖最后由 523066680 于 2026-4-2 08:27 编辑

相关帖子 求助PowerShell平衡组取所有匹配项
chatgpt写的PowerShell正则平衡组示例改错

因为最早接触的是批处理,所以涉及字符串处理的时候,总会想起效率并不那么高的 "占位符" 方案,
这个方法虽然效率不高,但是非常省心,不会用到很深的正则知识。
(在某些容易混淆的敏感情况,占位符可以制造"物理隔离")

  1. use Modern::Perl;
  2. STDOUT->autoflush(1);

  3. my $text = 'win s(dows s(" s(abc) ") )); s()';

  4. my $list = [];
  5. while ( $text =~ s/(s\([^\(\)]*\))/push @$list, $1; prn($1, $list); "\xfa". $#$list ."\xfb"; /e ) {}

  6. sub prn
  7. {
  8.     my ( $s, $list ) = @_;
  9.     while ($s =~ s/\xfa(\d+)\xfb/$list->[$1]/e) {};
  10.     say $s;
  11. }
复制代码


打印结果
  1. s(abc)
  2. s(" s(abc) ")
  3. s(dows s(" s(abc) ") )
  4. s()
复制代码


这个案例中 的右括号其实多了一个,可以在完成替换的时候做检测
printf "%s\n", $text if $text =~ /[\(\)]/;
 楼主| 发表于 2026-3-31 22:50:31 | 显示全部楼层

如果括号范围内出现带斜杠转义的括号?

本帖最后由 523066680 于 2026-4-1 11:05 编辑

如果括号范围内出现带斜杠转义的括号
例如
  1. 'win (dows (" \( (abc) ()") \) \) )); ()';
复制代码


依旧是 "占位符" 方案
  1. {
  2.     my $text = 'win (dows (" \( (abc) ()") \) \) )); ()';

  3.     my $list = [];
  4.     my $cmap = {
  5.         '\(' => "\xba", '\)' => "\xbb",
  6.         "\xba" => '\(', "\xbb" => '\)',
  7.     };
  8.     $text =~ s/\\[\(\)]/$cmap->{$&}/eg;
  9.     while ( $text =~ s/(\([^\(\)]*\))/push @$list, $1; prn($1, $list, $cmap); "\xfa". $#$list ."\xfb"; /e ) {}
  10.     printf "Found unmatched parentheses\n" if $text =~ /[\(\)]/;
  11. }

  12. sub prn
  13. {
  14.     my ( $s, $list, $cmap ) = @_;
  15.     while ($s =~ s/\xfa(\d+)\xfb/$list->[$1]/e) {};
  16.     $s =~ s/[\xba\xbb]/$cmap->{$&}/eg;
  17.     say $s;
  18. }
复制代码


输出结果:
  1. (abc)
  2. ()
  3. (" \( (abc) ()")
  4. (dows (" \( (abc) ()") \) \) )
  5. ()
  6. Found unmatched parentheses
复制代码


然后我又回去看了看 Text::Balanced 的方案,默认支持 括号内存在 \( 和 \) 的情况,然而转义括号也可能出现在常规括号之外,解决方法:在匹配括号起点时规避下 \( 即可
二楼的递归代码,while ($str =~ /\(/) 以及定位 的部分改为:
  1.     while ($str =~ /(?<!\\)\(/)
  2.     {
  3.         my $start_pos = $-[0];
复制代码


测试文本以及结果:
  1. orig: left (inside(lv2:(lv3:deep))) \( def(inside2(lv2:(lv3:deep1)(lv3:deep2 \) ))) right \)
  2. --------------------------------------------------
  3. Lv0: (inside(lv2:(lv3:deep)))
  4.   Lv1: (lv2:(lv3:deep))
  5.     Lv2: (lv3:deep)
  6. Lv0: (inside2(lv2:(lv3:deep1)(lv3:deep2 \) )))
  7.   Lv1: (lv2:(lv3:deep1)(lv3:deep2 \) ))
  8.     Lv2: (lv3:deep1)
  9.     Lv2: (lv3:deep2 \) )
复制代码
 楼主| 发表于 2026-4-3 17:28:27 | 显示全部楼层
本帖最后由 523066680 于 2026-4-3 17:36 编辑
zzz19760225 发表于 2026-4-3 15:51
请问版主,选择圆括号作为字符处理对象,而不是大括号{}或尖括号,有什么原因吗?
...


这不是随便换吗 有啥问题

评分

参与人数 1技术 +1 收起 理由
zzz19760225 + 1 哦。我想是不是有什么未知特性! ...

查看全部评分

您需要登录后才可以回帖 登录 | 注册

本版积分规则

Archiver|手机版|小黑屋|批处理之家 ( 渝ICP备10000708号 )

GMT+8, 2026-4-26 14:54

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表