正则表达式整理

整理正则表达式的常用语法

在看booster内联R的时候,看到过滤R$color这种内部类的时候,是通过正则表达式进行过滤的,然后它的匹配规则是:

1
2
internal const val R_REGEX = ".*/R\\\$.*|.*/R\\.*"
Pattern.matches(R_REGEX, klass.name)

我是想匹配到com/xc/lib/R$colorcom/xc/lib/R形如这种class的名字,第一眼看的时候有点懵,然后通过查看正则表达式的语法时候,发现正则是有规律可循的,先看一张表格:

符号 含义 示例
. 匹配任意单个字符(除换行符) a.b 可匹配 acb、a1b
\d 数字 (0-9) \d{3} 匹配 123
\w 单词字符 (a-z, A-Z, 0-9, _) \w+ 匹配 abc_123
\s 空白字符(空格、Tab、换行) Hello\sWorld 匹配 Hello World
\S 单个非空白字符 任意单个非空格字符都匹配
\b 单词边界 \bcat\b 仅匹配 cat,不匹配 catalog
^ 行首 ^Hello 仅匹配 Hello 在行首的情况
$ 行尾 end$ 仅匹配 end 在行尾的情况
* 匹配前一个字符 0 次或多次 ab*c 可匹配 ac、abc、abbc
+ 匹配前一个字符 1 次或多次 ab+c 仅匹配 abc、abbc,不匹配 ac
? 匹配前一个字符 0 次或 1 次 ab?c 可匹配 ac 或 abc
{n} 匹配 n 次 a{3} 仅匹配 aaa
{n,} 至少匹配 n 次 a{2,} 可匹配 aa、aaa
{n,m} 匹配 n 到 m 次 a{2,4} 可匹配 aa、aaa、aaaa
[] 字符集合 [aeiou] 匹配任意元音字母
` ` 或(匹配左边或右边)
() 分组 (ab)+ 匹配 ab、abab

上面".*/R\\\$.*|.*/R\\.*"可以拆解成两个正则,因为中间用了一个或,分别是.*/R\\\$.*.*/R\\.*,首先来看.*/R\\\$.*它可以分解成:

字符 含义 匹配字符
.* 表示有0次或多次的单个字符 com/xc/lib
/R 直接匹配字符 /R
\$ 这里由于和$有冲突,所以前面加了转义,匹配的是$字符 $
.* 和开头的匹配一样,表示有0次或多次的单个字符 color
所以最终com/xc/lib/R$color能被匹配到,再来看.*/R\\.*它可以分解成:
字符 含义 匹配字符
—— —— ——
.* 表示有0次或多次的单个字符 com/xc/lib
/R 直接匹配字符 /R
\.* 这里由于和.有冲突,所以前面加了转义,匹配的是0次或多次的.字符 .

所以最终com/xc/lib/R也能被匹配到。

更多正则的匹配: 测试用例: #key_mmkv_migrate_version# ,正则表达式(#[^#]*[^#\\s]+[^#]*#),使用

1
2
Pattern pattern = Pattern.compile("(#[^#]*[^#\\s]+[^#]*#)");
Matcher matcher = pattern.match("#key_mmkv_migrate_version# ")

该正则表达式作用:用于匹配 以 # 开头和结尾的内容,并且内容中至少包含 一个非空白的非 # 字符。 正则解析:

部分 含义
# 匹配 #(开始)
[^#]* 匹配任意多个非 # 的字符(0 次或多次)
[^#\s]+ 至少匹配 1 个非 # 和非空格的字符(保证内容不是空的)
[^#]* 再次匹配任意多个非 # 的字符(0 次或多次)
# 匹配 #(结束)
(…) 括号用于捕获匹配内容

Matcher#replaceAll方法

1
matcher.replaceAll("<a type="topic" >$1</a>")

会被替换成如下: <a type="topic" >#key_mmkv_migrate_version#</a>

正则: <a[^>]*?((>[\s\S]*?</a>)|(/>))

1
2
@kotlin.internal.InlineOnly
public inline fun String.toRegex(): Regex = Regex(this)

将string转化成Regex:

1
2
val regexStr = `<a[^>]*?((>[\s\S]*?</a>)|(/>))`
val regex = regexStr.toRegex()

用于 匹配 HTML 标签(包括自闭合 和 **带内容的 形式)。 正则解析:

部分 含义
<a 匹配 <a(开始的 标签)
[^>]*? 非贪婪匹配 内的所有属性(不包括 >)
`((… ) (/>))`
>[\s\S]*? 情况 ①:匹配 > 后的内容,直到
/ > 情况 ②:匹配 (自闭合标签)

CharSequence.contains(regex: Regex):

1
2
3
4
val source = `<a type="topic" >#key_mmkv_migrate_version#</a> `
val regexStr = `<a[^>]*?((>[\s\S]*?</a>)|(/>))`
val regex = regexStr.toRegex()
source.contains(regex)

contains方法表示是否符合这个正则判断。

Regex.find(input: CharSequence, startIndex: Int = 0): MatchResult?
Regex.find() 方法用于 查找字符串中第一个匹配的结果,返回 MatchResult?,如果找不到匹配项,则返回 null。 MatchResult.range:表示匹配的索引范围,分别用start和endInclusive来表示范围。 MatchResult.value:表示匹配到的内容

正则:(\\S+)=\"([^\"]+)\" 作用:主要是用来匹配html/xml属性及其值,比如<a type="topic" >#key_mmkv_migrate_version#</a> 能匹配到,它是匹配type="topic"。 正则解析:

正则部分 作用
(\S+) 匹配属性名(非空字符 \S,匹配至少一个字符 +)
= 匹配等号(固定字符 =)
"([^"]+)" 匹配属性值(被双引号 " 包裹,内容不含 “)

