今天是端午节,午饭媳妇要吃饺子,孩子要吃龙虾,这两个都是费时费力的活。最后午饭包饺子,晚饭烧龙虾。

烧龙虾的技能是多年前跟我的老同学学的,作为农村人深知近年龙虾生活环境的恶化,一般我是不主动吃龙虾的,特别是外面饭馆那种整烧的做法,他们不会花太多功夫在清理龙虾上。

好在老同学传授的那套烧龙虾流程重点就在清理龙虾上:清水洗,刷子挨个刷,剪掉步足,去头抽尾,特别是去头这一步尤为重要,龙虾头部实在太脏了。清理过后整个龙虾只剩螯足和腹部了。这种清理方式的好处自然是清理的更干净、烧起来更易入味了,坏处是整个清理过程比较费功夫。

略过烧制过程,起锅装盘即成,孩子吃的可香了。

昨天是六一儿童节,这天早晨我在睡梦中被孩子叫醒,此时尚未到起床时间,往常是我到点起床烧好饭再叫醒孩子,今天孩子提前醒来,叫醒并提醒我不要耽误了到校时间,原来孩子学校今天组织夏游。

等到送到学校门口,孩子是蹦蹦跳跳冲进学校大门的;然而今天早上孩子是迈着稍显迟缓的步子走进校园的,可能是昨天夏游一天太累了吧。😄😄😄

最近几天办公室几乎每天都在讨论今天谁谁家的孩子学校组织旅游了,给人的感觉是最近全市的学校都在有序地组织孩子去本地的几家头牌收费景点旅游。从幼儿园至今在学校的组织下光动物园就去好几次了。记得去年在本地的乐园春游孩子带了100元,买了两根冰棍,自己一根请同学吃一根,又买了一个大概是义乌产的小钥匙扣,最后只剩下5块钱。疫情重创下的本地旅游业,只能靠学校的孩子们来救急了。

学校组织的旅游本是好事,但是,在当下大量人员从上海流散出来的节骨眼,如此大规模组织孩子到社会人员流动频繁的景点真的合适么!但是,真的只能选那几家收费的头牌景点来回轮流去么!

最近用到了正则表达式,总结下常用的正则表达式。

所有单词

'pear orange apple abc123 oracle'.match(/\b[a-z]+\b/gi);
//pear orange apple oracle

以指定字符串开头的单词

一个条件

以 app 开头的单词

'pear orange apple abc123 oracle'.match(/\bapp[a-z]*\b/gi);
//apple

也可以写成这样:

'pear orange apple abc123 oracle'.match(/(\bapp)(?<=\1)[a-z]*\b/gi);
//apple

总体来讲,这个模式不如上一个模式直接,好在可以通过这个模式来了解断言和引用两种语法:
这里使用了后行断言语法 (?<=y)x,仅当 x 前是 y 时匹配 x:(?<=\1)中的\1 引用的是模式 (\bapp)所匹配的文本(即开头是 app 三个字母,\b 是词边界),(?<=\1)表达的意思是只在开头是 app 时匹配后续的字母。 (\bapp)(?<=\1)[a-z]*合到一起就是开头的 app 加上后续的字母 le,得到 apple 。

多个条件

以 app 或 ora 开头的单词

'pear orange apple abc123 oracle'.match(/\b(app|ora)[a-z]*\b/gi);
//orange apple oracle

这里通过 (x|y)语法实现多个条件任选

以指定字符串结尾的单词

le 结尾的单词

'pear orange apple abc123 oracle'.match(/\b[a-z]*\Ble\b/gi);
//apple oracle

不以指定字符串开头的单词

比较拗口,也可以说以非指定字符串开头的单词。这里以非 app 开头的单词为例

'pear orange apple oracle'.match(/\b(?!app)[a-z]*/gi);
//pear orange oracle

不以指定字符串结尾的单词

le 结尾的单词

'pear orange apple abc123 oracle'.match(/[a-z]*(?<!le)\b/gi);
//pear orange

以下为本次任务用到的模式:

匹配 sql 语句中的注释

