最近遇到的一些坑 & 知识汇总

忙里偷闲把最近遇到的一些问题总结一下 ^^

golang 序列化 json 时自动转换特殊字符

在使用 json.Marshal 时,会自动对其中的特殊字符进行转换,例如会将 & 替换为 \u0026 。一般情况下应该问题不大,但是需要知道它在这儿把数据内容做了转化。另外,golang 还会自动将 \b 转为 \u0008 ,也需要注意一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"bytes"
"encoding/json"
"fmt"
)

func main() {
json_content := map[string]string{
"url": "http://www.baidu.com?tag=search&data1=1&data2=2&data3=\b",
}

// 直接序列化
raw_content, _ := json.Marshal(json_content)
// 输出:{"url":"http://www.baidu.com?tag=search\u0026data1=1\u0026data2=2\u0026data3=\u0008"}
fmt.Println(string(raw_content))

// 关闭 SetEscapeHTML
buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
_ = enc.Encode(json_content) // end with \n
// 输出:{"url":"http://www.baidu.com?tag=search&data1=1&data2=2&data3=\u0008"}
fmt.Print(buf.String())
}

golang 和 python 在序列化 array 时的差异

golang 序列化数组时,数据各 item 之间不会添加空格,但是默认情况下 python 会。一般情况下不会造成啥影响,但是需要知道有这个差异。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"encoding/json"
"fmt"
)

func main() {
arr_content := []int{1, 2, 3}
raw_content, _ := json.Marshal(arr_content)
// 输出为 [1,2,3]
fmt.Println(string(raw_content))
}
1
2
3
4
5
6
7
8
9
10
#! /usr/bin/env python
# coding: utf-8
import json

if __name__ == '__main__':
arr = [1, 2, 3]
# 输出:[1, 2, 3]
print(json.dumps(arr))
# 输出:[1,2,3]
print(json.dumps(arr, separators=(',', ':')))

对于 separators 参数的解释,gpt 的解释如下

用于指定序列化时元素之间以及键值对之间的分隔符。这个参数接受一个包含两个元素的元组,第一个元素是用于分隔字典中的键值对的字符串,第二个元素是用于分隔列表或者字典中的元素的字符串。默认值为 (', ', ': ') ,这意味着字典中的键值对之间以及列表或者字典中的元素之间都会有一个空格


错误使用 awk 导致数据内容被误改

数据样例如下,按照空格分割,需要把第一列删除。注意,后面列中存在多个空格

1
url1 {"key":"value  123"}

按照 gpt 的回答,使用如果 awk 命令处理

1
awk '{ $1=""; sub(/^ /, ""); print $0 }' file.txt

但是结果中,后面列中多余的空格被删掉了

1
{"key":"value 123"}

可以使用如下 awk 命令

1
awk '{sub($1 FS, ""); print}' file.txt

gpt 对这个命令的解释如下

  • sub($1 FS, "")

    • $1 表示第一个字段

    • FS 是字段分隔符

    • $1 FS 组合起来就是第一个字段加上紧随其后的制表符

    • sub() 函数将 $1 FS 替换为空字符串 "",effectively 删除它们

  • print: 打印处理后的行

但其实最保险的方式还是写个 python 脚本来处理,或者用 sed 命令

1
sed 's/^[^ ]* //' file.txt

将 map 作为 bash function 的参数

去年年末在写一个 bash 脚本的时候,有一个需求是将 map 作为 function 的入参,实现起来比较麻烦,记录一下。bash 的版本为 GNU bash(bdsh), version 4.1.17(2)-release (x86_64-unknown-linux-gnu)

实现代码

ref: https://stackoverflow.com/questions/31307210/what-does-1-mean-in-bash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
set -eux

declare -A weapons=(
['Straight Sword']=75
['Tainted Dagger']=54
['Imperial Sword']=90
['Edged Shuriken']=25

)

