2015年1月29日 星期四

取得執行檔所在的目錄名稱

有時自己寫的程式, 會在執行檔所在的位置放其它資源檔或設定檔。如果偷懶直接用相對路徑讀檔, 可能因為先前有用 chdir 切換 process 所在的位置, 而讀不到檔案。保險見起, 可以改用執行檔所在的位置作為起始路徑來讀檔。

這裡有取得執行檔所在的目錄的程式碼, 重點是利用 /proc/self/exe 找出產生目前 process 的執行檔的路徑。平時需要寫 shell script 也可用同樣方法取得路徑

2015/02/06 更新

原本寫用 /proc/PID/exe, 經 Kito Cheng 提醒, 改為 /proc/self/exe 更方便。

2015年1月25日 星期日

The Python Standard Library By Example

看到不錯的書: The Python Standard Library By Example, Table of Contents 有不少有用的關鍵字,知道要做什麼事的時候,可以用什麼內建模組。

另外, 這裡有書上附的範例碼, 玩玩小程式比看文件快上手。

抓網站內容和使用 lxml.html 讀取 DOM 內容

lxml 功能強大, 不過提供太多 API, 不太容易在官網找資料 (或是我太沒耐性吧...)。記錄一下抓網站取內容常用的 code snippet:

下載網頁內容轉成 lxml.html 的 Element object

import sys

import requests
import lxml.html

DEBUG = False

def get_web_content(link):
    if not link:
        return None, lxml.html.fromstring(u'<html></html>')
    try:
        r = requests.get(link)
        try:
            content = r.content.decode('UTF-8')
        except UnicodeDecodeError, ude:
            if DEBUG:
                msg = (
                    'The content is not in UTF-8 (ude=%s). '
                    'Try ISO-8859-1 instead.\n' % ude                )
                sys.stderr.write(msg)
            # Try another encoding. If fail, just let it fail.
            content = r.content.decode('ISO-8859-1')

        if DEBUG:
            sys.stderr.write('Get content of link %s: %s\n' % (link, content[:50]))
        return r.status_code, lxml.html.fromstring(content)
    except Exception, e:
        if DEBUG:
            sys.stderr.write('Fail to get content of link: %s (e=%s)\n' % (link, e))
        return None, lxml.html.fromstring(u'<html></html>')

使用 cssselect 取值

取得 lxml.html Element 後, 用 cssselect() 和 CSS path 可輕鬆取值。剩下的就是在 ipython 上試 API, 看怎麼操作 Element 物件, 這樣比看文件快。

python multiprocessing 小記

因為 CPython 有 GIL 的緣故, 需要提升 CPU 效率時, 不會用 multi-thread, 會改用 multi-process。內建模組 multiprocessing 提供許多好東西, 實作 multi-process 簡單許多。習慣用 multiprocessing 後,有時不是 CPU bound 而是 I/O bound, 我還是用 multiprocessing。反正....不是 CPU bound 的話, 多耗 CPU 也無所謂。

之前寫 crawling 的程式,為了增加同時抓網頁的數量,就用 multiprocessing 加速 [*1]。以下是兩個例子:

