短網址系統開發

自己在空閒的時間,用 Django 開發的一個短網址系統,可以支援自訂 FB 網址縮圖的功能。獨立的 app 可以很方便地整合進已存在的 Django 專案裡面。

以下是程式碼:source code

需要討論的問題

開發這個系統,實作上沒有很困難,但網路上似乎沒有比較完整的方法,所以就記錄一下開發的細節。有幾個需要思考的點:

  1. 整體架構
  2. 社群網站如何產生預覽?
  3. 「短」網址如何產生?
  4. 使用哪一種方法 redirect? (javascript or python / 301 or 302)
  5. 如何記錄流量

第一版的功能

目前釋出的第一個版本,有幾個功能:

快速縮網址

用起來有點像 goo.gl

自訂短網址

可以自訂短網址的名稱,如:xxx.com/links/活動報名,還能自訂 FB 預覽縮圖、標題、內容,對於臉書粉絲團來說非常實用的功能。像這樣子:

查看數據

每次點擊都把他記錄起來,有點像是 google analytics 的作用,就自己刻了一個,像這樣子:

整體架構

短網址主要的思路很簡單,

  1. 使用者從 xxx.com/meow 進入伺服器
  2. 到資料庫裡找 meow 所對應的網址
  3. 做一些統計之類的事
  4. 將他導向目標
# models.py
class ShortURL(models.Model):
    timestamp = models.DateTimeField(default=timezone.now)
    target = models.CharField(max_length=200)
    name = models.CharField(max_length=200, blank=True)
    title = models.CharField(max_length=200, blank=True)
    description = models.TextField(blank=True)
    thumbnail = models.ImageField(upload_to='thumbnails/%m/', blank=True)
    permanent_url = models.CharField(max_length=20, blank=True)
    mode = models.IntegerField(default=301)

社群網站如何產生預覽?

基當你貼了一個網址之後,網站會讓一隻爬蟲程式去看看你的網站,FB 會用到 Title、Description、image 等內容,如果你有特別設定 meta tag 他會優先使用 meta tag 的內容,若沒有的話才是亂亂抓(通常是第一段文字及第一張照片)。

所以為了要「騙」過這支爬蟲程式,我就 render 一個假的、帶有自訂 meta 的網頁,然後再用 javascript redirect 至目標網站。在使用上因為馬上就被重導,所以使用者不會感覺到發生了什麼事。

# views.py
# ...
context = {
    'url': shortcut.target,
    'title': shortcut.title,
    'description': shortcut.description,
    'image': image_url
}
return render(request, 'links/redirect.html', context)
<meta property="og:title" content="{{title}}" />
<meta property="og:description" content="{{description}}" />
<meta property="og:image" content="{{image}}" />
<script type="text/javascript">
  window.location.href = "{{url}}";
</script>

永久短網址如何產生?

說到永久短網址,就要產生一段夠短的字串來對應原本的網址,要兼具「短」且要「唯一」,且必須具有一定的亂度,避免不公開的網址被連結到(如內部的表單)。

所以我使用的方法是「用objects.create() 的 id + hash_salt 經過 hash 後取前 6 位,再換成 BASE64」。加入 Hash salt 讓網址規則更難被找到。使用 BASE 64 讓網址可以為 A-Z,a-z,0-9 加上 -_ ,如:xxx.com/links/A-w0 變得更有短網址的樣子了呢。

permanent_url = base64.b64encode(
    hashlib.md5((str(shortcut.id) + HASH_SALT).encode('utf-8')).digest(), altchars=b"-_")[:6].decode("utf-8")

但這樣的方法有一個缺點,就是 md5[:6] 並不一定唯一,但經過實驗後大約要生成 100000 個網址之後才會發生碰撞,所以就先安心地用吧~若真的遇到了也可以用加字的方式解決。

或許可以試試看

將 id HASH 成 32 位元,2^32=4294967296 可以用 6 個字 64^6=68719476736 包起來。

使用哪種方法 redirect?

  • 301 redirect 301 Move Permanently。可以簡單地理解為該資源已經被永久改變了位置。
  • 302 redirect 302 Found,原始描述短語為 Moved Temporarily。可以簡單的理解為該資源原本確實存在,但已經被臨時改變了位置;換而言之,就是請求的資源暫時駐留在不同的 URI 下,故而除非特別指定了快取頭部指示,該狀態碼不可快取。

快速轉址

在實作快速轉址時,我是使用 301 重導的概念去做,因為使用者只是快速的經過這個網站,所以我在後端就直接 redirect(‘target.com’) ,也暗示 FB 直接去找原本的網站找東西。

自訂轉址

自訂轉址由於是要讓爬蟲程式造訪假網站,再由 前端javascript 重導至新網站,算是以 302 的概念去實作。

# views.py
if shortcut.mode == 301:
    return redirect(shortcut.target)
elif shortcut.mode == 302:
    # ...
    context = {
        'url': shortcut.target,
        'title': shortcut.title,
        'description': shortcut.description,
        'image': image_url
    }
    return render(request, 'links/redirect.html', context)

如何記錄流量?

每點擊一次,進到 views.py 時就 create 一個 object, ForeignKey 至連結的網址,到時候要製作統計圖時就能很方便的撈資料了~順便記錄了訪問 ip 也可以統計不重複的 ip 瀏覽量。

然後再用 Chart.js 畫圖。

# models.py
class Viewer(models.Model):
    timestamp = models.DateTimeField(default=timezone.now)
    short_url = models.ForeignKey(
        ShortURL, on_delete=models.CASCADE, related_name='viewers')
    ip = models.CharField(max_length=100)