匹配 sql 语句中的注释部分,sql 注释有两种形式:--/**/

/(\/\*(.|\r\n)\*\/|\-{2,}[^(\r\n)].*(\r\n)*)/gi

其中\/\*(.|\r\n)\*\/匹配/**/形式的注释,之所以将它方前面,是因为这里使用了任选模式 (x|y),在找到匹配项之前,它会从左到右依次匹配任选模式,一旦匹配到左边的就不会继续匹配右边的任选模式了。现实中/**/注释中可能会存在--形式的字符,这就要求匹配/**/形式注释的模式优先(靠左),否则将会出现匹配不到的情况。

匹配 sql 语句中用到的表和函数

/\b((call|join)[\s\r\n\t]+(?!(\"?\w+\"?\.?\"?)?(t[e]?mp_|ds_|unnest|query_str|json_|splittable|\())\1+\"?\w+\"?\.?\"?\w*)|\b(((from)[\s\r\n\t\b\(]+)(?!(\"?\w+\"?\.?\"?)?(t[e]?mp_|ds_|unnest|query_str|json_|splittable|(\bselect)))\1+\"?\w+\"?\.?\"?\w*)|[\s\r\n\t\b]*crm_\w+\b/gi

这是迄今我写的最复杂的正则表达式了,这个模式可以满足以下要求:

  • 匹配关键字 callfromjoin 和表名;
  • 支持开头关键字排除,如排除 tmp_开头的;
  • 支持开头指定关键字,如匹配所有 crm_开头的;
  • 支持以下 sql 写法:
select a.c1,d.c4
from (((t1 a left outer join t2 b on (a.c1 = b.c1))
left outer join t3 c on ( b.c2 = c.c2))
left outer join t4 d on ( c.c3 = d.c3))

参考:MDN 正则表达式

近日,公司要对相关查询页面中的用户名、手机号码、生日、地址等敏感信息进行脱敏处理,所谓脱敏也就是隐去其中的一部分,使之不完整,以保护用户个人信息。通过 sql 实现最为简单,在此总结整理一下。以下语句只保证在 Postgresql、MySQL、SQL Server 库中有效。

手机号码脱敏

手机号码脱敏要求中间四位以****代替。

PostgreSQL 方案

使用拼接

使用 left 和 right 拼接这个方法简单好用,易于维护。

select left('17817812598',3)||'****'||right('17817812598',4); --结果为 178****2598
使用 regexp_replace

regexp_replace 函数支持正则表达式,regexp_replace 的第一个参数是需要做脱敏处理的手机号码,第二个参数是一个正则表达式,第三个字段指定替换文本,即:替换对匹配到的子串进行替换。这是将手机号码分成了三个子串,其中第二个子串(中间 4 位数字)用****替换。

select regexp_replace('17817812598','(\d{3})(\d{4})(\d{4})','\1****\3'); --结果为 178****2598

regexp_replace 不会改变原始字段值,只是简单地返回替换后的副本。这里的正则表达式按数字长度将手机号码的前三位、中间四位和后四位分为 3 个部分,替换时首尾原样引用,中间部分则替换为****

MySQL 方案

使用拼接
select concat(left('17817812598',3), '****', right('17817812598',4)); --结果为 178****2598
使用 regexp_replace

regexp_replace 函数需要 MySQL 8.0 或更高版本。第一个参数是原始字符串,也就是需要脱敏的手机号码;第二个参数是匹配模式,这里是指定的4位数字;第三个参数指定替换字符串;第四个参数4指定模式匹配的开始位置(4即从参数1的第4个字符串开始应用模式匹配);第五个参数1用于指定需要用第三个参数替换的匹配项,1即仅替换第一个匹配项,指定0或省略此参数则替换调所有匹配项。

SELECT REGEXP_REPLACE('17817812598', '[0-9]{4}', '****', 4, 1); --结果为 178****2598

SQL Server 方案

select concat(left('17817812598',3), '****', right('17817812598',4)); --结果为 178****2598

邮箱脱敏

PostgreSQL 方案

邮箱脱敏要求 @前 1 位左边的部分以*代替,由于邮箱格式的特殊性:有且只能有一个 @符号,以及 regexp_replace 只替换最先匹配到的字符的特性。可以直接使用 regexp_replace 的基本形式,第一个参数指定邮箱,第二个参数指定需要替换的字符,第三个参数指定替换结果字符。

select regexp_replace('abc@123.com', left('abc@123.com',position('@' in 'abc@123.com')-2), '*'); --结果为 *c@123.com

也可以使用正则表达式实现

--处理方式与对手机号码使用的方式相同
select regexp_replace('abc@123.com','(\w*)(\w{1}@)(\w*)','*\2\3'); --结果为 *c@123.com

MySQL 方案

MySQL 使用以下两种 regexp_replace 都可以达到同样目的,第二种方式通过 concat 动态拼接正则表达式。

select regexp_replace('abc@123.com', left('abc@123.com',position('@' in 'abc@123.com')-2), '*'); --结果为 *c@123.com

SQL Server 方案

select '*' + SUBSTRING('abc@123.com', CHARINDEX('@', 'abc@123.com') - 1, len('abc@123.com')); --结果为 *c@123.com

生日的脱敏

PostgreSQL 方案

生日要求月份位和日期位分别以**代替,由于公司产品尊重个人选择,生日中的年份是可选的,因此脱敏后的结果可能出形如:1990/**/** 或 /**/** 两种形式。这里直接用 regexp_replace 配合正则表达式实现(也可以通过条件判断加字符串拼接来实现)。

select regexp_replace('12/30','(/{0,1}\d{1,2}/\d{1,2})\Z', '/**/**'); --显示/**/**
select regexp_replace('1990/12/30','(/{0,1}\d{1,2}/\d{1,2})\Z', '/**/**'); --显示 1990/**/**

这里的正则表达式是将月份和日期拆出来,月份的开头有个可选的/,结尾的\Z 标志用于告诉程序只在末尾部分匹配得到结果字符串(即这个正则表达式只取出来符合条件的月份和日期,接着用 regexp_replace 的第三个参数完成替换)。

MySQL 方案

select regexp_replace('12/30','(/{0,1}[0-9]{1,2}/[0-9]{1,2})$', '/**/**', 1, 1); -- 显示/**/**
select regexp_replace('1990/12/30','(/{0,1}[0-9]{1,2}/[0-9]{1,2})$', '/**/**', 1, 1); -- 显示 1990/**/**

SQL Server 方案

select replace('12/30' , right('12/30', 5), '**/**'); -- 显示**/**
select replace('1999/12/30' , right('1999/12/30', 5), '**/**'); -- 显示 1990/**/**

用户名和地址的脱敏比较好处理,直接使用 left 拼接实现反而更方便。

有一次领导在晨会上说书不能外借,一直没弄明白原因,今天我似乎有点眉目了,顺便在网络上搜索了一些人的见解,书之所以不能外借,总结如下:

  1. 怕借阅的人不爱惜,导致书籍破损、涂抹,甚至丢失收不回来;
  2. 有收藏价值的书和情感寄托的书,如:绝版书、善本、祖传书籍,不能借,接丢了就彻底没了。

于我自身的感悟,则需要通过一个实例来讲述:曾经为了备考某个证书买了两本工具书,备考期间就有同事向我预借了其中的一本,备考过程中我在这两本书中随手记录了大量的感想和注记。等到考完试,自然就借出了预借的那本,写本文当天我翻出了剩下的另一本,再次翻看这些注记,阅读时的多数情境都还历历在目,甚至彼时的坐姿、疑虑、感想,外界的冷暖、动静还能自然地在脑海里重放和回响。这些随手作的注记就像是记忆的催化剂,当你再次看到它们时这些记忆自然会浮现在眼前。我个人觉得这些注记是除书籍本身之外最宝贵的东西,甚至有几分私密性可言,借出去若是收不回来,等于这段记忆片区就丢失了。如果让我说一个书不外借的理由,书籍中这些注记就是其一。在这里并无对借书同事的抱怨,仅仅记录一些感想,况且,让更多人受益本就是工具书该有的作用。