function print_array {
input_map_raw="$(declare -p ${1})"
eval "declare -A arg_array="${input_map_raw#*=}
for i in "${!arg_array[@]}"; do
printf "%s\t%s\n" "$i ==> ${arg_array[$i]}"
done
}

# 传入变量名,而非变量的值
print_array weapons

参数解析流程分析

1
input_map_raw="$(declare -p ${1})"

declare -p 的作用是 “print the attributes and options of the variables” ,ref https://linuxhint.com/bash_declare_command/。作用可以理解为将参数的定义展开,感觉类似于 sql 的 show create table,这里的结果是

1
declare -A weapons='(["Imperial Sword"]="90" ["Tainted Dagger"]="54" ["Edged Shuriken"]="25" ["Straight Sword"]="75" )'

现在 input_map_raw 变量的值就是上面的表达式。随后第二行, eval 命令会执行一遍其后的命令,将一些变量进行替换,生成第二次执行的命令,随后再次执行,ref https://www.cnblogs.com/itcomputer/p/5035387.html,在这里,${input_map_raw#*=} 是一个删除表达式,格式是 ${xxx#pattern},会根据 pattern 删除 xxx 中的值,ref https://stackoverflow.com/questions/31307210/what-does-1-mean-in-bash ,这里是根据 *= 进行匹配,匹配到的值是 declare -A weapons= ,然后将其删除

1
2
3
4
# eval 生成的命令
declare -A arg_array='(["Imperial Sword"]="90" ["Tainted Dagger"]="54" ["Edged Shuriken"]="25" ["Straight Sword"]="75" )'

# 上面生成的命令会被执行,作用是声明了 map arg_array

此时 arg_array 的值和外部传入的 weapons 保持一致了。不得不说,太 trick 了,希望后面可以加一些语法糖


python2 & python3 字符串编码差异

知识介绍

https://reata.github.io/blog/how-python-does-unicode/

  • python2:

    • str 和 unicode 两种类型

    • str 实际上是 “字节串”(byte string/sequence of bytes),某种特定编码的字节序列,默认为 ascii

    • unicode 表示是 “unicode字符”

    • str 创建方式:str() 或 “xxx”

    • unicode 创建方式:unicode() 或 u”xxxxx”

  • python3

    • 一种字符串类型:str,是 unicode

    • 字节串用 bytes 数据结构表示

  • 何为 unicode

    • 不是一种编码,和 ascii、gbk、utf-8 不是一种类型

    • 本质上是一种字符集,每个字符对应一个唯一 id

    • 基本单元:码位(code point),一个字符对应一个或者多个码位,或一个码位能对应多个字符

    • unicode 是字符集(码位集)

      • 规定了一个码位的二进制代码

      • 没有规定这个二进制代码该如何存储

  • 总结:

    • 字节流 – [ decode ] –> unicode

    • 字节流 <– [ encode ] – unicode

    • 对于 py2,默认的 str 为字节流,需要转为 unicode 需要用 unicode() 包一下

    • 对于 py3,默认的 str 为 unicode,字节流为 bytes

代码样例

样例1 (gb18030 编码文件转 utf8)

1
2
3
4
5
6
7
8
# python2
with open('gb18030.txt', 'r') as f:
s = f.read()
ff = open('utf8_output.txt', 'w')
# 读出来的 str,为字节流类型,用 gb18030 解码为 unicode,然后在用 utf8 编码
# bytes - [decode] -> unicode - [encode] -> bytes
ff.write(s.decode('gb18030').encode('utf8'))
ff.close()
1
2
3
4
5
6
7
# python3
with open('gb18030.txt', 'r', encoding='gb18030') as f:
s = f.read()
# 读出来的是 str,为 unicode,直接制定 open 的编码即可
ff = open('utf8_output.txt', 'w', encoding='utf8')
ff.write(s)
ff.close()

样例2:(读取一个 gb18030 编码的json:{“a”:”運動”})

1
2
3
4
5
6
7
# python2
with open('gb18030_2.txt', 'r') as f:
str = f.read()
print(type(str)) # str <- 实际上是字节流
obj = json.loads(str.decode('gb18030').encode('utf8'))
print(type(obj['a'])) # unicode
print(type(obj['a'].encode('utf8'))) # str
1
2
3
4
5
6
7
# python3
with open('gb18030_2.txt', 'r', encoding='gb18030') as f:
str = f.read()
print(type(str)) # str <- 实际上是 unicode
obj = json.loads(str)
print(type(obj['a'])) # str <- 实际上是 unicode
print(type(obj['a'].encode('utf8'))) # bytes

从上可以看出,json.loads 生成的 json 字段默认为 unicode


c++ map emplace & insert 注意事项

这个问题是从内部的一个 case study 里了解到的,这个错误用法引起了比较严重的线上事故。

使用 insert 或 emplace 不会更新已有 key 的值,需要使用 m[k]=v 来更新,代码样例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <string>
#include <map>

int main() {
std::map<std::string, std::string> my_map = {
{"k1", "v1"},
{"k2", "v2"},
};

my_map.insert(std::make_pair("k1", "new-v1"));
my_map.emplace(std::make_pair("k2", "new-v2"));
// 输出:v1, v2
std::cout << my_map["k1"] << ", " << my_map["k2"] << std::endl;

my_map["k1"] = "new-v1";
my_map["k2"] = "new-v2";
// 输出:new-v1, new-v2
std::cout << my_map["k1"] << ", " << my_map["k2"] << std::endl;

return 0;
};

gpt 的解释是:

std::mapinsertemplace 方法设计的目的是保持 map 中的键的唯一性。当你试图插入一个已经存在的键时,insertemplace 方法不会替换现有的键值对,而是简单地忽略你的插入请求。这是因为 std::map 是一个关联容器,它通过键进行查找和访问值,所以需要保证键的唯一性。