顯示具有 django 標籤的文章。 顯示所有文章
顯示具有 django 標籤的文章。 顯示所有文章

2011年11月23日 星期三

超輕量級的 python web framework: Bottle

寫完上篇冗長的 Django 心得後, 改來介紹極端相反的 web framework: Bottle。

有一則關於 Python 的笑話是這麼說的:

Python is the only language with more Web frameworks than keywords.

在初評估 web framework 時, 對照一大串 web framework 看到這則笑話, 實在是令我哭笑不得。不過在有不同需求後, 到覺得這樣也是有好處啦。

當初要做 ego-post 的時候, 想找個很方便部署的框架, 比較方便推廣, 最終目的是讓一般人也能用 (後來發覺寫成 Chrome App 更適合)。我的主要功能用 javascript 和 html5 (用到 localStorage) 實作, web 後端只是用來存長期資料而已。一開始是試 Flask, 但裝 Flask 需要另外裝 Werkzeug 和 Jinja 2, 所以最後改用 Bottle。

Bottle 有多小呢? 全部就一個 bottle.py 而已, 所以懶得要求使用者另外裝 Bottle 的話, 將 bottle.py 加到目前專案就結束了。而且它不到 3000 行, 有興趣了解最基本的 web 應用需要那些功能, 看 bottle.py 應該可以學到不少東西。像 http response code 418 I'm a teapot 這種蠢東西, 就是從 bottle.py 裡學到的。

2011年10月4日 星期二

Django 的 runserver 如何實作 autoreload

做完上篇的勘查後, 好奇之下順便看一下 runserver 怎麼實作 autoreload。

看 core/management/commands/runserver.py 會看到 autoreload.main(inner_run)。然後看 utils/autoreload.py, 稍微看原始碼, 配合 pdb觀察行為, 還滿簡單的。

inner_run 是真正處理 web request 的函式, autoreload 的行為如下:

  1. 在執行 inner_run 前, 先 fork 出 child process
  2. 讓 child process 開另一個 thread
  3. parent process 等待 fork 出來的 child process 結束
  4. child process 的一個 thread 每秒掃一次所有載入的 module, 看看原始碼是否有更新。有的話, 結束這個 process, 傳回 exit code 3
  5. child process 的另一個 thread 執行 inner_run
  6. parent process 發覺 child process 的 exit code 為 3 時, 重 fork 一次, 然後回到步驟 2

要注意的是, 要用 parent process 重 fork, 而不是 child process 自己再 fork 新 process。這樣才能確保由原本乾淨的狀態重讀全部 modules (parent process fork 完就沒做事了, 確保狀態沒有一絲改變)。

偵測程式更動的程式是 code_changed(), 關鍵部份是如何找出所有相關程式:

filter(lambda v: v, map(lambda m: getattr(m, "__file__", None), sys.modules.values()))

然後將 .pyc, .pyo 改為 .py, 接著記錄所有 py 檔的上次更新時間, 留待下次呼叫此函式時比對。從這段程式可看出, 包含 site-package 或 virtualenv 裡的程式也在偵測範圍內, 還挺方便的。

小心 Django 的 ADMIN_MEDIA_PREFIX

踏到這個雷兩次了, 記錄一下。

在使用 runserver 開發程式時, runserver 會特別處理 ADMIN_MEDIA_PREFIX, 先處理 prefix 為 ADMIN_MEDIA_PREFIX 的路徑, 再將其它情況交回給原本 urls.py 裡定義的 handler。

這導致當自己的靜態檔案放在 media/ 下, 又忘了改 ADMIN_MEDIA_PREFIX 時 ( settings.py 預設為 media/), 會讀不到自己 media/ 下的檔案, 而出現 "Page not found: ..." 的錯誤訊息。

從以下兩個現象, 推測中間有人先處理掉 request:

  • browser 有送出 request 但得到 404
  • runserver 的 console 沒輸出訊息表示有收到 request 並回覆 404

所以推測有人從中做梗!!

解法很簡單, 就是將 ADMIN_MEDIA_PREFIX 隨便設一個其它名稱, 像是 "admin_media"。之前有人發 issue 請 Django 改掉 django-admin.py 的值, 不過下場是 wontfix。

相關的程式從 core/management/commands/runserver.py 找到 core/servers/basehttp.py, 然後看 AdminMediaHandler.call 的部份。

這段先看是否符合 ADMIN_MEDIA_PREFIX , 不是的話就由原本使用者寫的方式處理:

        # Ignore requests that aren't under ADMIN_MEDIA_PREFIX. Also ignore
        # all requests if ADMIN_MEDIA_PREFIX isn't a relative URL.
        if self.media_url.startswith('http://') or self.media_url.startswith('https://') \
            or not environ['PATH_INFO'].startswith(self.media_url):
            return self.application(environ, start_response)

