boto3 create_instances/run_instances

create_instances 是 boto3.resource(‘ec2’) 提供的,而 run_instances 是 boto3.client(‘ec2’) 提供的。

  • client(‘ec2’): A low-level client representing Amazon Elastic Compute Cloud (EC2)
  • resource(‘ec2’): A resource representing Amazon Elastic Compute Cloud (EC2)

主要不同點:Resources represent an object-oriented interface to Amazon Web Services (AWS). They provide a higher-level abstraction than the raw, low-level calls made by service clients. To use resources, you invoke the resource() method of a Session and pass in a service name。

也就是說實際上 ServiceResource 會去使用 client,簡單的說 ServiceResource 是一個 client wrapper (這可以從 boto3/session.py 看到)。

友情小提示:在閱讀程式碼時,用 function name 去找,會發現都找不到,實際上 boto3 library 會將 function call/parameters 轉換為 AWS API  呼叫送出去。

Ubuntu 14.04 + Python 3.7.2

我是使用 pyenv 來安裝,pyenv 的安裝:

# 一鍵安裝
curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash

在安裝 Python 3.7.2 時,出現 ERROR: The Python ssl extension was not compiled. Missing the OpenSSL lib? 這樣的錯誤。

參考錯誤訊息,先依據 https://github.com/pyenv/pyenv/wiki/Common-build-problems 上的說明去檢查並安裝,但安裝了所需的套件之後,仍然有錯誤。後來再去找,才在 pyenv issue #950 找到原因,原來是 Ubuntu 14.04 的 OpenSSL 版本過舊,Python 3.7 需要 OpenSSL 1.0.2 以後的版本才行。

看了一下 Ubuntu 16.04 的 openssl 套件,版本是 1.0.2,於是就拿了 16.04 openssl 的 source package 回來,在 14.04 上編譯出 deb 來安裝就可以了。

