在 python 中生成密码

我想在 python 中生成一些字母数字密码:

import string
from random import sample, choice
chars = string.letters + string.digits
length = 8
''.join(sample(chars,length)) # way 1
''.join([choice(chars) for i in range(length)]) # way 2

但我不喜欢两者都喜欢,因为:

  • 方法1 只选择惟一的字符,当 length > len (chars)时不能生成密码
  • 方法2 我们没有使用 i变量,我找不到避免这种情况的好方法

还有别的好选择吗?

另外,这里我们用 timeit对100000次迭代进行了一些测试:

''.join(sample(chars,length)) # way 1; 2.5 seconds
''.join([choice(chars) for i in range(length)]) # way 2; 1.8 seconds (optimizer helps?)
''.join(choice(chars) for _ in range(length)) # way 3; 1.8 seconds
''.join(choice(chars) for _ in xrange(length)) # way 4; 1.73 seconds
''.join(map(lambda x: random.choice(chars), range(length))) # way 5; 2.27 seconds

获胜者是 ''.join(choice(chars) for _ in xrange(length))

101070 次浏览

I wrote a script with my preferences, which mostly are concerned with avoiding mistakes when transcribing and remembering. (For example: remove somewhat ambiguous and no repeated characters.)

import optparse
import os
import random
import sys


DEFAULT_CHARS = "234679ADEFGHJKLMNPRTUWabdefghijkmnpqrstuwy"
DEFAULT_LEN = 18


def choices(options, length, choice=random.choice):
return (choice(options) for _ in xrange(length))


def choices_non_repeated(options, length, choice=random.choice):
assert len(options) > 1
last = choice(options)
count = 0
while count < length:
yield last
count += 1


while True:
value = choice(options)
if value != last:
last = value
break


def main(args):
op = optparse.OptionParser(add_help_option=False)
op.add_option("--help", action="help",
help="show help message and exit")
op.add_option("-b", "--bare", action="store_true", default=False,
help="print passwords without trailing newline")
op.add_option("-c", "--chars", metavar="SET", nargs=1, default=DEFAULT_CHARS,
help="character set to use (default: %default)")
op.add_option("--repeat", action="store_true", default=False,
help="allow repetition")
op.add_option("-l", "--len", dest="max", nargs=1, type="int", default=DEFAULT_LEN,
help="max length (default: %default)")
op.add_option("--min", nargs=1, type="int", default=None,
help="min length (defaults to max)")
op.add_option("-n", "--count", nargs=1, type="int", default=None,
help="number of passwords to generate (default: %default)")
op.add_option("--cols", type="int", default=None,
help="number of columns to use")
opts, args = op.parse_args(args)
if args:
op.error("unknown arguments")


if os.isatty(sys.stdin.fileno()) and (
opts.count is None and opts.cols is None
and not opts.bare
):
opts.cols = 80 // (opts.max + 1)
opts.count = opts.cols * 25
else:
if opts.count is None:
opts.count = 1
if opts.cols is None:
opts.cols = 1


if opts.bare and opts.cols != 1:
op.error("bare output requires --cols=1")


if opts.min == None:
opts.min = opts.max


if any(x < 1 for x in [opts.cols, opts.count, opts.min, opts.max]):
op.error("values must be >= 1")


choices_func = choices_non_repeated
if opts.repeat:
choices_func = choices
elif len(set(opts.chars)) < 2:
op.error("must allow repetition or provide a longer character set")
return "op.error shouldn't return"


col = 0
for _ in xrange(opts.count):
length = random.randint(opts.min, opts.max)
password = "".join(choices_func(opts.chars, length))
sys.stdout.write(password)
if not opts.bare:
col += 1
if col == opts.cols:
sys.stdout.write("\n")
col = 0
else:
sys.stdout.write(" ")




if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))

You may want to use map instead of list comprehensions:

''.join(map(lambda x: random.choice(chars), range(length)))

WARNING this answer should be ignored due to critical security issues!

Option #2 seems quite reasonable except you could add a couple of improvements:

''.join(choice(chars) for _ in range(length))          # in py2k use xrange

_ is a conventional "I don't care what is in there" variable. And you don't need list comprehension there, generator expression works just fine for str.join. It is also not clear what "slow" means, if it is the only correct way.

For the crypto-PRNG folks out there:

def generate_temp_password(length):
if not isinstance(length, int) or length < 8:
raise ValueError("temp password must have positive length")


chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
from os import urandom


# original Python 2 (urandom returns str)
# return "".join(chars[ord(c) % len(chars)] for c in urandom(length))


# Python 3 (urandom returns bytes)
return "".join(chars[c % len(chars)] for c in urandom(length))

Note that for an even distribution, the chars string length ought to be an integral divisor of 128; otherwise, you'll need a different way to choose uniformly from the space.

You should use the secrets module to generate cryptographically safe passwords, which is available starting in Python 3.6. Adapted from the documentation:

import secrets
import string
alphabet = string.ascii_letters + string.digits
password = ''.join(secrets.choice(alphabet) for i in range(20))  # for a 20-character password

