我的 Regex quiz 答案

正则表达式写起来比看别人的难,所以把我的过程也放在这里。

在哪里可以做到题目?Regex101.com

TASK 1 WORD BOUNDARIES

Check if a string contains the word word in it (case insensitive). If you have no idea, I guess you could try /word/.

我的答案 (11/11):

1
/\bword\b/i

\b 表示单词的边界。
/i 标记大小写不敏感。

TASK 2 CAPITIALIZING I

Use substitution to replace every occurrence of the word i with the word I (uppercase, I as in me). E.g.: i'm replacing it. am i not? -> I'm replacing it. am I not?. A regex match is replaced with the text in the Substitution field when using substitution.
就是把单独的 i 换成 I。(将英语里应该是「我」的 i 都改成大写。)

我的答案 (9/9):
REGULAR EXPRESSION

1
/\bi\b/g

SUBSTITUTION

1
I

\b 表示单词的边界。
/g 表示全局替换,不加就只替换第一个,提交会有如下提示:

Test 7/9: You are not replacing all occurrences of i. I think I would very much like to capitalize every “I” in my sentences! Remember, the engine will only match the first occurrence unless you tell it otherwise!

TASK 3 UPPERCASE CONSONANTS

With regex you can count the number of matches. Can you make it return the number of uppercase consonants (B,C,D,F,..,X,Y,Z) in a given string? E.g.: it should return 3 with the text ABcDeFO!. Note: Only ASCII. We consider Y to be a consonant! Example: the regex /./g will return 3 when run against the string abc.
找到所有的大写辅音字母: BCDFGHJKLMNPQRSTVWXYZ

我的答案 (32/16):

1
/[B-D]|[F-H]|[J-N]|[P-T]|[V-Z]/g

