为 json 内容索引生成唯一 ID

为 json 内容索引生成唯一 ID

我正在寻找使用 bash 脚本为以下内容生成有效且简单的 ID:

{"name": "John", "surname": "Gates", "country": "Germany", "age": "20", "height": "180"}
{"name": "John1", "surname": "Gates", "country": "Germany", "age": "20", "height": "180"}
{"name": "John2", "surname": "Gates", "country": "Germany", "age": "20", "height": "180"}
{"name": "John3", "surname": "Gates", "country": "Germany", "age": "20", "height": "180"}


{"id": "XXX", "name": "John", "surname": "Gates", "country": "Germany", "age": "20", "height": "180"}
{"id": "XXX", "name": "John1", "surname": "Gates", "country": "Germany", "age": "20", "height": "180"}
{"id": "XXX", "name": "John2", "surname": "Gates", "country": "Germany", "age": "20", "height": "180"}
{"id": "XXX", "name": "John3", "surname": "Gates", "country": "Germany", "age": "20", "height": "180"}

我将拥有大约 5,000,000 条类似记录,并且我希望生成可重复、可预测的 ID。由于处理以下文件的时间有限,因此我需要在 Linux 计算机上的 sql lite 数据库上在 20 分钟内完成此操作。

MD5、SHA1 太昂贵而无法使用,除非我可以在 AMD Ryzen 1900X CPU 上的 16 线程上执行类似 GNU Parallel 的操作,并且能够在几分钟内完成?

我尝试过使用MD5,在1分45秒内完成了28,000个ID的计算。使用 SHA1,我花了 2 分 3 秒。

我正在考虑创建 ID 非常简单:

JohnGatesGermany20180
John1GatesGermany20180
John2GatesGermany20180
John3GatesGermany20180

如果必须满足以下要求,您有什么建议:

  • 巴什
  • Linux
  • 需要处理 5,000,000 条记录
  • 20分钟以内
  • 对于相同的 json 行,id 必须相同

进行的测试:

#!/usr/local/bin/bash

while IFS= read -r line
do
   uuid=$(uuidgen -s --namespace @dns --name "www.example.com" )
done < testfile1.txt

1,000,000 行的 md5 散列:

$time bash script.sh 

real    13m6.914s
user    10m24.523s
sys 2m56.095s

cksum 对 1,000,000 进行 crc 校验:

#!/usr/local/bin/bash

while IFS= read -r line
do
#    uuid=$(uuidgen -s --namespace @dns --name "www.example.com" )
    echo "$line $uuid"|cksum >> test3.txt
done < testfile1.txt

$time bash script.sh 

real    12m49.396s
user    12m23.219s
sys 4m1.417s

答案1

我敢打赌,你的脚本花费这么长时间的原因是你对每一行的运行uuidgen(或)。cksum仅仅启动每个进程就会浪费大量时间。

将 5M 行的表单放入{"name": "John%d", "surname": "Gates", "country": "Germany", "age": "20", "height": "180"}tmpfs 文件系统上的文件中,以下 Python 脚本在几秒钟内完成:

#! /usr/bin/env python3

import hashlib
import sys
for line in sys.stdin:
    print(hashlib.md5(line.rstrip('\n').encode('utf-8')).hexdigest())

执行:

$ time ./foo.py < input > output
./foo.py < input > output  6.00s user 0.13s system 99% cpu 6.135 total
% wc -l input output
  5000000 input
  5000000 output
 10000000 total

由于这是 Python,因此您还可以对行进行 JSON 解码,并在每行中插入一个 ID。即使是低效的代码,例如:

#! /usr/bin/env python3

import hashlib
import json
import sys
for line in sys.stdin:
    l = line.rstrip('\n').encode('utf-8')
    o = json.loads(line)
    o["id"] = hashlib.md5(l).hexdigest()
    print(json.dumps(o))

不到一分钟就完成了:

% time ./foo.py < input > output
./foo.py < input > output  42.11s user 0.42s system 99% cpu 42.600 total

