前言
近期需要整一个活,这个活呢?听起来十分高大上,可能需要用到自然语言识别,从一串无规则,长度不一的字符串内,取出相对应的收货地址以及商品信息等,完成自动创建订单
流程
由于部分逻辑涉及业务,就简要书写大概流程
项目流程大概如下,主要是通过C端用户发送一段字符串,推送给相关机器人配置,然后完成一些列的信息截取和相关操作
graph TB
字符串-->发送机器人
发送机器人-->机器人(拆分字符串)
机器人(拆分字符串)-->创建并写入excel
机器人(拆分字符串)-->拆分商品
拆分商品-->创建并写入excel
创建并写入excel-->转移文件权限
创建并写入excel-->发货
发货-->用户导出订单信息
相关工具及准备
由于需要检验用户输入的地址错别字问题,业务上将解析包换成了百度的地址识别
准备操作
飞书机器人的配置: 飞书机器人配置及开发
唯一一点要提到的就是,在配置完机器人后,得书写轻服务应用(类似于云开发函数),不太理解的老哥们,可以点我!!
配置完云函数后,将机器人添加进相对应的群里,及可以试试收到相关用户信息
向群里发送信息
云开发
这里注意的是,需要配置一个云函数为默认接收机器人的消息,这里需要讲该云函数的HTTP链接放入配置机器人 配置事件回调地址中
日志后台
接下来我们可以就可以愉快的写js了,完成我们的需求
解析用户字符串
基础关键字库
商品的种类千奇百怪,配置了基础的关键字库,用来解析相关商品及其单位和数量
如下:
const unitKeyWords = ["箱", "单", '盒', '斤', '件', '个']
const goodsKeyWords = ['猪肉', '牛肉', '羊肉', '鸡肉', '罗汉果']
const goodsTitle = ['商品名称', '商品', '名称', '数量']
const goodsArr = [] // 商品
过滤特殊字符串
由于一串字符串内,可能存在大量的奇奇怪怪的分隔符,例如 ',,,!,@,‘’,?。,等等
所以拿到该字符串后,直接将特殊字符直接过滤
function stripscript(s) {
let pattern = new RegExp(
'[`~!@#$^&*()=|{}\':;\',\\[\\].<>/?~!@#¥……&*()——|{}【】‘;:”“’。,、?-]',
);
let rs = '';
for (let i = 0; i < s.length; i++) {
rs = rs + s.substr(i, 1).replace(pattern, ' ');
}
rs = rs.replace(/[\r\n]/g, '');
return rs
}
识别用户手机号码
将xxx xxxx xxxx 或者 xxx-xxxx-xxxx 转换成 xxxxxxxxxx
function transPhone(s) {
s = s.replace(/(\d{3})-(\d{4})-(\d{4})/g, '$1$2$3');
s = s.replace(/(\d{3}) (\d{4}) (\d{4})/g, '$1$2$3');
return rs;
}
过滤商品开头的标识
比如有些字符串内存在
- 张三 12345678901 深圳市南山区xxxx 商品:猪肉1斤
- 张三 12345678901 深圳市南山区xxxx 商品名称:猪肉1斤
- 张三 12345678901 深圳市南山区xxxx 名称:猪肉 数量1斤,羊肉 1
- 张三 12345678901 深圳市南山区xxxx 猪肉 1斤,羊肉 1
- 张三 12345678901 深圳市南山区xxxx 猪肉 一斤 羊肉 二
所以需要在识别字符串前,我们应当将干扰我们工作的字符全部替换掉
function transTitleStr(str) {
// 过滤商品如果前面包含-> 商品名称: 猪肉
// -> 商品: 猪肉
// 商品二字
for (let i = 0; i < goodsTitle.length; i++) {
rs = rs.replace(goodsTitle[i], '');
}
rs = rs.replace(/[\r\n]/g, '');
return str
}
地址与商品分离
当我们将干扰我们的字符给剔除后,剩下的就是有规律性的字符串了
上面的字符串也就变成了如下字符串
- 张三 12345678901 深圳市南山区xxxx 猪肉1斤
- 张三 12345678901 深圳市南山区xxxx 猪肉1斤
- 张三 12345678901 深圳市南山区xxxx 猪肉 1斤 羊肉 1
- 张三 12345678901 深圳市南山区xxxx 猪肉 1斤 羊肉 1
- 张三 12345678901 深圳市南山区xxxx 猪肉 1斤 羊肉 2
接下来一次解析相关的字符串即可
本文采用的方法是暴力for的写法来解析,还原xdm留言更好的方法
graph TB
正则匹配字符串-->字符串起始位置
字符串起始位置-->抓取单位数量
抓取单位数量 --> 删除字符串
删除字符串 --> for循序继续匹配
for循序继续匹配-->正则匹配字符串
for循序继续匹配-->结束跳出
循环删除
// goodsKeyWords为商品关键字库
function splitStr(str) {
for (let i = 0; i < goodsKeyWords.length; i++) {
let ds = str.match(goodsKeyWords[i]);
if (!!ds) {
let startKey = ds.index + ds[0].length - 1
// 抓取商品数量
while (startKey < str.length) {
startKey++
let nextStr = str[startKey]
if (!nextStr) {
break
}
let delLen = statiGoods(str, startKey, ds[0], 0)
str = delStrFn(str, ds.index, delLen.length)
break;
}
}
}
return str
}
}
抓取当前商品数量以及单位
// 计算当前商品后面的数量及单位
// 存在 1箱 10箱 111箱
function strNextIsNumber(str, ind) {
let numstr = ''
for (let i = ind; i < str.length; i++) {
// 数量 将大写数字转换成阿拉伯
let nowStr = qualityMap[str[i]] || str[i]
// 如果当前字符串是单位
if (unitKeyWords.includes(nowStr)) {
if (!numstr.length) {
numstr += 1 + nowStr
break;
}
numstr += nowStr
break;
}
if (nowStr == ' ') {
numstr += ' '
continue
}
// 如果下一个是数字就拼接,不是就跳出
if (!/\d/.test(nowStr)) {
break;
}
numstr += nowStr
}
return !!numstr && numstr || ''
}
删除字符串
function delStrFn(str, x, num) {
return str.substring(0, x) + str.substring(x + num, str.length);
}
统计商品
function statiGoods(str, strIndex, nextStr) {
let numstr = ''
let delStr = nextStr
for (let i = strIndex; i < str.length; i++) {
let nowStr = qualityMap[str[i]] || str[i]
numstr = strNextIsNumber(str, i)
break;
}
goodsArr.push({
name: delStr,
value: numstr.trim()
})
return delStr + numstr
}
搞完以上商品,新的问题又来了,如果用户输入的字符串如下咋办呢?
- 张三 12345678901 深圳市南山区xxxx 名称:(猪肉+羊肉)
- 张三 12345678901 深圳市南山区xxxx (猪肉+羊肉) 1斤
接下来针对 statiGoods改造,让他支持商品A+B的模式
function statiGoods(str, strIndex, nextStr, isHasParent) {
for (let i = strIndex; i < str.length; i++) {
//somecode
// 如果当前字符串是 +
if (nowStr == '+') {
let newStr = str.substring(i + 1, str.length);
let vals = forStrGetGoods(newStr)
if (vals.length) {
let startKey = vals.index + vals[0].length
let _valstr = statiGoods(newStr, startKey, vals[0], 1)
delStr += '+' + _valstr
}
}
}
// 这里加多了一个if
if (!isHasParent) {
goodsArr.push({
name: delStr,
value: numstr.trim()
})
}
return delStr + numstr
}
A+B商品遍历循环,判断后面是否是商品
// 拿到字符串内的商品
function forStrGetGoods(str) {
let json = null
for (let i = 0; i < goodsKeyWords.length; i++) {
let ds = str.match(goodsKeyWords[i]);
if (!!ds) {
json = ds
break;
}
}
return json
}
通过改造完上面的函数后,我们就可以得到如下的信息
// goodsArr
// [{name: 商品A}, {value: 1}]
// [{name: 商品A}, {value: 1件}]
// [{name: 商品A}, {value: 2箱}]
// [{name: 商品A+商品B 1件}, {}]
// addresInfo:
/*
{
"phone":"13911111111",
"province":"江苏省",
"city":"苏州市",
"area":"吴江区",
"detail":"干将东路678号江苏大厦11楼",
"name":"徐天宇",
"postalCode":"215000"
}
*/
postman.js:547
显然,目前的商品信息不太符合我们要的数据结构,再写一个函数将数据处理下就行了~~
function resetGoodsInfo() {
let arr = []
for (let i = 0; i < goodsArr.length; i++) {
let item = goodsArr[i]
if (item.name.indexOf('+') > -1) {
let md = item.name.split(' ')
let val = 1
for (let i = 0; i < md.length; i++) {
if (/\d/.test(md[i])) {
val = md[i].match(/\d/)
unit = md[i].substring(val.index + val.length, md[i].length);
val += unit
}
}
for (let i = 0; i < md.length; i++) {
if (md[i].indexOf('+') > -1) {
arr = md[i].split('+').map(item => {
return {
name: item,
value: val
}
})
}
}
}
}
goodsArr = goodsArr.filter((item, key) => item.name.indexOf('+') == -1)
goodsArr.push(...arr)
}
对接飞书api
本项目用的是excel文档,所以会用sheet工作表接口,不过其他文档应该大同小异
由于飞书文档api写的也比较详细,这里就不过多的贴上代码了,大概给XDM列举出运用到的api以及相关的作用
app_id和app_secret登陆后台飞书应用,拿到该应用相对应的信息即可
axios({
url: 'https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal/',
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8'
},
data: {
app_id: 'app_id',
app_secret: 'app_secret',
}
}).
步骤依次如下:
- 获取当前应用我的文件夹空间: open.feishu.cn/open-apis/d…
- 获取我的空间下的子文件集:open.feishu.cn/open-apis/d…
- 新建文件:open.feishu.cn/open-apis/d…
- 获取文件数据源:open.feishu.cn/open-apis/s…
- 将数据写入sheet:open.feishu.cn/open-apis/s…
- 转移文档权限:open.feishu.cn/open-apis/d…
温馨提示
1.如果文件夹内已经存在文件,直接写入并转移权限就行,没必要重新生成文件了 2.可以在创建后的文件,直接修改文件名字,这样可以达到更友好的提示
api如下:
结语
如果XDM有不一样的解析思路,可以提出来一起相互讨论下~~