C. Barker
C. Barker

Reputation: 51

Count the number of times multiple substrings appear in a string at once

I am making a simple script in Python that evaluates the strength of a password on a score system that gives and takes points based on whether or not it contains upper or lowercase letters, numbers and symbols for school.

One of the requirements is that it checks for 3 letters or numbers that are consecutive from left to right on a UK QWERTY keyboard and takes away 5 points for each instance. For example the password 'qwer123' would lose 15 points for 'qwe', 'wer' and '123'. How could this be accomplished? My current code below.

def check():
  user_password_score=0
  password_capitals=False
  password_lowers=False
  password_numbers=False
  password_symbols=False
  password_explanation_check=False
  ascii_codes=[]
  password_explanation=[]
  print("The only characters allowed in the passwords are upper and lower case letters, numbers and these symbols; !, $, %, ^, &, *, (, ), _, -, = and +.\n")
  user_password=str(input("Enter the password you would like to get checked: "))
  print("")
  if len(user_password)>24 or len(user_password)<8:
    print("That password is not between 8 and 24 characters and so the Password Checker can't evaluate it.")
    menu()
  for i in user_password:
    ascii_code=ord(i)
    #print(ascii_code)
    ascii_codes.append(ascii_code)
  #print(ascii_codes)
  for i in range(len(ascii_codes)):
    if ascii_codes[i]>64 and ascii_codes[i]<90:
      password_capitals=True
    elif ascii_codes[i]>96 and ascii_codes[i]<123:
      password_lowers=True
    elif ascii_codes[i]>47 and ascii_codes[i]<58:
      password_numbers=True
    elif ascii_codes[i] in (33,36,37,94,38,42,40,41,45,95,61,43):
      password_symbols=True
    else:
      print("Your password contains characters that aren't allowed.\n")
      menu()
  if password_capitals==True:
    user_password_score+=5
  if password_lowers==True:
    user_password_score+=5
  if password_numbers==True:
    user_password_score+=5
  if password_symbols==True:
    user_password_score+=5
  if password_capitals==True and password_lowers==True and password_numbers==True and password_symbols==True:
    user_password_score+=10
  if password_numbers==False and password_symbols==False:
    user_password_score-=5
  if password_capitals==False and password_lowers==False and password_symbols==False:
    user_password_score-=5
  if password_capitals==False and password_lowers==False and password_numbers==False:
    user_password_score-=5
  #print(user_password_score)
  if user_password_score>20:
    print("Your password is strong.\n")
  else:
    print("That password is weak.\n")
  #don't forget you still need to add the thing that checks for 'qwe' and other stuff.
  menu()

Upvotes: 4

Views: 182

Answers (4)

Ruan
Ruan

Reputation: 912

If regular expressions are allowed, you can do this in one line:

import re
user_password_score = 42
pwd = 'qwer123'
user_password_score += (lambda z : -5 * len([match.group(1) for match in re.compile('(?=({0}))'.format('|'.join(["({0})".format(w) for w in [x for y in [[s[i:i+3] for i in range(0,len(s)-2)] for s in ["qwertyuiopasdfghjklzxcvbnm", "azertyuiopqsdfghjklmwxcvbn", "abcdefghijklmnopqrstuvwxyz", "01234567890"]] for x in y]]))).finditer(z) ]))(pwd)

This code is equivalent:

import re
user_password_score = 42
pwd = 'qwer123'
seqs = ["qwertyuiopasdfghjklzxcvbnm", "azertyuiopqsdfghjklmwxcvbn", "abcdefghijklmnopqrstuvwxyz", "01234567890"]
pattern = re.compile('(?=({0}))'.format('|'.join(["({0})".format(w) for w in [x for y in [[s[i:i+3] for i in range(0,len(s)-2)] for s in seqs] for x in y]])))
penalty = -5 * len([match.group(1) for match in pattern.finditer(pwd) ])
user_password_score += penalty

And the following code is also equivalent ( and hopefully, also human-readable ). We'll print it step by step to see better what it's doing.

import re

def build_pattern(sequences):
    all_triplets = []
    triplets = []
    for seq in sequences:
        for i in range(0, len(seq) - 2):
            triplets.append(seq[i:i+3])
        all_triplets.append(triplets)
        triplets = []
    expanded_triplets = [ x for y in all_triplets for x in y ]
    print("Plain list of triplets: " + str(expanded_triplets))
    string_pattern = '|'.join( [ "({0})".format(x) for x in expanded_triplets ] )
    lookahead_pattern = '(?=({0}))'.format(string_pattern)
    print("Regex expression: " + lookahead_pattern)
    return re.compile(lookahead_pattern)

password = 'qwer123'
user_password_score = 42
print("User password score: " + str(user_password_score))
sequences = ["qwertyuiopasdfghjklzxcvbnm", 
             "azertyuiopqsdfghjklmwxcvbn", 
             "abcdefghijklmnopqrstuvwxyz", 
             "01234567890"]
pattern = build_pattern(sequences)
matches = [ match.group(1) for match in pattern.finditer(password) ]
print("Matches : " + str(matches))
matches_count = len(matches)
penalty = -5 * matches_count
print("Penalty: " + str(penalty))
user_password_score += penalty
print("Final score: " + str(user_password_score))

This is the output:

