ExcelHome技术论坛

 找回密码
 免费注册

QQ登录

只需一步,快速开始

快捷登录

搜索
EH技术汇-专业的职场技能充电站 妙哉!函数段子手趣味讲函数 Excel服务器-会Excel,做管理系统 效率神器,一键搞定繁琐工作
HR薪酬管理数字化实战 Excel 2021函数公式学习大典 Excel数据透视表实战秘技 打造核心竞争力的职场宝典
让更多数据处理,一键完成 数据工作者的案头书 免费直播课集锦 ExcelHome出品 - VBA代码宝免费下载
用ChatGPT与VBA一键搞定Excel WPS表格从入门到精通 Excel VBA经典代码实践指南
查看: 9189|回复: 29

[原创] 正则表达式之庖丁解牛: 如何去掉VBA代码中的注释

[复制链接]

TA的精华主题

TA的得分主题

发表于 2020-6-25 19:36 | 显示全部楼层 |阅读模式
[广告] VBA代码宝 - VBA编程加强工具 · VBA代码随查随用  · 内置多项VBA编程加强工具       ★ 免费下载 ★      ★使用手册
本帖最后由 ggmmlol 于 2020-6-26 22:13 编辑

问题:
VBA代码中,以单引号作为注释内容开始的标识符,有些情况下,我们希望能快速清除这些注释内容。但是实际上,在VBA的可执行代码中,可以包含有字符串常量,在这些常量里面,也可以包含多个单引号;同时,在注释内容中,也可以包含多个单引号和双引号,如下图所示,注释字符的标识符和字符串常量内的字面字符互相混杂,如何才能准确地分辨出其中的注释内容然后予以替换清除呢?
VBA代码注释清除.jpg
解:
这是一个很典型的字符串拆分类实用问题,难点在于目标字符与其它字符互相干扰,纠缠在一起,很难区分。
要高效解决这样的问题,用正则表达式是上上之选。用正则表达式描述注释内容,同时排除VBA代码中可执行语句中的常量字符串的干扰,使用正则的Replace方法,可以精准高效地清除注释内容。

经过研究,我写出了几个有效的正则表达式,最简单的一个是(其中还考虑了以Rem为注释内容的标识符的情况):

