一、前言
这篇文章只是记录一些自己编写脚本时用到的零碎知识而已。这也是这一系列的最后一篇。
二、shell 的异常处理
有时我们希望在 shell 脚本命令执行出现异常时进行处理,比如说输出异常情况或退出等等。我们知道发生异常时返回值不为 0,如果是在 c 语言中我们可以这样处理
if (!do_something()) {
// handle exception
}
类似的在 shell 中
dosomething arg1 arg2
if [ $? -ne 0 ]
then
# handle exception
if
但是这样编写起来太过麻烦了,我们可以采用另一种方法,那就是使用短路逻辑运算符。
我们还先以 c 语言为例,假设现在我们有两个函数,对他们取逻辑或
int r = func1() && func2()
那么在 func1
的返回值为 0 时,就会发生短路,不执行 func2
。而在返回值为;而当 func1
的返回值不为 0 时,则会继续执行 func2
。我们可以让 func2
完成 func1
的异常处理。
当然,在 c 语言中这种方法很是牵强,因为不同函数的栈帧并不一样,很难跨函数进行处理。可是 shell 中就不同了,所有的变量都是全局变量。
但还需要注意一点,shell 中 0 表示真,1 表示假。(因为 0 表示程序正常结束,所以为真。)所以在 shell 中就需要使用 ||
而非 &&
。
cmd1 arg1 arg2 || cmd2 arg3 arg4
举我写的脚本中的例子。
echo "try to import csv files in $source_path, execute insert.py script to do this"
python3 ./insert.py $host $port $username $password $source_path $database || ! echo "error: fail to import all csv files" || exit
echo "import csv files success!!!"
需要注意这里用 ||
连接了三条命令,其中第二条输出错误提示,我们为了在该命令正常执行后依旧执行后面的 exit
操作,在该命令前面加上 !
进行取反。需要注意这里不能将 !
和 echo
连接起来 !echo
。
另外在学习使用命令行时你一定见过这样的操作,只是使用的是 &&
。
cmd1 arg1 arg2 && cmd2 arg3 arg4
虽然各位应该理解两者的区别,但这里还是说明一下:
||
表示如果发生错误,才执行下一条&&
表示只要不发生错误,就不断依次执行
三。执行 sql 脚本
当进行项目维护的时候,可能需要使用大量的 sql 语句,这时将这些语句编写成 sql 脚本,批量执行更有效率。
我自己的数据导入脚本就有用到了 sql 脚本。实际上执行脚本不过就是进行了一个文件重定向而已。
echo "try to create tables by create.sql..."
mysql -h $host -P $port -u $username -p$password < ./create.sql || ! echo "error: fail to execute create.sql" || exit
echo "tables are now created"
当然还有使用管道的方法,如果想要执行多个 sql 脚本,可以使用通配符将文件连接起来统一执行
cat ./*.sql | mysql -h $host -P $port -u $username -p$password
还可以在命令行中执行单一语句,这需要在使用 mysql
命令时设置 -e
参数
mysql -h $host -P $port -u $username -p$password -e "SHOW DATABASES"
四、csv 数据导入
mysql 中有 LOAD DATA INFILE
语句用于文件导入,不过其他数据库似乎并不支持这种方法。更普适的方法还是通过 INSERT
插入数据。这里我写了一个 python 脚本用于通过 insert
从 csv 文件中导入数据。我个人认为还是编写地比较灵活,可以通过简单修改导入不同数据。
from sys import argv
import pymysql
insert_template = """
insert into {}({})
values ({})
"""
tables = ['departments', 'employees', 'titles', 'dept_emp', 'dept_manager']
_, host, port, username, password, source_path, database = argv
port = int(port)
error = False
db = pymysql.connect(
host=host,
user=username,
passwd=password,
database=database,
port=port,
)
cursor = db.cursor()
for table in tables:
print(f"start to import csv file to table {table}...")
with open(f'{source_path}/{table}.csv', 'r') as f:
args = list(map(lambda line: tuple(line.strip().split(',')), f.readlines()))
fields = ', '.join(args[0])
argc = len(args[0])
del args[0]
sql = insert_template.format(table, fields, ', '.join(['%s']*argc))
try:
cursor.executemany(sql, args)
db.commit()
print(f"import csv file to table {table} success!!!")
except Exception as e:
db.rollback()
print(f"error: fail to import into table {table}, {e}")
error = True
db.close()
if error:
exit(1)
这里使用了 executemany
用于一次执行许多条类似的语句,相较于逐个插入数据有很大的速度提升。这似乎是因为 INSERT
语句执行后默认会直接提交到数据库,写入磁盘。过多的 INSERT
语句就会增加大量的磁盘写入时间。
直接使用 sql 插入数据也有同样的问题,一个解决办法是在进行多次插入之前禁用自动提交 SET AUTOCOMMIT = 0;
,再插入结束后再开启 SET AUTOCOMMIT = 1;
。
这里我们举一例子,如下的 sql 语句创建了一个存储过程,该存储过程会向表 table_random
中插入 400000 条随机生成的数据。在 WHILE
循环的前后,我们设置了 AUTOCOMMIT
的值以禁用和开启自动提交。可以试着注释这两条语句,在注释前和注释后分别执行该存储过程,看一下完成所需时间。
DELIMITER $$
CREATE PROCEDURE random_insert()
BEGIN
DECLARE i INTEGER DEFAULT 0;
DECLARE sparse_val INTEGER;
DECLARE dense_val INTEGER;
DECLARE num INTEGER;
SET AUTOCOMMIT = 0;
WHILE i < 400000 DO
SET sparse_val = ROUND(RAND()*5000001 - 0.5);
SET dense_val = ROUND(RAND()*10 - 0.5);
INSERT INTO table_random(sparse, dense)
VALUES (sparse_val, dense_val);
SET i = i + 1;
END WHILE;
SET AUTOCOMMIT = 1;
END $$
DELIMITER ;