For more information on recipes and best practices, see this section on recipes in the Python documentation. You can also consider adding string.punctuation or even just using string.printable for a wider set of characters.

I think this'll do the trick. random.SystemRandom uses the same underlying crypto random function as os.urandom but it uses the familiar random interface. This function won't be subject to the weird 128 byte thing as in Ben's answer.

import random
import string


def gen_random_string(char_set, length):
if not hasattr(gen_random_string, "rng"):
gen_random_string.rng = random.SystemRandom() # Create a static variable
return ''.join([ gen_random_string.rng.choice(char_set) for _ in xrange(length) ])


password_charset = string.ascii_letters + string.digits
gen_random_string(password_charset, 32)

I suggest the following for those stuck on python <3.6:

import os, math, string, struct


def generate_password(pass_len):
symbols = string.printable.strip()
return ''.join([symbols[x * len(symbols) / 256] for x in struct.unpack('%dB' % (pass_len,), os.urandom(pass_len))])

This has the advantage over Ben Mosher's solution that the each symbol from symbols has an equal change of occurring whereas using modulus slightly favors the first symbols in the alpabet. The alphabet of symbols is also larger in this suggestion.

Here in the line 9 of above coding:

return (choice(options) for _ in xrange(length))

Actually the xrange is not defined, so the correct coding for this is:

return (choice(options) for _ in range(length))

Same in 76 line too.

Two recipes using the builtin secrets (python 3.6+)

1. secrets.token_urlsafe

This is much faster than the accepted answer. (see timings below)

import secrets
password = secrets.token_urlsafe(32)

Example output:

4EPn9Z7RE3l6jtCxEy7CPhia2EnYDEkE6N1O3-WnntU

The argument for token_urlsafe is number of bytes. On average, one byte is 1.3 characters (base64 encoded).

2. Enforce amount of digits/upper characters etc

This is slighly modified copy from the docs of secrets. With this you have more fine grained control on how to generated passwords have to look. Of course, this is not fast option if you need to generate a lot of passwords.

  • Forcing length to be 20 characters
  • Forcing at least 4 lower case character
  • Forcing at least 4 upper case characters
  • Forcing at least 4 digits
  • Special characters can be added to alphabet. In this example, there are just - and _ added.
import string
import secrets
alphabet = string.ascii_letters + string.digits + '-_'
while True:
password = ''.join(secrets.choice(alphabet) for i in range(20))
if (sum(c.islower() for c in password) >=4
and sum(c.isupper() for c in password) >=4
and sum(c.isdigit() for c in password) >=4):
break

Example output:

HlxTm2fcFE54JA1I_Yp5

3. "I don't need the finer-grained control"

If considered speed, you can also drop the while-loop. In this case, it actually simplifies to gerrit's answer (but then you loose the finer-grained control):

import string
import secrets
alphabet = string.ascii_letters + string.digits + '-_'
password = ''.join(secrets.choice(alphabet) for i in range(20))

Speed comparison

1. secrets.token_urlsafe

1.62 µs ± 96.6 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

2. Enforce amount of digits/upper characters etc

107 µs ± 11.9 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

3. "I don't need the finer-grained control"

77.2 µs ± 9.31 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Speed comparison setup: python 3.8.5 64-bit on Win10, 43 characters in each password (=32 bytes for token_urlsafe).

What I once did while using PyCryptodome module was this:

from Cryptodome.Random import get_random_bytes
from base64 import b64encode, b64decode


def get_password(lenght):
return b64encode(get_random_bytes(lenght)).decode('utf-8')


password = get_password(21)

You decide how long you want the password to be at "get_random_bytes(lenght)".

You can use random and chr with symbols from 33 to 127:

from random import randint


symb_count = int(input('Enter count of symbols:'))
passwd = []
while len(passwd) < symb_count:
passwd.append(chr(randint(33, 127)))
print(''.join(passwd))

Yet another one-liner:

python -c "import random;print(''.join([random.choice(random.choice([['a','e','f','g','h','m','n','t','y'],['A','B','E','F','G','H','J','K','L','M','N','Q','R','T','X','Y'],['2','3','4','5','6','7','8','9'],['/','*','+','~','@','#','%','^','&','//']])) for i in range(16)]));"

Samples:

L+f##Q~H88NBe6Ny

&7@M7gt4J^///gH3e

5e2455hgn2h^//ffh

Advantage:

Change char list explicitly, exclude similar chars(etc. i 1 l I, 0 o O).(Just add/del in the list)

Change password length from 16 to whatever you want.

Disadvantage:

Readability.

PS. readable version:

python -c "import random;\
print(''.join([random.choice(random.choice(\
[\
['a','e','f','g','h','m','n','t','y'],\
['A','B','E','F','G','H','J','K','L','M','N','Q','R','T','X','Y'],\
['2','3','4','5','6','7','8','9'],\
['/','*','+','~','@','#','%','^','&','//']\
])) \
for i in range(16)]));"