% head output 
{"name": "John1", "surname": "Gates", "country": "Germany", "age": "20", "height": "180", "id": "2dc573ccb15679f58abfc44ec8169e52"}
{"name": "John2", "surname": "Gates", "country": "Germany", "age": "20", "height": "180", "id": "ee0583acaf8ad0e502bf5abd29f37edb"}
{"name": "John3", "surname": "Gates", "country": "Germany", "age": "20", "height": "180", "id": "a7352ebb79db8c8fc2cc8758eadd9ea3"}
{"name": "John4", "surname": "Gates", "country": "Germany", "age": "20", "height": "180", "id": "2062ad1b67ccdce55663bfd523ce1dfb"}
{"name": "John5", "surname": "Gates", "country": "Germany", "age": "20", "height": "180", "id": "5f81325c104c01c3e82abd2190f14bcf"}
{"name": "John6", "surname": "Gates", "country": "Germany", "age": "20", "height": "180", "id": "493e0c9656f74ec3616e60886ee38e6a"}
{"name": "John7", "surname": "Gates", "country": "Germany", "age": "20", "height": "180", "id": "19af9ef2e20466d0fb0efcf03f56d3f6"}
{"name": "John8", "surname": "Gates", "country": "Germany", "age": "20", "height": "180", "id": "2348bd47b20ac6445213254c6a8aa80b"}
{"name": "John9", "surname": "Gates", "country": "Germany", "age": "20", "height": "180", "id": "090a521b4a858705dc69bf9c8dca6c19"}
{"name": "John10", "surname": "Gates", "country": "Germany", "age": "20", "height": "180", "id": "fc3c699323cbe399e210e4a191f04003"}

我的规格:

  • 英特尔® 酷睿™ i7-8700 CPU @ 3.20GHz × 12
  • 2666MHz DDR4内存

基于你uuidgen的脚本在 4 分钟内勉强完成了 50 万行。修改为保存输出:

#!/usr/bin/bash

while IFS= read -r line
do
   uuidgen -s --namespace @dns --name "$line"
done < input > uuid

执行:

% timeout 240 ./foo.sh
% wc -l uuid
522160 uuid

答案2

在 awk 中实现简单的 ID 想法,假设 JSON 行如您所指示的那样——所有行都在一行上:

awk -F'"' 'BEGIN{OFS=FS} {$1=$1"\"id\": \""$4$8$12$16$20"\", "; }1' < input

我没有与你的类似的系统,所以你必须看看时间是否可以接受。

答案3

作为一个思想实验,我想看看我们可以在多大程度上推动 CLI 工具解决此类问题。为此,我想尝试使用快速哈希 CLI 工具xx哈希值来做这项工作。

xxHash 是一种速度极快的非加密哈希算法,其工作速度接近 RAM 限制。它有两种版本:32 位和 64 位。

它适用于每种编程语言,但对于本实验,我将使用 CLI 风格,xxhsum特别是 32 位模式,因此xxhsum -H0.

正如您所发现的以及其他人所说的那样,一遍又一遍地调用哈希函数 CLI 工具或任何工具通常是这些类型的方法失败的地方。调用xxhsum此处 5M 次将是使用它的次优方式。它的优势在于文件 I/O,那么如果我们将 5M 行转换为 5M 文件呢?

该任务在 Linux 中实际上很简单,使用以下split命令:

split -l 1 afile

对这些文件(例如 1M)进行哈希处理(每个文件中包含一行)的速度有多快?

示例 1 行文件
$ cat datadir/xzeyw
{"name": "John4000", "surname": "Gates", "country": "Germany", "age": "20", "height": "180"} 
包含1M文件的目录
$ ls -l datadir | wc -l
1000002
是时候对它们进行哈希处理了
$ { time xxhsum -H0 * > ../nfile 2>&1 ;} |& awk '/real|user|sys/ {print $1": "$2"\t"}' | tr -d '\n'
real: 0m6.998s  user: 0m5.007s  sys: 0m1.569s

是的,没错,花了大约 7 秒!我觉得这相当令人印象深刻。通过这种方式使用xxhsum,我们只产生了运行一次的成本,并且允许它循环遍历1M个文件。

该方法的缺点

因此,这样做的缺点之一当然是split.正如您可以想象的那样,这成为我们最昂贵的操作。因为我们必须获取一个包含 X 行的单个文件,并将其作为包含单行的 X 个文件分解到 HDD 上。

以下是其中一些数据:

./hashy.bash

make data
---------
real: 0m17.492s user: 0m12.434s sys: 0m4.788s

split data
----------
real: 2m15.180s user: 0m0.700s  sys: 2m4.443s

hash data
---------
real: 0m6.487s  user: 0m5.798s  sys: 0m0.459s

在这里我们可以看到我们的split操作花了大约 2 分钟。笔记:此输出中的第一行显示了构建包含 1M 行 JSON 的文件的时间。

另一个缺点是我们在命令行上处理的文件数量。我在某些地方使用*,因此这将扩展到 1M 或 5M 文件名,这可能被认为是危险的,确实如此。请记住,当您增加文件数量时,您可能会面临超出分配给命令行参数的空间量的风险。