User password score: 42
Plain list of triplets: ['qwe', 'wer', 'ert', 'rty', 'tyu', 'yui', 'uio', 'iop', 'opa', 'pas', 'asd', 'sdf', 'dfg', 'fgh', 'ghj', 'hjk', 'jkl', 'klz', 'lzx', 'zxc', 'xcv', 'cvb', 'vbn', 'bnm', 'aze', 'zer', 'ert', 'rty', 'tyu', 'yui', 'uio', 'iop', 'opq', 'pqs', 'qsd', 'sdf', 'dfg', 'fgh', 'ghj', 'hjk', 'jkl', 'klm', 'lmw', 'mwx', 'wxc', 'xcv', 'cvb', 'vbn', 'abc', 'bcd', 'cde', 'def', 'efg', 'fgh', 'ghi', 'hij', 'ijk', 'jkl', 'klm', 'lmn', 'mno', 'nop', 'opq', 'pqr', 'qrs', 'rst', 'stu', 'tuv', 'uvw', 'vwx', 'wxy', 'xyz', '012', '123', '234', '345', '456', '567', '678', '789', '890']
Regex expression: (?=((qwe)|(wer)|(ert)|(rty)|(tyu)|(yui)|(uio)|(iop)|(opa)|(pas)|(asd)|(sdf)|(dfg)|(fgh)|(ghj)|(hjk)|(jkl)|(klz)|(lzx)|(zxc)|(xcv)|(cvb)|(vbn)|(bnm)|(aze)|(zer)|(ert)|(rty)|(tyu)|(yui)|(uio)|(iop)|(opq)|(pqs)|(qsd)|(sdf)|(dfg)|(fgh)|(ghj)|(hjk)|(jkl)|(klm)|(lmw)|(mwx)|(wxc)|(xcv)|(cvb)|(vbn)|(abc)|(bcd)|(cde)|(def)|(efg)|(fgh)|(ghi)|(hij)|(ijk)|(jkl)|(klm)|(lmn)|(mno)|(nop)|(opq)|(pqr)|(qrs)|(rst)|(stu)|(tuv)|(uvw)|(vwx)|(wxy)|(xyz)|(012)|(123)|(234)|(345)|(456)|(567)|(678)|(789)|(890)))
Matches : ['qwe', 'wer', '123']
Penalty: -15
Final score: 27

Inside the build_pattern function, [ x for y in all_triplets for x in y ] is a trick to expand a list of lists into a plain list. A regex pattern like (lmw)|(mwx)|(wxc) used inside a finditer() tells we want to find all matches for lmw, mwx and wxc. And when we wrap this pattern inside a lookahead ( (?=()) ) we're telling re that it should also include overlapping matches on the result.

Upvotes: 0

J. Taylor
J. Taylor

Reputation: 4855

I would create a list of sequences of adjacent keys, as others mentioned above. Then I would create a sliding window function to generate all sequences of length 3, and match each of them against the password:

from itertools import islice

keyboard_rows = ['1234567890', 'qwertyuiop', 'asdfghjkl', 'zxcvbnm']

def window(seq, n=3):
    it = iter(seq)
    result = tuple(islice(it, n))
    if len(result) == n:
        yield result
    for elem in it:
        result = result[1:] + (elem,)
        yield result

for row in keyboard_rows:

    for seq in window(row, n=3):
        if "".join(seq) in password:
            user_password_score -= 15

    # scan other direction <--
    for seq in window(row[::-1], n=3):
        if "".join(seq) in password:
            user_password_score -= 15

Upvotes: 0

Beno&#238;t P
Beno&#238;t P

Reputation: 3265

You could store forbidden sequences in a set of strings, and decrement the score every time someone uses that sequence.

password = "qwert123"
score = 42          # initial score
sequences = {       # all in lowercase because of the `lower()` in the loop
    "qwertyuiopasdfghjklzxcvbnm",
    "azertyuiopqsdfghjklmwxcvbn",
    "abcdefghijklmnopqrstuvwxyz",
    "01234567890"
}
match_length = 3                        # length threshold for the sanction
sequences.update({s[::-1] for s in sequences})      # do we allow reverse ?

for c in range(len(password)-match_length+1):
    for seq in sequences:
        if password[c:c+match_length].lower() in seq:
            score-=5
            print(f"'{password[c:c+match_length]}' => -5 !")
            break   # Don't flag the same letters more than once

print(score) # 22 (42-4*5)

Upvotes: 1

Ahmed Karaman
Ahmed Karaman

Reputation: 531

The simplest way is to brute force through all the possible sequences.

Create 4 string: "1234567890", "qwertyuiop", "asdfghjkl", "zxcvbnm" and loop through each with 3 characters from the user_password.

You can initialize this list at the beginning of the check function:

sequences = ["1234567890", "qwertyuiop", "asdfghjkl", "zxcvbnm"]

and then inside the for i in range(len(ascii_codes)) loop add:

if(i<len(ascii_codes)-2):  # since we will be checking for characters up to i+2 in our loop
    flag = False  # initialize a flag to signal finding a match
    for s in sequences:  # loop through each of the 4 keyboard sequences
        if(s.find(user_password[i: i+3].lower()) != -1): 
            user_password_score -= 5
            flag = True
            break 
        if(flag): break

Upvotes: 0

Related Questions