Scala实战:巧用集合实现数据脱敏

(原文在:《http://www.yangbajing.me/2016/07/25/Scala实战:巧用集合实现数据脱敏
,转载请注明!)

在日常开发中,经常会遇到对数据进行脱敏处理的需求。像隐藏身份证或者手机号中间几位。比如对于:13812345678这个手机号,我们会使用*号替换中间4位来达到隐藏的目的,就像这样:138****5678。这是一个很常见也很简单的功能需求,这里记录下开发中对这个需求的实现。从一开始命令式的风格到函数式风格,从一开始硬编码隐藏范围和替换字符到调用者可以自定义,从繁琐和代码实现到清晰、简洁……本文将一步一步的给读者展示Scala强大的表现力、丰富的API和高效的生产率体现。

命令式

首先来看看一开始实现的隐藏手机号函数:

1
2
3
4
5
6
7
scala> def hidePhone(phone: String): String = {
| phone.substring(0, 3) + "****" + phone.substring(7)
| }
hidePhone: (phone: String)String

scala> hidePhone("13812345678")
res1: String = 138****5678

咋一看,代码量很少嘛,也很简洁明了,需求实现的非常好。但其实这段代码有很多坏的和不完善的地方。如:有3个数字,它们决定了哪些字符需要原样保留。但万一业务需求是隐藏末尾5个字符呢?难到我们需要再写一个hideLastPhone函数?这样子太low了……

于是,对hidePhone完成第一次改进,我们让调用方来决定需要隐藏哪些字符而不是在代码里写死保留哪些字符。

1
2
3
4
5
6
7
8
9
10
def hidePhone(phone: String, start: Int, end: Int): String = {
val builder = new StringBuilder(phone.substring(0, start))
var i = start
while (i < end) {
builder.append('*')
i += 1
}
builder.append(phone.substring(end))
builder.toString()
}

代码看起来有点多,但实现了调用方设置隐藏范围,实用性更好了。来看看测试效果:

1
2
3
4
5
scala> hidePhone("13812345678", 6, 6 + 5)
res2: String = 138123*****

scala> hidePhone("13812345678", 3, 3 + 4)
res3: String = 138****5678

不错,效果很好。正确!但是,我们换个参数再试试……

1
2
3
4
5
scala> hidePhone("13812345678", 6, 6 + 6)
java.lang.StringIndexOutOfBoundsException: String index out of range: -1
at java.lang.String.substring(String.java:1931)
at .hidePhone(<console>:18)
... 32 elided

Oh……数据越界错误。要修正这个错误也很简单:

1
2
3
4
5
6
7
8
9
10
11
12
def hidePhone(phone: String, start: Int, end: Int): String = {
val builder = new StringBuilder(phone.substring(0, start))
var i = start
while (i < math.min(phone.length, end)) {
builder.append('*')
i += 1
}
if (end < phone.length) {
builder.append(phone.substring(end))
}
builder.toString()
}

我们修复了两个地方:

  1. while语句不是直接小于end变量,而是小于phone.lengthend两个变量之间更小的那个
  2. 最后一个builder.append语句加上了一个if防卫措施,只有当end小于手机号长度时才添加。

这时,我们再次尝试刚才错误的那个示例。发现它已经可以正确的执行了。

1
2
scala> hidePhone("13812345678", 6, 6 + 6)
res5: String = 138123*****

函数式

我们已经看过了命令式的数据税敏代码,没想到这样一个简单的功能还是需要写不少代码的。现在已经使用了具有函数式特性的高级的Scala语言,我们能不能把代码写得更functional更漂亮(代码丑陋也是不可忍的……)?我们尝试着再次改进一下。

1
2
3
4
5
6
def hidePhone(phone: String, start: Int, end: Int): String = {
phone
.zipWithIndex
.map { case (ch, idx) => if (idx >= start && idx < end) '*' else ch }
.mkString
}

相比之前那段命令式的代码,这是不是简洁、清晰了很多?我们先使用.zipWithIndex方法将字符串变成一个带索引的Tuple序列,形如:Seq(('1', 0), ('3', 1), ('8', 2), ....)。再通过判断索引idx是否在[start, end)范围来判断返回对应字符还是返回替换后的*号字符。最后再调用mkString方法把Seq[Char](字符序列)格式化成一个字符串。

到了这里,我们还有改进的地方:

  1. 我们想可以自定义替换字符,用户可以使用’-‘、’|’等符号来替换,而不是默认的’*’号。
  2. 对于end这个参数,当前代码实现是不替换这个索引的字符。那万一我们的需求是要替换这个索引的字符呢?当然,你可以说传入参数时将end + 1不就行了,但感觉不太好……因为我们还有更好的方案。
1
2
3
4
5
6
def hidePhone(phone: String, replaceRange: Range, replaceChar: Char = '*'): String = {
phone
.zipWithIndex
.map { case (ch, idx) => if (replaceRange.contains(idx)) replaceChar else ch }
.mkString
}

这时的使用方式就和之前不一样了:

1
2
3
4
5
6
7
8
9
10
11
scala> hidePhone("13812345678", 6 until 6 + 5)
res6: String = 138123*****

scala> hidePhone("13812345678", 3 until 3 + 4, '-')
res9: String = 138----5678

scala> hidePhone("13812345678", 3 to 3 + 4, '-')
res7: String = 138-----678

scala> hidePhone("13812345678", 6 until 6 + 6, '^')
res8: String = 138123^^^^^

总结

一个常用的数据脱敏函数,需要注意的地方还是挺多的。而函数式风格的实现相对命令式风格来说从可读性上更具优势。Scala以一种从左到右顺序编写、链式调用实现了数据脱敏这个功能,同时兼具了灵活性和健壮性。

分享到