这个我真不会(。

更靠谱的答案 (19/16):

1
/(?![AEIOU])[A-Z]/g

没想到可以这么简单,先用 {?!} 排除掉 AEIOU 再匹配 [A-Z] 就可以了。[^] 的话只能排除单个字符。

{?!} 这个东西叫 Negative Lookahead,有的翻译成否定式向前查找,我感觉这个向前太具有迷惑性了,不若改成否定式正向查找。例如 hello{?!world} 就是指 hello 后面不能有 world

所以上面的本题答案的意思就是**不能是AEIOU但还要属于[A-Z]**。

但是!虽然很接近,但这仍不是本题最短解。从哪里还可以再扣掉三个字符呢?

更更短的答案 (18/16):

1
/(?![EIOU])[B-Z]/g

But hey! 你这也只少了一个字符啊,16 怎么来的呢?

最短的答案 (16/16):

1
[^U+0001-AEIOU[-ÿ]

下面是抄来的解释:

  • U+000 匹配了所有在 U+0 列表里的单个字符(大小写敏感)[1]
  • 1-A 匹配了 1A 之间的所有单个字符
  • EIOU 匹配了 E I O U (废话)
  • [-ÿ 匹配了 [ÿ 之间的所有字符

其中后三条应该都是指的 ASCII 和 Extended ASCII 中的顺序,第一条应该是 Unicode。

注意,上面的四条全部是要被排除的字符!所以剩下的就只有要求的字母了。

TASK 4 RETRIEVE NUMBERS

Count the number of integers in a given string. Integers are, for example: 1, 2, 65, 2579, etc.
别被这个 Count 迷惑了,它其实只是让你匹配整数一下而已。

我的答案 (6/6):

1
/\d+/g

为什么这题不放在 TASK 1?

TASK 5 WHITE SPACE

Find all occurrences of 4 or more whitespace characters in a row throughout the string.

我的答案 (9/9):

1
/\s{4,}/g

这个没什么好说的,用 {3,} 就可以了

TASK 6 BROKEN KEYBOARD

Oh no! It seems my friends spilled beer all over my keyboard last night and my keys are super sticky now. Some of the time whennn I press a key, I get two duplicates. Can you ppplease help me fix thhhis?
简单来说就是有三个连续相同字符就替替换成单个字符。例如:

1
2
3
4
aaa -> a
aaaa -> aa
aaaaa -> aaa
aaaaaa -> aa

我的答案 (11/11):

1
/(.)\1{2}/g

匹配任意一个字符非常简单,但如果用 (.){3} 匹配并用 $1 替换的话,你会发现所有连续的字符都会被判定为替换,例如 abcdefg
会被替换成 adg,所以需要加上一个 \1,意思是只有相同的才行。

TASK 7 VALIDATE AN IP

Validate an IPv4 address. The addresses are four numbered separated by three dots, and can only have a maximum value of 255 in either octet. Start by trying to validate 172.16.254.1.
TLDR:匹配一个 IP 地址

虽然读者应该都知道一个 IPv4 地址长啥样,但还是要特别提一句,1.01.1.1 是不合法的。

我的答案 (85/39):

1
/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|\d{2}|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|\d{2}|\d)$/

首先每一位不可能大于 255,但正则没有办法直接比较数字的大小,所以只能拆开了判断。

  • 如果是三位数
    • 如果第一位是 2
      • 如果第二位是 5,第三位就可以是 [0-5]
        1
        (?:25[0-5])
      • 如果第二位是 [0-4],第三位就可以是 \d
        1
        (?:2[0-4]\d)
    • 如果第二位是 1,后 2 位就可以是 \d
      1
      (?:1\d{2})
  • 如果是 2 位数,那么这 2 位都可以是 \d
    1
    (?:\d{2})
  • 如果是一位数,那就只能是 \d,因此可以和 2 位数结合起来
    1
    (?:\d{1,2})

    这里确实有个小问题就是,\d{1,2} 是可以匹配到 01 的,在 IPv4 地址中并不合法,但题目并没有针对这一点的测试,所以如果在意的话就不要放在一起了。

把上面结合起来就是「一个」数字(下面我会用 NUM 来代替):

1
(?:25[0-5]|2[0-4]\d|1\d{2}|\d{2}|\d)

然后前三「个」数字后都会跟一个点 .

1
(?:NUM\.){3}

最后再附上一「个」数字,并标注开头和结尾:

1
^(?:NUM\.){3}NUM$

突然想到,数字后面可以是点 .,也可以是结尾 $,那我就可以把这两者合并:

我的优化后答案 (57/39):

1
/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|\d{2}|\d)(?:\.|$)){4}\b$/

注意这个 \b,如果不加的话 1.1.1.1. 也会被匹配到,报错:

Test 24/139: There should be only three periods.

优化后的答案也还跟最短答案有很大差距,好想知道只用 39 个字符是怎么匹配的。

TASK 8 HTML TAGS(OPTIONAL)

Strip all HTML tags from a string. HTML tags are enclosed in < and >. The regex will be applied on a line-by-line basis, meaning partial tags will need to be handled by the regex. Don’t worry about opening or closing tags; we just want to get rid of them all. Note: This task is meant to be a learning exercise, and not necessarily the best way to parse HTML.
把所有 HTML 标签删掉(用空字符串替代)。这里的 HTML 标签范围比较广,形如 <body> </br> 甚至 <html + 行尾 行首 + script> 这种不闭合的都算在内。
至于标签的名字无所谓,题目保证 < > 不算在内。

我的答案 (26/14):
REGULAR EXPRESSION

1
/(?:^|<)[^<>]*>|<[^<>]*/mg

SUBSTITUTION

1

首先想到的就是左右一个 <>,然后中间用 .填充。前面再加上一个可选的 /。(关于这里要不要加上对 / 的判断不能确定,毕竟标签内部的内容只要不是 < > 什么都行,所以暂时存疑)

1
<\/?(?:.)+>

提交一下会有如下报错:

Test 3/15: Your pattern is too greedy, and it is consuming the text between two tags. You’re incorrectly consuming the string foo in the following example: <div>foo</div>.

经过测试会发现,<html>foo</html> 确实会被整段替换掉。那就

所以打开 Ungreedy 开关再试试:

Test 11/15: Also remove empty tags: <>.

没有考虑到空标签,好吧,这个好解决,把 . 也用 * 标记为可选或多个:

1
<\/?(?:.*)+>

依旧报错:

Test 12/15: Tags can sometimes span multiple lines. For that reason you should also remove partial tags, which may be missing either < or > if they occur at the start or end of the string, respectively. For example, body>text</body

看来我没有考虑到未闭合标签的情况,但是直接给 < > 都加上 ? 也是不可行的,这样的话,模式中的每一项都成了可选的,即空也会被匹配到,所以我只能用笨办法,复制一份,然后用 | 连接,此外直接用 ? 标记也是不可行的,因为未闭合的标签只能在行首或行尾:

1
(?:^|<)\/?.*>|<\/?.*(?:$|>)

结果分数还更低了:

Test 5/15: Your pattern is not working. I believe it’s stripping more than it should when applied to the text before<br>after. You can read about negated character classes here.

报错给出了一个例子 before<br>after,这个例子会把 before<br> 都匹配到,看来是标签的内容匹配出了问题,把左边这个 < 当做了标签名字的一部分,然后整体变成了左边未闭合的标签,那我就把匹配标签名称的 . 扣掉 <> 这两个符号:

1
(?:^|<)\/?[^<>]*>|<\/?[^<>]*(?:$|>)

其实这样已经通过了,但还可以再优化,因为我们已经规定了标签的内部不能是 <>,所以可以把 Ungreedy 关掉,并且把用来匹配 / 的也删掉。

1
(?:^|<)[^<>]*>|<[^<>]*(?:$|>)

以及其实 | 的前半部分已经把完整的标签匹配到了,后面可以只考虑位于行尾的右开标签:

1
(?:^|<)[^<>]*>|<[^<>]*

不过我的分数依然距离最短长度有不小差距。


我的 Regex quiz 答案
https://blog.butanediol.me/2022/02/18/我的 Regex quiz 答案/
作者
Butanediol 丁二
发布于
2022年2月18日
许可协议