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 啦!

10 一月

座標轉換

我一直以為, 只要是用到 GPS 座標, 都是使用類似這種「23.973875,120.982025」latitude,longitude 的表示方式, 頂多是時分秒轉換成十進位而已。天曉得, 我錯了! 今天向內政部不動產成交案件實際資訊資料供應系統買了資料之後, 才發現內附的座標是一個我完全沒有看過的格式, 至少它的橫坐標跟縱坐標不在一般台灣的經緯度的值上, 而是長得像這樣「248170.787,2652129.936」。

經過了一翻查證, 終於看懂這種格式叫做 TWD97, 而一般在 Google Map 輸入的那種則是叫座 WGS84。TWD97 可說是 UTM 的一種, 原本的 UTM 分為很多不同 zones, 但是台灣應該是因應需求, 而有些參數有些調整

查了 Wikipedia, 發現 UTM 的頁面有提到了簡易版的轉換公式, 雖然要把它看懂真的是對於數學能力已經退化很多的我來說非常困難 (論文1) (論文2), 但如果只是照著他的公式寫程式, 照理說應該是寫得出來的。

Google 了一下 TWD97 以及 UTM, 有一些詳細的介紹, 也有不少人寫了一些程式碼 (python UTM 套件) (Vexed’s TWD97 in PHP) (ola 的 python/java version), 但是似乎都是根據另外一篇文章 (Converting UTM to Latitude and Longitude (Or Vice Versa)) 所述的公式而成的。

於是乎, 我就在想是否能夠 implement Wikipedia 的那個版本 (in python)。所以, 東西就放上 github 啦! 兩個小 function, 也可以直接執行, 這個轉換似乎都是有點誤差的。

18 十月

ElasticSearch 的小雷

又一段時間沒 post 文章了。今天採到一個小雷, 但他也不算雷啦, 只能說我沒有看清楚說明文件。

ElasticSearch 可以使用 Lucene 的 Query Parser Syntax, 基本上就是所謂的 Query String Query。若是使用 pyelasticsearchelasticutils, 都有相對應的 function 可以直接下 Query String, 其實還滿方便的。因為我做了一個小後台, 讓我自己下 Query, 撈我想要看的 ElasticSearch 裡面的項目。

原本用得好好的, 但是我發現我想要查詢某個 Y 開頭, 或 S 開頭的東西, 不論我下 column_name:Y* 或者是改用 regular expression 下 column_name:/Y[0-9]*/ 或者是 column_name:/[a-zA-Z0-9]*/ 都沒有任何作用。但詭異的是, 如果只查詢數字, 就 work, 例如 column_name:/[0-9]*/ 這樣是 ok 的。然後又偶然的發現, 另外一個欄位的查詢也是 work 的, 例如 column_name2:x* 這樣也搜得到。這樣我不禁懷疑, 難道是欄位有問題嗎?

但詭異的事情是, 當我使用 wildcard  query 的時候, 下 Y* 又是 ok 的。因此, 我開始傾向於想, 該不會是 ElasticSearch 在 query string query 的 implementation 有 bug 吧 orz

正當我真的有點無言的時候, 重新把 documentation K 了一遍 (雖然她現在的 documentation 已經好很多, 但我覺得還是一樣沒有非常清楚), 赫然發現一件事情…

有個東西叫做 lowercase_expanded_terms, default 是 true。

Whether terms of wildcard, prefix, fuzzy, and range queries are to be automatically lower-cased or not (since they are not analyzed). Default it true.

所以我所有打大寫的東西, 都被改成小寫了…!!!

而在 elasticutils 的 query(field__query_string=’QUERY’) 無法設定, 只好使用 query_raw 了。

以上的踩地雷, 花了我大約兩個小時 orz

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

07 三月

python pipe 到 more 或檔案會出現 UnicodeDecodeError

好久沒有更新這個部落格了。今天剛好有東西可以 share 一下。

這個問題困繞我很多次, 不過我之前都沒有仔細看到底是怎麼回事。今天查了一下, UnicodeDecodeError when redirecting to file 這篇就寫得很清楚。基本上就是用 pipe 導出去的時候無法判別 encoding, 所以非 ASCII 的就會出錯。這篇文章提到用下列的方式就可以解決這個問題:

import codecs
import locale
import sys

# Wrap sys.stdout into a StreamWriter to allow writing unicode.
sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout)
uni = u"\u001A\u0BC3\u1451\U0001D10C"
print uni

這樣不管是直接執行

yychen$ python test.py

或是把他導出來

yychen$ python test.py > test.txt
yychen$ python test.py | more

都 ok 了。

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

18 九月

踩到 ubuntu libjpeg 的 bug

今天花了一些時間, 想要把 gunicorn 的 script 整理一下。於是找了幾個 solution, 最後決定搭配 upstart 以及一支自己寫的 start.sh 讓 gunicorn 跑起來。後來我發現有一個很奇怪的訊息, 每一次跑起來, 有 request 進來, 就會丟

Error opening file for reading: Permission denied

非常奇妙的事情在於, 不同權限執行 gunicorn 會有不同的結果。以上的錯誤發生在我用 root 執行 gunicorn --user=web 才會發生。我如果使用 web 執行 gunicorn, 或是用 root 執行 gunicorn (但是不加上 --user=web) 都不會有這樣的問題。

Trace code 了許久, 最後發現是在 import PIL 的這一行出錯

import Image from PIL

上網查了一下, 是 ubuntu libjpeg 的問題。Reference 第二篇寫得很明確來龍去脈。

 

Reference:

  1. Error opening file for reading: Permission denied
  2. seteuid apps report perror() on trying /proc/self/auxv