接下來是針對 ADMIN_MEDIA_PREFIX 的特殊處理, 也只是看檔案、讀檔案而已:

        # Find the admin file and serve it up, if it exists and is readable.
        try:
            file_path = self.file_path(environ['PATH_INFO'])
        except ValueError: # Resulting file path was not valid.
            status = '404 NOT FOUND'
            headers = {'Content-type': 'text/plain'}
            output = ['Page not found: %s' % environ['PATH_INFO']]
            start_response(status, headers.items())
            return output
        if not os.path.exists(file_path):
            status = '404 NOT FOUND'
            headers = {'Content-type': 'text/plain'}
            output = ['Page not found: %s' % environ['PATH_INFO']]
        else:
            try:
                fp = open(file_path, 'rb')
            except IOError:
                status = '401 UNAUTHORIZED'
                headers = {'Content-type': 'text/plain'}
                output = ['Permission denied: %s' % environ['PATH_INFO']]
        ...

2011年5月27日 星期五

在命令列執行有載入 django 相關模組的程式

剛用 django 時會有個困擾, 不透過 manage.py 無法執行有 import django modules 的 scripts。後來有找到 django-standalone, 可以方便在其它 scripts 內使用 django modules。不過我並不需要執行 manage.py 的其它 command, 而是單純想在命令列執行 scripts。以這需求來說, django-standalone 有點冗。

今天仔細地追了一下前因後果, 發覺解法很簡單, 必須做兩件事:
  • 設環境變數 DJANGO_SETTINGS_MODULE=APP.settings
  • 要在載入 module 的路徑中加上 django project 的根目錄, 即放 APP 的位置
所以若 myscript.py 有使用 django moduels (比方說 django.db.connection), 執行的方式就是:
$ PYTHONPATH=.. DJANGO_SETTINGS_MODULE=APP.settings myscript.py
myscript.py 位在 project root。明白運作方式後, 做起來很簡單。

不設 DJANGO_SETTINGS_MODULE 的話會出現下面的 exception:
Traceback (most recent call last):
  File "/.../site-packages/django/db/__init__.py", line 14, in 
    if not settings.DATABASES:
  File "/.../site-packages/django/utils/functional.py", line 276, in __getattr__
    self._setup()
  File "/.../site-packages/django/conf/__init__.py", line 38, in _setup
    raise ImportError("Settings cannot be imported, because environment variable %s is undefined." % ENVIRONMENT_VARIABLE)
ImportError: Settings cannot be imported, because environment variable DJANGO_SETTINGS_MODULE is undefined.
順著 traceback 看相關程式就會明白, django 會載入 DJANGO_SETTINGS_MODULE 作為整個 django 的設定檔。所以一定要設這變數, 而且載入 module 的路徑裡包念它的父目錄, 才能正確載入它。

若不想在命令列加那兩個變數, 一個簡單的替代方案是參考 manage.py, 在程式裡先 import settings, 再執行 django.core.management.execute_manager(settings)。execute_manager 會從 settings 中取得相關路徑和 module 名稱, 用來設 DJANGO_SETTINGS_MODULE 和在 sys.path 中附加 settings 的父目錄。

不明白為啥要用這種繞彎的方式設定環境, 也許和 django 的載入和執行模組的設計哲學有關。附帶一提, django 裡有太多 lazy initialization, 增加讀程式碼的難度, 令人有點困擾。

2011年2月18日 星期五

設定 apache2 和 django 壓縮輸出內容

用 Google Page Speed 分析後, 發現 javascript 和 css 可以壓個七八成, 實在滿可觀的, 就試著來設壓縮。官網 mod_deflate 看看就會設了, 下面的範例是壓縮 css 和 javascript:
AddOutputFilterByType DEFLATE text/css text/javascript application/javascript application/x-javascript
可以查 MIME 列表, 看要壓那些類型的檔案。或是先設全部都有, 再加條件不壓圖。

比較擾人的是, 早期的一些瀏覽器不能正確處理壓縮文件, 官網教學說要這麼設:
BrowserMatch ^Mozilla/4 gzip-only-text/html
BrowserMatch ^Mozilla/4\.0[678] no-gzip
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
說明如下:
The third BrowserMatch directive fixes the guessed identity of the user agent, because the Microsoft Internet Explorer identifies itself also as "Mozilla/4" but is actually able to handle requested compression. Therefore we match against the additional string "MSIE" (\b means "word boundary") in the User-Agent Header and turn off the restrictions defined before.

剛看始看看不懂, 後來看到 Internet Explorer User Agent Strings 才知道 IE 會自稱 Mozilla/4, 但它可以處理壓縮, 所以要再加第三行取消前兩行的限制。How To: Optimize Your Apache Site with Mod Deflate
文末提到 IE6 聽說有問題, 但他測不出來, 所以他沒有另外關掉 IE6。

Django 的話, 可以讓 apache2 協助處理, 加個 text/html, 或是加入 GZipMiddleWare, 都很簡單。

