给产品同学解决一个小问题
短、平、快。
背景
产品同学给了一个店铺 shopId 列表,想要知道这些店铺的最近一段时间的同城送订单的数量。
shop.txt
111,222,333
有个现成的命令 curl_search 可以拿到单个店铺的同城送订单的数量:
curl -s -H "Content-Type: application/json" -X POST -d '{"source":"xxx", "requestId":"111", "searchParam": {"shopId": 111, "endTime":1583424000,"startTime":1580486400, "deliveryType":"local_delivery"}}' http://127.0.0.1:7001/app/order/search
curl_search 的结果如下。 total 即是同城送订单的数量。
{"result":true,"code":0,"message":null,"data":{"success":true,"code":200,"message":"successful","data":{"orderNos":["202003051700001"],"total":22}}}
想拿到的结果是:每行一个 shopId total
111 22
222 256
333 1024
方案
使用Shell
先编写一个 脚本 curl_orders.sh:
#!/bin/sh
shopId=$1
curl -s -H "Content-Type: application/json" -X POST -d '{"source":"xxx", "requestId":"'"${shopId}"'", "searchParam": {"shopId":"'"${shopId}"'", "endTime":1583424000,"startTime":1580486400,"deliveryType":"local_delivery"}}' http://127.0.0.1:7001/app/order/search | echo $shopId $(sed -r 's/^.*total.*:([0-9]+)\}.*$/\1/')
然后使用 :
cat /tmp/shop.txt | tr "," "\n" | xargs -I {} sh curl_orders.sh {} > result.txt
使用Python
编写一个 Python 脚本 fetch_totals.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import commands
import json
def fetchTotal(curl_cmd, success, fail):
(status, result) = commands.getstatusoutput(curl_cmd)
if status == 0:
return success(result)
return fail(result)
def success(result):
obj = json.loads(result)
if obj['result'] and obj['data']["success"]:
return obj['data']['data']['total']
return "Error"
def fail(result):
return "Error"
if __name__ == '__main__':
f = open('shop.txt')
for line in f:
shopIds = line.strip().split(',')
for shopId in shopIds:
cmd = """curl -s -H "Content-Type: application/json" -X POST -d '{"source":"xxx", "requestId":"""+ str(shopId) + """, "searchParam": {"shopId":""" + str(shopId) + """, "endTime":"1583424000","startTime":"1580486400", "deliveryType":"local_delivery", "exportedFieldNames":[]}}' http://127.0.0.1:7001/app/order/search"""
print shopId, " " , fetchTotal(cmd, success, fail)
使用Python+Shell
实际上,Shell 更适合构建任务处理的命令流,而 Python 更适合处理具体逻辑。两者结合食用更佳。先编写处理单个店铺的 Python 脚本 fetch_total.py :
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import commands
import json
import sys
def fetchTotal(curl_cmd, success, fail):
(status, result) = commands.getstatusoutput(curl_cmd)
if status == 0:
return success(result)
return fail(result)
def success(result):
obj = json.loads(result)
if obj['result'] and obj['data']["success"]:
return obj['data']['data']['total']
return "Error"
def fail(result):
return "Error"
def buildCmd(args):
shopId = args[1]
return """curl -s -H "Content-Type: application/json" -X POST -d '{"source":"xxx","requestId":"""+ str(shopId) + """, "searchParam": {"kdtId":""" + str(shopId) + """, "endTime":"1583424000","startTime":"1580486400", "deliveryType":"local_delivery"}}' http://127.0.0.1:7001/app/order/search"""
if __name__ == '__main__':
args = sys.argv
shopId = args[1]
print str(shopId), " " , fetchTotal(buildCmd(args), success, fail)
然后使用:cat shop.txt | xargs -d "," -I {} python fetch_total.py {}
即可。
求解
方案选择
最先想到用 Python 来解决。但是 python 解决要拼参数,写调用 curl 的脚本。有点麻烦。 接着想去数据平台用 hive 捞,但是没有同城送订单的标识。想到既然有 curl 命令可以拿到单个店铺的结果,只要解决批量的问题即可。
这里有两个问题要解决:1. 从 curl_search 命令的结果中解析出 total ; 2. 批量调用 curl 命令,必须把 shopId 作为参数传进去。
解析total
解析 total , 使用 sed + 正则捕获能力。
要领是:将 需要的东西 用 ( ) 括起来,然后用引用替换。比如,我只要 total 冒号后面的 数字, 首先将需要的部分 ([0-9]+)
括起来 ,然后使用 左边 "total".*:
和 右边 \}
进行识别, 最后通过引用符号 $1 拿到。数字标识第几个(被捕获的分组)括号。
正则表达式基础见: "正则表达式基础知识"
参数化调用curl
需要把 shopId 传到 curl 命令中。 但是 curl 命令的选项 -d 的参数 '{"shopId":"$shopId", "field2":"yyy"}'
,既有 双引号也有单引号,这些都会使得变量引用 $shopId 变成普通的字符串,而不是被替换成指定的值。 可以使用 "'${shopId}'"
来传入 shopId。
xargs
xargs 可以读取文件并将每一行数据作为参数传给指定的命令执行。 基本形式是: cat file.txt | xargs -I {} Command {}
。 {} 就是 file.txt 里的每一行数据。
这里之所以将调用 curl 并解析出 total 写成一个 curl_orders.sh 脚本文件,也是为了 xargs 命令更加简洁。 xargs -d [sep] 是使用分隔符 sep 将输入分割成多条数据,然后将多条数据分别作为一个入参依次传递给指定命令处理。
命令替换
$(command) : 用命令 command 的计算值来替换 command 本身,从而能将命令的执行结果作为参数直接传给其它命令。
相关示例:
cat shop.txt | xargs -d "," -I {} echo $(echo \{\"source\":\"xxx\",\"requestId\":\"98765432\",\"searchParam\":\{\"shopId\":{},\"endTime\":1583424000,\"startTime\":1580486400,\"deliveryType\":\"local_delivery\"\}\})
curl -s http://127.0.0.1:7001/app/order/search -H "Content-Type: application/json" -X POST -d $(echo \{\"source\":\"xxx\",\"requestId\":\"98765432\",\"searchParam\":\{\"kdtId\":37,\"endTime\":1583424000,\"startTime\":1580486400,\"deliveryType\":\"local_delivery\"\}\})
管道
Shell 最强大的一点是其”胶合“能力,可以将任意多个小程序组合起来完成一件事。 管道 | 将上一个程序的输出定向到下一个程序的输入,从而将两个相邻的程序连接起来。管道是实现胶合能力的强大机制。
重定向
> 是重定向符号,可以将命令执行结果重定向到文件里。当命令的结果非常大时适用。
完整命令
一般做法是,先将简单的命令调试好,再整合到一起。
不过,依然太难了。需要把 “参数传入 curl 命令”,“命令替换”,“字符转义” 等全部整合。 尤其是 “参数传入 curl 命令” 这一步。
完整的命令如下:
sed 's/,/\n/g' shop.txt | xargs -I {} sh -c 'echo {}" "$(curl -s -H "Content-Type: application/json" -X POST -d "{\"source\":\"xxx\", \"requestId\":\"123456879\", \"searchParam\": {\"shopId\":{},\"endTime\":1583424000,\"startTime\":1580486400, \"deliveryType\":\"local_delivery\"}}" http://127.0.0.1:7001/app/order/search | sed -r "s/^.*total.*:([0-9]+)\}.*$/\1/")'
sed 's/,/\n/g' shop.txt | xargs -I {} sh -c 'total=$(curl -s -H "Content-Type: application/json" -X POST -d "{\"source\":\"xxx\",\"requestId\":\"123456879\", \"searchParam\": {\"shopId\":{},\"endTime\":1583424000,\"startTime\":1580486400, \"deliveryTypeDesc\":\"local_delivery\"}}" http://127.0.0.1:7001/app/order/search | sed -r "s/^.*total.*:([0-9]+)\}.*$/\1/");echo {} $total'
你可能好奇为啥 -d 里面弄了那么多转义。主要是Java 服务端要整型,shell 默认给传字符串。搞这么多转义就是为了拼那个 json 参数串同时把店铺ID当做整数传过去。
本来想省事写到一行,但写到一行太容易出错而且难以排查,而且有多个参数要传入时,可扩展性不佳。关键 curl 命令的选项参数既有单引号又有双引号,命令也很长,放在 shell 一行上,完成正确的字符串拼接都是很头疼的事情。 看来就是不适合整在一行的。不做这种不必要的折腾了。带有单双引号的较复杂的长命令行的拼接,写在脚本文件里更合适。
引申
使用循环如下。 有命名参数果然容易传参。
for shopId in $(sed 's/,/\n/g' shop.txt); do echo $shopId $(curl -s -H "Content-Type: application/json" -X POST -d '{"source":"xxx", "requestId":"$shopId", "searchParam": {"shopId":"'$shopId'", "endTime":"1583424000","startTime":"1580486400", "deliveryType":"local_delivery"}}' http://127.0.0.1:7001/app/order/search | sed -r 's/^.*total.*:([0-9]+)\}.*$/\1/'); done
sed 's/,/\n/g' shop.txt | while read line; do echo $line $(curl -s -H "Content-Type: application/json" -X POST -d '{"source":"xxx","requestId":"$line", "searchParam": {"shopId":"'$line'", "endTime":"1583424000","startTime":"1580486400", "deliveryType":"local_delivery"}}' http://127.0.0.1:7001/app/order/search | sed -r 's/^.*total.*:([0-9]+)\}.*$/\1/'); done
小结
此例展示 Shell 命令用来解决临时小任务所具备的”短、平、快“的特征。 不过,要构造复杂的命令,要对 Shell 非常熟悉才行,因为其中需要整合多种东西,尤其涉及到字符串转义和命令传参的时候;否则反而失去了“快”的优势。 此外,最好能将复杂命令拆解成单个命令或脚本,这样更容易写出简洁的整合命令。
要构造较为复杂的任务脚本,还是采用自己较为熟悉的语言,比如 Python 。使用 Python 实现,拿到 curl 结果后,使用 json 模块解析即可得到 total ; 然后再写个 for 循环即可解决批量调用问题。
结论是:用 python 写个脚本处理单条记录,然后用 xargs -I {} python xxx.py {} 就可以了。 这样,需要有一个正交互补的 python 工具包。 Shell 更适合构建任务处理的命令流,而 Python 更适合处理具体逻辑。两者结合食用更佳。