(?:'|^ *Rem\b).*|("[^"\n]*")


去除VBA注释字符的最简正则表达式.png


先上传附件供大家测试,我接着整理一下当时写这几个表达式时的思路方法,后续将把我的一些想法做一些讲解,也欢迎大家一起参与讨论,如果发现其中有不准确的地方,也请点评指正。

正则表达式去除VBA代码中的注释.rar (28.93 KB, 下载次数: 167)



评分

9

查看全部评分

TA的精华主题

TA的得分主题

 楼主| 发表于 2020-6-25 19:38 | 显示全部楼层
本帖最后由 ggmmlol 于 2020-6-26 20:56 编辑

本主题讨论的实例来源,是xlsber会员的求助帖子:
去掉这个语句的注释的正则如何写?
http://club.excelhome.net/thread-1544424-1-1.html
(出处: ExcelHome技术论坛)

在1楼的附件中,第一个工作表,演示的是源字符串是单行代码的情况,正是源于xlsber会员的求助举例。
在此情况下,我给出了一个正则表达式,它的字面形式是:
^(?:\s*Rem\b|((?:"[^"]*"|[^'])+))|[\d\D]+
并给出代码中把它赋值给一个变量的示例用法:
  1. myPattern = "^(?:\s*Rem\b[\d\D]*|((?:""[^""]*""|[^'])+))|[\d\D]+"
复制代码

接下来我将要讲述这个正则表达式是如何写出来的。
但在此之前,你很可能有一个疑问:
这个正则表达式的字面形式和它在赋值语句中的形式是完全不同的,字面形式中,每个双引号都是单的,而在赋值语句中,为什么都是两两成双出现的呢?这是程序猿送出的免费狗粮吗?!
这里我顺便解答一下:
在工作表函数公式或VBA代码中,都是用一对双引号做为字符串常量两端边界的标识符,把字符串常量的字面形式放入一对双引号中,即成为一个字符串常量,以此区分于变量名称或VBA语句中的关键词。因此,作为边界标识符的双引号存在了特殊的含义。
但这里存在一个特例,当字符串常量的字面形式中含有双引号本身时,就会造成这个字面双引号与作为标识符的双引号的混淆。为解决这个问题,采用了对字符转义的处理方式,具体方式是:对于字面形式的字符串,在把它纳入边界标识的双引号之前,对其中每个字面的双引号之前都添加一个转义符作标记,这样就可以区分字面的双引号和作为边界标识的双引号了。但这个转义符,也被规定为双引号,这样一来,字面上的每一个双引号,就被写成了两个紧连在一起的双引号了。
单身狗的怨恨咆哮:“一个转义符,好好的用啥不行呢,非得用这个双引号吗?让人看得头疼,而且还真特么拉仇恨呐!”
双引号对此表示很“尴尬”:
“我其实是好意,是为了方便大家快速书写字符串常量。
“首先,直接写出字符串常量的字面形式,
“然后,把它的字面形式中的每个双引号都替换成两个,
“最后,像对其它字符串一样,在替换后的字符串两边各加一个标识字符串边界的双引号,就完成书写了。“
所以,当用VBA代码给单元格填写公式时,如果公式中有包含了字面双引号的字符串常量,经过公式和VBA代码的两次”双倍“加成之后,原来的一个字面双引号,就会变成4个相连的双引号了。如附件中的代码所示如下:

  1. Sub Test1()
  2.     S = "=REGREPLACE(A1,""(^|\n)(?:\s*Rem\b|((?:"""".*?""""|[^'\n])*)).*"",""$1$2"")"
  3.     Range("B1").Formula = S
  4. End Sub
复制代码


请上3楼

评分

1

查看全部评分

TA的精华主题

TA的得分主题

 楼主| 发表于 2020-6-25 19:39 | 显示全部楼层
本帖最后由 ggmmlol 于 2020-6-26 21:13 编辑

2楼只给出一个正则表达式,以及讲了一些题外话。现在,正式解析这个正则表达式。
首先分析源字符即VBA代码中的注释内容的特征。
VBA语法中规定:
一、代码中的注释内容,有两种标识方式,最常见的标识方式是用一个单引号做前导符;另一种是用单词Rem做前导符,它实际是一个以Rem为关键词的特殊语句。因此,有这么一个论断:“对于Rem语句,它的关键词‘Rem’到行首之间,除了空格之外不能有任何其它字符”(本人说明:这一论断并不完全准确,因为VBA同一行代码中,可以用分隔符‘:’来连接多个语句,所以Rem语句也可以连接在其它语句后面。这是一种比较少见的例外情况,现在为简化问题起见,先按前面的结论进行处理,暂不管这种特例,后面再补充讨论)

(行文说明:本主题中的正则表达式,都以红色字体的字面形式表示。如果要写到VBA代码的字符串常量,请对其中的双引号做双倍处理)

二、VBA代码中,同一行最多有一个注释,它从注释内容的引导符开始,直到该行结束为止,不可跨行。
根据以上两点,可以推论出两点:
1、在一行代码中,如果有可执行的语句,则必然是从行首开始,直到注释内容的前导符之前为止;
2、如果不考虑Rem语句形式的注释方式,则可执行代码中必定是由非单引号的字符,以及包含在字符串常量中的任意字符组成。
由上面的第2点推论,用正则表达式分别描述构成可执行代码的字符本身的两种情形:
非单引号的字符:[^']
字符串常量(即成对双引号及其之间的字符):"[^"]*"
以上两者的任意组合,即:(?:"[^"]*"|[^'])+
以上就是需要保留的目标字符部分,因此,要用捕获分组整体获取它,也就是:((?:"[^"]*"|[^'])+)
由推论的第1点所述,目标字符是从行首位置开始的,因此需要在前面加上一个锚定字符串开始位置的元字符‘^’,
即:
^((?:"[^"]*"|[^'])+)      记作
但是推论的第2点中有一个前提条件是“不考虑Rem语句形式的注释方式”,因此需要从目标字符中提前排除这种Rem注释语句的内容。
按前述由VBA语法得到的关于Rem语句的论断,从Rem关键词到它的行首之间只允许有空格,因此这一部分字符的正则表达式是:
^\s*Rem\b    记作
注意,对于英文关键词,在其右边必须要加单词右边界锚定符\b,以避免把其它由Rem开头的单词语句误判为Rem语句,而在关键词左边,因为已有其它元字符作出了限定,所以,可以省略其左侧的边界限定符\b。
在目标字符和Rem关键词之后,还可以有多个任意字符,即
[\d\D]+    记作
①、②、③式并列起来,得到:
^\s*Rem\b|^((?:"[^"]*"|[^'])+)|[\d\D]+
如果把①、②式合并共同的锚点(即字符串的开始位置),放入一个非捕获分组里面,则可以写成:
^(?:\s*Rem\b|((?:"[^"]*"|[^'])+))|[\d\D]+    记为原式
这就是2楼所给出的正则表达式的由来,把它填写到附件表格的“单行代码”工作表B6单元格,经过2个例子的简单测试,证明它可以对单行VBA代码准确有效地清除注释内容。
上述这个表达式是针对源字符串是单行VBA代码的情况,即源字符串中必定没有换行符,因此还可以做一些简化:
字符串常量的表达式"[^"]*",可以简化成:".*?"
③式表示的任意字符串[\d\D]+,可以简化成:.+
于是整体可以简化成:
^(?:\s*Rem\b|((?:".*?"|[^'])+))|.+    记作 简①式
简①式填写到附件表格的“单行代码”工作表B7单元格,经测试,它的结果是正确的。
事实上,由于式所表示的其它字符,总是顺序出现在①或②式的后面,如果把③式①、②式由并列关系改为顺序关系,会更加合理,但这样一来,就不再是可选分支,因此原①式和③式中的次数限定符都应由‘+’改成‘*’,以表示它们都是可以出现0次或任意多次。
再次简化的结果如下:
^(?:\s*Rem\b|((?:".*?"|[^'])*)).*  记作 简②式
对于这个简②式,也可以在附件中验证它是否正确。

评分

1

查看全部评分

TA的精华主题

TA的得分主题

发表于 2020-6-26 10:21 | 显示全部楼层

TA的精华主题

TA的得分主题

发表于 2020-6-26 19:55 | 显示全部楼层

TA的精华主题

TA的得分主题

 楼主| 发表于 2020-6-26 21:25 | 显示全部楼层
[广告] Excel易用宝 - 提升Excel的操作效率 · Excel / WPS表格插件       ★免费下载 ★       ★ 使用帮助
3楼已经分析了清除单行VBA代码中注释字符的正则表达式,
而实际上,能够正常运行的VBA代码都是多行的,因此,我们更希望能用正则表达式清除多行VBA代码中的注释内容。
分析的方法与此前类似,我们甚至可以几乎完全套用原来的正则表达式。区别在于,一个包含多个换行符的VBA代码字符串中,每行代码的可执行语句的开始位置,与此前的单行代码不同,它既可以是整个字符的开始位置,也可以在紧接任意一个换行符后的位置,
所以,把简②式^(?:\s*Rem\b|((?:".*?"|[^'])*)).*中的开始位置锚定符‘^’,要改成(^|\n),而且,其中取反的字符集[^']里面也要排除换行符,改写的结果就是:
(^|\n)(?:\s*Rem\b|((?:".*?"|[^'\n])*)).*  记为 简②改
对于单行的VBA代码,也可以看作是多行VBA代码中的一个特例,因此,这个正则表达式如果它理论上对多行的VBA代码字符串是正确的,那它对单行VBA代码字符串必定要同样有效,所以,也可以用与前面相同的方法,测试一下它是否正确。把它填入“单行代码”工作表的B10单元格。需要注意的是,这个正则表达式多了一个捕获分组,因此对应C10、D10单元格公式中REGREPLACE的第3参数要由"$1"相应改为"$1$2",例如C10单元格中的公式要改为:=REGREPLACE(C1,$B$10,"$1$2")
经过验证,简②改 对于单行代码中的注释内容是可以准确清除的,接下来,就要测试它对于多行VBA代码串的处理结果。
我在附件中,是用VBA填写公式的方法来验证这个正则的,也就是在“多行代码”工作表中,“按钮一”执行后,在其B1单元格中填写的公式,就应用了这个正则表达式,其结果确实是正确的。
到此,我们得到了一个对单行或多行的VBA代码字符串都可以正确清除注释字符的正则表达式。
这也说明,我们此前的分析方法也是正确的。
但是,这个分析方法,主要把需要保留的可执行代码部分作为目标字符,正则表达式主要以其特征为基准,再排除必定不符目标字符要求的整行Rem语句注释字符,而得到最终结果。

这个目标字符的基准表达式是一个比较复杂的组合形式,如下所示
^((?:".*?"|[^"'\n])*)
此前我们通过一番细致耐心的分析,才得到这个结果。而能够完成这种分析,需要对正则表达式的理论有较深入的理解,对于初学者来说,难度颇大。但如果我们可以换一种思路,以庖丁解牛的思想方式,准确地把握字符串内各个部分的特征,把我们主观需要的目标字符串和实际字符串中客观存在的干扰字符串,它们的基本客观特征,都用正则表达式各自准确表示出来,然后按它们之间的固有的相互关系,排列其主从顺序,就可以很容易地剖析清楚字符串各种特征段,从而能够实现按我们的主观需要进行分拆取舍。


掌握这种方法之后,你见到一个需要拆分的字符串之后,就好像庖丁见到一头待分解的牛一样,“目无全牛”,根本不需要用眼睛去看其外表,但官止而神行,在他心中,早已深入感受到这头牛躯体之内的经络骨骼之间的间隙,因此早已有了进刀游刃的步骤方法,早已有了这头牛各个部位解分出来后的模样,按庖丁之所言,即“依其天理,批大郤,导大窾,因其固然”,“彼节者有间,而刀刃者无厚;以无厚入有间,恢恢乎其于游刃必有余地矣”,因此“动刀甚微,謋然已解,如土委地”,片刻间就轻松拿下。

下一节,将以我在一楼所给出的那个最简化的正则表达式为例,来讲述这种拆分字符串的方法。


评分

1

查看全部评分

TA的精华主题

TA的得分主题

 楼主| 发表于 2020-6-26 21:42 | 显示全部楼层
分析1楼给出的“最简”正则表达式
1楼的“最简”正则表达式,当你第一眼看到时,你会觉得它简单到难以置信,但它确实有效。那么,它是怎么来的呢?

(首先做一个说明:
这个正则表达式必须在开启正则对象的Multiline属性时,才能对多行的VBA代码字符串有效。



因为一个模块或过程的VBA代码,通常都是多行字符串,而且,以Rem关键词的整行注释语句,需要以各行的行首位置为锚点,所以,开启正则对象的Multiline属性,可以简化正则表达式,因为脱字符"^"在这种情况下,可以匹配字符串中每个行首位置,不需要用捕获分组(^|\n)来代替,处理起多行字符串,就象单行字符串一样容易了。
所以,在附件表格的“多行代码”工作表中,所给出的正则表达式,都是以开启正则对象的Multiline属性为基础的。






在讲解之前,我们看看附件表格中“多行代码”工作表里面提供了近9个不同的正则表达式,它们都可以实现对多行VBA代码准确清除注释字符。这9个表达式虽然各个不同,但如果仔细观察它的各个分支或分组表达式,就会发现,其中只有5个基本的部件,各种表达式都是由这5种基本的表达式组合连接而成,把它们具体列表分析如下,就会很清楚明白了:

image.png



第1项、第2项,是VBA代码中注释字符的两种不同的表示方式,一个是Rem整句注释,一个是分号开头的灵活注释。
第3项,是一个代码行里面可能出现的字符串常量,它可以出现在代码行的可执行部分,也可以出现在注释内容之中,当出现在前者时,如果其中包含了单引号,就会干扰注释内容的拆分位置。因此必须明确出来。(在1楼时,常量字符串的表达式写成"[^"\n]*",而在上表中,只有".*?"那个比较类似,这两者是等价的表达式吗?答:是等价的。因为VBA代码中的字符串常量是不允许跨行的,因此中间必定不会有换行符,除此之外,它可以是任意字符,直到出现最近相邻的单引号为止,因此中间也不能有单个的双引号出现,即也要排除双引号。)

第1、2、3项是最基本的项,也就是可以直接写出正则表达式的项,第4、5项都要依赖前3项的相互关系进行复杂的推导得出。
如此分析下来,源字符串的这5个关键部分的表达式一一列出来了,你就可以像庖丁一样“目无全牛”而“心中有牛(的躯体的各个基本部位)”了,这样就能“以刀刃无厚而入有间”,只找容易处着力,使得“动刀甚微,謋然已解,如土委地”,也就是在用正则表达式拆分字符串时,仅量使用能直接写出表达式的基本项,这样很容易,而不要用推导项,因为要得出正则表达式的推导项的写法,是很麻烦的,需要强大的逻辑思维能力才能准确推导出来,除非是想跟自己斗气的,否则不要这样自寻烦恼。

现在,再回过头来看1楼的正则表达式,是不是发现了一个真相:它正是由上述3个基本项以分支运算符“|”连接而成?!
事实正是如此,而且我们只是简单地把它们并列在一起,然后根据主观需要,而进行了合理取舍:即对前2项合并后而使用了非捕获分组,而对第3项则单用了捕获分组。

这是因为我们主观意愿是想要清除代码中的注释内容,所以,表示注释内容的前2个基本项的表达式自然都不要需要捕获了,即使它们需要用分组的元字符组合到一起,也尽量要使用(?:exp)这样的非捕获分组(因为我们不需要玩“捉放曹”,所以尽量只在切实需要的情况下,才进行“捕获”)。
第3个基本项也就是字符串常量的表达式,它为什么要使用捕获分组呢?
因为我们主观要求保留可执行代码部分的所有字符。而如果只按前2项的注释字符规则清除字符的话,那么,存在于可执行代码部分的字符串常量中的单引号开始的部分内容,也会被误判为注释内容,这样就会被超预期地给“咔嚓”掉了。所以,我们需要把这些字符找补回来,而正则表达式提供的捕获分组运算符,就是用来事先获取并保存需要保留的内容,当进行替换时,就能以相应的分组编号引用回来。
总结来说,如果是删除式替换,则要用捕获分组把可能多余删除掉的部分找补回来,反过来,如果是保留式替换,则要用优先级更高的非捕获分支或分组表达式,先于捕获分组行动,预先消耗排除掉那些主观上并不允许保留的多余字符。
又问:1、2两项合并之后与第3项以分支选择运算符"|"相连,此前你讲过这个运算符两边的分支是左边分支优先的,现在怎么确定哪个的实际优先级更高、需要放在左边呢?
答:在1楼的问题描述中,就提到了,可执行代码中的常量字符串,和注释内容可以互相混淆,如果只从字符的特征形式来看,两者可以互相包含,但又不会绝对由一项完全包含另一项,这种情况下,它们自身的特征决定了与左右分支优先级无关,而是以它们匹配的字符串中的字符实际匹配的先后为准,“先到先得”,也就是说,它们的公共的字符即是单引号,如果被依次相邻成对的某一对双引号所包含,则么为个单引号就属于常量字符串的一个字面字符,否则,就是注释内容的前导符。也就是说,本例中,两者可以在分支运算符左右的各自任占其一,无论把哪个写在左面,都不影响实际结果。这可以从附件表格的左“多行代码“工作表的A5和A10两个单元格中的正则表达式,就是在分支运算符左右互换分支表达式的写法,所得结果一致。

但是,如果分支符左右两侧的一个分支绝对包含另一个分支,则通常应当把被完全包含的那个特例的分支放在左边,使其获得优先匹配,否则,如果放在低优先级有右侧,则它所能匹配到的任何内容,都必然被前者所“截胡”,那它的这个分支就实际失去了作用、根本没有保留这个分支的意义了。


评分

1

查看全部评分

TA的精华主题

TA的得分主题

 楼主| 发表于 2020-6-26 21:47 | 显示全部楼层
我的这个主题内容主要讲完了,你是不是觉得结束得太突然呢?里面还有好多个正则表达式的写法没有讲解它的具体解题思路呢?

如果一定要我再多说一些的话,那就再啰嗦一遍:
在用正则拆分字符串时,需要你按庖丁解牛那种剖析入理的思想方法,找出其所有关节要害,特别要分解出所有隐藏起来的但是必然对结果造成影响的基本项,以此为最容易着力的突破点,就像庖丁一样把刀刃只从关节的间隙这样最容易通过的地方进行拆分,“以无厚入有间”、游刃有余,你所面临的问题也必然迎刃而解!
在本例子中,我们正是分析出了“常量字符串”就是这样一个隐藏起来干扰注释内容拆分的基本项,当把它揪出来放到明处,一切就不言而喻了!反之,如果没有找出这个隐藏的基本项,那这个问题就几乎是无从下手的难题了!

评分

1

查看全部评分

TA的精华主题

TA的得分主题

发表于 2020-6-26 23:42 | 显示全部楼层
你遍历字符串,数一下双引号个数,如果奇数后面出现了单引号,那单引号开始都是注释,删除即可

TA的精华主题

TA的得分主题

 楼主| 发表于 2020-6-27 08:21 | 显示全部楼层
[广告] VBA代码宝 - VBA编程加强工具 · VBA代码随查随用  · 内置多项VBA编程加强工具       ★ 免费下载 ★      ★使用手册
本帖最后由 ggmmlol 于 2020-6-27 09:26 编辑
liucqa 发表于 2020-6-26 23:42
你遍历字符串,数一下双引号个数,如果奇数后面出现了单引号,那单引号开始都是注释,删除即可
  1. Sub 常规方法清除VBA代码注释()
  2.     [b1] = 清除VBA多行代码注释([a1])
  3. End Sub
  4. Function 清除VBA多行代码注释(ByVal S$) As String 'sfgh
  5.     Dim aa, SS, i&, j&
  6.     SS = Split(S, vbLf)
  7.     For i = 0 To UBound(SS)
  8.         If LTrim(SS(i)) Like "Rem *" Then
  9.             SS(i) = Empty
  10.         ElseIf SS(i) Like "*'*" Then
  11.             aa = Split(SS(i), """")
  12.             For j = 0 To UBound(aa) Step 2
  13.                 If aa(j) Like "*'*" Then
  14.                     n = InStr(aa(j), "'")
  15.                     aa(j) = Left(aa(j), n - 1)
  16.                     ReDim Preserve aa(0 To j)
  17.                     SS(i) = Join(aa, """")
  18.                     Exit For
  19.                 End If
  20.             Next
  21.         End If
  22.     Next
  23.     清除VBA多行代码注释 = Join(SS, vbLf)
  24. End Function
复制代码
您需要登录后才可以回帖 登录 | 免费注册

本版积分规则

手机版|关于我们|联系我们|ExcelHome

GMT+8, 2024-11-18 22:42 , Processed in 0.044758 second(s), 10 queries , Gzip On, MemCache On.

Powered by Discuz! X3.4

© 1999-2023 Wooffice Inc.

沪公网安备 31011702000001号 沪ICP备11019229号-2

本论坛言论纯属发表者个人意见,任何违反国家相关法律的言论,本站将协助国家相关部门追究发言者责任!     本站特聘法律顾问:李志群律师

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