R で文字列の SHA1 値が欲しかったので検索してみたら digest っていうライブラリがヒット、ドキュメント読まずに適当に試してみたら、それらしい結果になったんだけど、ちょっと違和感を感じて確認してみたら私の期待する値ではないみたい。
でもってさらに検索したら openssl ライブラリがヒット。そっちは適当に書いても思った結果になったので、そっちを使うことにしたっていう話です。
なにがいけなかったのか忘れないようにのメモです。
はじめに試した digest を使う方法
という訳で、まず始めに試した digest による SHA1 計算です。以下のような感じで実行しました。ドキュメントもほとんど読まずに…。
install.packages("digest")
digest::sha1("hello!")
# -> [1] "85ef2b23c7b82fb6bf0e019b64d9046cb66124ca
それっぽい結果が表示されたので、おー!簡単じゃんって思ったんですが…うん?うーーん。なんか値に違和感を感じたので、ためしに別の関数を使って計算してみると…。
digest::digest("hello!", algo="sha1")
# -> [1] "96cef7fabbc2bfb6d09965f40daa547d12f6d649"
あれ?値が違うようです。どっちが正しいの?っていうかどっちも間違ってない?って心配になってきます。
欲しい結果(値)は?
そもそもどんな結果になって欲しいのかを確認することとしました。
まずは思いついた Linux コマンドで試してみることに。
# sha1sumを使った例
echo -n "hello!" | sha1sum
# -> 8f7d88e901a5ad3a05d8cc0de93313fd76028f8c -
# opensslを使った例
echo -n "hello!" | openssl sha1
# -> (stdin)= 8f7d88e901a5ad3a05d8cc0de93313fd76028f8c
良い感じじゃないですか。でも心配なので nodejs でも計算。これもしっかりと確認することなく思いついたままを打って実行
node -e 'console.log(require("crypto").createHash("sha1").update("hello!").digest("hex"))'
# -> 8f7d88e901a5ad3a05d8cc0de93313fd76028f8c
お一!さっきと一致しました(←ちょっと驚いている)。でもなんか心配なので python でも計算。これもやっぱり確認することなく思いついたままを打って実行
# 2.7
python -c 'import hashlib;print(hashlib.sha1("hello!").hexdigest())'
# -> 8f7d88e901a5ad3a05d8cc0de93313fd76028f8c
# 3.5
python3 -c 'import hashlib;print(hashlib.sha1("hello!".encode("utf-8")).hexdigest())'
# -> 8f7d88e901a5ad3a05d8cc0de93313fd76028f8c
お一!お一!また一致しました(←もの凄く驚いている)。てな訳で、私の求めている結果(値)、つまり直感で打って求められる値は、 8f7d88e901a5ad3a05d8cc0de93313fd76028f8c
であることが分かりました。
で、どうすれば良いの?
で色々と試してみたら serialize=FALSE
を指定すると期待した結果になることが分かりました。
digest::digest("hello!", algo="sha1", serialize=FALSE)
# -> [1] "8f7d88e901a5ad3a05d8cc0de93313fd76028f8c"
どうやら digest ライブラリの digest や sha1 関数の第 1 引数は「文字列」ではなく「オブジェクト」のようで、デフォルトではオブジェクトをシリアライズしてから処理を行うようです。
digest::digest のヘルプにも冒頭からしっかりと書かれていました。
digest package:digest R Documentation
Create hash function digests for arbitrary R objects
Description:
The ‘digest’ function applies a cryptographical hash function to
arbitrary R objects. By default, the objects are internally
serialized, and either one of the currently implemented MD5 and
SHA-1 hash functions algorithms can be used to compute a compact
digest of the serialized object.
(省略)
Usage:
digest(object, algo=c("md5", "sha1", "crc32", "sha256", "sha512",
"xxhash32", "xxhash64", "murmur32", "spookyhash"), serialize=TRUE, file=FALSE,
length=Inf, skip="auto", ascii=FALSE, raw=FALSE, seed=0,
errormode=c("stop","warn","silent"),
serializeVersion=.getSerializeVersion())
digest::sha1 のヘルプにもしっかりと書かれているようです。また digest との違いも書かれているようです。
sha1 package:digest R Documentation
Calculate a SHA1 hash of an object
Description:
Calculate a SHA1 hash of an object. The main difference with
‘digest(x, algo = "sha1")’ is that ‘sha1()’ will give the same
(省略)
つまり用途を理解せずに使った私が悪い?
そんなこともあって、ドキュメントを読まず、適当に使った私が悪いことが判明しました。相手( digest )のことを理解せずに強引に進めようとした私が悪かった、それは私だけでなく誰しもが認める事実でしょう。
反省しつつ振り返る中で、ふと、 Linux も nodejs も python も適当に打っても私の思う結果が得られたじゃないか。もしかしたら、そうゆうライブラリもあるんじゃないか、ということにようやくここで気付きました。
openssl ライブラリに出会った
てなわけで、私と相性のよいライブラリを探す旅にでることになりました。ゴールがあるかも分からない果てしなく遠い旅に…、って思っていたら一瞬でみつかりました。 openssl っていうライブラリです。ネーミング的にもグットきます。早速ためしてみることにしました。また懲りずに直感で適当に打って実行です。
install.packages("openssl")
openssl::sha1("hello!")
# -> [1] "8f7d88e901a5ad3a05d8cc0de93313fd76028f8c"
おーー!思い通りの結果になりましたー!適当に使っても期待した結果になったってことは digest よりも openssl の方が私には向いているみたいです。
実行速度も測定してみました。
library(purrr)
library(dplyr)
system.time(
seq(1,10000) %>%
map(function(x) {
return(digest::digest(as.character(x), algo="sha1", serialize=F))
}
)
)
# user system elapsed
# 1.388 0.656 2.060
system.time(
seq(1,10000) %>%
map(function(x) {
return(openssl::sha1(as.character(x)))
}
)
)
# user system elapsed
# 1.096 0.000 1.097
速度の問題はなさそうです。
あと openssl::sha1 はベクトルも扱えるみたいです。
openssl::sha1(c("1","2","3"))
# [1] "356a192b7913b04c54574d18c28d46e6395428ab"
# [2] "da4b9237bacccdf19c0760cab7aec4a8359010b0"
# [3] "77de68daecd823babbb58edb1c8e14d7106e83bb"
# digest 関数はベクトルが扱えない、
digest::digest(c("1","2","3"), algo="sha1", serialize=F)
# [1] "356a192b7913b04c54574d18c28d46e6395428ab"
# そうゆう時は map を使う
c("1","2","3") %>% map(function(x) {
digest::digest(x,algo="sha1",serialize=F)
}) %>% as.character
# [1] "356a192b7913b04c54574d18c28d46e6395428ab"
# [2] "da4b9237bacccdf19c0760cab7aec4a8359010b0"
# [3] "77de68daecd823babbb58edb1c8e14d7106e83bb"
ということは、速度の比較も以下のようにシンプルに書けるし、速度も速くなりそう。
system.time(
seq(1,10000) %>% as.character %>% openssl::sha1()
)
# user system elapsed
# 0.052 0.000 0.054
# 又は
system.time(
openssl::sha1(as.character(seq(1,10000)))
)
# user system elapsed
# 0.052 0.000 0.054
ということは dplyr の mutate とも相性が良いかな。
library(dplyr)
data.frame(n=seq(1,3)) %>%
mutate(hash=openssl::sha1(as.character(n)))
'
n hash
1 1 356a192b7913b04c54574d18c28d46e6395428ab
2 2 da4b9237bacccdf19c0760cab7aec4a8359010b0
3 3 77de68daecd823babbb58edb1c8e14d7106e83bb
'
# digest はベクトルが扱えないからか結果が全部一緒になっちゃう
data.frame(n=seq(1,3)) %>%
mutate(hash=digest::digest(as.character(n),algo="sha1",serialize=F))
'
n hash
1 1 356a192b7913b04c54574d18c28d46e6395428ab
2 2 356a192b7913b04c54574d18c28d46e6395428ab
3 3 356a192b7913b04c54574d18c28d46e6395428ab
'
# ベクトルが扱えないなら map を使う
library(purrr)
data.frame(n=seq(1,3)) %>%
mutate(hash=map(n, function(x) {
return(digest::digest(as.character(x),algo="sha1",serialize=F))
}
))
'
n hash
1 1 356a192b7913b04c54574d18c28d46e6395428ab
2 2 da4b9237bacccdf19c0760cab7aec4a8359010b0
3 3 77de68daecd823babbb58edb1c8e14d7106e83bb
'
```
## てな訳で、
私の場合、直感的に使えそうなのは openssl ライブラリのようです。もちろん digest でも文字列の SHA1 値求めることができますが、それよりは「文字列の SHA1 値を求めるなら openssl ライブラリ」、「オブジェクトの SHA1 値を求めるなら digest ライブラリ」ってな使い分けがシンプルだし、使い勝手も良さそうかな。
めでたしめでたし。