從例子裡找片段程式碼來用比看文件快。這裡記錄一下相關心得:

  • 若要讓不同 process 共享資料,需要用 multiprocessing 訂的物件, 有含 list, dict 等,單一物件 (如 int, float, str) 則是用 Value 指定 typecode, 如: shared_integer = multiprocessing.Value('i', 0)
  • 使用 shared data 會有 race condition, 要用 Lock 保護,不然就直接用 Queue 或 Pipe
  • 還有 EventSemaphore 等物件,要作比較複雜的機制時,可以拿來用。
  • 有提供跨機器的 multi-processing, 有需要再來細看。
  • 程式架構照 producer-consumer 的方式寫比較容易, 也就是各個 process 共用 Queue, 然後 producer 用 blocking put, consumer 用 blocking get, 另有一個 main process 監控情況, 確保程式會結束 (這大概是最麻煩的地方)。
  • 注意量大的時候 put 會因 queue full 卡住,所以不能在 main process 用 blocking put。即使很容易填入 Queue 的初始資料,用另外的 process 填入初始資料比較保險, 因為在初始資料太多時,可能因 queue full 卡住不動了。不然就是先產生 consumer process 再初始化 Queue, 避免因為沒有 consumer 而在 queue full 時卡死。我偏好用另外的 process 初始化 Queue 的資料, 因為新增 process 成本不高 (只有一次), 程式邏輯比較單純。
  • 雖然有 non-blocking put/get, 但是使用 non-blocking 操作, 要多記操作是否失敗, 失敗要另排時間重試, 還是新增 process (or thread) 用 blocking 操作易寫易懂。
  • 各種意外都有可能發生,process 因 exception 掛掉的話,可能會讓 main process 搞錯情況而不會結束,process 執行的函式最上層要加個 try-catch 確保發生意外時能重置管理 process 的狀態,確保 main process 會結束。crawl_ratings.py 分成 _parse_url() 和 _do_parse_url() 就是要處理這個問題。
  • multiprocessing 有提供 log, 可開啟輸出到 stderr, 方便追蹤各 process 情況。用法是: multiprocessing.log_to_stderr(logging.INFO)

備註

*1 遇到 I/O bound 時, 除 multi-thread, multi-process 外,還有一個選擇是用 single thread event-based 的解法。Python 有 gevent 可用。

但是 gevent 的缺點是,一但用了 gevent, 全部 I/O 都要用 gevent, 不然 main thread 就被卡住了。於是 third-party 程式也要找使用 gevent 的版本。比方說 requests 很好用,但用了 gevent 後要用 gevent 版的 requests, 用其它公司提供的 SDK, 也要自己 patch 成 gevent 版本, 所以使用前要三思。不是要作到 C10K 等級的 daemon, 還是用 multiprocessing 省事。之前作一個上線的服務是用 gevent, 為此費了一些工夫作到全盤使用 gevent

相關資料

2015-09-01 更新

懶得研究這麼多可以考慮用 prunner.py

2015年1月23日 星期五

用 jQuery load 跨 HTML 檔使用重覆的 HTML 內容

寫網頁到一定規模後, 會抽出重覆的 JavaScript 到獨立的 JavaScript 檔, 再用 <script src="FILE.js"></script> 載入, 以供不同 HTML 使用; CSS 則是用 <link rel="stylesheet" type="text/css" href="FILE.css"> 載入外部檔案。那 HTML 怎麼辦?

若是會寫 PHP 的人, 會使用 include 載入重覆使用的 HTML; 用 Python/Ruby/Perl 寫 CGI 的話, 會搭配 web framework 內 template language 的語法, 所以也沒問題。但若是只會寫 HTML + CSS + JavaScript 的人該怎麼辦? 查了一下, 發覺用 jQuery load 可以輕易做到 [*1]。唯一的問題是, 在本機電腦實作, 用瀏覽器開啟本機檔案後會發現行不通。有如下的 JavaScript 錯誤訊息:

XMLHttpRequest cannot load file:///C:/.../FILE.html. Cross origin requests are only supported for HTTP.

這是因為瀏覽器基於安全考量, 禁止 JavaScript 讀取本機檔案

