Thursday, September 17, 2009

Creating an url shortener using Python

Creating an url shortener using Python

On twitter, everybody is using an url shortener. Though, there can be several problems using an external service, as for example the fact that there’s no garantee that your shortened urls will always be available. So, what about creating our own URL shortner, using the Python language?

Author : Jean-Baptiste Jung

Jean is a 27 years old self-taught web developer and WordPress specialist who lives in Wallonia, the French speaking part of Belgium. In addition to Cats Who Code, he also blogs about WordPress at WpRecipes and about Photoshop at PsdVibe.

Technical choices

Acknowledgements

There's many tools to help you creating an url shortener, for example, ur1 using PHP and urly using Python. In this tutorial, you'll learn how to create your own url shortener from scratch.

This tutorial uses the Python programming language. Python's basic concepts aren't explained here, but this tutorial can be easily followed by anyone having at least some experience in another language as such as PHP or Ruby.
Full source code is available here.

Goals

Let see what we need to achieve :

  • Being able to easily redirect to blog posts
  • Being able to choose a pertinent shortened url name
  • A minimalistic administration interface

1. Redirections

Let's start by redirections. For now, we just have to add the following URLs :

urls = (
"/p/(\d+)", "RedirectToPost",
"/t/(\d+)", "RedirectToThought",
)

Then, it's time to create the redirection classes :

class RedirectToPost:
def GET(self, post_id):
return web.redirect(POST_REDIRECT_URL % post_id)

class RedirectToThought:
def GET(self, thought_id):
return web.redirect(THOUGHT_REDIRECT_URL % thought_id)

As you can see, many variables are defined on the beginning of the file, to allow you to easily adapt the code to your own needs.

Now, let's work on redirection to other urls by adding an entry in the urls :

urls = (
"/(.*)", "RedirectToOthers",
)

