小松鼠嚇了一跳,有了魔法眼鏡後,這世界看起來完全不一樣了

2014年12月31日 星期三

別讓分數殺了你


先說個故事,關於一個充滿心機的老人的故事
有個老人愛清靜,可附近常有小孩玩,吵得要命。 於是他把小孩召集過來, 說:「我這很冷清,謝謝你們讓這更熱鬧」 說完分給每人三顆糖孩子們很開心,之後就天天來玩。幾天後,每人只給2顆,再後來給1顆,最後就不給了。於是,孩子們生氣了,說:「以後再也不來這給你熱鬧了」 
很多人詬病考試領導教學,也有不少建議要如何改善考試的方式。 其中很多意見都不錯,雖然實際執行還有很多技術細節及現實上的問題(勿忘建構式數學),但整體來說,我對於整體教育的改善是樂觀的。
不過,我想講一些更基本的東西。就像上面的故事一樣。
上面的故事還挺有趣的,只不過,事實比虛構的故事更加有趣。

70 年代的時候,心理學家 Lepper 和 Nisbett 就做過這樣的實驗。 他們在幼稚園(位於史丹佛大學的 Bing 托兒所)中找來一些本來就喜歡畫畫的四五歲小朋友,問他們願不願意畫一張畫給。接下來就是三組不同的地方了: 

  • 預期得獎組:事先告訴小朋友們,那個朋友帶了一些乖寶寶獎狀(上面有像下圖一樣,帶有權威認證的榮譽標誌),只要畫圖就能贏得獎狀。 
  • 無獎品組:事先只問小朋友是否願意幫忙。最後只是謝謝他,不會給獎狀。 
  • 意外得獎組:事先只問小朋友是否願意幫忙,但是畫完之後告訴他因為感謝他,所以給他乖寶寶獎狀。 
可能有人會擔心不同的問法會造成抽樣偏差, 不過其實大多數被問到的小朋友都答應了。
每個小孩有六分鐘的時間畫圖,詳細的研究方式可以參考下面列的原始論文。
結果呢?
兩星期之後,觀察這些小朋友自由時間從事的活動,發現無獎品組以及意外得獎組還是一樣喜歡畫圖,但預期得獎組畫圖的時間卻只剩一半了。




Undermining children's intrinsic interest with extrinsic reward: A test of the "overjustification" hypothesis.
Lepper, Mark R.; Greene, David; Nisbett, Richard E.
Journal of Personality and Social Psychology, Vol 28(1), Oct 1973, 129-137.  PDF

仔細想想,短短的六分鐘,就足以造成這麼大的差別。
你從小到大,有多少時間是處於類似的環境?
會講「要考試了,怎麼還不唸書」這種話嗎?

其實還有不少實驗( Daniel Pink 的 Drive 動機,單純的力量裡面列了不少),像知名的蠟燭實驗,不斷的印證了同樣的結果。不過對我來說,因為我還沒有真的仔細研究過這些論文,讓我相信這個結論的主因還是自身的經驗與觀察。

想一想,你有多少次在考前準備考試的經驗? 據說考試是用來測量學習成效用的,那為什麼要在考前額外做一些事情來影響測量結果? 如果能在幾天一週加強準備,就能準備好的東西,為什麼要花一個學期來學?

制度的存在,是為了解決 Scalability 的問題。 因為人(或者選項)太多,才需要評鑑來幫忙做。 制度可以改善,但再好的制度都無法完美,都是一種簡化的過程。最後一定會有比較,一定會有分數。
而且,一定會有漏洞。
一定有人會鑽漏洞。
而一但有人開始鑽漏洞,就便成了軍備競賽了。
你的評量標準是高度,就會有人揠苗助長。
考試領導教學是無法避免的。但可以某種程度改善。
讓鑽漏洞的難度變高,代價是 Scalability 就會下降,所以要投入更多資源。讓能鑽漏的人變少,所以要特別注意公平性的問題。
隨著老師、社會整體觀念、素質的改變、提昇,進而引導制度越來越合理(才不會重蹈建構式數學的覆轍)。

但有些問題是更本質性的,因為改革只能讓制度更合理、更合乎某種價值觀(不像現在的制度處處自我矛盾)。
有人的地方就有江湖。
個人能做的,就是意識到這件事情,不讓自己被殺掉。
也許這樣講稍微誇張一點。比較持平的講法是,不讓自己的內在動機被殺掉。
不過一個人的自我扣掉內在動機後,我很懷疑還剩下什麼。

 當然,老師們就處在一個更重要的位置了。

但問題是,如果你自己平常就被評鑑、金錢、榮譽、績效追著跑(或者追著他們跑),你要怎麼教學生用平常心來對待分數?

2014年12月19日 星期五

Leap motion 擴增實境


利用 OpenCV 校正攝影機以及 Leap Motion Sensor 的位置,再利用 Three.js 來達成擴增實境的效果。

這是試玩的結果(還是跟 IPython notebook 一起探索),所以並沒有以最佳效果為目標。實際上 leap motion sensor 可以放在比較好的位置。

稍早拿來和 google hangout api 配合的效果。

原理上不難,只是中間要解決一堆技術上的小麻煩。

2014年11月30日 星期日

用 Spynner 來抓 8Comic 的漫畫 (4): 多線程