有关命令行长度的信息,请参阅以下链接:

结论

正如您可以想象的那样,使用 1M 或 5M 文件解决这样的问题看起来几乎是荒谬的。我也不得不同意。但这仍然是一个有趣的实验,因为它表明,如果您以适当的方式利用 CLI 工具,您可以从中获得出色的性能。

hashy.bash 的代码

如果有人对代码感兴趣:

$ cat hashy.bash
#!/bin/bash

echo ""
echo "make data"
echo "---------"
rm -f afile
{ time for i in {0..1000000};do echo "{\"name\": \"John${i}\", \"surname\": \"Gates\", \"country\": \"Germany\", \"age\": \"20\", \"height\": \"180\"}">> afile ;done ;} \
  |& awk '/real|user|sys/ {print $1": "$2"\t"}' | tr -d '\n'
echo ""
echo ""

rm -fr datadir && mkdir datadir && cd datadir

echo "split data"
echo "----------"
{ time split -l 1 ../afile ;} |& awk '/real|user|sys/ {print $1": "$2"\t"}' | tr -d '\n'
echo ""
echo ""

echo "hash data"
echo "---------"
{ time xxhsum -H0 * > ../nfile 2>&1 ;} |& awk '/real|user|sys/ {print $1": "$2"\t"}' | tr -d '\n'

cd - > /dev/null 2>&1
echo ""
echo ""

参考

答案4

首先将数据导入 SQLite 数据库。在这里,我使用 Miller ( mlr) 将您提供的 JSONL 数据转换为 CSV,然后将其读入data新数据库中的表中:

mlr --l2c cat file.json | sqlite3 database.db '.import --csv /dev/stdin data'

完成后,您可以使用 UPDATE 语句使用建议的方案创建标识符:

sqlite> .mode box
sqlite> SELECT * FROM data;
┌───────┬─────────┬─────────┬─────┬────────┐
│ name  │ surname │ country │ age │ height │
├───────┼─────────┼─────────┼─────┼────────┤
│ John  │ Gates   │ Germany │ 20  │ 180    │
│ John1 │ Gates   │ Germany │ 20  │ 180    │
│ John2 │ Gates   │ Germany │ 20  │ 180    │
│ John3 │ Gates   │ Germany │ 20  │ 180    │
└───────┴─────────┴─────────┴─────┴────────┘
sqlite> ALTER TABLE data ADD COLUMN id TEXT;
sqlite> UPDATE data SET id = concat(name,surname,country,age,height);
sqlite> SELECT * FROM data;
┌───────┬─────────┬─────────┬─────┬────────┬────────────────────────┐
│ name  │ surname │ country │ age │ height │           id           │
├───────┼─────────┼─────────┼─────┼────────┼────────────────────────┤
│ John  │ Gates   │ Germany │ 20  │ 180    │ JohnGatesGermany20180  │
│ John1 │ Gates   │ Germany │ 20  │ 180    │ John1GatesGermany20180 │
│ John2 │ Gates   │ Germany │ 20  │ 180    │ John2GatesGermany20180 │
│ John3 │ Gates   │ Germany │ 20  │ 180    │ John3GatesGermany20180 │
└───────┴─────────┴─────────┴─────┴────────┴────────────────────────┘

显然,您可以让米勒id即时创建您的专栏。以下使用每个记录的空格分隔字段的 MD5 哈希:

mlr --l2c put '$id = md5(joinv($*," "))' file | sqlite3 database.db '.import --csv /dev/stdin data'
sqlite> .mode box
sqlite> SELECT * FROM data;
┌───────┬─────────┬─────────┬─────┬────────┬──────────────────────────────────┐
│ name  │ surname │ country │ age │ height │                id                │
├───────┼─────────┼─────────┼─────┼────────┼──────────────────────────────────┤
│ John  │ Gates   │ Germany │ 20  │ 180    │ 150c35e2efb7093e1c30a46a0226f82c │
│ John1 │ Gates   │ Germany │ 20  │ 180    │ c58a8be627dc1d6c9da36dd6de9fa62d │
│ John2 │ Gates   │ Germany │ 20  │ 180    │ e41b62a821f51c13eea2191ebcbb5837 │
│ John3 │ Gates   │ Germany │ 20  │ 180    │ 8e1012a599356fee66727107b750ba1a │
└───────┴─────────┴─────────┴─────┴────────┴──────────────────────────────────┘

在最近的(2020 年)MacBook Air (M1) 上对此进行测试,使用 Miller 计算 MD5 哈希值并将 500 万条记录导入数据库大约需要 42 秒。

相关内容