Now we have to take the URLs which have been created via the admin interface (We'll see that point later) and are stocked in a shelve object, which allow a quick key/value correspondance.

class RedirectToOthers:
def GET(self, short_name):
storage = shelve.open(SHELVE_FILENAME)
# shelve does not allow unicode keys
short_name = str(short_name)
if storage.has_key(short_name):
response = web.redirect(storage[short_name])
else:
response = FAIL_MESSAGE
storage.close()
return response

Definitely nothing hard here. The code redirects to the corresponding url, or display and error if non is found.

2. Administration

If you always have a shell connected to your server, you can fill your shelve file using Python shell. But creating an admin interface is simple and useful, so let's do it.

To "protect" the admin interface URL, we'll just make it harder to find. Just use what you think is the more pertinent, in a variable :

urls = (
ADMIN, "Admin",
ADMIN+"/done/(.*)", "AdminDone",
)

The Admin class allow you to display the form and to submit a new shortcut/url correspondence:

class Admin:
def GET(self):
admin_form = web.form.Form(
web.form.Textbox("url", description="Long URL"),
web.form.Textbox("shortcut",description="Shortcut"),
)
admin_template = web.template.Template("""$def with(form)
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset=utf-8>

<title>URL shortener administration</title>
</head>
<body onload="document.getElementById('url').focus()">
<header><h1>Admin</h1></header>

<form method="POST" action="/admin">
$:form.render()
<input type="submit" value="Shorten this long URL">
</form>
</body>
</html>
""")
return admin_template(admin_form())

def POST(self):
data = web.input()
shortcut = str(data.shortcut) or random_shortcut()
storage = shelve.open(SHELVE_FILENAME)
if storage.has_key(shortcut) or not data.url:
response = web.badrequest()
else:
storage[shortcut] = data.url
response = web.seeother(ADMIN+'/done/'+shortcut)
storage.close()
return response

Webpy forms and templates are directly used in the code because they're super concise. If the request is GET we create the form and send it to the template, if the request is a POST, we add the url to the base and redirect user to the confirm page.
The code itself don't verify a lot of things: It just makes sure that it will not erase an existing url and that a url has been submitted.

Then we just have to display the confirm page with our newly created link and a shortcut to directly tweet the link :

class AdminDone:
def GET(self, short_name):
admin_done_template = web.template.Template("""$def with(new_url)
<!DOCTYPE HTML>
<html lang="en">
<head>

<meta charset=utf-8>
<title>URL shortener administration</title>
</head>
<body>
<header><h1>Done!</h1></header>

<p>You created: $new_url</p>
<p><a href="http://twitter.com/home?status=$new_url"
title="Tweet it!">Tweet it?</a></p>
</body>
</html>

""")
return admin_done_template(SERVICE_URL+short_name)

That's all. This code can easily be enhanced, but right now it does what we needed: It create short urls.

3. Going live

I'm using lighty, which have to be adapted according to your server configuration.

Don't forget to make code.py executable and modify ADMIN!:

$HTTP["host"] =~ "bgk.me" {
server.document-root = "/path/"

fastcgi.server = (
"/code.py" => (
"main" => (
"socket" => "/path/bgkme.socket",
"bin-path" => "/path/code.py",
"max-procs" => 1,
"bin-environment" => (
"REAL_SCRIPT_NAME" => ""
),
"check-local" => "disable"
)
)
)

url.rewrite-once = (
"^(/.*)$" => "/code.py$1",
)
}

 the full code

 

#!/usr/bin/env python

import web
import shelve
from random import choice

# Settings
SERVICE_URL = 'http://bgk.me/'
SHELVE_FILENAME = "bgkurls.db"
ADMIN = "/admin" # obfuscate it!
POST_REDIRECT_URL = 'http://www.biologeek.com/journal/%s/'
THOUGHT_REDIRECT_URL = 'http://www.biologeek.com/bistrot/%s/'
PHOTO_REDIRECT_URL = 'http://www.biologeek.com/photos/%s/'

# Messages
INDEX_MESSAGE = """<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset=utf-8>
<title>URL shortener administration</title>
</head>
<body>
<header><h1>My own URL shortener that doesn't suck.</h1></header>
<section>
Get your own at <a href="http://code.welldev.org/bgk/"
title="Access mercurial repository">http://code.welldev.org/bgk/</a>.
</section>
</body>
</html>
"""
FAIL_MESSAGE = 'Redirection failed, verify your link...'

# URLs
urls = (
"/", "Index",
ADMIN, "Admin",
ADMIN+"/done/(.*)", "AdminDone",
"/p/(\d+)", "RedirectToPost",
"/t/(\d+)", "RedirectToThought",
"/pic/(\d+)", "RedirectToPhoto",
"/(.*)", "RedirectToOthers",
)
app = web.application(urls, globals())

# Stolen from django.contrib.auth.models
def random_shortcut(length=10,
allowed_chars='abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'):
return ''.join([choice(allowed_chars) for i in range(length)])

class Index:
def GET(self):
return INDEX_MESSAGE

class Admin:
def GET(self):
admin_form = web.form.Form(
web.form.Textbox("url", description="Long URL"),
web.form.Textbox("shortcut",description="Shortcut"),
)
admin_template = web.template.Template("""$def with(form, adminurl)
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset=utf-8>
<title>URL shortener administration</title>
</head>
<body onload="document.getElementById('url').focus()">
<header><h1>Admin</h1></header>
<form method="POST" action="$adminurl">
$:form.render()
<input type="submit" value="Shorten this long URL">
</form>
</body>
</html>
""")
return admin_template(admin_form(), ADMIN)

def POST(self):
data = web.input()
shortcut = str(data.shortcut) or random_shortcut()
storage = shelve.open(SHELVE_FILENAME)
if storage.has_key(shortcut) or not data.url:
response = web.badrequest()
else:
storage[shortcut] = data.url
response = web.seeother(ADMIN+'/done/'+shortcut)
storage.close()
return response

class AdminDone:
def GET(self, short_name):
admin_done_template = web.template.Template("""$def with(new_url)
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset=utf-8>
<title>URL shortener administration</title>
</head>
<body>
<header><h1>Done!</h1></header>
<p>You created: $new_url</p>
<p><a href="http://twitter.com/home?status=$new_url"
title="Tweet it!">Tweet it?</a></p>
</body>
</html>
""")
return admin_done_template(SERVICE_URL+short_name)

class RedirectToPost:
def GET(self, post_id):
return web.redirect(POST_REDIRECT_URL % post_id)

class RedirectToThought:
def GET(self, thought_id):
return web.redirect(THOUGHT_REDIRECT_URL % thought_id)

class RedirectToPhoto:
def GET(self, photo_id):
return web.redirect(PHOTO_REDIRECT_URL % photo_id)

class RedirectToOthers:
def GET(self, short_name):
storage = shelve.open(SHELVE_FILENAME)
short_name = str(short_name) # shelve does not allow unicode keys
if storage.has_key(short_name):
response = web.redirect(storage[short_name])
else:
response = FAIL_MESSAGE
storage.close()
return response

if __name__ == "__main__":
app.run()

Posted via web from swathidharshananaidu's posterous

No comments:

Post a Comment