Help us understand the problem. What is going on with this article?

JavaScriptでお手軽にランダム文字列の生成

お手軽さを重視しているため、作り方に偏りがある事が分かっている物もある。

以下の環境で確認

  • Node: v10.15.0
  • ブラウザ: Chrome 71.0.3578.98, Firefox 64.0.2

Edgeやモバイルの動作確認はしてない。

Node/ブラウザ共通

短いコードで

[a-v][0-9]の32文字で12桁以下なら、これでそれっぽいのが作れる。StackOverflowで知った

しかし、末端の0がくると省略されるため、任意の桁数が得られるとは限らない。以下のコードでも100万回ぐらい生成を繰り返すと6桁のものがでてきたりする。

Math.random().toString(32).substring(2) // 'a6dpgjqlq8g' 等

任意の桁数・任意の文字で

var S="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
var N=16
Array.from(Array(N)).map(()=>S[Math.floor(Math.random()*S.length)]).join('')

一番最初に考えたやり方がこれだった。

なお、Array(N)だけでは長さNの配列になっているが要素が無い状態のため、mapが機能しないらしい。
そのため、Array.from(Array(N))としてやることで、undefinedな要素が入りmapが機能するようになる。(ES6以降なら、Array.fromの代わりに[...Array(N)]が使える。以降はES6/ES5ごちゃまぜになっている。)

ブラウザ

よりセキュアな乱数生成器で

「Math.randomはセキュアではないのでは?」という難癖をつける場合、crypto.getRandomValuesがあるのでこちらを使う。結構古いブラウザもサポートしてる模様。IE11もmsCryptoから使える。

以下の方法は除算を使うので特定の文字に偏りが起きる。なのでセキュア志向なら使ったらダメ。(以下の例だと[a-h]が出やすい)

var S="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
var N=16
Array.from(crypto.getRandomValues(new Uint8Array(N))).map((n)=>S[n%S.length]).join('')

なお、Uint8ArrayからUint32Arrayにすると偏りは少しはマシになる。後述。

これよりも任意の文字を偏りなく出す方法を他のAPIで出来ないか考えたけど、すぐに思い浮かばなかったので諦めてます。

Base64で

文字を用意するのが面倒な場合、String.fromCharCodeで変換してからbtoaでBase64変換を使う手がある。

複雑な記号は使えないが、除算を使わない分こっちのほうが偏りも少ない気がする。(未調査)

var N=16
btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(N)))).substring(0,N) 

Nodeのみ

よりセキュアな乱数生成器で

Nodeにはブラウザとは異なるAPIを持つcryptoモジュールがある。

ブラウザのcrypto.getRandomValuesと同じ動作をするcrypto.randomFillSyncがあるのでこれで。

以下の方法も同じように除算を使ってるので偏ります。

const crypto = require('crypto')
const S="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
const N=16
Array.from(crypto.randomFillSync(new Uint8Array(N))).map((n)=>S[n%S.length]).join('')

Base64で

Base64も同じ要領で...と思ったが、btoaがないので、ブラウザのようには行えない。

代わりにNodeにはcrypto.randomBytesがある。これはN個のランダムなバイト列を作り、Node固有のBufferという型で返す。(crypto.randomFillSync(new Uint8Array(N))に似ているが、戻り値の型が違う。)

そして、このBufferがBase64変換に対応している。なので、もう少し短く書ける。

const crypto = require('crypto')
const N = 16
crypto.randomBytes(N).toString('base64').substring(0, N)

Nodeはこれが一番スマートな気がする。

おまけ:剰余計算による偏り

質の良い乱数生成期を使ったとしても、剰余を求めるやり方を行ってしまうと、偏りが起きる場合がある。

const S="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" // 62文字
let p=Array(S.length).fill(0)

// 100個づつ乱数を取り出し集計する処理を6200回繰り返す(620000回行う)

for(let i=0;i<100*S.length; ++i){
  const arr = crypto.randomFillSync(new Uint8Array(100))
  for(let j=0; j<arr.length; ++j){
    p[arr[j]%S.length]++;
  }
}
p // => 均等にばらければ全部が10000前後になるはずだが、あきらかに0-7番目の要素のカウントが大きいことが分かる。

これは、0~255(256個)がランダムで得られたとしても、剰余計算によって0~61(62個)のいずれかになる。
すると、0~61,62~123, 124~185,186~247の間は均等に出現するが、残りの248~255は0~7しか出現しえないため、この分が他と比べて多く出る。これで偏りが起きる。
逆に256を割り切れる数(2,4,8...64,128)で割れば計算による偏りは起きない。もし偏る場合は元の乱数の問題。

また、ランダムで得られる範囲0-255と狭いせいというのもあるので、この範囲を十分に大きくすると剰余計算による偏りは減らすことができる。
上記のコードなら、Uint8Array => Uint32Array にしてみると、偏りが目立たなくなることがわかる。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away