測試資料是否有壓縮有很多方法, 最保險的是用 Firefox Live HTTP Headers 看回傳結果有沒有 gzip。不會看 header 的話, 可以透過 Page Speed 或 GIDZipTest 測看看。

2011年1月29日 星期六

Django middleware 基本應用: 過濾黑名單

參考了幾份程式, 寫法大同小異, 這裡有簡單的 code snippet, 另外 django-ban 大小適中, 不會太複雜而不想看, 又附 sample project 說明怎麼設定, 若沒用過 middleware 的話, 可以參考看看。

概念就是自己寫個 middleware, 加入 settings 裡。middleware 有實作 process_request, 在函式裡檢查 REMOTE_ADDR (我看了好幾份程式都先優先用 HTTP_X_FORWARDED_FOR, 但這樣並不安全), 若在黑名單內, 就傳回 http.HttpResponseForbidden()。感覺這種寫法滿乾淨的。

剛開始一直在找類似 pre-processor 之類的語法, 因為學生時期玩 Rails 1.8 時, 好像有這樣的東西, 就「直覺」地找類似的東西。後來才想到, 這種要擋在全部連線之前的程式, 似乎正好適合寫在 middleware 裡。找對方向後, 就找到一堆東西了。

備註: 找的過程中看到有人用 ipcalc, 計算 ip subnet 的好東西, 之後需要再裝來用。

在 Django 發生 exception 時記錄相關訊息

從官網文件可知, views 裡發生任何 exception 時, Django 會執行 handler500, 所以在 urls.py 裡設自己的 handler500 就可以攔截所有 500 server error, 接著就是如何取得 exception 內容。後來看到 ticket #4007 才知道可以從 sys.exc_info 裡取得相關資訊。後來想想, 這表示 logging.exception() 應該已包好對應的操作, 所以做法相當簡單:
  • 改 urls.py 加入自己的 handler500
  • 在 handler500 裡先呼叫 logging.exception(MESSAGE)
  • 再讀入自己的 500.html, 回傳結果
附帶一提, 404 的行為也值得一看, 可以參考內建的 404 handler 自己寫一份, 方便記錄查不到的頁面。官網建議改 template 就好, 不要自己寫 handler404, 不過我覺得複制程式碼, 指定到自己的 handler, 日後方便加東西。