解套方式是在本機跑 web server, 透過 http (而不是 file://) 讀取檔案。

於是問題變成: 如何讓這類不擅長程式設計或系統管理的人, 能在自己的電腦測試 jQuery load? 畢竟會有這種需求的人, 大概也不擅長在 Windows 或 Mac 裝 apache2 或 lighttpd。從這裡看到有人推薦用 mongoose

作法如下:

  1. 下載 mongoose 執行檔
  2. 執行檔放在網頁目錄下
  3. 執行它就可以連到 port 8080 看到結果 (即連往 http://localhost:8080/ 瀏覽目錄下的網頁)。

也可參考官網的教學了解更多設定

要關掉 mongoose 的話, 可以從右下角的系統選單找到 mongoose 的圖示, 按右鍵再選 Exit。或從系統管理員直接結束它。

Btw, 另一個作法是安裝 Python, 然後跑 SimpleHTTPServer:

python -m SimpleHTTPServer 8080

不過得和非程式設計非系統管理背景的人解釋一下命令列就是了。

備註

*1: 事實上這個作法沒有 PHP include 或 CGI-based + template language 的作法好, 因為網頁內容不是一次載入, 而是透過 AJAX 補上, 會延遲內容出來的時間。不過作小東西或雛型的時候, 可忽略這一點負擔。

2015年1月18日 星期日

網頁顯示彈出畫面同時禁止捲動主畫面的技巧

作法應該有相當多種,這裡提兩種我從一些網站中觀察到的作法。

設定 body 為 overflow:hidden

準備兩個 div, 一個是 main content (id=main), 一個是 pop-up (彈出畫面), 平常隱藏 pop-up, 要顯示 pop-up 時,設 body 的 overflow: hidden,並顯示 pop-up。這樣主頁面就不能捲了。

關掉 pop-up 時再設 body 的 overflow: visible (預設值), 主頁面就可以捲了。

程式碼像這樣:

var popup = document.getElementById('popup');
function show_popup() {
  popup.style.display = 'block';
  document.body.style.overflow = 'hidden';
}

function close_popup() {
  popup.style.display = 'none';
  document.body.style.overflow = 'visible';
}

對 main content 為 position:fixed

這個作法比較複雜,是拆解 facebook 彈出照片的頁面得知的。

準備兩個 div, 一個是 main content (id=main), 一個是 pop-up (彈出畫面), 平常隱藏 pop-up, 要顯示 pop-up 時,設 main content 的 position: fixed,並顯示 pop-up。這樣主頁面就不能捲了。但是因為 scroll offset 歸零 (content height 改變的副作用), 要自己維護 scroll offset (後述)。

這個作法與上個作法有幾點差異:

  • 網頁主動管理頁面大小, 造成主頁面不能捲; 上個作法是設 body 為 overflow: hidden, 隱含瀏覽器會禁止捲動主頁面。這個規則有點詭異,多數瀏覽器應該有這麼作吧。
  • 因為 main content 變成 position: fixed, 它的高度也就變成 viewport height, 然後 body 的 height 也因此縮水, 最後主頁面的內容高度和 viewport height 一樣,所以就不能捲。但也因如此, scroll offset 會歸零,若之前有捲動主頁面才顯示彈出畫面, 背景的主頁面會跑掉,所以要自己記下 scroll offset, 並在 main content 設 top = -scrollY, left = -scrollX, 這樣畫面才會定住。取消彈出畫面後,要再自己 scroll 回當初的位置。程式碼如下:
var main = document.getElementById('main');
var popup = document.getElementById('popup');
var scrollX = 0;
var scrollY = 0;
function show_popup() {
  popup.style.display = 'block';
  scrollX = window.pageXOffset || document.documentElement.scrollLeft;
  scrollY = window.pageYOffset || document.documentElement.scrollTop,
  /*
    Set "fixed" and then the height becomes to fit to the window height.
    and then the body height becomes window height.
    You can observe the change by typing "document.body.scrollHeight" in DevTools console
  */
  main.style.position = 'fixed';
  main.style.left = -scrollX;
  main.style.top = -scrollY;
}

function close_popup() {
  popup.style.display = 'none';
  delete main.style.left;
  delete main.style.top;
  main.style.position = 'static';
  window.scrollTo(scrollX, scrollY);
}
  • 優點: 跨瀏覽器 (大概吧,不然 facebook 不會這麼作)
  • 缺點: 寫起來比較囉嗦, 而且顯示或關掉彈出畫面後, 頁面內容高度和 scroll offset 會改變, 若網頁有針對這些事件作事,要另外處理這裡觸發的「false event」

其它

若要幫彈出畫面設背景是半透明的黑畫面,要用 background + rgba 設秬明度。另一個作法是用 opacity,但是 opacity 會順便影響到子元素,所以用 background 配上 rgba 設值,比較合適。例如:

background: rgba(0, 0, 0, 0.5);

2015年1月10日 星期六

Python 處理 UTF-16 的 CSV

Python 內建的 csv 模組預設無法處理 UTF-16, 直接讀的話會出現

Error: line contains NULL byte

自行轉換編碼成 UTF-8 即可,作法如下:

import codecs
import csv

# 用 codecs 解碼 utf-16 為 unicode object
f = codecs.open(filename, 'rU', 'utf-16') 

# 再轉回 utf-8 給 csv.reader 用
cr = csv.reader(line.encode('utf-8') for line in lines)
for row in cr:
   # row 的內容就是 CSV parse 完的結果

或是先用 iconv 轉檔, 再用 Python csv 模組處理亦可:

$ iconv -f utf-16 -t utf-8 file_in_utf16 > file_in_utf8

備註: Python csv 模組不接受 unicode 的輸入,若來源不是檔案,也要記得轉成 utf-8 再傳入 csv.reader()

自動統計 app 在 Google Play 內各版本的的分數和常見問題

最近想統計 app 在 Google Play 上各版的分數,了解最近版本是否比較不穩定。如果有問題的話,統計一下問題分成那幾類。透過 Google Play 觀看 review,如果沒有邊看邊統計分類,看過只會有個模糊印象,不知道那類問題比較嚴重。但是.......又懶得邊看邊記。

為了自動分類 reviews, 首先要抓下 Google Play 上的 review。幸好 Google Play 存放 reviews 和其它資料在 Google Cloud Storage, 並提供工具 gsutil 可以從命令列取出 Google Cloud Storage 的資料, 作起來並不費工。官方說明在這裡的 "Export ratings and reviews"。摘要步驟如下:

1. Mac 或 Linux 下載解開 gsutil:

https://cloud.google.com/storage/docs/gsutil_install

2. 更新、設定 gsutil:

$ export PATH=$PATH:/path/to/gsutil
$ gsutil update
$ gsutil config

其中 gsutil config 會認證帳號, 照著作即可。最後要求輸入 project id 的時候, 輸入 app package name,例如 com.mycompany.myapp。相關說明見這裡

3. 下載 review

從 Google Play App Console -> App -> Ratings & Reviews 最下方得知 reviews 存放的位置

Ratings and reviews are also available for programmatic access through Google Cloud Storage and the gsutil tool. Your data, updated daily, is stored in this private bucket:
pubsite_prod_rev_NUMBER

接著用 gsutil 列出目錄下的檔案

$ gsutil ls gs://pubsite_prod_rev_NUMBER

再來就簡單了,gsutil 用法和 shell 指令差不多,有 ls, cp 之類的。下面的指令會取出所有 reviews 到本機目錄下:

$ data=$(gsutil ls gs://pubsite_prod_rev_NUMBER/reviews/)
$ for f in $data; do gsutil cp $f . ; done

取出來的檔案是每個 app 每個月一份 CSV 檔, 編碼為 UTF-16。

取得 CSV 後,就可以自行寫程式分析了。這裡有我寫的一份簡單版本,將要觀察的 CSV 檔放到同一目錄下,執行 script 帶入目錄名稱,會輸出各版本的分數還有統計最新版 reviews 裡面出現關鍵字 (如 "crash", "freeze" ) 的數量。需要用的話,記得修改 OBSERVERED_WORDS 以符合自己的需求。

2015年1月4日 星期日

線上刷卡流程的簡介

同事介紹關於線上刷卡流程的介紹文章, 滿淺顯易懂的, 改天有需要再複習一下:

git 常用指令

有鑑於一兩年沒用 hg 就忘得差不多了, 還是備忘一下目前覺得好用的 git 指令, 比較長的就加到 alias 裡了。

還沒實際用過, 先記著:

C++ 能否用 memcpy 複製 class / struct 的資料?

答案是: POD (plain old data) type 可以。POD type 可和 C 互通, CPP Reference POD Type 的介紹: Specifies that the type is POD (Plain Old Data) type. Thi...