Regex.findAll

1
public actual fun findAll(input: CharSequence, startIndex: Int = 0): Sequence<MatchResult> 

用于获取字符串中所有符合正则的结果,返回的是一个Sequence的MatchResult

1
2
3
val attributeRegex = "(\\S+)=\"([^\"]+)\"".toRegex()
val source = "<a type="topic" >#key_mmkv_migrate_version#</a> "
val matchResultList = attributeRegex.findAll(source)

MatchResult.destructured属性
用来提取匹配结果的捕获组,例如上面提取的是type和topic这一组内容。

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
val regex = Regex("(\\d{4})-(\\d{2})-(\\d{2})") // 匹配日期格式 YYYY-MM-DD
val input = "2024-03-07"

val matchResult = regex.find(input)

if (matchResult != null) {
    val (year, month, day) = matchResult.destructured
    println("Year: $year, Month: $month, Day: $day")
}

输出
Year: 2024, Month: 03, Day: 07

然后通过MatchResult.destructured.toList()方法转成list,就可以获取type和topic内容了。

SpannableStringBuilder使用

在 Android 开发中,SpannableStringBuilder 主要用于富文本(Styled Text),可以让字符串的一部分具有不同的格式,比如 颜色、大小、样式、点击事件、图片等。它比 Html.fromHtml() 性能更好,更适合动态拼接文本。

使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
val spannable = SpannableStringBuilder("Hello, Android!")

// 给 "Android" 设置红色
spannable.setSpan(
    ForegroundColorSpan(Color.RED),
    7, 14, 
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)

// 应用到 TextView
textView.text = spannable
  • SpannableStringBuilder(“Hello, Android!”) 创建一个可变的 Spannable 对象。
  • setSpan(ForegroundColorSpan(Color.RED), 7, 14, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
  • ForegroundColorSpan(Color.RED) → 设置前景色为红色。
  • 7, 14 → 应用范围(“Android” 字符的索引)。
  • SPAN_EXCLUSIVE_EXCLUSIVE → 该 Span 只作用于当前字符范围,后续文本修改不会继承。
  • textView.movementMethod = LinkMovementMethod.getInstance(),设置 TextView 可点击

所以如果想要多个效果,可以添加多个span给SpannableStringBuilder,通过多次调用setSpan方法,然后设置不同的坐标信息。

SpannableStringBuilder.append(),可以追加多个span的样式,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
val spannable = SpannableStringBuilder()

// 添加普通文本
spannable.append("Normal Text ")

// 添加红色文字
val redText = SpannableString("Red Text")
redText.setSpan(ForegroundColorSpan(Color.RED), 0, redText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
spannable.append(redText)

// 添加蓝色斜体文字
val blueText = SpannableString(" Blue Italic")
blueText.setSpan(ForegroundColorSpan(Color.BLUE), 0, blueText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
blueText.setSpan(StyleSpan(Typeface.ITALIC), 0, blueText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
spannable.append(blueText)

// 应用到 TextView
textView.text = spannable

上面用到了SpannableString,它是实现了CharSequence,是对字符串进行样式的扩展类,然后把它添加到SpannableStringBuilder中。

span中插入图片,通过imageSpan实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
val spannable = SpannableStringBuilder("Text with image ")
val drawable = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground)!!
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)

// 在文本末尾添加图片
spannable.setSpan(
    ImageSpan(drawable, ImageSpan.ALIGN_BASELINE),
    spannable.length - 1, spannable.length, 
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)

textView.text = spannable
Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy