15 十月

Django 1.7 Migration

雖然之前就有用 South, 基本上是一樣的東西, 但是當 Django 1.7 把他納入成為 Migration 之後, 就有種扶正的感覺, 當然也開心的使用、試用。

其中一個我很喜歡的功能是 RunPython。他可以讓你執行 arbitrary code, 也許是新增一些你想要的 fixtures, 或是針對你想要的狀況作一些資料的轉換、增減等等。

不過如果你按照官方文件上面的作法, 可能有些 behavior 會跟預期的不同。例如以下的 code

def forwards_func(apps, schema_editor):
    # We get the model from the versioned app registry;
    # if we directly import it, it'll be the wrong version
    Country = apps.get_model("myapp", "Country")
    db_alias = schema_editor.connection.alias
    Country.objects.using(db_alias).bulk_create([
        Country(name="USA", code="us"),
        Country(name="France", code="fr"),
    ])

你可能會預期 Country 就是跟你現在 models.py 裡頭的 Country 一樣。但事實上, 他是依照整個 migrations 裡頭依序建構出來的 models。譬如說, 假設 0005_blablabla.py 定義了 Country, 但是上述的 procedure 是在 0004_blablabla.py, 那麼當 django 在 migrate 的時候, get_model 就會噴錯告訴你找不到 Country。

因此, dependency 要做好… (雖然 documentation 有提到, 但有時候看到 sample code 太興奮就直接用了 XD 最後 trace code 才發現是怎麼回事 orz)

另外還有一件事情就是他的 schema_editor 有提供一些 functions, 例如 alter_db_table(…) 可以把一個 table 改名字, 或是 create_model、delete_model 等, 但由於這些東西你可能會寫在 RunPython 呼叫的 function 裡頭, 導致 django migration 在 traverse 整個 migration tree 的時候無法追蹤, 因此就會造成不一致的狀況。其實目前也不知道這是該怎麼解… 所以… 就盡量避免使用吧。

13 二月

UTF-8 正體中文 (繁體) 的筆畫排序

好像很久以前, 就已經發現到, 中文的排序是有一些問題的, 但我就放著。直到昨天在處理某個頁面時, 我發現, glossary 之類的東西, 按照筆畫排序是很重要的, 所以就 Google 了一下。

一開始, 我以為是 postgresql locale 的問題, 找到了幾個 link, 但是都沒有下文, 而且重點是我一開始在 Ubuntu 裝 postgresql 的時候, 我沒有更改設定, 所以 zh_TW 並沒有進去。開了台新機器隨便測一測, 仍然沒有達到我想要的結果。

直到我赫然發現我整個方向都錯了。後來才發現, 字元編碼本身, UTF-8 並不是按照筆畫順序排的, 但 Big5 是, 所以可以把該欄位轉成 Big5 之後再用這個欄位排序。

以下可以找到很多 MySQL 的版本:

以下是 postgresql 的 sample code。一般來說, 原本的搜尋就是像下面這樣:


db=# select id, title from kb_article order by title;
id | title
-----+------------------------------------------
158 | 丁種建築用地
80 | 不動產奢侈稅可用公告現值並扣成本嗎
78 | 不動產奢侈稅的課徵時點為何
79 | 不動產的奢侈稅稅率為何
157 | 丙種建築用地
179 | 主建物
161 | 乙種工業用地
156 | 乙種建築用地
214 | 事故屋
33 | 交屋注意事項
12 | 交易安全機制
116 | 什麼是區段化、去識別化方式提供查詢

從上面可以看到, 乙只有一劃, 但是他並沒有排在最前面。


db=# select id, title from kb_article order by convert_to(title, 'BIG5');
id | title
-----+------------------------------------------
42 | 「銷售契約」是指公契還是私契
161 | 乙種工業用地
156 | 乙種建築用地
158 | 丁種建築用地
34 | 入住前準備事宜
175 | 土地改良物
153 | 土地使用分區
152 | 土地持份
94 | 土地相關
44 | 土地重劃後之「持有期間」如何計算
191 | 大公
220 | 女兒牆
192 | 小公
79 | 不動產的奢侈稅稅率為何
80 | 不動產奢侈稅可用公告現值並扣成本嗎
78 | 不動產奢侈稅的課徵時點為何
116 | 什麼是區段化、去識別化方式提供查詢

沒想到… 這年頭… BIG5 還有這個作用 orz, 不過在 django 裡頭就只能下 raw 不能直接用他的 ORM 啦!

13 六月

Django, celery, gevent 實作 long polling

我們的一個新的網站平台, 有讓使用者上傳圖片的功能。當照片上傳過後, 我們會縮放圖片至適當的比例, 並且貼上一些浮水印之類的東西。如果我是貼上一個單眼拍下來的相片, 使用 PIL 縮圖是挺耗費時間的, 因此為了有更好的 User Experience, 我覺得這些耗費時間變成 offline 來做。然而, 我希望 server 在做完之後, browser 能夠被通知到並且將縮圖抓回來。

一開始, 我打定的主義是使用 node.js 的 socketio, 可惜我完全沒碰過 node.js, 雖然 socketio 在前後台之間的結合很完美, 但有下列問題要先解決:

  1. Ubuntu 12.04 LTS 上的 nginx 版本還沒有支援 websocket。當然也是可以更新版本。
  2. nginx 的後端有兩種 solution, django + node.js。
  3. 我要想辦法讓 django 跟 node.js 可以一起溝通, 因為網站整個都是 django, user upload 完照片後, 整個都是在 django/python 裡頭, task 的資訊要在兩邊共享

基於上述會耗費太多開發時間, 因此我多 survey 了一下, 發現 long polling 基本上就是把 request 卡住, 等到一有結果就直接回傳。用最基本的東西就可以實作。

Celery

Celery 之前就大概用過, 這一次想說直接用 SQS 作為 Message Queue, 文件上有很基本的說明, 但是有一行講得非常模糊

If you specify AWS credentials in the broker URL, then please keep in mind that the secret access key may contain unsafe characters that needs to be URL encoded.

看完沒有 sample 真的很痛苦, 因為 python urllib 的 API 文件好像找不到合用的, 一開始看大概會選擇 urllib.urlencode(query[, doseq]), 但其實這是用在 GET parameter, 會有 key 跟 value, 而 urllib.quote(string[, safe]) 用起來卻不會將 / (slash) 做 encode。搞了半天, 原來要把 safe 變成空的才 ok:

BROKER_URL = 'sqs://%s:%s@' % (AWS_ACCESS_KEY_ID, urllib.quote(AWS_SECRET_ACCESS_KEY, ''))

然而, 實作之後, 發現有不穩定的狀況, 才發現在 Brokers 的頁面上有寫著, SQS 的 status 是 experimental, 因此就火速換掉使用之前用的 rabbitMQ。(不過後來發現不穩定的狀況應該是我自己耍腦殘, 請看下面段落敘述)

gevent & long polling

查了很多 long polling, python 還有 django 的資料, 後來發現可以使用 Django+gevent 來實作。如果不使用 gevent, Django 是不太適合直接拿來做這件事情, 因為如果有多個 request 都在等待, 很快就會把 worker 全部都佔滿 (如果有錯請糾正 >”<), 而使用 gevent, 則可以使用底層的 libevent 來做多工的切換, Gevent Tutorial 的這張圖有很清楚的表達他的運作方式。因此, 如果有多個 user 在做 long polling, 也不致於會卡住。

結合在一起

接在一起的流程很簡單, 步驟如下:

  1. User 上傳檔案到後端的某隻 view。
  2. 這個 view 製作一個 celery task (apply_async), 然後將 task_id 回傳給 browser
  3. Browser 定期帶著這個 task_id 去 check 另一個叫做 poll 的 view
  4. poll 的 view 只做一件事情就是 get celery task result, 並且設定一個 timeout。所以, 只要一 timeout, browser 就會再發一次 request 卡在 poll 裡頭。

當然, 如果一次上傳多個圖片, 就可以使用 gevent 的 group, 一次開多個 greenlet 去等待, 哪一個先回傳就把整個 group 幹掉回給 browser。我寫了一個很簡單的 django-longpolling-example 放在 github, 就只是簡單的示範這個概念。

Debug 很久之自己耍笨

前天下午在睡午覺的時候, 收到 E-mail 說有使用者講無法上傳圖片, 我就跳起來 debug, 用了非長久, 發現會有漏 task 的事情發生。於是乎我 trace code trace 到了 celery、kombu、amqp 等 python library, 才發現 message 是真的都有丟出去, 但是為什麼我的 celery worker 就是會掉 task 呢? 我今天才突然驚覺, 原來我 dev、stage、production 用到了同一個 queue, 所以我在測試機器上傳的圖片, 都委派給其他 server 去做了, 而這邊 code 沒有 sync, 或是檔案也不在那台 instance 上, 所以就無法完成 task。

這告訴了我們, follow tutorial 還滿重要的。在 Using RabbitMQ 的 Setting up RabbitMQ 提到, 要設定 virtual host, 才會把 queue 給隔開。

Deployment

在 django 使用 gevent, 若是使用 gunicorn, 可以直接將 worker 換成 gevent


gunicorn app.wsgi:application --worker-class gevent

如果直接執行的話, 可以參考 django-longpolling-examplerungevent.py

12 十月

以 django 來開發網頁

今天剛好碰到以前的同事問我關於 django 開發網頁的事情。我才想到, 上一次想到 django 相關的 packages 似乎講得不夠詳細。這邊稍微補充一下。

Web Server

如果不是很 heavy 的網站, 流量不大的話, 其實 apache2 就很足夠了。個人偏好是因為從很早就開始使用 apache, 也習慣他的 configuration 了。至於如果常見大家所使用的 deployment, 就是使用 nginx 外加 gunicorn 了。
A Django setup using Nginx and Gunicorn 這篇文章寫得頗清楚的。我個人是使用 Ubuntu, 因此選用 upstart 來寫他的 service script。

Environment

上一段提到的部落格文章有特別提到一個東西就是 virtualenv, 個人也相當推薦使用。他會將你的 python environment cage 在某個目錄, 因此 django 所使用到的套件並不會跟系統的 python 混淆。Tools of the Modern Python Hacker: Virtualenv, Fabric and Pip 這篇文章有提到。目前我還沒有使用 fabric 來做 deployment, 不過 virtualenv + pip 就非常好用了。

Settings

Django package ship 出來的 settings.py 其實不太適合 development/production 使用。SplitSettings 這個頁面寫了很多種方式來把兩邊分開來。

Template

Django 1.4 的 template 其實已經比以前好用非常多, 不過看到 instagram 使用 jinja2, 再看到 jinja2 官網的說明, 就會想要做這個 optimization。缺點就是如果你有使用第三方套件, template tags 以及 plugin 可能需要自己重刻。比較常見的搭配方式就是 jinja2 + coffin

CSS / js compression

django 有幾個套件可以做這件事情, 我目前有使用的是 django-pipeline。其他 solution 可以參考 Django packages -- asset managers。用這個東西的時候, 如果有 include 額外自己寫的 js, 記得要將變數 export 出來給 window, 否則變數都會被 cage 起來。

幾個月前看到這篇很棒的文章 -- What Powers Instagram: Hundreds of Instances, Dozens of Technologies, 裏頭提到了非常多不同面向的 solution, 值得參考。剛剛也在 quora 上看到這個 What technology stack is Instagram built with? 裡面也提到不少東西。

26 九月

Django + jinja2 + mptt

當初毅然決然的把 template 換成 jinja2, 其實真的是 over-optimized 了。不過既然都已經換了, 就換了, 代價就是 3rd-party package 有使用到 template tags 的, 要自己重寫!

最近在重新整修後臺系統, 索性也把 template 換成 jinja2, 不過這時麻煩了, 我們使用 django-mptt 來做 tree structure, 在 template 這邊有使用到 mptt 自己的 tags。有時候自己真的很衝動, 昨天就開始看 code, hack, 想要自己把它 port 成 jinja2 的版本。碰到重重的困難後, 突然發現 jinja2 自己就有 recursive 可以使用。而且也弄成了。

我真的是太衝動了。不過還好, 更了解 jinja2 的 extension 怎麼寫的, 也可以準備來 rework 之前寫的 pipeline tags for jinja2。

19 九月

django 的 staticfile

今天一直在搞這個, 搞不定。以下是整理的結果。

若 DEBUG = False, django 會跑去 STATIC_ROOT 設定的 folder 底下找 css/js 等檔案。如果這邊找不到, 會直接在 manage.py 噴 error 並且出現下列訊息

ValueError: The file 'mod.font.css' could not be found with
<pipeline.storage.PipelineCachedStorage object at 0x7faca89dbf90>

另外, 也是在 DEBUG = False 的情況之下, urlpatterns += staticfiles_urlpatterns() 也是沒有用的。詳情請見 https://github.com/django/django/blob/master/django/conf/urls/static.py