平行化的好處

之前抓檔案的方式是,
  1. 用瀏覽器抓 .html,找到圖片 url。
  2. 下載圖片
  3. 換下一頁,跳到第一步。
但是網路可以同時開好幾個連線。所以我們要利用這點來加速。我們的策略是,
  1. 用瀏覽器抓 .html,找到圖片 url。
  2. 把圖片 url 丟進一個 Thread Pool 裡面(平行下載,但是不等待)。
  3. 換下一頁,跳到第一步。
當然也有其他的方式平行化,比方連抓 .html 的工作也一併丟入 Thread Pool。不過這樣代表要多開好幾個 browser,而且相對來說,抓網頁會比抓圖片快,所以我們選擇上面的方式。

import 將會用到的 module

因為使用 python 2.7, 我們利用 urllib2 來抓圖, ThreadPool 來 multi threading 平行處理。
In []:
import spynner
import os, sys
from PyQt4.QtWebKit import QWebSettings # 用來設定 QtWebKit
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest # 控制 browser 的網路連線
from PyQt4.QtCore import QUrl # Qt 的 Url 類別

# 下面是新增的兩個 module
import urllib2
from multiprocessing.pool import ThreadPool

# 下面是 IPython 相關
from IPython.display import display, Image
from IPython.html.widgets import ImageWidget, IntProgressWidget

建立瀏覽器

這部份一樣
In []:
# 建立瀏覽器
browser = spynner.Browser(debug_level=spynner.ERROR, debug_stream=sys.stderr)

# 建立一個 webview
browser.create_webview()
settings = browser.webview.settings()
# settings.setAttribute(QWebSettings.AutoLoadImages, False)
settings.setAttribute(QWebSettings.JavaEnabled, False)        # 不需要  Java
settings.setAttribute(QWebSettings.DnsPrefetchEnabled, True)  # 試著節省 Dns 花的時間
settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True) # 不需要瀏覽紀錄或者 cookie

# 建立一個空的  url
BLANK_REQUEST = QNetworkRequest(QUrl())
# 建立一個空的圖片 url
DUMMY_IMG_REQUEST = QNetworkRequest(QUrl("data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="))

# 客製化的 NetworkAccessManager
class EightComicNetworkAccessManager(QNetworkAccessManager):
    # 只需要取代  createRequest 這個 method 即可 
    def createRequest(self, op, request, device=None):        
        url = str(request.url().toString()) # 參數很多,但只取 url 就夠用        
        if 'comic' not in url[:20]: 
            # 用很醜的方式來判斷非 8comic 網站的 url 
            # 用空的 url  取代原本的 url
            return QNetworkAccessManager.createRequest(self, self.GetOperation, BLANK_REQUEST)
        elif not url.endswith('js') and not url.endswith('css') and '.html' not in url:
            # 凡是  .js .css .html 之外的,都用空的圖片 url  取代原本的 url
            return QNetworkAccessManager.createRequest(self, self.GetOperation, DUMMY_IMG_REQUEST)
        else:
            # 傳回原本的 url
            return QNetworkAccessManager.createRequest(self, op, request, device)

# 設定  browser 的 NetworkAccessManager
browser.webpage.setNetworkAccessManager(EightComicNetworkAccessManager())

設定 Widget

增加一個 Progress Bar, 分別來顯示分析過的 .html 數字以及已經下載的圖片數
In []:
browser.show()
# 漫畫的網頁
base_url = 'http://new.comicvip.com/show/cool-5614.html?ch='

# 要下載第一本
book_no = 1

# 取得總頁數
browser.load(base_url+str(book_no))
total_pages = browser.runjs('ps').toInt()[0] 

# 建立 Image Widget 用來顯示圖片預覽
img = ImageWidget()
img.set_css("height", 300) # 讓圖片不要太大

# 顯示下載進度的 Progress bar
html_progress = IntProgressWidget(min=1, value=1, max=total_pages)
img_progress = IntProgressWidget(min=1, value=1, max=total_pages)

# 顯示 Widget
display(html_progress)
display(img_progress)
display(img)

利用 ThreadPool 來下載

另用 ThreadPool 來達成 multithreading (多線程) 即為容易,只要將想丟進 pool 的程式碼包進函數裡面即可。
In []:
# 建立一個下載目錄
dir_name = "download/{:02d}".format(book_no)
if not os.path.exists(dir_name):
            os.makedirs(dir_name) 
print "Download to {}/{}".format(os.getcwd(), dir_name)
sys.stdout.flush()

# 建立 ThreadPool, 5 條 thread
pool = ThreadPool(5)
        
# 開始下載
downloaded_images = 0
for page in range(1, total_pages+1):
    # 取得 image url
    browser.load("{}{}-{}".format(base_url, book_no, page))
    img_url = str(browser.runjs('document.getElementById("TheImg").getAttribute("src")').toString())

    # 將下載圖片的工作包成 save_img,推進 pool 裡
    def save_img(img_url, page):
        global downloaded_images
        fn = "{}/{:03d}.jpg".format(dir_name, page)
        data = urllib2.urlopen(img_url).read()
        with open(fn, "wb") as f:
            f.write(data)
        # 更新 widget 的狀態
        downloaded_images += 1
        img_progress.description = "img: %d/%d"%(downloaded_images, total_pages)
        img_progress.value = downloaded_images
        img.value = Image(filename=fn).data
    pool.apply_async(save_img, (img_url, page))
    
    # 更新 Widget 的狀態
    html_progress.description = "html: %d/%d"%(page, total_pages)
    html_progress.value = page

    # 等待所有任務結束
pool.close()
pool.join()
    

結果

到這裡,探索期正式結束,該有的技術已經完整。
有興趣的話,也可以實測比較一下有 multithreading 和沒有 multithreading 的差異。但這裡就發現我們需要封裝了,因為沒有封裝、整理好的關係,要測試兩種程式碼,必須要把程式碼寫兩遍,而無法共用相同的部份。
另外,程式碼裡面用到了 global 這個 keyword, 常常也代表我們需要封裝了。



用 Spynner 來抓 8Comic 的漫畫 (3): 節省頻寬


要解決的問題

  • 還是有文字廣告這樣不需要的流量來浪費頻寬和時間。
  • QtWebKit 的 QWebSettings.AutoLoadImages=false 有 bug,記憶體已用不還。

解法

如同這裡 http://stackoverflow.com/questions/21357157/is-there-any-solution-for-the-qtwebkit-memory-leak 以及相關的連結、討論,記憶體的 bug 目前找得到的解法就是定期砍掉 process 重新再開。
理論上,也可以深入 QtWebKit, 找到配置的記憶空間,然後手動釋放,順便將解法回給上游。這樣雖然是正解,但是與我們的主題無關。
既然無解,那要等 QtWebKit 修正 bug 之後再來抓? 太久。
定期砍 Process 重開? 可以,但太醜。
不用 QtWebKit, 改用其他套件來解? 可以,但其他套件可能有其他問題,人生不能一直逃避,會養成習慣的。
仔細思考,其實我們的目標是要讓 browser 不去抓不必要的網路資源,這個原則包含了圖片以及文字廣告,但不僅限於這兩個,還有像是 google 統計等等。
所以只要我們封鎖這些不必要的連線,看起來瀏覽器的圖片就不會被顯示了,不需要真的設定 QWebSettings.AutoLoadImages=false
比方可以設定 proxy,由 proxy 來控制網路資訊流。不過 QtWebKit 有幾個內建的功能,可以讓你控制網路的存取,比 proxy 更裡層一點。一個是 browser.webpage 的 acceptNavigationRequest, loadStarted, loadFinished. 這幾個搭配起來,你可以限制 browser 不抓取子 iframe。 但這個方法對我們來說,不夠用。我們想要控制更多,所以我們用另外一種方式,直接控制比較外層一點的 QNetworkAccessManager 。

一樣 import 所有我們將會用到的東西

In []:
import spynner
import os, sys
from PyQt4.QtWebKit import QWebSettings # 用來設定 QtWebKit
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest # 控制 browser 的網路連線
from PyQt4.QtCore import QUrl # Qt 的 Url 類別

# 下面這行是 IPython 相關
from IPython.display import display, Image
from IPython.html.widgets import ImageWidget, IntProgressWidget

建立瀏覽器

In []:
# 建立瀏覽器
browser = spynner.Browser(debug_level=spynner.ERROR, debug_stream=sys.stderr)

# 建立一個 webview
# 我們不設定 AutoLoadImages=False, 但增加一些其他設定
# 這裡並不是重點,但適合我們的應用
browser.create_webview()
settings = browser.webview.settings()
# settings.setAttribute(QWebSettings.AutoLoadImages, False)
settings.setAttribute(QWebSettings.JavaEnabled, False)        # 不需要  Java
settings.setAttribute(QWebSettings.DnsPrefetchEnabled, True)  # 試著節省 Dns 花的時間
settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True) # 不需要瀏覽紀錄

建立一個 QNetworkAccessManager 子類別

當 browser.webpage 在要求網路資源前,會先詢問 QNetworkAccessManager 來確定要不要抓,或者怎麼來抓這個資源。 我們可以用 browser.webpage.setNetworkAccessManager 來指定自己客製過的 manager。
In []:
# 建立一個空的  url
BLANK_REQUEST = QNetworkRequest(QUrl())
# 建立一個空的圖片 url
DUMMY_IMG_REQUEST = QNetworkRequest(QUrl("data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="))

# 因為只需要用一次,可以取個又臭又長的名字
class EightComicNetworkAccessManager(QNetworkAccessManager):
    # 只需要取代  createRequest 這個 method 即可 
    def createRequest(self, op, request, device=None):        
        url = str(request.url().toString()) # 參數很多,但只取 url 就夠用        
        if 'comic' not in url[:20]: 
            # 用很醜的方式來判斷非 8comic 網站的 url 
            # 用空的 url  取代原本的 url
            return QNetworkAccessManager.createRequest(self, self.GetOperation, BLANK_REQUEST)
        elif not url.endswith('js') and not url.endswith('css') and '.html' not in url:
            # 凡是  .js .css .html 之外的,都用空的圖片 url  取代原本的 url
            return QNetworkAccessManager.createRequest(self, self.GetOperation, DUMMY_IMG_REQUEST)
        else:
            # 傳回原本的 url
            return QNetworkAccessManager.createRequest(self, op, request, device)

# 設定  browser 的 NetworkAccessManager
browser.webpage.setNetworkAccessManager(EightComicNetworkAccessManager())

後面的程式碼都一樣

In []:
# 漫畫的網頁
base_url = 'http://new.comicvip.com/show/cool-5614.html?ch='

# 顯示瀏覽器,確認 browser 內容乾淨清爽
browser.show()

# 要下載第一本
book_no = 1

# 取得總頁數
browser.load(base_url+str(book_no))
total_pages = browser.runjs('ps').toInt()[0] 

# 建立 Image Widget 用來顯示圖片預覽
img = ImageWidget()
img.set_css("height", 300) # 讓圖片不要太大

# 顯示下載進度的 Progress bar
progress = IntProgressWidget(min=1, value=1, max=total_pages)

# 顯示 Widget
display(progress)
display(img)

# 建立一個下載目錄
dir_name = "download/{:02d}".format(book_no)
if not os.path.exists(dir_name):
            os.makedirs(dir_name) 
print "Download to {}/{}".format(os.getcwd(), dir_name)
sys.stdout.flush()

# 開始下載
for page in range(1, total_pages+1):
    # 取得 image url
    browser.load("{}{}-{}".format(base_url, book_no, page))
    img_url = str(browser.runjs('document.getElementById("TheImg").getAttribute("src")').toString())
    # 下載圖片
    fn = "{}/{:03d}.jpg".format(dir_name, page)
    with open(fn, "wb") as f:
        browser.download(img_url, outfd=f)
    
    # 更新 Widget 的狀態
    progress.description = "%d/%d"%(page, total_pages)
    progress.value = page
    img.value = Image(filename=fn).data

結果

和第一篇的結果比較,
的確清爽很多,廣告和圖片都消失了。速度也快了很多。其實 .css 檔案也可以不用抓,不過因為有 cache 的緣故, .css 和 .js 本來都只會抓一次,所以影響有限。
因為這一部分的探索已經告一段落,所以現在是封裝的時候。不過因為之後我們要介紹兩種不同的封裝方式,所以在這之前,我們要再做一件事情,那就是更加節省一點。
看起來不是我們不是已經夠節儉了嗎?只抓有需要的東西,沒有多抓任何東西。 也許 .html 也可以不用抓?沒錯,也許可以直接反推出每一頁漫畫圖片的 url,但即使這個例子可以,但一般來說,伺服器端完全可以將必要的資訊放在 .html 裡面,讓你必須要抓 .html 才能獲得必要資訊。 這系列的目的是介紹一個簡單的萬用抓資料方式,所以做到這裡就可以了。
那還有什麼可以節省的? 頻寬就這樣了,但是時間還可以節省。 下一篇介紹簡單的 multithreading 抓圖。
In []:



2014年11月29日 星期六

用 Spynner 來抓 8Comic 的漫畫 (2): 用 Widget 美化介面


已經可以抓了,還有什麼問題?

  • browser 瀏覽頁面時,已經顯示圖了。之後,又再 download 一次,浪費頻寬。
  • 介面不夠美觀,無法看到進度。

頻寬問題

概念上,有兩個方向。 一是既然 browser 顯示了圖片,表示 browser 有這份圖,我們跟 browser 要就好了。另一個剛好相反,告訴瀏覽器,不要顯示圖片,把圖片的 url 交給我們即可。
這兩個方向各有利弊,以現在這個應用來說,我選擇第二個。原因有三:

  •  QtWebKit 有選項讓你這樣做。 
  • 這樣可行。 browser 仍然會傳回正確的圖片 url。 
  • 可以順便擋住廣告圖片。

介面問題

因為我們用使用 IPython notebook,所以使用 IPython notebook 的 interactive widget。

接下來,一樣先 import 所有我們將會用到的東西

In []:
import spynner
import os, sys
from PyQt4.QtWebKit import QWebSettings # 用來設定 QtWebKit
# 下面是 IPython 相關
from IPython.display import display, Image
from IPython.html.widgets import ImageWidget, IntProgressWidget

再來是建立瀏覽器,並且設定不要載入圖片

In []:
base_url = 'http://new.comicvip.com/show/cool-5614.html?ch='
# 建立瀏覽器
browser = spynner.Browser(debug_level=spynner.ERROR, debug_stream=sys.stderr)

# 建立一個 webview,並且設定不要自動載入圖片
browser.create_webview()
settings = browser.webview.settings()
settings.setAttribute(QWebSettings.AutoLoadImages, False)

# 要下載第一本
book_no = 1

# 取得總頁數
browser.load(base_url+str(book_no))
total_pages = browser.runjs('ps').toInt()[0] 

再來是建立 Interactive Widget

In []:
# 建立 Image Widget 用來顯示圖片預覽
img = ImageWidget()
img.set_css("height", 300) # 讓圖片不要太大

# 顯示下載進度的 Progress bar
progress = IntProgressWidget(min=1, value=1, max=total_pages)

下載

跟之前一樣,不過外加進度顯示
In []:
# 顯示之前建立的 Widget
display(progress)
display(img)

# 建立一個下載目錄
dir_name = "download/{:02d}".format(book_no)
if not os.path.exists(dir_name):
            os.makedirs(dir_name) 
print "Download to {}/{}".format(os.getcwd(), dir_name)
sys.stdout.flush()

# 開始下載
for page in range(1, total_pages+1):
    # 取得 image url
    browser.load("{}{}-{}".format(base_url, book_no, page))
    img_url = str(browser.runjs('document.getElementById("TheImg").getAttribute("src")').toString())
    # 下載圖片
    fn = "{}/{:03d}.jpg".format(dir_name, page)
    with open(fn, "wb") as f:
        browser.download(img_url, outfd=f)
    
    # 更新 Widget 的狀態
    progress.description = "%d/%d"%(page, total_pages)
    progress.value = page
    img.value = Image(filename=fn).data

看起來還不錯?

這樣似乎看起來還不錯,沒有幾行程式碼,邊抓還能邊看 preview。 盯著朝著目標奔跑的 progress bar,有種莫名的療癒效果。而且沒有重複抓圖,省了不少頻寬。似乎探索期已經結束,接下來只要把這些程式碼封裝起來,方便重複使用就行了。
但仔細一看,還是有不少問題:

  •  如果用 browser.show() 打開瀏覽器,會發現還是有許多廣告被載入。我們希望這些廣告也不見。 
  • 用了一段時間之後,發現機器越來越慢,仔細一看,記憶體使用量大得驚人。相當詭異。 
第一個問題的成因很單純,有些廣告是文字廣告。第二個問題就比較棘手一點了, QWebSettings.AutoLoadImages 有 bug,會造成記憶體無法回收的問題: http://stackoverflow.com/questions/21357157/is-there-any-solution-for-the-qtwebkit-memory-leak 仔細查看討論之後,會發現這個問題目前基本無解,要等 Qt 解決。(不然就只能很醜的 fork,然後 close process。
那怎麼辦? 放棄這個方案? 碰到無解的問題,如果是駭客(hacker),要深入系統,修正 bug,釋放記憶體。身為自造者(maker),那要捨棄壞掉的系統,自幹一個。
但不過是抓個漫畫,這樣搞,未免太累了點。下一篇,我們要用廢客(faker)的方式來解決問題。那就是,假裝解決問題就行了。
In []:



2014年11月24日 星期一

用 Spynner 來抓 8Comic 的漫畫 (1): 基本技術



需要套件

  • Spynner (需要 PyQt4 或 PySide, autopy)
  • IPython notebook (因為這個範例是用 IPython notebook 示範,不然跳過 IPython 相關部份也行) 安裝 先安裝 Python, IPython notebook, PyQT4

在 mac 下(如果 autopy 安裝不起來):

  • 先安裝 Qt。 brew 的話, brew install qt 即可。
  • 安裝 PyQt 。 brew, pip 都行。
  • easy_install -N spynner
  • 在適當的地方, touch autopy.py,如 touch /usr/local/lib/python2.7/site-packages/autopy.py。假裝有 autopy 就行了,因為 autopy 其實用不到。

在 windows 下:

  • 安裝 python(x,y) 2.7
  • 接下來打開 IPython 然後輸入 !easy_install spynner(win8 可用搜尋找到 IPython)
  • 最後,打開 IPython notebook,按下 New notebook 開始。
In []:
# This is for windows 
# on linux, simply sudo easy_install spynner in command line
!easy_install spynner 
# restart the kernel

先 import 所有我們將會用到的東西

In []:
import spynner
import os, sys

# 下面這行是 IPython 相關
from IPython.display import display, Image
In []:

再來我們試試看建立瀏覽器

browser = spynner.Browser(debug_level=spynner.ERROR, debug_stream=sys.stderr)
如果看起來什麼事情都沒發生,那大概就對了。 spynner 已經在背景建立了一個 webkit 瀏覽器(叫做 browser)。
通常我們不需要 browser 真的被顯示出來,不過為了方便了解發生了什麼事情,我們先讓它能夠被顯示。
In []:
browser.show() # 告訴  browser,要它之後不要隱身
# 為了避免法律上的疑慮,這裡你要自己找到適當的 url,把 ???? 換掉
base_url = 'http://???.com/show/????-????.html?ch='  
browser.load( base_url+'1')
這時候,成功的話,一個瀏覽器會跳出來,顯示漫畫第 1 話的封面。
瀏覽器能夠改變大小,但是看來像是當掉一樣,沒有回應。
這其實是好事,因為我們希望能夠完全控制瀏覽器,所以先凍結它,再慢慢來蹂躪它。
接下來,我們要把封面圖的 url 抓出來。
In []:
browser.load_jquery(True)   #  spynner 內建有 jquery,用這個 method 載入,比較方便。
img_url = str(browser.runjs('$("#TheImg").attr("src")').toString())
print img_url
# 當然不用 jquery 也可以
img_url = str(browser.runjs('document.getElementById("TheImg").getAttribute("src")').toString())
print img_url
上面先用 runjs 跑 javascript 得到一個結果。
這個結果是一個 Qt (C++)物件,可能是數字、字串或者物件。因為我們知道我們要的是字串,所以用 .toString 讓他成為一個 Qt 字串。
最後,再用 str 轉成 Python 字串。

抓圖

知道了圖片的 url, 那要如何將圖片抓下來呢?
可以用 browser.download(img_url, outfd=fd) 直接下載到檔案裏面。
不過這裡先直接在 IPython notebook 裡面秀一下圖片。
In []:
# 直接顯示 url 看看
display(Image(url=img_url, width=200))
In []:
# 先用 browser 抓下圖檔內容, 然後顯示
display(Image(data=browser.download(img_url), width=200))
漫畫每一頁的 url 格式是 .......ch=M-N 其中 M, N 是數字, 分別是卷數及頁數, 所以現在我們只要知道有幾頁就行了。
一般來說,可以從 html 內容中找到資訊。 8comic 控制 UI 的 javascript 就有這個資訊了,我們直接利用。
一樣先用 runjs 得到 ps 這個 javascript 變數的內容, 然後轉成整數。
因為 toInt 的結果包含一些額外資訊,所以我們用 [0] 取出數字。
In []:
total_pages = browser.runjs('ps').toInt()[0] 
print total_pages
所以我們用一個迴圈把每一頁都抓下來吧
In []:
book_no = 1
for page in range(1, total_pages+1):
    browser.load("{}{}-{}".format(base_url, book_no, page))
    img_url = str(browser.runjs('document.getElementById("TheImg").getAttribute("src")').toString())
    print page, img_url
    display(Image(url=img_url, width=100))
    continue
    # 上面只是顯示每一頁的圖片
    # 如果你現在就想真的抓檔案下來, 把上面那個 continue 註解掉
    with open("{}-{}.jpg".format(book_no, page), "wb") as f:
        browser.download(img_url, outfd=f)
        print "File saved in", os.getcwd()

到這裡為止,基本的功能已經有了,下一篇將會討論一些細節問題。


題外話。
寫這篇一部份是因為「如何用 Python 抓網站內容」的詢問度一直很高,另一部分是因為發現  JComicDownloader 無法抓 8Comic 的圖。我看了一下,覺得這是一個不錯的例子。




2014年11月21日 星期五

地堡系列、14 號門、起點人系列、黑塔系列、別相信任何人、控制、記憶傳承人


這半年看的小說。
地堡系列

羊毛記看起來還挺不錯的,特別是第一段。故事的感情與情境、情節融合的很好。
續集星移、塵土故事也說得不錯,情節很吸引人。雖然也是一個未來反烏托邦的故事,但是主角(或者說作者)比飢餓遊戲、分歧者這類的要成熟一點。對這點感冒的人,可能會比較喜歡這本。整個故事的謎底還算令人滿意。
整體來說,相當推薦,是好看的小說。當初買來羊毛記之後,我立刻熬夜讀完。

14 號門


這本的情節很吸引人,但是謎底讓人有點失望。不是特別爛的結局,但感覺只是一個普通的結局。
故事敘述一間公寓中,充滿著各種詭異的事情。一個被重重鎖住,相當可疑的門。奇怪的規矩和事件。中間解謎的過程可以說是緊張、懸疑中帶著歡樂。我還挺享受這一段的閱讀。相比之下,謎底和結局也不是那麼重要了。
推薦程度中等。

起點人、終點人

這個系列也是以青少年當主角、青少年、兒童取向的反烏托邦小說。但跟飢餓遊戲和分歧者比起來,比較適合成年人的口味一點點。
設定很有趣,講的是一個某種原因之下,所有的大人都死亡,只剩下起點人(小孩)和終點人(老人)的世界。老人掌握資源和權力,可以制定法律。這樣的法律表面上看起來冠冕堂皇,但沒有例外的偏袒掌權者。沒有老人照顧的小孩,因為未成年,連工作權也沒有。
故事情節也不差。其中的謎團解答繞了幾圈,還算有趣。
推薦程度中上。

黑塔系列

黑塔系列很有名,評論的人也相當多。我看了一到七集,還沒看外傳。
對我來說,第一本不會構成障礙。不過第一本的確故事性較低。其實整個黑塔系列的故事進展都不算太快,就像史蒂芬金的其他小說一樣。有趣的是情境、心境描述、氣氛營造。
這個系列我從圖書館每個星期兩本兩本的借,每一本的感覺都不太一樣。但多半都挺好看的。其中一本是出門時從圖書館借出,然後搭捷運的來回途中就看完了。回來剛好直接還書,借下一本。但其中讓我感受最深的是結局。以這個系列的份量來說,這是一個完美的結局。稍微有一點驚訝,一點失落,但也很感動。不是那種會掉眼淚的感動,而是感受到整個世界的深度。
這本是否推薦是見仁見智,因為相對來說需要花一些力氣來讀。不過如果你能接受史蒂芬金的文字風格的話,我覺得會感受很多。

別相信任何人

從一開頭就相當吸引人。主角在設定上雖然年齡不小了,但是在心智成熟度上,感覺跟飢餓遊戲的主角差不了太多。
主角有失憶症,所以寫本日記當成外部記憶體。也因此常會出現故事中的故事中的故事(像是小說中的主角讀著日記裡面寫著聽到別人說的事情)。因為這樣的設定,作者可以巧妙的運用各式各樣的手法技巧,來誘導、誤導、引導你。讓你分不清真假。
各種線索間的衝突,新事證(或者偽證)不斷的像是擠牙膏一樣的慢慢被擠出來。爆點、疑點不斷,相當過癮。
結局不算驚豔,但至少還合格。
非常推薦。

控制

這本我讀英文版,讀了好久,前後大概快一個月吧。雖然別相信任何人也是讀英文,但份量多很多,而且用字也稍微難一點。
控制跟別相信任何人一樣,主軸是夫妻的事情。一開始的情節並不像「別相信任何人」那麼吸引人,但也不算無聊。
第一部的內容也參雜了許多日記,所以前面說的技巧,也一樣可以使用,只是沒有那麼霸氣外露,相對看起來比較平淡。即使是真實的場景,作者也用了一些手法來處理真假難分的氣氛。各種線索也像擠牙膏一樣慢慢擠出來。不過這些線索不像「別相信任何人」一樣接連引爆,很多只是默默的埋梗。
但剛開始相對平實的步調只是用來襯托後來的瘋狂而已。從第一部大約三分之二開始,勁道就開始變強。前面的部份開始發酵。前面埋的一些梗開始爆了。
第二部就直接露出瘋狂的本性,情節突然加快。除了埋更多梗之外,前面的梗也接二連三引爆。
到了第三部以後,更可以說是超越了瘋狂,直接到達了變態的境界。前面你以為已經爆開的地雷,到這裡居然又爆開了一次。
如果喜歡閱讀的人,這本非常推薦。

記憶傳承人

讀完前面兩本後(中間只看了鋼之鍊金術師漫畫),讀這本有種失速的感覺。
除了內容上的差異外,份量上也輕薄很多。
主角設定比之飢餓遊戲、分歧者、向達倫這類比較現代的青少年小說,沒有那麼.......要怎麼說,中二?這樣可能有點濫用。總之,就是沒有特別血緣,也不會設定成太勇敢、太特別、特救世主、太主角投射、太瑪麗蘇。主角就是很平常的青少年,帶有一點叛逆沒錯,有一點點的「特異能力」沒錯,但沒有那種莫名其妙的多愁善感。
開頭的設定沒有太驚奇之處,情節描述也很平淡。但還算有趣,而且進展很快(因為篇幅小),所以讀起來沒什麼問題。中後段開始漸入佳境,幾個爆點不錯,頗有狂人日記的禮教吃人感覺。
後段情節開始翻轉,有種青少年版的重裝任務的感覺。就當翻轉中又有轉折,計畫趕不上變化,開始讓人期待的時候,慢慢發現感覺有點怪,進展有點緩慢。然後情節戛然而止。
這時候你就會開始計較設定模糊奇怪、謎底沒有解答。
持平而論,這一系列有四本,也許看到後面會好一點,
推薦程度: 比較推薦給小朋友看。大人看也可以,但是不要期待太高,注意裡面的優點即可。

2014年10月15日 星期三

Python 超級新手教學




暑假錄的 Python 入門教學影片,對象是程式語言的超級新手。所以進度非常的慢。

一些細節會省略,等著有人問的時候再回答。

但拍出來後,一直覺得效果還可以再改進,所以一直沒放上來。

但現在想想,還是先放上再說。內容還會陸續增加,短期目標是寫出一個可玩的遊戲。
有任何可以利用的地方,歡迎自行取用。



2014年10月13日 星期一

桃蛙源記


因為兒子看到報紙上電影廣告,指名要看的關係,就帶他去看了他生平第一部看的電影。
我兒子對電影的品味實在不怎麼樣,在電視上看到「無敵鯊魚」、「賭城風雲」的片段,居然還連翻叫好。所以我對桃蛙源記的期待並不很高。 況且,光片名就像是有點年紀的人才會取的。



先說 3D 動畫品質。一些水、天空等等的風景弄得還挺美的,青蛙的紋理大概是有考據的,可能也挺真實的。不過呢,要怎麼說,簡單來講,這部電影的成本只有三千萬。
不要對 3D 動畫要求太高。大體上來說,我覺得動畫品質足以讓我順利融入劇情。

我比較不喜歡的是說故事的技巧。
比方青蛙離開故鄉的原因是:美國牛蛙入侵、農藥、農地被破壞(大「普」開發案)。在短短的時間,要塞入這麼多「有意義」的東西,其實需要良好的敘事技巧。雖然跟後面老鼠、蛇、鷹那段有點相互呼應,但還是過於隱晦、快速。表現大草澤美好的時間太少,很難突顯後面被破壞的反差。
說故事的時間被拿去哪裡了呢? 大部分的時間被教育以及懷舊用掉了。
比方一開始的片頭,就故事性來說,可以很快就帶過。但為了教育性,篇幅大概變成了三倍左右。又為了快速進入主題,所以根本沒時間突顯大草澤的美好。
不過片頭的情節其實一小段一小段來看,其實還不錯,只是沒有跟後面連起來。但至少主題是相關的。

後面有很多懷舊的橋段,就完全是天外飛來一筆,跟前後劇情、主題都沒有關係。


但上面寫的這些完全不重要。 因為這部電影的目標對象不是我,而是小朋友。
從現場的反應來看,小朋友的反應挺熱烈的。
就像我一開頭說的,小鬼的品味實在不怎麼樣。但又有誰真的能決定品味是什麼?
我還是推薦這部國片。
如果你的小孩還小,讓他自己建立自己的品味。
如果你的小孩年紀大一點,可以辯論溝通、互相了解。順便也試著理解一下電影想傳達、或無意間傳達的價值觀。

2014年9月27日 星期六

新部落格:松鼠爸爸講故事


部落格:松鼠爸爸講故事
這是這星期弄出的部落格,裡面有許多小故事,目前有六十多則故事,計畫持續更新。
雖然要在網路上找到故事,並不困難,但故事弄容有好有壞。
我為了自己方便,將我自己喜歡的故事,整理蒐集在這個部落格裡。故事內容大多有改寫過,符合自己的需求。
比方說伊索寓言的「牛和青蛙」,讓無辜的青蛙家庭先是死掉小孩再死掉媽媽,實在太慘無人道了。安徒生童話有些很長,所以我就刪減節錄。
還有一些故事(像是中國的一些經典),後面會感嘆和寓意,這些我也刪除。
總之,盡量不讓故事太。也不蒐集太無聊的故事。阿凡提系列的故事,小時候讀覺得挺幽默有智慧的,但現在看看,卻覺得挺蠢,所以只收錄一小部分。
所謂的不無聊,一般來說是內容有點寓意或者意思、情結有點轉折,或者是某種可以用到的典故(像伊卡洛斯)。
有一些故事其實是大故事的藍本/原始版本(像是雙夢記是牧羊少年的奇幻之旅,還有冰雪奇緣源自安徒生的雪后)
一些不適合小孩的故事,雖然很有趣,也暫時不放(像是天堂踩鴨子的故事,還在考慮怎麼改寫比較好)
此外,將來也會多放一些原創的故事。
非原創的故事,我儘量將我知道的來源寫下,像是瞎子摸象其實是佛經中的故事。
相信很多人也會有講故事的需求,可能是給小孩聽,也可能是演講中可以運用。因為每個人的需求及口味不同,所以我喜歡的,不見得適合你。不過參考看看無妨,說不定會喜歡。


2014年9月11日 星期四

真‧程式師用 One-Way Hash 來寫程式


每隔一段時間,一定會看到關於真‧程式師要使用哪種編輯器的爭論,到最後一定會變成直接用 echo,直接用磁鐵改硬碟之類的。
GNU nano is a text editor - a program often used to edit the source code of other programs. Emacs, Vim and ed are all progressively more "hard core" editors. cat is a Unix program that concatenates and outputs the contents of files. Things get steadily more ridiculous from here. Using a magnetised needle to flip bits on a hard drive requires nanometer precision and binary mastery, but in the early days of programming people did use needles sometimes to fix bugs on Punched cards. The use of a magnetized needle may also be a reference to the Apollo AGC guidance computer, whose instructions were physically written as patterns of wires looped around or through cylindrical magnets in order to record binary code.  -- http://www.explainxkcd.com/wiki/index.php/378:_Real_Programmers
但對一個真‧程式師而言,直接寫 binary code 實在太容易了,即使是用磁鐵也一樣。真正的硬核程式師應該要用像是 sha2、 md5 這樣的東西來寫程式才有挑戰性。

就像之前的 Great Python Challenge 一樣,之前也提出了一個挑戰,挑戰真‧程式師用 one-way hash 來寫程式,比方:
cat 0.c| shasum -a 384 | xxd -p -r > a.out && chmod a+x a.out && ./a.out
會選擇 sha384 是因為他剛好夠長,可以塞進一個 ELF 執行檔(請參考 http://www.muppetlabs.com/~breadbox/software/tiny/teensy.html).

當然也不能只挑戰別人,所以我自己也嘗試求解。最後的解如下面
照下面的步驟就能測試 (Copy& Paste) :
mkdir test
cd test
wget https://raw.githubusercontent.com/tjwei/tjw_ipynb/master/0.c
sh 0.c
gcc 0.c && ./a.out
cat 0.c | shasum -a 384 | xxd -p -r > a.out && chmod a+x a.out
./a.out


過程

第一版的解答很快就出來了 https://github.com/tjwei/tjw_ipynb/blob/master/a.c
基本上能執行,符合要求,但我還是希望至少能印出 "Hello World!"。
第二個解答不久後也出來, https://raw.githubusercontent.com/tjwei/tjw_ipynb/aa733143a5a76f460122e93028f49693d53931e8/0.c
在有些環境下,的確符合要求,能印出 "Hello World!",程式碼看來也挺漂亮的,但不夠完整。
最後我只好用更暴力的方式來搜尋解答,首先將程式碼縮短, comment 都去掉,只留下一個簡短的網址來取代原來的說明。 因為長度小於 112 byte,才能用一次 sha512 block 運算完成編碼。 這樣大概至少讓速度快個五倍。 在沒有超頻的 HD7970 ghz edition,可以用160 mhash/s, 的速度來搜尋。如果單純的 preimage 搜尋,甚至可以接近 180 mhash/s。而且還有一些加速的空間。
總之,結果在 https://github.com/tjwei/tjw_ipynb/blob/master/0.c
中間工作的過程在
http://nbviewer.ipython.org/github/tjwei/tjw_ipynb/blob/master/RealMan2.ipynb

而既然這是一個挑戰,我的確接到一些回應,其中一個由 Kuo-Tung 提供的解答,也挺有趣的,可以在這裡看到  http://paste.plurk.com/show/1982392/