5
\$\begingroup\$

Here's my attempt at a Caesar Cipher encoder/decoder.

If given a key, it will encrypt the given string. However, if you do not specify a key, it checks each of the 26 possible keys and returns the one with the highest percentage of words that appear in this file of English words (with a couple additions including 'a' and 'I'). The program also returns the 'assurance' in its choice; for a specified key it is always 100%, for an unspecified key it is the percentage of words that are in English.

How can I improve the speed of the program? I have gone through 5 versions and improved the method that I use to decrypt each time, but now I would like to improve the speed. As I am a beginner, I am not very sure on how to do this.

The code is fairly readable, I think.

CaesarCipher_v5.py [47 lines]

# CaesarCipher_v5.py
# 
# CSTAICH 2014

from string import maketrans, ascii_uppercase, ascii_lowercase
import re
from operator import itemgetter
from itertools import chain

input = ''
key = ''



# variable input and processing
while input == '': input = raw_input('Input ciphertext: ')
if key == '': key = raw_input('Input key; leave blank for auto-detection: ')
if key != '':
    key = int(key)
    if key >= 26: raise Exception("invalid key, must be [0-25]")
alphabet = list(chain(*zip(ascii_uppercase, ascii_lowercase)))
english_words = re.sub(r'\r', '', open('sowpods.txt', 'r').read().lower()).split('\n')

# =================================-------------------------------

def key_shift(input, key): # shifts an input by a key number of characters
    return input.translate(maketrans(str(alphabet), str(alphabet[key * 2:] + alphabet[:key * 2])))

def english(sentence): #returns percentage of words in input that are english words
    sentence = re.sub(r'[?,.!:;/]', '', sentence).split(' ') #strip punctuation and split into words
    number_english_words = 0
    number_words = len(sentence)
    for word in sentence:
        if word.lower() in english_words: number_english_words += 1
    return round(number_english_words / float(number_words) * 100, 2)

# begin body
if key != '': #behavior for defined-key shift
    output = key_shift(input, key)
    assurance = '100%'
else: #behavior for non-defined-key shift
    options = [key_shift(input, s) for s in xrange(1,27)] #list of 26 options for shift
    options_dict = zip(options, [english(s) for s in options]) #list of tuples: (option, percent eng)
    output, assurance = max(options_dict, key=itemgetter(1))[0:2]

print ' :: '.join([str(output), str(assurance) + '%'])

Here are a couple interactions with the code:

$ python CaesarCipher_v5.py
Input ciphertext: Here is some plain English that I would like to translate over by, let's say... 17 characters? Sound good?
Input key; leave blank for auto-detection: 17
Yviv zj jfdv gcrze Vexczjy kyrk Z nflcu czbv kf kirejcrkv fmvi sp, cvk'j jrp... 17 tyrirtkvij? Jfleu xffu? :: 100%%

$ python CaesarCipher_v5.py
Input ciphertext: Yviv zj jfdv gcrze Vexczjy kyrk Z nflcu czbv kf kirejcrkv fmvi sp, cvk'j jrp... 17 tyrirtkvij? Jfleu xffu?
Input key; leave blank for auto-detection: 
Here is some plain English that I would like to translate over by, let's say... 17 characters? Sound good? :: 89.47%

$ python CaesarCipher_v5.py
Input ciphertext: Yberz vcfhz qbybe fvg nzrg, pbafrpgrghe nqvcvfpvat ryvg, frq qb rvhfzbq grzcbe vapvqvqhag hg ynober rg qbyber zntan nyvdhn. Hg ravz nq zvavz iravnz, dhvf abfgehq rkrepvgngvba hyynzpb ynobevf avfv hg nyvdhvc rk rn pbzzbqb pbafrdhng. Qhvf nhgr veher qbybe va erceruraqrevg va ibyhcgngr iryvg rffr pvyyhz qbyber rh shtvng ahyyn cnevnghe. Rkprcgrhe fvag bppnrpng phcvqngng aba cebvqrag, fhag va phycn dhv bssvpvn qrfrehag zbyyvg navz vq rfg ynobehz
Input key; leave blank for auto-detection: 
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum :: 36.23%

That last one was an experiment to see if it could pick out Latin. Turns out it can simply because short, two- or three-letter words are often shared between languages even if they do not have the same meaning. Even though the assurance was only 36.23%, the next best was 13.04% and many of the keys returned only one or two recognized words.

\$\endgroup\$
4
\$\begingroup\$

Your code would be much clearer if split into multiple functions, with clear inputs and outputs, e.g.:

def encode(plaintext, key):
    ...

def decode(ciphertext, key=None, dictionary=None):
    ...

def get_int_input(prompt, max_=26):
    ...

This is much better than relying on scope for access to the objects your functions need.


You could then have a loop at the end to run it all, something like:

if __name__ == '__main__':
    english_words = create_dictionary()
    while True:
        choice = get_int_input("1. Encode\n2. Decode\n3. Exit\n", 3)
        if choice == 1:
            plaintext = raw_input("Enter the plain text: ")
            key = get_int_input("Enter the key: ", 26)
            print encode(plaintext, key)
        elif choice == 2:
            ciphertext = raw_input("Enter the cipher text: ")
            key = None
            if raw_input("Do you know the key? (y/n) ".lower()) == "y":
                key = get_int_input("Enter the key: ", 26)
            plaintext, match = decode(ciphertext, key, dictionary)
            print "{0} :: {1:.2f}".format(plaintext, match)
        else:
            break

This improves the reusability of your code by allowing you to import the functions elsewhere without actually running the


In terms of speed, one obvious improvement would be to use a set, which provides very fast membership testing using hashing, for your dictionary. Also, you seem to be building it in an awkward way, try:

def create_dictionary(filename='sowpods.txt'):
    with open(filename) as f:
        return set(line.strip().lower() for line in f)

As you can now loop multiple times, you could also think about storing the translations in a dictionary {key: translation}, so you only build them once.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.