# On Ubuntu 16.04
apt-get source openssl
# On Ubuntu 14.04
cd openssl-1.0.2g
dpkg-buildpackage
ls ../*.deb
# Output:
# ../libssl1.0.0_1.0.2g-1ubuntu4.14_amd64.deb      ../libssl-dev_1.0.2g-1ubuntu4.14_amd64.deb  ../openssl_1.0.2g-1ubuntu4.14_amd64.deb
# ../libssl1.0.0-dbg_1.0.2g-1ubuntu4.14_amd64.deb  ../libssl-doc_1.0.2g-1ubuntu4.14_all.deb
# Install them
ls ../*.deb | xargs sudo dpkg -i

再次安裝 Python 3.7.2

pyenv install -v 3.7.2
# 將當前目錄的 python 設定為 3.7.2,也就是以後切換到這個目錄時,自動使用 python 3.7.2
pyenv local 3.7.2
python --version
# Output:
# Python 3.7.2
# Use local python to make virtualenv
mkvirtualenv myproject --python=$(pyenv which python) -r requirements.txt

Celery log 出現 Received and deleted unknown message. Wrong destination

在查 Periodic task 為什麼沒執行,beat 是有發出訊息,但 task 卻沒被執行。在 worker log 裡找到

Received and deleted unknown message. Wrong destination

的訊息,查了以後,找到這些資料:

所有的矛頭都指向 librabbitmq ,所以解法有兩種,一種是移除 librabbitmq,一種則是將 protocol 改為 1

 CELERY_TASK_PROTOCOL = 1

Django 節省記憶體的一些紀錄之二

這篇會順帶提一些提高效能的紀錄。

strftime

datetime.strftime 可以將日期時間格式化為需要的字串,但是,在經過 profiling 以後,我發現呼叫這個函式相當的花時間。在網路上搜尋以後,發現有人在 stackoverflow 上問相似的問題,有人回答說改用 python 的 string format 就可以大大的提高效能。

# 原作法
from datetime import datetime
dt = datetime.now()
dt.strftime("%Y/%m/%d")  # slower
"{:04d}/{:02d}/{:02d}".format(dt.year, dt.month, dt.day)  # Fast!!

JSONEncoder.iterencode

這是在Use StreamingHttpResponse by default for JSON 這個 gist 上看到的,裏面使用了 JSONEncoder.iterencode 搭配 StreamingHTTPResponse 處理。一般來說,在輸出為 JSON 時,都是整個物件或資料丟給 json.dumps(),但這樣在處理大量資料的情況時,其實是有可能佔用大量記憶體的。Python json 模組的 JSONEncoder 提供了 iterencode() 函式,iterncode() 會回傳 generator 回來,之前有提到使用 generator 可以確保在使用到的時候,才將值回傳出來,可以避免佔用過多的記憶體。再加上 StreamingHTTPResponse/HTTPResponse 的 content 都支援使用 generator,這樣就可以節省大量記憶體了。

日期時間時區的轉換

本來我是使用 Arrow 在處理時區的轉換,但是,在 profiling 以後,發現這個步驟會花掉蠻多時間,於是看過 Arrow 的原始碼以後,發現 Arrow 只是使用 python datetime 模組裡面的函式在做,所以將原本時區轉換的部份改寫掉,就大幅提升速度了。


from arrow import Arrow
from dateutil import tz
from django.utils import timezone

dt = timezone.now()  # utc time
new_timezone = tz.gettz('Asia/Taipei')  # get local timezone
new_local_time_1 = Arrow.fromdatetime(dt).to(new_timezone).datetime  # slower
new_local_time_2 = dt.astimezone(new_timezone)  # Fast!!

Django 節省記憶體的一些紀錄

前一陣子在改寫程式,避免使用過多記憶體時的一些紀錄。

pympler

pympler 很好用,主要是用來察看記憶體用量。缺點是得自己安插 code 觀看,之後再移掉。文件裡有不少用法,我只用到兩種。

第1種用法是 asizeof.asizeof(obj) ,這可以看物件使用的記憶體用量。


from pympler import asizeof
obj = {'foo': 'bar'}
print(asizeof.asizeof(obj))

第2種用法是 summary/muppy,這可以察看一段程序執行後的記憶體差異。


from pympler import summary, muppy
sum1 = summary.summarize(muppy.get_objects())
# do works
sum2 = summary.summarize(muppy.get_objects())
diff = summary.get_diff(sum1, sum2)
summary.print_(diff)
# 要輸出到 logging 的話,可以用 format_ 或 _format_table
# for line in summary.format_(diff):
#   logger.debug(line)

這段根據文件也可以使用 tracker.SummaryTracker() 來代替。

參考資料:

yield / generator

函式如果要傳回大量的資料,使用 yield ,這可以避免佔用大量記憶體,只有在 iterate 時,才一次一次拿回來。


def foo():
  for i in range(100000000):
    yield i  # 這讓 foo 的傳回結果變成 generator 物件

for x in foo():
  print(x)

Django Queryset

可以的話,儘量使用 values() 跟 values_list() 並指定所需的欄位,除了可以提升速度之外,也減少記憶體用量。

values() 是將記錄作為一個 dict 傳回,而 values_list() 則是將紀錄作為一個 tuple 傳回。這樣可以省去還原為 Django model object 的程序,因此速度會更快,記憶體使用的更少。而使用 values_list() 比 values() 來的更省,這是因為 tuple 所佔用的記憶體比 dict 來的少。

Using jinja2 as django template engine

Django 1.8 以後已經可以選擇使用自家的 template engine 或是使用 jina2 template engine 了,而且是可以混用,不限制你只能使用其中一種。

第一步是先安裝 jinja2:

pip install jinja2

第二步是在 settings 裡的 TEMPLATES 裡增加 jinja2 的設定:

TEMPLATES = [
    # 省略 django template 的設定
    {
        'NAME': 'jinja2',
        'BACKEND': 'django.template.backends.jinja2.Jinja2',
        'DIRS': [
            # insert more TEMPLATE_DIRS here
            # join(BASE_DIR, 'templates'), 
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'environment': 'your_project.jinja2.environment',
        },
    },
]

 

第三步,是在 your_project 下建立 jinja2.py ,內容:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import    # 使用 absolute_import 很重要!!!
import logging
from django.utils import translation
from django.contrib.staticfiles.storage import staticfiles_storage
from django.urls import reverse
import jinja2


logger = logging.getLogger(__name__)


def environment(**options):
    env = jinja2.Environment(
        extensions=['jinja2.ext.i18n', 'jinja2.ext.with_'],
        **options)
    env.globals.update({
        'static': staticfiles_storage.url,
        'url': reverse,
    })
    env.install_gettext_translations(translation)    # 使用 django 的 i18n/l10n
    return env

在這裡,我遇到一個奇怪的狀況,python 2 一直告訴我 jinja2 沒有 Environment,後來看了這篇,才知道是 absolute/relative import 問題,使用 absolute_import 以確保 import 的 jinja2 是正確的,而不是 django 的 jinja2。我想,python 3 應該不會有這問題。

django template 是使用 load 載入模組,jinja2 則需要在 Environment 初始化時就定義。

第四步,是在繼承 TemplateView 的類別裡,加入

template_engine = 'jinja2'

,然後在 your_app 下建立 jinja2 路徑,將 template 檔案放在那裡即可。

class Jinja2Test(TemplateView):
    template_name = 'jinja2test.html'  # template file 的名稱是 jinja2test.html ,檔案必須放在 your_app/jinja2 下。
    template_engine = 'jinja2'  # 表示要使用 jinja2 template engine,這個 jinja2 是定義在 settings 裡的 NAME

    def get_context_data(self, **kwargs):
        ctx = super(Jinja2Test, self).get_context_data(**kwargs)
        ctx['foo'] = 'bar'
        ctx['hello'] = 'world'
        return ctx

參考資料:

如何客製 Django package 的 locale?

在 Django 裡使用了某個 package,但該 package 沒有自己所用的 locale 時,該怎麼做呢?

以下以 django-mptt 為例:

  1. 先確認 site-packages 下 django-mptt 的目錄名稱,在安裝 django-mptt 以後,實際上是 mptt,所以在專案目錄下建立 mptt/locale 。
  2. 到 site-packages/mptt 下,執行 django-admin makemessages –locale=zh_TW
  3. 將 site-packages/mptt/locale/zh_TW 搬移到專案目錄下的 mptt/locale
  4. 使用 poedit 或其他工具進行翻譯
  5. 調整 django settings 裡的 LOCALE_PATHS,例如:
    from os.path import dirname, join
    BASE_DIR = dirname(dirname(dirname(__file__)))
    # ...略...
    LOCALE_PATHS = [
        join(BASE_DIR, 'locale'),
    
        # customize translations which package is in site-packages
        join(BASE_DIR, 'mptt', 'locale'),
    ]
    

這樣就大功告成了。

檢查 module 裡是否有指定的 module

好饒舌。

只是因為程式目錄下有好幾個 module ,想檢查裏面是否有指定名稱的子 module,有的話就載入,沒有的話就跳過。

在網路上找了一下,說是用 imp 就可以解決了:

程式如下:

# -*- coding: utf-8 -*-
# 依序去 module1, module2, module3, module4 看有沒有 routing,有的話就載入並取出指定的變數。
import imp


channel_routing = []
apps = ['module1', 'module2', 'module3', 'module4']
for app in apps:
    try:
        app_info = imp.find_module(app)
        module = imp.load_module(app, *app_info)
        routing_info = imp.find_module('routing', module.__path__)
        routing = imp.load_module('routing', *routing_info)
        if hasattr(routing, 'channel_routing'):
            channel_routing.extend(getattr(routing, 'channel_routing'))
    except ImportError:
        pass

其實應該再參考 django autodiscover() 或是 django test runner 原始碼的,只是在那個時間點沒想到。

pdfkit/wkhtmltopdf

wkhtmltopdf 是一個可以將網址或是 HTML 檔案轉換為 PDF 的程式。

pdfkit 則是一個 python package (或 library),用來將網址或是 HTML 轉換為 PDF,底層用的是 wkhtmltopdf 。

django-pdfkit 則是給 Django 使用的 app,主要使用 pdfkit ,提供了 PDFView 。要提供產出 PDF 的網頁,只要寫一個繼承自 PDFView 的 View,撰寫 template ,就可以了。

以下是幾個試過以後的心得:

  1. 內部處理順序是先使用 Django template 輸出為 HTML,再用 pdfkit 輸出為 PDF。
  2. 預設用 PATH 去找 wkhtmltopdf,但如果有指定 WKHTMLTOPDF_BIN 這個環境變數的話,就用這裏面的。
  3. 在網址加入 html 參數,表示顯示 HTML,例如:http://server/show_pdf/?html
  4. 在網址加上 inline 參數 ,表示直接顯示 PDF,而非下載,例如:http://server/show_pdf/?inline
  5. 要自訂 pdfkit 使用 wkhtmltopdf 的參數,在繼承的 View 裡加入 pdfkit_options (型態為 dict) 或是自訂 get_pdfkit_options method。
  6. 要自訂輸出的檔名,可以在繼承的 View 裡加入 filename 或是自訂 get_filename method。
  7. wkhtmltopdf 預設不使用 print media-type ,所以在 css 裡加的 print media-type 相關樣式都沒用,除非自訂 pdfkit_options ,加入 {“print-media-type”: “”}。雖然加了 print-media-type 可以強制讓 wkhtmltopdf 參考 print media-type,但是一般可以在網路上查到,使用 print media-type 在每頁加頁次的方法是行不通的,一樣要自訂 pdfkit_options 才行 (參考 wkhtmltopdf -h 說明):{ ‘footer-center’: ‘[page] / [toPage]’}。其他還有調頁面邊界、紙張大小的,也都是要看 wkhtmltopdf 的說明來調整 pdfkit_options 。
  8. bootstrap 3 本身就有支援 print media-type ,直接使用就會有不錯的效果。如果要更漂亮,可以參考 Natshah/bootstrap-print: To manage print media for Twitter Bootstrap v3.
  9. 表格跨頁標題主要是使用 table/thead/tbody 跟調整 css,之前參考的網頁找不到了,這裡直接貼相關的 css 跟 html:
    /* CSS */
    @media print {
      table {display:table;}
      thead {display:table-header-group!important;page-break-after:avoid!important;}
      tbody {display:table-row-group!important;page-break-after: auto!important;}
      tr, img {display:table-row!important;page-break-inside:avoid!important;page-break-after: auto!important;}
      td, th {display:table-cell!important;}
    }
    
    <!-- HTML -->
    <table class="table table-bordered">
      <thead>
        <tr>
            <th class="text-center">編號</th>
            <th class="text-center">姓名</th>
        </tr>
      </thead>
      <tbody>
        <tr>
            <td>1</td>
            <td>王小明</td>
        </tr>
      </tbody>
    </table>
    

Python profiling decorator

前幾天想知道我 Django 程式裡某段函式的瓶頸,所以查了 Django 怎麼做 profiling 。是有查到有 jazzband/silk: Silky smooth profiling for Django 這個 package ,但是有點太大。如果是用常找到的

python -m cProfile xxx.py xxx.py

,Django 程式又不太適合。畢竟我只是想查某個函式而已。所以後來查到這兩個 decorator:

使用上很簡單,程式放進去,在想做 profiling 的函式前加上 decorator 就可以了。

我用的是第一個,在加上 decorator,執行過程式(應該說是瀏覽網頁)以後,在程式當前資料夾裡會找到 .profile 的檔案,為了後續方便說明,假設產生出來是 func.profile 。有這個檔案以後,就可以用

python -m pstats func.profile

開啟,開啟以後,是 pstats 的 shell ,一般要查哪個地方花的時間最多,會用

sort time

依照執行花費時間來排序,再用

stats 10

列出前十個花費時間最多的函式。

參考資料: