2015年2月23日 星期一

字型雜記

sans-serif vs. serif

單字的字義就說明了一切: serif (襯線字) 是在字的邊端加裝飾 (如垂直線條), 用意是利於辨識相似的字, 而 sans-serif (無襯線字)則是沒有加裝飾。serif 最早是用在西方文字印刷體的內文, 如今在中文字或是電腦、行動裝置螢幕上, 情境大不同, 無此必要。

中文相對於的例子是明體的橫線右上角有個三角形, 類似 serif; 而黑體則沒有, 類似 sans-serif。字型的運用大家有不同的說法, 原本我以為內文該用明體, 標題用黑體, 就如同 serif 和 sans-serif 的用法。但是《字型散步:日常生活的中文字型學》用內文用黑體, UDN 改版後內文也用黑體。我現在也覺得黑體較順眼 [*1]。

另外, 明體也有可能用在大字, 《字型散步》舉例福音戰士用明體作為大字, 就用得很漂亮, 所以還是要看選用的字型。換句話說, 了解這些知識是清楚它們的特色。如同其它CS技術一樣, 要依配套措施決定如何使用。

明體特色

除水平線的右上角有個三角形外, 還有水平細, 直筆粗, 避免文字擁擠。

黑體特色

筆劃筆直, 大多無襯線。

康熙字典體

直接取用康熙字典內的字, 沒有修正, 相較之下較破碎和零亂。原本應該是缺點, 但因為與眾不同, 反到成了優點, 每個字看起來比較「有感覺」, 比較文青。

定寬字和比例字

中文字無此差異, 主要是西方文字、數字和符號有差。

細明體是定寬字, 裡面附的英數字每個字都占「半格」; 新細明體是比例字, 英數字是依字的實際大小決定寬度。用 terminal 或開發程式的 IDE, 習慣用定寬字。閱讀內文則是用比例字。

字體和字型

字體是概念, 字型是實踐的產品格式。設計一套字體, 然後作成字型供電腦使用。類似音樂和 MP3。不過大多數情境兩者混用也通。

瀏覽器用字的順序和方塊字

瀏覽器會依 CSS font-family 定的順序找字, 有找到就用, 沒找到就用下一個。依此規則, 一般會先放英文字型再放中文字型, 因為英文字型不會附中文字, 但中文字型會附英文字。藉此用比較好看的英文字型, 沒有的時候再套用中文字型附的英文字。看到方塊字的話, 我猜應該是字型裡沒作這個字的樣子, 卻填了個方塊, 瀏覽器就拿去用了。如果字型檔不附上方塊圖, 應該可以選用下一個字型。實際情況如何, 要研究字型檔和字型函式庫才知道了。

日文字型和中文字型

有些日文字型比較漂亮, 但是用日文字型有個風險, 少部份字的樣子可能寫法和中文不同, 或是缺字造成方塊字。待有看到強烈的例子再來思考是否因此避免使用日文字型。

像 UDN 目前用的字體順序是「Helvetica,Heiti TC,Segoe UI,Meiryo,微軟正黑體」, 其中 Meiryo 是日文的 sans-serif, 我用的 Windows 7 有內建 Meriyo微軟正黑體, 因此瀏覽器會選用 Meriyo。但是... Meriyo 真的比微軟正黑體好看, 沒遇到什麼狀況的話, 我應該也會用 Meriyo 吧。

Web Font

Web Font 是在載入網頁時再立即下載用到的字的字型, 因為用到的字不多, 理論上載入速度很快。Web Font 讓使用字型更為靈活, 可避免用圖替代文字。利於日後修改還有內文搜尋、複製貼上。

我覺得有兩個情況不適合使用 Web Font:

  • 內文: 用在內文時, 有時會看到內文一片白, 然後才顯示出來。慢了一點還以為網頁壞了, 就關掉網頁了。
  • 行動裝置, 特別是手機: 用 2.5G/3G 上網時 latency 遠大於一般的情況, 即使下載的字型檔只有 1 byte, 也可能等個一會兒。雖然 4G 大幅縮小 latency, 但普及率不高, 目前還不是使用時機。

另外使用在粗體字時 (如標題), 要小心 Web Font 的設定。如果設定只取普通的字型, 瀏覽器會自己模擬出粗體, 結果就不好看了。配套解法見 Fake Bolding of Web Fonts

其它

還有許多有趣的知識, 照樣抄寫下來有些瑣碎, 這裡只有留日後自己可能會用到的東西。另外留下參考資料供日後備查。

參考資料

備註

1. 懶得改既有 blog 設定了, 因為不能只是改字型, 字高段落間隙和標題等都要改。要作新網站的時候, 再多放些心思在字型上吧。

2015年2月6日 星期五

跳掉檔案開頭數 bytes 的作法

假設有個大檔案 (e.g. 500MB) 在開頭存有 meta data, 後面存有內容, 要怎麼跳掉開頭的 meta data, 只留下內容呢?

命令列的作法

這裡看到兩個有效率的作法:

使用 subshell: 先用一個 dd 跳掉開頭 1131 bytes (block size=1131, read 1 block), 再用第二個 dd 用正常的 block size 讀寫檔, 這樣效率就和直接用 dd 複製檔案差不多了。

$ ( dd bs=1131 count=1 of=dev_null && dd bs=4K of=out.mp3 ) < 100827_MR029_LobbyControl.mp3

使用 pipe 配合 a group of commands: 概念差不多, 只是改用 pipe。

$ dd if=filtered.dump bs=512k | { dd bs=1131 count=1 of=/dev/null; dd bs=512k of=trimmed.dump; }

Python 不建立新檔的作法

產生新檔案的時間成本有點高, 可能會花到數秒。如果是 Python 程式內要用的話, 可以直接用 file object 然後想辦法跳掉開頭資料。

偷懶的作法是直接用 file object, 然後先呼叫 file.read(N) 跳掉開頭 N bytes。缺點是若 file object 會傳入 third-party lib 使用, 其它程式碼可能會用 seek() 回頭讀開頭的內容。

保險的作法是自訂一個 file object wrapper。這個 wrapper 行為和內建 file object 一模一樣, 只是讀不到開頭數個 bytes。這樣效率和一般的 file object 幾乎沒差。 程式碼見這裡, 另外這裡是測試碼。關鍵是覆寫和移動檔案位置有關的 seektell。然後用 Python 提供的 __getattr__(self, attr) 實作剩下沒改到的 method。

那麼, 要怎麼知道該覆寫那些 method 呢? 用 ipython 建一個 file object, 然後利用補字功能, 再一個個看 method 說明, 很快就知道該覆寫那些了。

以下是示意的操作過程:

$ ipython

In [1]: f = open('/etc/fstab')

In [2]: f.<TAB>
f.close       f.encoding    f.fileno      f.isatty      f.name        f.next        f.readinto    f.readlines   f.softspace   f.truncate    f.writelines
f.closed      f.errors      f.flush       f.mode        f.newlines    f.read        f.readline    f.seek        f.tell        f.write       f.xreadlines

In [2]: f.close?
Type:       builtin_function_or_method
Base Class: <type 'builtin_function_or_method'>
String Form:<built-in method close of file object at 0x1a9a660>
Namespace:  Interactive
Docstring:
close() -> None or (perhaps) an integer.  Close the file.

Sets data attribute .closed to True.  A closed file cannot be used for
further I/O operations.  close() may be called more than once without
error.  Some kinds of file objects (for example, opened by popen())
may return an exit status upon closing.

In [3]: f.encoding?

...

In [21]: f.seek?

Type:       builtin_function_or_method
Base Class: <type 'builtin_function_or_method'>
String Form:<built-in method seek of file object at 0x1a9a660>
Namespace:  Interactive
Docstring:
seek(offset[, whence]) -> None.  Move to new file position.

Argument offset is a byte count.  Optional argument whence defaults to
0 (offset from start of file, offset should be >= 0); other values are 1
(move relative to current position, positive or negative), and 2 (move
relative to end of file, usually negative, although many platforms allow
seeking beyond the end of a file).  If the file is opened in text mode,
only offsets returned by tell() are legal.  Use of other offsets causes
undefined behavior.
Note that not all file objects are seekable.

...

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 補上, 會延遲內容出來的時間。不過作小東西或雛型的時候, 可忽略這一點負擔。

在 Fedora 下裝 id-utils

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