其餘 views 在找不到頁面時, 直接 raise http.Http404(), 就會進入 handler404。當 settings.DEBUG = False 時, url routes 找不到對應的 url 時也會呼叫 handler404。

    2010年11月27日 星期六

    Web benchmark tool

    提到 web benchmark 大家第一個提到 ab, 超簡單上手, 但只能連同一頁。到 stackoverflow 查了一下, 注意到 JMeter,  Siege 和 Pylot。最多人推 JMeter, 不過看起來有點複雜, 所以先選 Siege 來試。

    Siege 很簡單,  試一下馬上就能上手:
    1. 讀 Siege 官網幾個 link
    2. 用 siege.config 產生 ~/.siegerc
    3. 讀設定檔裡的詳細註解
    4. 寫好 urls.txt 檔
    Siege 可以設定要依序讀 urls.txt 的網址, 還是隨機挑。這樣的簡單設定已可滿足一些常用模式。若想模擬熱門網址較多人連, 大可重覆多放幾次。若需要測使用者的功能, 要在 ~/.siegerc 裡填 login-url, 設定檔裡有範例。Siege 會在進行測試前先連一次 login-url。2.69 版後支援用不同帳號登入。我一開始用 Ubuntu 8.04 包的 Siege 2.66, 結果 login-url 無效, 改用最新的 2.70 就 OK 了。

    注意 Django 1.2 開始有 CSRF middleware, Siege 無法用 POST 的方式登入。我另外寫了一個用 GET 登入的網址, 自己用 auth.login 登入。反正別放到 production server 就好了。

    即使這種測試無法反應真實情況, 有測的話可以抓到一些明顯的錯誤, 像是開太多 WSGI processes, 卻沒提高 MySQL max-connections, 結果負載高時會 MySQL 會發生 "Too many connections" 的錯誤。初步使用上覺得挺不錯的, 接著要規劃一些情境來測試複雜的情況。

    2010年11月21日 星期日

    South migrate --list 和 db 內資料不符的原因

    常遇到這個問題, 最近找到兩個原因:
    • 有設 PYTHONPATH, 執行到別個 repository 的 mange.py, 用到別組 settings, 拿到另一個 db 的 migration history。在 A 目錄設完 PYTHONPATH 做些事, 再跑去別的目錄做事, 容易發生到這個問題。
    • code 沒有更新到最新版。 South 會以目前 app/migrations 目錄下的 py 檔為準, 若在 A 目錄新增 migration M, 跑完 migration; 接著到 B 目錄執行 "migrate --list", 即使 A、B 的 settings.py 一樣, 結果不會看到剛才新加的 migration M。讀 South 的原始碼後才明白這件事。
    第一個問題無解, 像 PYTHONPATH 這樣的環境變數有其便利之處, 但出錯時卻都很難查覺, 踏到好幾次不同的雷。第二個問題可以提供細心的警告訊息, 說明 migration history 內的資料和目前目錄下的 script 不一致, 算小問題啦。

    2010年10月19日 星期二

    小心瓶頸就在資安軟體

    這是一個很囧的心得。

    今天下午發覺有個簡單的操作竟然有時候要花一秒鐘, 做了許多交叉比對, 發覺以下驚人的跡象:
    • 同一份程式、同一份資料庫, 連用 apache 跑的服務時, 有時要花一秒; 但連 django development server 時卻是瞬殺。
    • log 顯示, 不管連那種 web server, 我的程式都只花不到 0.05s。
    • 用 Chrome Developer Tool 的 Resource 觀看, 慢的時候, 大部份時間是 waiting。
    • 用 VNC 連到 server, 再從本機連 apache, 也是瞬殺。
    • 從我的電腦或同事的電腦連 apache 都是有時會延遲, 我的情況比較頻繁。
    • 同樣的連結點第二次時會變快, 但過一陣子再點似乎又會變慢。
    • 我主要是測 AJAX + GET。
    綜合以上線索, 我下了一個很腦殘的推論: apache 有鬼!! 不知是 apache 讀檔案還是做些奇怪的事, 或是 mod_wsgi 做奇怪的事, 造成延遲。也許 apache 有奇妙的 cache, 造成同一連結點第二次時會變快。
    和來和同事 R 討論後, 他問了一個問題: 「該不會是是資安軟體阻擋吧?」 (*1) 於是同事 P 在 server 隨便開個奇怪的 port, 將該 port 的封包轉到本機的 port 80。我再試的結果, 速度就變正常了!!
    事後回顧發覺自己的推論太腦殘了, apache + 80 和 django development server + some port, 我卻忽略了 port 的事, 只看到 web server 的差異。差點要去找 apache 或 mod_wsgi profiling 的方法, 幸好有先和同事閒聊, 省了一堆工, 也放下心裡一塊大石頭。

    註 *1: 資安軟體會攔截對外連的 URL, 阻止連某些網站。沒想到連對內也有攔截.....。

    2010年10月18日 星期一

    profile Django 的方法 + 關掉 I18N 減少 template rendering 的時間

    今天發現 template rendering 吃掉不少時間, views 和 db 等操作還可以自己寫個簡單的 profile function 記錄, template 就不方便啦。查了一下, 看到 "Speeding up built in Django Templates" 提到, 可以參考 "Profiling Django" 的說明 profile 全部東西, 該篇有提到用 django-command-extensions 的 runprofileserver 來 profile Django development server, 試了一下, 真不錯用, 之後再來試試 WSGI 的部份。

    profile 的結果, 和作者差不多, 卡在 I18N 做了一堆工, 我不確定 stringformat 是否有效, 至少我胡亂在很多地方加上去沒啥效果, 後來直接在 settings.py 裡將 I18N 設為 False, template rendering 的時間就從 0.4s 降到 0.25s。不過 django/utils/importlib.py:18(import_module)、django/utils/translation/__init__.py:23(delayed_loader)、django/utils/formats.py:10(get_format_modules) 還是占了一堆時間。程式碼看起來像只會執行一次的東西, Django runserver 大概是每次都重讀全部 modules 才會吃這麼多時間吧, 之後再來看是否用 WSGI 時也會如此。

    2010年10月8日 星期五

    django template tag 傳參數 + 使用 template 的方法

    之前眼殘一直沒看到, 還埋怨 Django template 不人性。上 news groups 查了一下相關問題, 發覺都沒有人有這種困擾, 又回頭看文件, 才發現自己眼殘。

    template tag 有兩種模式, 一種是先 parse, 產生 node, 再 render 回傳字串。雖然語法較彈性, 用起來有點複雜, 而且在 python code 裡產生 html 字串, 而不是在 template 裡嵌入變數, 較不容易維護。 另一種用法是 inclusions tag, 就和用 template 的 include 差不多, 不過變數的來源有兩種。

    一種是用傳參數的, 這正是我需要的, 抽出重覆的 template codes, 傳變數用在不同地方。另一種是不接收參數, 直接用 context 當全域變數取出 template 內的東西。但這樣就無法在一個 template 裡使用多次。

    2010年9月25日 星期六

    django 自行新增使用者的作法以及為何密碼檔裡要加 salt

    寫測試程式時需要先建個測試帳號, 為了確保任何人取得原始碼後能直接跑成功測試, 我在測試碼執行時檢查是否已建立測試帳號, 若沒有則自己建一個。

    單純地新增 User 放入 username 和 password 是行不通的, 順著命令 createsuperuser 看原始碼, 發覺正確的寫法如下:
    try:
        user = models.User.objects.get(username='test')
    except models.User.DoesNotExist, e:
        print 'Test account doest not exist. Now create it.'
        user = models.User(username='test', is_active=True)
        user.set_password('testtest')
        user.save()
    

    好奇之下看了一下 set_password 怎麼寫的 (./contrib/auth/models.py):
    def set_password(self, raw_password):
        import random
        algo = 'sha1'
        salt = get_hexdigest(algo, str(random.random()), str(random.random()))[:5]
        hsh = get_hexdigest(algo, salt, raw_password)
        self.password = '%s$%s$%s' % (algo, salt, hsh)
    

    可以看到 Django Auth 用 sha1 和 salt 產生加密後的字串。於是查了一下 salt 的功效, 這回才真的明白它的用處。

    Wikipedia 上舉了幾個情境:
    • 在沒用 salt, 且用單字當密碼的情況, 一但密文被取得後, 攻擊者只要查事先建好的表即可馬上找出原始密碼。表的 key 是 sha1 加密的結果, value 是原碼。
    • 同上, 改用 Rainbow table 建一般性的表, 而不是單純用字典檔。題外話, Rainbow table 的概念滿有趣的, 用到機率和時間空間取捨的技巧。
    所以, 若加密時有用 salt, 攻擊者得另外取得 salt 才能開始攻擊。即使 salt 也存在密文裡, 攻擊者也得照「規矩」來試密碼, 不能用事先建好的表。
    若整份密碼檔用同一份 salt, 攻擊者可以在取得 salt 後開始建表, 之後整份密文就可以用同一份表查出結果。但是當每個使用者的 salt 都不同時, 攻擊者只好每個密文都全試一次, 無法建表加速破解。換句話說, 破解單一特定使用者密碼的時間一樣, 但用多份 salt 讓破解所有人密碼的時間大增。
    當然, 若使用者的密碼較長, 或是有摻雜攻擊者建表時沒用到的字元 (如攻擊者只用字母和數字, 使用者的密碼有含符號), 那加不加 salt 也沒差。不過任何當過系統管理員的人都知道, 絕大多數使用者的密碼都是很簡單的..., 簡單地加個 salt 可以降低不少風險, 讓攻擊者要花更多時間取得明文, 發覺密碼檔外洩時, 可以減少損失。

    2010年6月5日 星期六

    Django 和 Python 操作 database 時的額外負擔

    為了搞清楚 Django ORM 的效率瓶頸, 做了個簡單的測試, 每筆實驗只有跑個兩三次, 不過結果差不多。

    SQL x 1, new object x N

    從本機的資料庫取出大量資料 (>1m筆), 然後將結果寫入 /dev/null, 資料型別為 utf-8。結果如下:

    methodtime (min / sec)memory (G)
    MySQL client0'080.54
    python client1'021.4
    django client without using iterator17'395.x
    django client using iterator3'161.7
    django client using raw sql1'021.5

    實驗細節說明:
    • Django 1.2, MySQL 5.0。
    • 用 time 測時間。
    • memory 是我用眼睛注意 htop 的數據。django client without using iterator 跑太久了, 只好邊玩猴子守城四代邊跑實驗, 最大值就沒抓準了。
      (2012-02-02 更新) 只需定時執行
      grep VmPeak /proc/PID/status | awk '{print $2}'
      就不用「辛苦」地邊玩遊戲邊抓數據了。年輕時不懂事, 了解一些系統知識差異真大。
    • MySQL client 的用法: mysql < my_select.sql > /dev/null。沒做 encoding 轉換。
    • python client 的操作內容只有從 utf-8 轉成 unicode (MySQLdb 做的) 再轉回 utf-8, 補個換行字元, 就寫入檔案。
    • django 會自動把 utf-8 轉成 unicode。django client 的操作和 python client 幾乎一樣。
    從上面的結果可以看出, 資料大時還是用 raw SQL 較好, 時間差了兩倍。看來生成 Django 物件比生成 Python 物件貴了一些。

    至於 python 花了 mysql client  8 倍的時間, 只好當作用 python 的必要成本。不知其它語言 (C++、Java、Ruby、PHP) 這方面的額外負擔有多大。

    為了弄清楚時間花在那裡, 另外試了 java client 和不轉 utf-8 的情況:

    methodtime (min / sec)memory (G)
    python client1'021.4
    python client without encoding and decoding0'43
    java client (using mysql connector)0'511.1

    看來轉換 utf-8 花了不少時間, 到是用 Java 也沒省下多少時間。有可能 python client 已幾乎都是 native code 了。

    SQL x N, new object x N

    我用 primary key (id = 1 ~ 100,000) 分別做 100,000 次操作取出 100,000 筆資料, 結果如下:

    methodtime (min / sec)
    django client1'26
    django client using raw sql0'20
    實驗細節說明:
    • django client 用 get(id=ID)。
    • raw sql 用 where id = ID。
    用 ORM 花了四倍的時間。根據 time 的結果, 我猜 real - user - sys 大概就是 IO time, 而 django client 確實多花了大量的 user time (1'13 vs. 0'09)。上面的實驗 ORM 花了三倍時間, 這裡多出一倍的時間有可能是 ORM evaluation 或生成 Django 物件造成的。對照上個實驗 (evaluation x 1, new Django object x N), 看來問題比較可能出在生成 Django 物件。之前用 python 的經驗裡, 發覺產生大量資料的情況下, 生成 python 物件還挺貴的。 另外, 用一個 SQL 取回 100,000 筆資料的時間只要不到 1s, 相較於 ORM 的負擔, 下太多次 SQL 是更大的問題。若只是做 1,000 次操作, ORM 的額外負擔就不到 1s, 感覺還好。

    相關討論

    這篇提到取出「大量」資料時的解法,  可以拆成多個 SQL 分次用 primary key 取出。留言裡有提到不要用 slicing (即 MySQL 的 offset + limit), DBMS 會取出大量資料再丟掉 limit 量之外的資料, 效率很差。我以為 offset 會一次取到對的資料, 一開始跑實驗時用 offset 沒用 primary key, 結果瓶頸變 IO, 反而沒測出 ORM 的額外負擔。 上面提的作法可以解掉 memory 的問題。但若資料量更大, 要跑得更快時, 還是得用 raw SQL。
    不過這也不表示資料量大就要用 raw SQL, 得看應用場合。若瓶頸在其它地方, 用 raw SQL 只會省掉整體的 1/10 時間, 用 ORM 也不壞, 方便日後維護。

    2010年5月29日 星期六

    Django 內建的 template

    Django 內建的 template 定了一套完整的使用規範:
    • template 內不能 call function, a.b 會視情況解讀成  a.b, a[b], a.b()。所以不管 a 是 dictionary, list 還是其它物件, 「照理說」都能順利地拿到 b 的值。但若 b 是變數的話, 拿不到 a[b]。
    • filter 可以接受一個參數。比方說 my_list|join:', ' 相當於 ', '.join(my_list), 不過這裡的 join 是 Django 定的 template filter。透過 filter 能做到如同 Python function call 傳兩個參數。另外 filter 如預期般可以串起來, 像是 my_list|join:', '|safe。我常用的兩個例子是顯示 verbose_name 和用變數當 key 取出 dictionary 的值。
    • template tag 可以接受多個參數, 想用函式做的事, 它都能做。唯一不同之處, 在於 template tag 定了一套 parse -> new node -> node.render 的流程。不像平時的習慣: 傳參數到函式, 函式直接回傳字串。詳情可以參考官網的說明。
    若有照 template 的規範寫, 應該能將 model、view (Django 的 controller)、template 切得很乾淨, template 的 helper function 一致放在 templatetags 目錄, 裡面有自制的 filter 和 template tag。但是 template tag 寫起來實在太冗了, 幾行可以搞定的事, 得拆成三個步驟做。另外我懷疑這樣是否會有效能的隱憂。

    若沒照 template 的規範寫, 程式碼會變得比沒規範的情況更難懂。傳 class A 的物件 a 給 template 時, 就塞些 template 要用的東西到 a.x, a.y 裡, 而 x, y 不是 a 原始的 member field。或是在 A 裡寫和 A 不是很相關, 不接參數的函式, 目的只是要讓 template 可以取用值。

    難怪 Django FAQ 裡有這麼一項: "I can't stand your template language. Do I have to use it?"。看到有人推 Jinja2 和 Mako。初步掃了一下語法, Jinja2 看起來滿漂亮的, Mako 看起來有點亂, 很像 Python 但又不是 Python。Jinja2 的 filter 可接受多個參數, 又有 macro 的語法, 應該可以輕鬆滿足常用需求。

    不過稍微研究一下換 template 的事後, 發現有些 plugin 和內建 template 綁在一起, 如內建的 Django auth 和 django pagination。換用別的 template 就不能直接使用, 得自己用新的 template 改寫。想想還挺麻煩的。各家 template 也會有點小問題, 像 Jinja2 用到 ctypes 加速, Google App Engine 上沒有 ctypes, 造成 Jinja2 無法提供正確的錯誤訊息。還是先繼續用內建的 template 吧。

    2010年5月10日 星期一

    在 Django 內建 template 中存取 dict[key]

    參考這篇的作法, 終於解掉心頭大恨。詳細問題見該篇文章最後的例子, 當 key 是變數時, 無法在 template 中用 dict.key 取值。解法是用 filter:
    @register.filter(name='dict_get')
    def dict_get(value, key):
        return value.get(key, '')  # 注意 filter 不能丟出 Exception
    

    filter 的相關說明可參考之前的文章。

    實在是不知該說什麼。之前為了這問題, 我笨笨地用 AJAX 分多次取回我想要的值, 實在太蠢了。

    除非又遇到解不掉的功能, 或是 template 太慢, 暫時不會想換 template, 有太多東西要學啦。

    2010年5月6日 星期四

    制作簡單的 API: 在 Django 中回傳 JSON

    將 HttpResponse 的 mimetype 設為 json 即可, 程式如下:
    # views.py
    def myapi(request):
        data = {
           'name': 'Alice',
            ...
        }
        return HttpResponse(simplejson.dumps(data), mimetype='application/json')
    

    設好 urls.py 後 (/myapi/), 就能用 jQuery 的 $.ajax 輕鬆地 call /myapi/ 取得 JSON ( callback function 的參數會是 JavaScript object, 不用再呼叫 JSON.parse )。為了減少出錯的機會, 我將 ajax 的參數 aysnc 設為 false。

    回傳值是 JSON 有許多好處, 除方便用 AJAX 拿資料外, 也方便寫 unit test:
    class MyApiTest(django.test.TestCase):
        data = { ... }
        response = self.client.post('/myapi/', data)
        self.assertEqual(200, response.status_code)
        expected = { ... }
        self.assertEqual(expected, simplejson.loads(response.content))
    

    附帶一提, 有人寫了個 decorator 將一般的 view 轉成回傳 json, 目前沒這需求, 留著備忘。

    2010年5月5日 星期三

    django + mod_wsgi + virtualenv 注意事項

    官網文件寫得滿清楚的, 這裡摘錄重點:
    • 為了避免用到系統裝的 package, 要將 virtualenv 載入的 package 放在 sys.path 前面, 這有幾種解法。
    • 第一種做法是在 apache config 裡寫: WSGIPythonHome /usr/local/pythonenv/BASELINE。BASELINE 是自己建的空 virtualenv。如此一來  mod_wsgi 就不會載入系統裝的 package, 自然沒這問題。
    • 但若 apache 同時用了 mod_python 和 mod_wsgi 的話, WSGIPythonHome 會失效 ( 我之前裝的 ReviewBoard 就是用 mod_python, 不知後來如何)。這裡有一點相關說明。
    • 在沒設 WSGIPythonHome 的情況下, 執行 mod_wsgi 時, sys.path 已載入系統裝的 packages 了。在這種情況下有兩種解法。
    • 若用了 mod_wsgi 2.4 之後的版本, mod_wsgi 有修改 site.addsitedir(), 讓它把 package path 加到 sys.path 的前面, 所以不用多做任何事, 就沒有這個問題。
    • 但是 Ubuntu 8.04 用的是 mod_wsgi 2.0, 所以得採用另一個作法, 也就是官網在 Application Environments 章節最後那段程式, 呼叫 site.addsitedir() 後自己重排 sys.path 內的順序。
    • 另外一點, 若只有一組 virtualenv, Application Environments 和 Process Environments 兩者做一個就行了。前者是在 WSGI  script (也就是 python script) 裡加 site-packages 到 sys.path 裡;後者是用 apache directive 指定 python path。
    • Process Environments 一次設好全部 WSGI application 的 virtualenv。但若有多個 WSGI application 需要不同的 virtualenv, 就得用 Application Environments 的方式各別設定。比方說 project A 用 Django-1.1, project B 用 Django-1.2, 那就得建兩個 virtualenv, 用 Application Environments 的方式各別設。
    我最後是用 WSGIPythonHome + Application Environments + Daemon mode 的作法搞定。
    附帶一提, 官網說一般情況 embedded mode 效能可能較好 (但官網也說應該差不了多少)。而我選 daemon mode 是因為這樣改程式後要重載入的話, touch WSGI script 即可, 不用 apache reload。

    2010年5月1日 星期六

    Django URL 相對路徑的設法

    看似很單純的事, 結果做起來有夠麻煩, 不知大家是怎麼做的? 網路上的範例幾乎都是將 project 放到根路徑, 即 http://.../。但若想在一台機器上放多個 project 的話, 就得額外處理。可能的做法有:
    • 註冊多個 domain name, 配合 apache 的 virtualhost 將不同 domain name 對到不同的 project。如此一來全部 project 都能用根路徑。
    • 將 project 放到子路徑: http://.../foo/, http://.../bar/。
    我不想申請 domain name, 所以就選第二個方案。需要更改的東西有:
    • settings.py
    • urls.py
    • myapp/views.py
    • template/*.html
    • apache conf
    做法如下。

    settings.py

    # 不同 project 設不同的 site_root, 像是 '/foo/' 或 '/bar/'
    SITE_ROOT = '/'
    MEDIA_URL = SITE_ROOT + 'media/'
    ADMIN_MEDIA_PREFIX = SITE_ROOT + 'admin_media/'
    # auth 要用到的 URL 設定
    LOGIN_URL = SITE_ROOT + 'login/'
    LOGIN_REDIRECT_URL = SITE_ROOT
    
    在 views 裡記得用 RequestContext, 這樣 template 裡才能直接取用 {{ MEDIA_URL }}。

    urls.py

    # 路徑都改用 url 設名字, 之後才能在 template 中用 url tag 指回 view。
    url(r'^$', views.index, name='index'), 
    url(r'^login/$', 'django.contrib.auth.views.login', name='login'),
    url(r'^logout/$', 'django.contrib.auth.views.logout_then_login', name='logout'),
    ...
    # 順便提一下, 跑在 production server 上時會直接用 apache 傳靜態檔案, 
    # 但開發時是用 Django 的 web server, 所以要要自己處理靜態檔案
    if settings.DEBUG:
        # Define url routes for static files in development mode.
        urlpatterns += patterns('django.views.static',
                                (r'^media/(?P<path>.*)$',
                                 'serve', {
                                     'document_root': settings.MEDIA_ROOT,
                                     'show_indexes': True}),)
    
    

    myapp/views.py

    一般情況直接 call views 中的 function 名稱, 但用 HttpResponseRedirect 時就得用 SITE_ROOT, 比方像這樣:
    @login_required()
    def some_page(request, project_id):
        if not request.user.is_staff:
            return HttpResponseRedirect(settings.SITE_ROOT)
        ...
    

    template/*.html

    用 url tag, 比方說:
    <a href="{% url index %}">Homepage</a>
    

    apache conf

    我設在 /etc/apache2/conf.d/MY_DJANGO_CONF.conf。大致設法和 Django 用 mod_wsgi 差不多, 只是改一下路徑:
    # Alias other operations to wsgi script.
    WSGIScriptAlias /foo FILE_PATH_TO_WSGI_SCRIPT
    WSGIScriptAlias /bar FILE_PATH_TO_WSGI_SCRIPT
    
    <Location "/foo/media">
        SetHandler None
    </Location>
    
    <Location "/bar/media">
        SetHandler None
    </Location>
    
    Alias /foo/media FILE_PATH_TO_MEDIA
    Alias /bar/media FILE_PATH_TO_MEDIA
    
    # 若用 virtualenv 的話就指到 virtualenv 內的 site-packages
    Alias /foo/admin_media /usr/lib/python2.5/site-packages/Django-1.1.1-py2.5.egg/django/contrib/admin/media
    Alias /bar/admin_media /usr/lib/python2.5/site-packages/Django-1.1.1-py2.5.egg/django/contrib/admin/media
    

    2010-05-06 更新

    今天發現要在 JavaScript 內用 AJAX 連線, 於是得在 JavaScript 裡取得 settings.SITE_ROOT, 作法如下:
    • 在 settings.py 裡註冊一個 context processor:
      TEMPLATE_CONTEXT_PROCESSORS = (
          'django.core.context_processors.auth', # 頭四個是預設的, 覆寫 TEMPLATE_CONTEXT_PROCESSORS 時記得加進去
          'django.core.context_processors.debug',
          'django.core.context_processors.i18n',
          'django.core.context_processors.media',
          'mysite.myapp.views.add_constants',  # 自己用的
      )
    • 在 views.py 裡寫 add_constants:
      def add_constants(request):                                            return {
              'SITE_ROOT': settings.SITE_ROOT,
          }
      
      這兩步的目的是讓每個 view 都傳 SITE_ROOT 到 RequestContext 裡。
    • 在 template 裡置入 SITE_ROOT 的值, 寫到 base.html 裡:
      <script language="javascript" type="text/javascript">
          my_app.site_root = '{{ SITE_ROOT }}'; // my_app 是 global object, 作為我的 js code 的 root object
      </script>
      
      如此一來, 就能用 my_app.site_root 取得值。
    真不是普通的麻煩啊。

    寫個 filter 取出 model 欄位中的 verbose_name

    查了一下 Django 好像沒內建這個功能, 要自己從 model 的 _meta 取出 field 的 verbose_name。就自己寫了個 filter:
    # mysite/myapp/templatetags/myapp_extras.py
    from django import template
    from django.db.models.fields import FieldDoesNotExist
    
    from mysite.myapp import models
    
    register = template.Library()
    
    @register.filter(name='verbose_name')
    def verbose_name(value):
        model_name, field_name = value.split('.')
        model = getattr(models, model_name, None)
        if not model:
            return ''
        try:
            return model._meta.get_field(field_name).verbose_name
        except FieldDoesNotExist, e:
            return ''
    
    在 template 裡的用法:
    {% load myapp_extras %}
    ...
    {% mymodel.myfield|verbose_name %}
    ...
    
    filter 的寫法可以參見官網的《Custom template tags and filters》, 滿簡單的。

    附帶一提, 要從字串拿出 model 時, 我楞了一下, 才發覺過去都沒想過「Python reflection API」。第一印象想到的是這種東西:
    exec('model = %s' % model_name)
    
    後來才想到不就是用 getattr 嗎..., 簡單到忘了它就是 reflection API。

    在 Fedora 下裝 id-utils

    Fedora 似乎因為執行檔撞名,而沒有提供 id-utils 的套件 ,但這是使用 gj 的必要套件,只好自己編。從官網抓好 tarball ,解開來編譯 (./configure && make)就是了。 但編譯後會遇到錯誤: ./stdio.h:10...