我的網誌已搬家至blog.miaout17.net,本網誌將不再更新

2011年2月9日 星期三

Blog搬家

之前就murmur很久了,嫌blogger功能不夠多,再搬回自架的wordpress。

迷之音:你都沒在寫文章,還嫌blogger不夠好用

總而言之,新址在這裡

http://blog.miaout17.net/

舊的feedburner會被導向新的blog

也歡迎改用新的feedburner

2011年1月26日 星期三

Ruby Tuesday #16: Learning Ruby with RubyWarrior

我在Ruby Tuesday #16分享的投影片已經上線。



所有相關的範例源碼都放在github上:https://github.com/gaagaaga/warrior-tuesday

祝武運昌隆 XD

2010年10月26日 星期二

AlgoCat 開發筆記 (Rails3+MongoDB)

大概從三、四個月前起,為了加強自己coding的基本功,我開始參加TopCoder及Codeforces的線上算法解題競賽。到目前為止,成績其實不太理想。在這些比賽的過程中,最能督促自己努力上進的事,莫過於被其他人慘電吧(Ex. 白俄羅斯高中生tourist名列TopCoder Algorithm Rating前五名)TopCoder官網雖然可以簡單的查詢一個人的成績,但無法追踨其他人的競賽狀況,並做出比較。

因為最近在練Rails3 + MongoDB,於是選了這個當作自己練習的題目,專案名稱暫定為"AlgoCat",也就是「演算貓」(Algorithm Cat)。這個名字是想告訴自己「在演算法的世界中,一隻大貓努力向上爬」。AlgoCat的目標為「追踨算法競賽的活動及競賽成績,並標記算法競賽及題庫中,解過及待學習的題目」。

AlgoCat還相當的不完整,可以說還在Pre-Alpha的階段吧。目前AlgoCat提供的功能僅有:查詢TopCoder Algorithm Rating的排行、觀看TopCoder使用者的成績記錄、追踨某些特定的TopCoder使用者並進行比較(以折線圖方式呈現)。

這篇文章其實不是打算announce這個網站,而是將自己的開發筆記整理一下。目前的成果可以在algocat.heroku.com看到。

Environment & Framework
  • Rails 3
  • MongoDB
  • Mongoid
  • Devise
目前是Deploy在Heroku+MongoHQ上,不排除移到其他地方。這些環境的設定可以參考上一篇。

Fetching Data from TopCoder

TopCoder本身提供了XML Feed的介面,用Ruby抓取資料只要簡單地用Net::HTTP和和REXML即可。

由於AlgoCat並不是一開始就抓好TopCoder的所有資料。有些資料是等使用者查詢才向TopCoder抓,並存放在資料庫。這方面的行為應該用Delayed Job之類的方法放在背景處理。AlgoCat在開發初期就有想到這點,但因為早期開發實作方便,還是暫時在前景處理。之後會移到background task去。BTW, Heroku第一隻worker就要收錢....之後可能會想些替代方案。

Mongoid

基本上照著官網的教學,應該可以做到大部份的功能。但要用NoSQL的資料庫,只學Object Mapper而不了解資料庫本身,應該會造成最佳化的困難。討論一些我踩到的問題好了:

@users = User.asc(:rating).limit(10)
現在,呼叫多次@users.each,其實會造成多次的query。解決方法官網有寫,只要呼叫cache,或是直接@user=@user.to_a亦可

上面這個算是小問題,只是要熟析Mongoid的細節。但另一個問題是Mongoid缺乏了對MongoDB的一些簡單低階操作。舉例來說,如果db中有一個articles的collection,而每篇文章中有一個以tags為key的array,array中存放了tag的字串。依MongoDB的官方教學,要增加一個tag的query可以這樣下:
db.articles.update( { title: "Rails3" }, { $addToSet : { tags: "Web" } } );
或者這樣亦可
db.articles.update( {title: "Rails3", tags: {"$ne": "Web"}}, {"$push": { tags : "Web" } } );
這兩者者可以確保「在文章原本沒有該tag的情況下,將tag插入」

但Mongoid的封裝似乎只支援這樣的動作:
article = Article.where( :title=>"Rails3" ).first
unless article.tags.elem? then
article.tags << Devise
article.save
end

原本是DB上的一個query,變成一定要將資料拉回app server判斷,再將「所有tags」寫回去,效能應有不小的差距。

Rails jquery-ujs

Rails內建了一些與JavaScript&AJAX相關的helpers,並以rails.js與prototyle javascript library橋接。如果想用jQuery,又想保留rails的helpers功能的話,可以用jquery-ujs中的rails.js將原本的蓋掉,就可以將原本prototype相關的javascript刪掉,換上jquery。

AlgoCat目前的「追踨使用者」(Watch)功能是用AJAX + jQuery做的。

Chart Library
要在網頁中繪製圖表,目前應該有三大主流:
1.在Server端繪製圖表,再傳送圖片到Client端
2.萬惡Flash (?)
3.以JavaScript為基礎,操作HTML Canvas之類的方式繪作圖表

其中第一點是傳統的方法,從以前的GnuPlot,到現在可以把資料丟到雲裡給Google畫圖(Google Chart API),但其缺點是比較不容易做圖表的互動操作。我個人不喜歡使用Flash作圖表,覺得多了一層整合上的麻煩,而且有點拖慢網頁的速度,但Flash應是最容易實作圖表互動操作的方式。而隨著瀏覽器對Canvas的支援趨近完整,JavaScript Engine運行的速度越來越快,用JavaScript在Client繪製圖表也成了選擇之一。而且靠JavaScript及DOM,同樣也可以進行與圖表的互動操作。

在開發AlgoCat時,一開始我選擇嘗試googlecharts gem,結果發現目前的版本還滿雷的。首先,我遇到了Linc Chart中,y軸座標值完全不對的問題。後來我發現其實這個Issue是同一個問題。試了一下後,決定git clone一份googlecharts下來,git bisect找bug。後來是把這個問題修掉了,但似乎還有其他小問題存在。之後有空應該會整理一下,發patch或pull request吧…

後來考慮到可能要在圖表上做互動,所以還是換回JavaScript-based的flot。Flot是一套以jQuery為基礎的JavaScript Plotting Library。

BTW,因為flot相依於jQuery,所以rails內建的Prototype一定要移掉,不然會相衝。我使用了前段提到的jquery-ujs。

Ongoing work
目前最大的問題,應該就是Fetch XML DATA應該要放在背景做吧。然後會補強自動更新的機制、加上Codeforces或Uva題庫功能等…

Source Code?
目前還沒有打算open source,而且可能還會大規模的重構,等我整理好code後有可能會open。

Rails3, Mongoid, Devise, Heroku, MongoHQ筆記

最近在看一些NoSQL的東西,於是想以MongoDB進行一些實作的練習。這篇文章是我在練習過程中,環境設定的筆記。

Installing Rails 3

請看我的上一篇文章

Installing MongoDB

我的開發環境是Ubuntu和Windows(在家用Ubuntu、出外用Windows,所以試了兩個環境)。Linux的話,個人建議不要用package system(apt/yum)的MongoDB。首先,絕對不要apt-get install mongodb。我一開始練習MongoDB時,是用apt-get裝的,會抓到一個舊到爆炸(1.4.4)的stable版本,這個版本mongo console client打錯指令還會crash(這是stable版嗎?)。

如果一定要用package的話可以考慮apt-get install mongodb-unstable或apt-get install mongodb-snapshot。但我是直接去官網抓build好的版本,再sudo copy到/usr/bin就好了。

MongoDB Object Mapper: Mongoid

Mongo的Ruby Object Mapper有很多種,可以參考官網上的列表。我選用Mongoid的原因有二:
  1. Mongoid的官網做的還滿有模有樣的,看樣子應該有認真維護 XD
  2. Devise認證機制內建支援Mongoid
安裝照著官方installation文件做就好了:

gem i mongoid --pre
gem i bson_ext

如果是Windows系統的話,mongo gem需要build一些native code,所以推薦裝RubyDevKit,裝完就可以順利的安裝mongo相關gems。

Mongoid使用上非常的簡單,Mongoid蓋掉了ActiveRecord的generators。要快速建出一個能動的環境,在設定好gems後只需要:

rails g mongoid:config
rails g scaffold article title:string content:string

只要mongod開著,這樣就可以動了,不用做data migration。唯一會踩到的雷可能是用了舊版的mongodb,所以就如前面提到的,請不要apt-get install。

Authentication: Devise

Devise是一套不錯的rails認證機制,而且內建對Mongoid的支援
一樣照著官方文件做就能動,整合非常簡單。安裝:
gem i devise

整合進app:
rails generate devise:install
rails generate devise User

接下來只要在view裡把link接一接就好。如果原本就用過devise,基本上用ActiveRecord或Mongoid沒啥差別。要速查link path helpers的話可以用rake routes。

唯一要注意的地方是要記得自己打index,我原本以為Model中的
devise :database_authenticatable, :registerable ......
這行,會自動在Mongoid Model上加上index。但rake db:mobgoid:create_indexes後,User collection上還是沒有任何index,所以要自己在Model上加:
index :email, :unique => true
然後再執行一次rake db:mongoid:create_indexes,把index打上去。

另外,有人做了Rails3+Mongoid+Devise的整合包。但Devise和Mongoid的整合實在太簡單了,不彷自己做一遍,也比較容易了解細節。

Deployment Environment: Heroku & MongoHQ

Heroku是一個雲端的Rails Hosting Platform,其使用方式應該很多文章介紹過了,這裡就不多廢話。基本上注冊好heroku帳號後,在rails app路徑下:
heroku create appname
git push heroku master
就可以把app deploy上去。

MongoHQ是一個雲端的MongoDB Hosting服務,提供了免費(16MB Limit)及付費的MongoDB空間。由於Heroku與MongoHQ有合作,並將MongoHQ做成了Heroku的一個addon,所以整合特別簡單。這裡是官方文件,只要下一行指令:
heroku addons:add mongohq:free
heroku就會自動向MongoHQ註冊一個資料庫,並設定rails的環境變數。注意官方文件有個地方沒講的很清楚,在config/mongoid.yml中,多加上mongohq的設定:
production:
uri: <%= ENV['MONGOHQ_URL'] %>
設定好後,MongoID就可以吃到MongoHQ的環境變數,並正確地建立資料庫連線。

順便列一下Heroku合作的資料庫中,免費方案的比較:
  • Heroku內建Database: 5MB (Postgre SQL)
  • Redis To Go: 5MB (Redis)
  • MongoHQ: 16MB (MongoDB)
  • Cloudant: 100MB, 500k requrest per month (CouchDB)
開發筆記待續…

2010年10月18日 星期一

Rails 3練習筆記

這幾天有空時,把rails 3的東西練了一下。基本上我是照著xdite的這篇練習中,rails部份的七個步驟做一遍。雖然xdite在寫這篇時用的應該還是rails 2.x,但要練的概念應該大同小異吧。我手上的參考書是AWDR2(是的,是第二版),和Beginning Rails 3(買來後發覺有點太淺了,但對於與rails脫節太久的我來說,還是不錯的參考書)。如果要玩rails 3的,要查東西似乎還是先查Rails Guide比較快 -- 因為Rails 3太新了,直接Google很常會搜到舊版本的solution。

以下是我印像較深的問題筆記,內容包括把xdite的練習跑一遍,並試著deploy到Heroku和自己的Linux Server上…

安裝ruby + rails development環境…我使用的OS是Ubuntu。一開始沒看到xdite這篇的說明,很白目的用了apt-get install ruby。結果就踩中了ruby 1.9.1 + rails 3.0.0當機的雷(rails 3現在官方只支援ruby 1.8.7及1.9.2)。我想不只是這個原因,用套件的ruby還會有很多相依性的問題吧。最後我因為想同時試rubby 1.9.2和1.8.7,所以選用了rvm,就算沒有要切版本,感覺用rvm管理ruby也滿乾淨的。

gem install rails不知為何速度超慢,而且console完全沒有回應。改用gem i --verbose rails,才能確認真的有在跑,沒有當機。
( 3)論壇的文章要能夠分頁,每一頁 20 筆,每一個論壇要秀出現在論壇裡有多少 post 數量。可用 params 配合 named_scope 在文章列表排序。
一開始我寫好了counter cache,在「看版列表」中成功的使用了counter cache減少select count query。接下來做paginate時,一開始我看別人的範例寫成這樣(省略無關的condition)

Article.paginate :per_page => 20, :page => params[:page]

但在server console中發現還是有select count的query,trace了一下will_paginate源碼,發現雖然will_paginate沒有正確讀到cached count,但可以在paginate時當參數塞進去:

Article.paginate :per_page => 20, :page => params[:page], :total_entries => @forum.articles_count,

當然,最後才發現rdoc根本有寫可以這樣下參數 Orz
( 4)每篇文章可以上傳附件
雖然可以用paperclip plugin,但這功能我選擇試著手刻看看。我在開發過程中有丟上Heroku,然後才想到「啊...Heroku沒辦法寫入靜態檔案,要的話要用S3」。
( 5)Admin 可以刪改所有文章,建立一個後台做這件事。網址是 http://example.org/admin/。只有身分是 admin 的人可以進後台。admin 的判別方是 column 裡加一個 boolean,判斷是否 admin。這個 attribute 必須用 attr_accessible / attr_protected 保護。
Admin namespace我是照這篇做的,做了看版管理及文章管理。就一般論壇而言,像「看版管理」這種東西,一般人不會有新增、修改、刪除的權限(版主可能有修改),所以scaffold架出來的東西應該的確比較適合移到admin去,而給使用者的邏輯再手動打造。但我的實作有種不夠DRY的感覺,把view和controller的code複置兩份,感覺不太好。可以想見,其實可以利用繼承、共用函式等方式,將程式碼變的更精簡。這部份有更好的best practice嗎?
( 6)用 Rake 撰寫自動化步驟,生假資料。寫一個 rake 可以達成以下步驟:「砍 db => 建 db => 跑 migration => 生種子資料」,另一個 rake 是生假文章與假論壇。
heroku不給直接跑rake db:drop。也就是說不能heroku rake db:drop,但可以heroku db:reset; heroku rake dev:fake。當然,這應該也可以寫成一個local的task。
( 7)在租來的 VPS 上面建置 Ruby on Rails production 環境,使用 Ruby Enterprise 與 mod_rails。使用 capistrano 佈署 application。
首先,我選用的VPS是linode。之前因為好奇,租過一個月的linode來玩,所以就選擇了這個較熟析的環境。灌了linode提供的Debian image後,接下來照著xdite建議的rails-nginx-passenger-ubuntu這篇做。一路上沒碰到什麼問題。

接下來,聞capistrano之名已久,但不知為何這個ruby上最有名的deploy工具,文件似乎有點少。Anyway,看著AWDR2和一些網上的教學,還是可以做的出來。我一開始照著網路上的一些文件,把deploy時的symbolic link功能做好。但bundle gems的問題我一時找不到best practice…看了這個影片,雖然覺得解法好像不夠好,但還是先照著做了。bundle path找不到的問題,除了在run中打absolute path外,也可以在deploy.rb中下

default_environment["PATH"] = "/opt/ruby/bin:/usr/bin:/bin:/usr/sbin:/sbin"

去指定path。

最後我的deploy.rb大概致上如這個gist。但我對capistrano的best practice仍有許多疑問…bundle install要自動化嗎?如果不自動化,在機器很多時要一台一台bundle install?自動化的話,要讓capistrano use_sudo嗎?不sudo的話,gems要裝在vendor/gems嗎?如果這樣做的話,vendor/gems要不要做symbolic link呢?(像我現在每cap deploy一次,就把所有系統沒有的bundle gems裝一次,其實有點慢)。

目前練習到這邊。基本上要跑完這份練習沒有難度,但很多地方會覺得處理的不夠好。Best practice可能需要看書或資料、有人指導、或經驗的累積吧…

2010年10月4日 星期一

再度考慮搬家

最近應該比較有閒寫心得了,又開始考慮搬Blog的可能性。其實一年前就murmur過這件事了。如果要搬的話,應該就是搬到自己架的Wordpress吧。主要的原因還是這些:
  • 貼code:Bloggger貼code縮排好像還是有機會跑掉。我真的不喜歡一天倒晚embed gist,然後python&haskell縮排跑掉真的很杯具。另外是Wordpress有儲存文章revision的功能,code不小心貼爛還可以回退,而Blogger就沒招了,只能從原本的code從貼一次。
  • 貼數學式:雖然有好幾個方法可以在Blogger上貼數學式(先自己用tex轉圖、用Google Graph API...),可是我都嫌不太方便。我還是較喜歡直接在文章中打類似tex的控製碼。Wordpress有一些plugin可以做到這一點。MathML是一個理想的解法,但目前只有Firefox有較完整的支援,而在Chrome慘不忍睹。
有人有什麼好建議嗎?

2010年9月19日 星期日

OpenFoundry疑似烏龍事件?

今天無意間發現中研院OpenFoundry有一篇文章,作者名稱是我的「本名」,但該篇文章並不是我發的。事實上,我的姓氏及名字都不是很常見,google一下大概我的本名,大概可以猜測台灣地區華人並沒有與我同名同姓的人。

中午才看完「盜圖者,剛剛賠了我十一萬」這篇文章,遇到這個狀況真怕有人誤會我盜文。為求自我保護,我立即去信OpenFoundry反應此事,在此公佈我的信件內容。因為該文上還有我的個人資料,故涉及該文及我的個資的部份,我會消音處理:
您好:

今天在瀏覽OpenFoundry網站時
無意間發現了一篇文章:「AAAAAAA」
(該文網址: OOOOOOOOOOOO )
作者姓名竟然與我的名字相同,但本人並未在貴站發表該篇文章。

原懷疑為同名同姓,但本人姓名相當罕見
資訊領域有人與我同名同姓的機率應相當低。
另外,以該文標題至Google搜尋
竟然發現BBBBBB索引網站收錄該文時,附帶著我的真實姓名及E-Mail
(目前在此網址可以看到:OOOOOOOOOOO )

因此,本人懷疑此次事件為OpenFoundry網站問題,造成我的個人資料外洩
若真的是這樣的問題,我想要麻煩貴站站方做以下處理:
1.將我的名字從該篇文章中刪除
2.聯絡BBBBBB站方,將我被索引的個資刪除
3.釐清並解釋問題發生的原因,避免此問題再度發生

為避免有人懷疑我抄襲原作者的文章
基於自我保護,我會立即將本信部份內容公開於本人Blog上

期盼站方盡速處理此事

Best Regards,
XXX

2010年8月15日 星期日

Blogtrans更新

剛剛突然想到,之前Blogtrans有改一些code,但一直沒放出來。看了一下git log....2009/05/26(汗)。

順手打包了一下,上傳到sourceforce,雖然code是一年多前寫的(汗)

* 增加Blogger匯入功能
* 處理一些無名XML不是well-form的狀態,或者是出現UTF-8不合法的字元
* 增加UnitTest,順便修正了一些minor problem....

新版的程式一樣可以在Sourceforce抓到

2010年7月2日 星期五

[FLOLAC]心中有Monad,萬物皆Monad

1.打完之後才突然發現....我原本只是想打個輕鬆的心得文,怎麼搞的那麼嚴肅 Orz

2.竟然遇到小我X(消音)屆的高中+大學學弟,嚇然發現自己變老人了

3.我給了幾個人聯絡方式,可是後來才想到我沒拿任何人的聯絡方式。如果有同學看到歡迎回個文。

正文開始 Orz



星期二時,scm老師談到「今年FLOLAC中,Functional Programming的課比較少,選用的是OCaml而不是Haskell,不會討論Monad之類的東西」。有趣的是,我覺得「就Operation Semantics的角度,來討論imperative program的derivation」,這件事本身就有濃厚的FP味道存在。

首先,從最基本的<s1, s>->s'談起:給予一個statement S1及初始狀態s,會得到最終狀態s'。對於deterministic semantics,<s1, s>->s' and <s1, s>->s'' imply s'=s''。換句話說,如果將S1視為一個routine,給予相同的input,會得到相同的output,這不就是pure function的定義嗎?另外就像Josh Ko談過的:與其說FP沒有side-effect,不如說FP沒有implicit state。然而<s1, s>->s'這樣的表達法,也讓imperative language中的所有狀態變化被攤開來,使其在某些方面可以像FP一樣地分析。

接下來,舉出老師在課堂上給的兩個quiz:

* 原本derivation只做到statement的層次。如果要將arithmetic expression做derivation,其型別應該是什麼?(Ex. x = y + 5; )
* 如果語言中出現「具有side effect的arithmetic expression」,其derivation的型別是什麼? (Ex. x = ++y; )

假設算數運算的值域為整數,其中第一個應為<AExp, State> -> int。第二個應為<AExp, State> ->(int, State)。這個答案其實很直觀:前者只要求值,後者求值順便變更狀態。這不是跟State Monad中的一些型別很像嗎?
  • 語意為「修改狀態不求值」
    <Statement, State>->State
    execState :: State s a -> s -> s
  • 語意為「不改狀態只求值」
    <AExp, State> -> int
    evalState :: State s a -> s -> a
  • 語意為「又改狀態又求值」
    <AExp, State> ->(int, State)
    runState :: State s a -> s -> (a, s)

State Monad是pure functional language中可以模擬imperative language的關鍵,而Operational Semantics又以接近pure functional的方式去分析imperative language(在不涉及parallel與nondeterministic的情況下)。上OP的課時,我反而在心中印證了對於FP的認知。

2010年6月6日 星期日

2010-06 久違的Blog更新

十個月沒更新文章了,把最近的事摻在一起記一記:
  • 「大鳥共和國」改名為「大貓共和國」(Kero?)
  • 看了龍紋身的女孩(?)
  • 參加了Google Code Jam 2010的比賽,在Round 2(3000名取前500名)被刷掉了
  • 今年終於跟公司請了十天的假,去參FLOLAC 2010的課程

看完了龍紋身的女孩三部曲,讓我熱血(?)的研究了一陣子破解技術
雖然目前還只是基礎Wargame都可能卡關的肉腳程度啦

我大概還滿嚮往「在資訊領域中極度自由」的感覺的
不管是「在Open Source的世界中,想要什麼都可以自己來」
或是「在資訊安全的領域中,如入無人之境」

不過這種「想要什麼,信手拈來」的自由,先決條件是在該領域各方面都很強吧...

GCJ打到前3000名不是什麼太好看的成積
沒啥好說的,就是基礎功不夠強吧...

一直以來我都用這樣的理由說服自己
「對於這方面競賽的能力不夠強,
 只是我沒針對Online Judge比賽需要的能力(演算法、解題速度等)做特訓,
 而把學習重心放在程式語言、API設計等等,需要架構高階複雜邏輯的地方」

其實這根本是藉口藉口藉口藉口藉口
在比賽中,基本功的不足之處,會一一無所遁形的暴露出來
演算法、離散數學、機率統計(這個比較少,但寫練習時有一題是要用機率的)
甚至資料結構的規劃也有問題,有時code寫到一半才發現結構redundant & stupid

從大學、研究所、工作,常常聽到
「這些東西還好啦,除非你從事演算法或學術相關工作,否則用不太到」這樣的說詞
真的是這樣嗎?我想不是的
這些基本功的強度會直接影響思考邏輯、工作速度、程式碼品質

依我前陣的子的個性,可能是不會把這段寫出來的
(因為過個一兩年,回頭看自己的文章時會覺得「啊啊啊,當時的自己又弱又愛胡說八道」)
但依我懶惰的個性,搞不好過一陣子就忘了這件事,一年後還是沒有針對基本功做加強
寫下來給自己當的目標吧,目標GCJ 2011前25名,加油!


最後,關於FLOLAC 2010
自我一兩年前對Functional Programming産生興趣起,就一直對這個課程有興趣
今年終於向公司請假去參加了,大概也會遇到一些收看此Blog的網友吧 :)

2009年8月21日 星期五

用git管理具有external的svn repository

睡不著爬起來寫blog,明天要早起的說 Orz



git是一套目前非常流行的分散式版本控制系統
相信很多人和我一樣,熟析git後就不會想用svn或cvs來管理源碼

我最近碰到這樣的問題:
專案主要使用svn做版本控制,但是我個人想用git管理
大家第一個想到的應該是git-svn
但git-svn對於svn external的支援並不完善

然而我目前的專案大量的使用了svn:external

src/
src/A => external to another project
src/B => external to another project
src/C => external to another project

更糟的是,因為專案開發時間的因素
A,B,C專案與主專案是共同開發的
有的時候我必需修改external library的API,並commit回去
有時其他人修改了external library,主專案必需跟著修改
(I know it doesn't sound good, but it's real world problem...)

我試過不管是單純的使用git-svn,或者借助一些shell script
都無法順利的處理這個工作流程

直到今天我想到一個實在是很瞎的方法:

mkdir gitrepo
cd gitrepo
git init
svn checkout svn://my-svn-repo-addr svnrepo
echo .svn > .gitignore
git add .
git commit
git chekcout -b svn-tracker

白話解釋一下:
在gitrepo/ 開一個git repository
在gitrepo/svnrepo 開一個svn repository
把.svn ignore掉 =.=
做一個名為svn-tracker的branch

這個看來愚蠢的方法 (對我目前的環境) 出手意料的好用
在改code時我都切換到其他branch (並且忘了svn的存在)
做任何svn操作時,我先切回svn-tracker branch

要做svn update的話就是
svn update
git add .
git commit -a

要commit自己的東西回svn mainstream就是
git merge work-branch
svn add *
svn commit

當然這些事可以寫shell script來完成



目前最大的缺點大概是沒法將git branch對應到svn branch (很麻煩)
不過還好我沒有很在乎....反正mainstream svn上沒有人在用branch & tag 功能,大家都只改trunk



這樣做還得到了意外的好處:
當要回退到舊版本時,如果沒有特別指定external的revision
就算update trunk to older revision,external還是會是最新版的
這樣會造成舊trunk + 新external,版本不相容的問題 (如果external行為或API有變)

上面的流程,每跑一次svn update
就等於把當時的情況做一次snapshot存進git
之後也就可以正確地回退版本....

2009年7月28日 星期二

無名備份檔問題

早上收到網友email,說無名的網誌備份檔用我的程式打不開
看了一下,找到問題並這樣回復該網友:

用記事本打開您的備份檔
搜尋 2003-02-29
改成 2003-02-28
即可開啟

(註:2003年不是潤年,沒有29號,無名的程式真的很厲害)


科科科

2009年7月26日 星期日

Murmuring....

又開始覺得blogger貼code有點麻煩了,有點想轉回自架的wordpress

用內建的Editor縮排常會跑掉,對python和haskell這種indention關鍵的語言是很嚴重的
貼gist還OK,可是有時候三四行的東西還要貼gist去做syntax highlight很麻煩。
另外覺得最近github好慢,在一篇文章裡放好幾個embed gist,載入時會一直卡在gist那邊

考慮一下要不要搬回自架的wordpress

2009年7月25日 星期六

Haskell Monad : 以Brainfuck Interpreter為例 (下)

承上篇,今天來解釋第三個版本的實作(源碼)。

我看Monad

在解釋下面的東西前,必需先多討論一下Monad。

Haskell的文件並不多,大部份介紹Monad的文章都是從fail, Computation, Maybe Monad下手。我用一句話來說明我「目前」對Monad的領悟:「Monadic value是一種靜態資料,通常表示了一種狀態的轉移」我不知道這個領悟是否完全正確,如果有錯的話歡迎指教討論。

初學Haskell時,我將IO Monad視為一種神奇的指令。在一切都是純函數的世界中,只有IO Monad是特立獨行的。像是putStr getStr這些指令,當出現在程式碼中時,IO Monad會主動去破壞純粹函數的世界,造成side effect....blah blah。

在多看一些書和文件之後,我了解到IO Monad也是單純的靜態資料。在ghc下觀察一下:
Prelude> :t putStr
putStr :: String -> IO ()
Prelude> :t putStr "Hello"
putStr "Hello" :: IO ()
Prelude> let a = putStr "Hello"
Prelude> :t a
a :: IO ()
Prelude> a
Hello
Prelude>

putStr是一個函數,給予String後就會吐出一個IO ()型別的資料。let a = putStr "Hello",這個Monadic value不是什麼可怕的指令,既不會動也不會咬人,更不會主動地破壞美好的函數世界。一直到a這個值被evaluate,才會真正地與外在世界互動。

那為什麼要說Monadic value通常記載了狀態的改變呢?讓我們從State Monad開始討論吧。ghc標準庫中Control.Monad.State.State的定義為
newtype State s a = State { runState :: (s -> (a,s)) }
s為該State Monad記住的狀態型別,a為附帶的回傳值之型別。我們可以看到,一個State Monadic value中只有一種資料:給定一個狀態,得到一個新的狀態,並且回傳一個附帶的值。Monadic value本身描述了「狀態如何轉移」。

讓我舉個實例:



這個例子簡單地使用Int做為狀態,並且定義了一些狀態的轉移。inc為將原本的整數加一,twice為將原本的整數乘以二。calculate則是狀態轉移的合併:乘以二,加一,再乘以二。

*Main> :t inc
inc :: IntState () -- inc是IntState ()的Monadic value
*Main> :t calculate
calculate :: IntState () -- calculate也是
*Main> execState calculate 1 -- 將原本的state(1)經過calculate的狀態轉換
6
*Main> execState calculate 3 -- 將原本的state(3)經過calculate的狀態轉換
14
*Main> let c = calculate >> calculate -- 將兩個calculate疊加起來,產生一個新的狀態轉換
*Main> execState c 3 -- 將c這個狀態轉換施加於3上
58


模組化
  1. HBF.TapeList : 如同上一篇文章所述,我們在記憶code及memory時都需要一個類似磁帶的結構。TapeList將這個結構及其運算抽象化。
  2. HBF.CharIO : Brainfuck只需要用到兩種IO:字元輸入及字元輸出。CharIO定義了一個class將這件事抽象化,以方便Brainfuck Interpreter橋接到IO及pure function。
  3. HBF.VM : Brainfuck Virtual Machine (事實上只是Interpreter核心而已,我沒有做複雜的事)。
  4. HBF.Test.xxx : 單元測試。
HBF.TapeList模組

data TapeList a = TapeList {
leftTP :: [a],
rightTP :: [a]
} deriving(Show)

這個沒啥好講的,就把重複利用的資料結構汎化而已。完全透過吃TapeList吐TapeList的pure function來操作。

HBF.CharIO模組

這個模組很簡單,但卻是重頭戲。Brainfuck需要兩種IO指令:一次輸入一個字元、一次輸出一個字元。在Haskell我們會盡量希望pure (functional) code和impure code分離。理論上Brainfuck的確需要輸入輸出,但我卻希望能將VM與標準IO解耦,讓後端的VM code同時可以達到兩種效果:
  1. 正常地使用標準IO運行
  2. 化為pure function,給定輸入及要運行的BF程式,得到輸出字串,類似這樣的效果:
    run :: String -> String -> String
    run code input = xxx output
要達到這樣的效果,第一件要做的事就是將IO抽象化。我們先定義一個class:
class (Monad m) => CharIO m where
getCh :: m (Maybe Char)
putCh :: Char -> m ()

首先,這個class是Monad的subclass。putCh為輸出字元的動作,是不會失敗的。但getCh的動作可能會因無法讀取而失敗(原因後述),因此我們將其形別定為m (Maybe Char)。

接下來,我們將標準IO橋接至CharIO。方法很簡單,將IO變成CharIO的Instance即可:
instance CharIO IO where
getCh = do
c <- getChar
return $ Just c
putCh = putChar

putCh直接接到putChar,非常簡單明瞭。另外,在標準輸入中我們不考慮讀取失敗的情況,所以就直接c $lt;- getChar再return $ Just c。這樣就完成了CharIO及標準IO的橋接工作。

接下來,考慮要怎樣「模擬」出一套字元輸入輸出的環境,首先來看看資料結構:
data MockIOData = MockIOData {
moInput :: [Char],
moOutput :: [Char]
}

MockIOData是模擬程式運行時IO的資料結構。我假設所有輸入資料都在程式運行前準備好了,放在moInput中。而moOutput在程式運行開始時是空的,準備要將東西放進去。

資料結構定義好了,接下來該將MockIO橋接到CharIO:


首先,CharIO必需要是Monad,於是我就借用了標準庫中的State Monad。putCh時就簡單的將字元接在moOutput尾端。getCh時檢查有input queue中還有沒有值,有值就回傳Just c,沒值就回傳Nothing。

HBF.VM模組

終於輪到Monad變形金鋼....ㄜ....是Monad Transformer出場了。基本上Brainfuck Interpreter的架構跟上一版差不多,但為了橋接IO,我們必需使用StateT(State Monad Transformer)。

基本的資料結構:
data VMData = VMData {
vmMem :: TapeList Word8,
vmCode :: TapeList Char,
isRunning :: Bool
} deriving(Show)

type VMState = StateT VMData


注意VMState的型別:他是一個以VMData為狀態型別的State Monad Transformer。這樣說可能會很混亂,請聽我慢慢解釋......讓我們從一個函數型別開始看吧:

fwdCode, backCode :: CharIO m => VMState m ()

fwdCode及backCode的用途是將指令碼前進或後退一格。先讓我們回憶一下,在上一個沒有用Monad Transformer的例子中,這些函式的形別大概是這樣的:

fwdCode, backCode :: State VMData ()

fwdCode是一個Monadic value,在狀態轉移時會回傳empty tuple ()。

fwdCode, backCode :: CharIO m => VMState m ()

VMState m ()其實就是StateT VMData m ()....
讓我們拆解這個型別的意義:fwdCode是一個monadic value,表示了一個VMData的狀態轉移 (將指令碼向前移動),並且會回傳一個型別 m ()的資料 (這句話不精確,但請容我先這麼敘述) ,其中m會是CharIO的instace。 (更,講完我頭都昏了)

不涉及IO操作的function,大概只要單純將State VMData換成StateT VMData m就好了,沒啥特別的。讓我們看看涉及IO操作的function:

outputCh = getMem >>= lift . putCh . word8ToChar
inputCh = do
d <- lift getCh
case d of
Just c -> setMem $ charToWord8 c
Nothing -> terminate


雖然code有點亂,先注意lift這個指令就好。在outputCh中,putCh的型別原本是CharIO m => Char -> m (),透過lift函式,我們要把它提升到CharIO m => StateT VMData m () 這個型別。

如果以上的東西看不懂也沒關係,雖然過程複雜,結果確是簡單漂亮的。HBF.VM只對外匯出兩個極為簡單的函式,讓我們來看看:

runBF :: CharIO m => String -> m ()
runBF code =
let vmData = newVMData code
in do runStateT runVM vmData >> return ()


runBF函式吃進一個字串,內容為Brainfuck code,回傳一個 (CharIO m) => m ()
別被嚇到,這個型別一點都不複雜了

*HBF.VM> :t runBF "+++++++++++++++++++++++++++++++++.........."
runBF "+++++++++++++++++++++++++++++++++.........." :: (CharIO m) => m ()
*HBF.VM> runBF "+++++++++++++++++++++++++++++++++.........."
!!!!!!!!!!

為什麼(CharIO m) => m () 可以直接執行呢?因為IO其實是CharIO的Instance,而在ghci環境中下的指令是會被視為在IO Monad中被evaluate的。

或許這樣會更清楚:

*HBF.VM> let r = runBF "+++++++++++++++++++++++++++++++++.........." :: IO ()
*HBF.VM> :t r
r :: IO () -- 是的,r就是我們熟悉的IO ()神奇指令
*HBF.VM> r
!!!!!!!!!!*


runMockBF :: String -> String -> String
runMockBF code input =
runMockIO input (runBF code)


那另外一個指令呢?
runMockIO 的第二個參數型別為MockIO (),所以runBF code的型別也會被deduction到MockIO ()。我們終於達成我們另一個目標:使用pure function來執行brainfuck interpreter,而且pure function的性質還是從compiler層次可證明的。

HBF.Test.xxx

我選用HUnit做單元測試 (我的case沒啥QuickCheck的用武之地),試了一下,總覺得沒有python or ruby的測試工具好用 (而且差了一大截),這就不提了。

這裡就看runBFMock這個BF interpreter pure function和unittest的表演啦。我們可以模擬BF VM的輸入輸出,運行程式,再以pure function的性質保證其沒有side effect,不管運行幾次都會是對的。 (不過這樣有意義嗎? XD)

尾聲:我沒有要傳教

研究這個單純是好玩,我沒有要喊啥Haskell Rocks, Monad Rules之類的話 XD

寫haskell好累,可能是因為我是新手的關係?腦力激蕩還滿有趣的,把Monad了解到一個程度也是非常高興的一件事,看著型別轉來轉去變成pure function也很賞心悅目。Haskell我大概還是會抽空繼續研究,可是現在要我拿這個語言當practical的主力還太困難了,同樣的時間我大概可以用Python, Ruby, C, C++, JavaScript各Implement一遍還有剩吧....XD

先寫到這邊,對於Haskell的讚美及抱怨就等以後有空再寫吧....

以上有問題歡迎指教

2009年7月23日 星期四

Haskell Monad : 以Brainfuck Interpreter為例 (上)

斷斷續續的看Haskell也有一段時間了,也拿它來寫過一些解數學題的小程式,但是關於Type System, Monad之類的東西一直沒有靜下心來好好研究。直到最近開始看Real World Haskell這本書,才對於Monad有了一些基本的領悟。不過沒動手寫過,總會覺得不是真正的了解這些東西。於是我就想要寫一個小小的application。

和之前一樣,選了Brainfuck Interpreter這個題目開刀。這也是我第一次寫Haskell application,coding手法上還很生疏。以下的例子分成三個版本。第一個版本是簡單的實作,第二個版本加入了State Monad。第三個版本是最完整的實作,加入了Monad Transformer, Unit Test並切分模組。

編程環境:GHC 6.10.3



首先,我試著完全不使用Monad,實作了第一個版本的Brainfuck Interpreter (實作一源碼)。先來看看資料結構:data BFData = BFData {
curr :: Word8,
left :: [Word8],
right :: [Word8]
} deriving(Show)

data BFState = BFState {
codeLeft :: [Char],
codeRight :: [Char],
bfData :: BFData
} deriving(Show)


BFData表示Brainfuck運行時的Memory,Word8是Haskell中的8-bit unsigned integer。Brainfuck在運行時,記憶體像是被單一指標指著的陣列,我使用兩個list及一個數值表示這樣的資料結構:curr表示目前的值,right表示指標位置右側的值,left表示指標位置左側的值。當指標向右移動的時候,將curr的值塞到left的前端,將right的head塞到curr中。眼尖的人或可發現:left中的值排放順序是相反的。這樣可以在常數時間內做完指標移動的動作。

BFState為整個Brainfuck interpreter的狀態。codeLeft和codeRight使用了和data相似的資料表示方式:未執行及已執行的code。BFState裡也包括了BFData的資料結構。

再來是函數的定義,版本一中盡量都是以pure function去操作資料。我們先略去函式的定義,觀察一些函式的型別

fwd :: BFData -> BFData -- 資料指標前進
back :: BFData -> BFData -- 資料指標後退
modifyValue :: Word8 -> BFData -> BFData -- 修改資料指標
output :: BFData -> Char -- 讀取資料指標
blockBack :: BFState -> BFState -- 遇到右括號 (branch on nonZero) 時回退

可以發現,底層大部份的操作都寫成pure function。將一個資料轉為令一個資料,no side-effect, no implicit state。

在這個版本中我刻意的不做兩件事:不使用Monad記住狀態,不使用function呼叫的轉移來表示程式進行的狀態轉移(Erlang idiom)。寫一寫我才理解到為什麼Monad是重要的。Haskell是一個Single-assignment,大部份變數immutable的語言 (如同Erlang) 。在這樣的語言中,要修改一個複雜的狀態常常是很麻煩的,會出現x' = f(x); x'' = g(x'); x''' = h(x'')之類的敘述。另外就是錯誤狀態的傳遞 (ex. Maybe Monad)。這些東西通常使用Imperative Language更容易描述。

* Update: 這段話我寫的不夠好。剛剛回去看Josh Ko部落格,他做了更精確的解釋。引述一下他的說法:「把全部的 states 都寫出來,就不會為了顧及 implicit states 而在推理轉換時絆手絆腳;但當 state 規模變大時,處理起來就不那麼順手了,此時 imperative programs 的專門語法反而比較方便。」

在下一個例子我們可以看到如何使用State Monad來打造Imperative的環境,但再更高階的地方仍保有Pure functional的性質。



接下來,我實作了第二個版本,這個版本使用了State Monad (實作二源碼) 。

節錄一部份的Code來討論好了:



我先簡化了一下資料結構,將code及data都放在BFState的資料結構中。為了實作方便,這個版本不吃standard input,而把output丟到一個字串當中(bfOutput)。這個偷吃步可以完全隔離IO,並簡單地直接使用State Monad。

再來,觀查fwdCode, backCode, fwdData, backData四個函式,其型別都是State BFState ()。State其實是Control.Monad.State提供的Type Constructor。State s a即是一個內部狀態為s,回傳值為a的State Monad。這些東西我就不細述了,有興趣的話請去參考文件吧。

stepBF函式負責將直譯器推進一個指令。看看
c <- currCode
d <- getData
fwdCode

是不是回到了我們熟悉的Imperative programming的寫法?fwdCode看似造成了side effect (雖然這個side effect是被封裝在monad內的) 。

進ghci觀查一下型别,我們可以發現一下有趣的事。

*Main> :t bfOutput . snd . execBF
bfOutput . snd . execBF :: [Char] -> [Char]

我們可以將這些函式串起來,將State Monad的型別消掉,得到一個顯然的pure function:給定一個brainfuck code的字串,得到一個輸出的字串。而Pure function對測試顯然是有利的,我們可以保證每次的運行結果都一樣,不會受外在環境影響。

這個版本的缺點是:沒有辦法綁定標準IO。如果可能,我們會希望BFState的狀態機與輸入輸出可以解除耦合,透過形別系統可以同時綁定在標準IO/模擬的IO上。BFState綁定在標準IO時當然是Impure的:程式運行會受外部輸入影響。當透過模擬的IO時,我們可以將整個BFState的運行化為Pure Function。下個範例會顯示如何做到這一點。



(註:前兩個版本的Brainfuck interpreter algorithm其實是錯的:左括號當值為零的時候忘了向右branch,只是跑hello, world的例子剛好會對,我也懶得修了。另外輸入的指令其實沒有實作)



接下來應該要講正題了,第三個範例:
  • Haskell Brainfuck Interpreter
  • Monad Transformer:以Control.Monad.State.StateT為例
  • 使用StateT切割IO,同時以IO及pure function形式執行brainfuck interpreter
  • 在Unit Test中以pure function運行brainfuck interpreter
不過進入正題前就打了一堆字,拖稿等下集吧 (不知道什麼時候有空寫 XD)。

先放source code:有稍微切一下module,應該沒有前兩個範例那麼亂
source code available at Github

2009年6月20日 星期六

Apache下以不同uid及pid執行不同VirtualHost

這篇的東西我沒有很確定是對的,筆記一下,有錯請指正喔。



考慮以下情境:
  • 在一台Linux主機上執行Apache伺服器
  • 主機上有兩個使用者:a及b
  • 使用Apache VirtualHost將a.gaaga.com導向/home/a/www,將b.gaaga.com導向/home/b/www
一般來說,如果使用apt, yum之類的套件管理程式自動安裝Apache的話,Apache會自動被設定在某一個系統帳號權限下執行(www-data, nobody...etc)。在這個地方會遇到一些小問題:Apache可能無法讀寫使用者的www目錄 (讀取的問題在預設權限755得情況不會發生,但一般我還是覺得將權限開成700比較有保障)。

網路上可以找到一些解法:
  • chmod 777 -R www (超危險暴力法,強力不推薦使用)
  • chgrp -R www-data www; chmod -R g+a www
  • etc....
但這些做法都還有安全性上的隱憂:www-data有權限讀寫任何用戶的www資料夾,也就是a其實可以寫一段php code盜取b的資料,或將b的資料殺掉。

這個問題我google了一下,有點抓不到正確得關鍵字,於是直接上irc freenode#TOSSUG發問,感謝snowian pct的回答,最後找到了這個東西:
apache2-mpm-itk (just mpm-itk for short) is an MPM (Multi-Processing Module) for the Apache web server. mpm-itk allows you to run each of your vhost under a separate uid and gid — in short, the scripts and configuration files for one vhost no longer have to be readable for all the other vhosts.
在debian下的設置很簡單:
# apt-get install libapache2-mpm-itk
# vi /etc/apache2/sites-available/a-site
加上
AssignUserID a a (分別是userid, groupid)
即可

(找方法找了很久,找到後花三分鐘就搞定了)




另外,看到2007年的mailing list上的問題:會跟mod_python相衝。不知道這個問題解決了沒,我可能會在上面跑Django

2009年5月30日 星期六

在Linux/Mac作業系統上運行Blogtrans

這篇文章說明如何在其他作業系統上運行Blogtrans。

目前Blogtrans只固定製作Windows版的執行檔。在其他平台上運行Blogtrans可能麻煩一些,以下也假設讀者對於這些作業系統的操作有一定的認知。

筆者無法允諾有空一一回復相關問題。筆者並非微軟教徒,但考慮Windows使用人數眾多,Blogtrans在Windows上應該算是經過相當的測試。如果使用上還是有問題,借個有灌Windows的電腦來用似乎是最穩定的選擇。

如果有人要幫忙做Linux和Mac的Build我也是很樂意啦....

環境設定

目前Blogtrans只相依於兩個東西:
  1. python 2.5
  2. wxPython-unicode
(舊版本曾經用過gdata,現在的Blogger Atom XML是硬寫出來的)

以ubuntu (debain) 為例:
sudo apt-get install python
sudo apt-get install python-wxaddons

我手邊沒有Mac (之前有Mac桌機但賣掉了) , 不過Mac印像中也是內建Python2.5,應該灌個wxPython-unicode就OK吧......

執行Blogtrans

然後去Github的Blogtrans頁面抓取程式源始碼,目前穩定的程式碼都放在master branch。如果會使用git的話可以用git抓,否則直接抓tarball下來解壓縮也可以。

之後進到shell,切換到blogtrans目錄,執行python blogtrans.py即可。

在Ubuntu上運行Blogtrans的截圖:



2009年5月22日 星期五

Blogtrans 更新 090522

今天赫然發現一件事,在上次的更新中我以為已經修正無名文章日期的問題了。結果發現修改的程式忘記commit,上次壓出來的版本日期還是有問題的。所以今天又壓了一個版本,只更新了無名日期這件事。

離無名關閉備份只剩四天。

有需要者請至Sourceforge抓取最新版本。

2009年5月11日 星期一

Blogtrans雜記

先聲明,筆者跟無名沒啥深仇大恨,也不在Web界工作。更不是其他BSP的打手,我與任何BSP沒有利益關係。

Blogtrans專案的起源是幫朋友(終於變成女友XD)搬Blog寫的一個console小程式,後來閒暇時補上UI放出來給想搬家的人使用。這也是我剛第一個嘗試用Python寫的中小型應用程式 (所以程式架構很亂,有人要trace的話別笑我 Orz)。

維護這隻程式算是吃力不討好的工作。無名的規格問題層出不窮:在XML檔案後面添加HTML碼的做法是一大經典,而且這是推出備份服務的後來才加上去的,我懷疑這是刻意讓企圖讀取無名備份檔的程式無法運作。另外在XML檔中可以出現不合法的XML字元,還有現在的備份檔不完全問題。 (Well, 我必需承認無名匯出檔根本是封閉格式,或許該慶幸他沒有用亂七八糟的binary format再加個密?) 另外也有些MT匯出檔的格式不依標準 。最麻煩的是上傳到Blogger,我花了許多時間在測試上傳Blogger備份檔上。




話說回來,無名從以前到現在的做法著實另人惱火,我相當不相信「關閉備份是為了專注在其他更好的服務」。

筆者在思考是不是該在無名關畢備份服務後,再寫另一個轉出無名的功能。要達到私密文章備份,這次可能就要靠HTTP認證,再從前台或後台撈HTML出來了。跟之前的作法比,可能這樣的做法更為麻煩。只要站方稍稍變動規格,撈文章的parser就可能失效。

我另一方面也希望國內其他BSP可以繼續做到這樣的功能 (撇開轉出Blogger部份不提的話,pixnet一直提供了比Blogtrans更穩定的轉出無名功能)。畢竟跟商業(又有幾位程設能力超強大神在)的BSP比,筆者一人利用空閒時間的力量是相當有限的。

2009年5月10日 星期日

無名小站備份檔問題

按,我家樓下常有外國人在開轟趴,半夜一點多大聲說話,吵到我關著窗子都聽的到。剛快睡著結果被吵起來寫blog,考慮下次再被吵要不要去暱名檢舉了....



言歸正傳,昨天收到一位網友來信,說Blogtrans無法匯入他的備份檔。他的網誌有九百多篇文章,但是從無名試著匯出好幾次備份檔,使用Blogtrans匯入時都不成功。使用Notepad++打開他的備份檔,檢視檔案尾端,大鳥眉頭一皺,發現案情並不單純 (對不起我老梗了):


因為沒有取得轉錄同意,雖然原文是公開的,應該在合理引用的範圍,我還是把文字消音了。

注意紅框的地方,一個E9的特殊字元中斷了備份檔。我試著手動修復備份檔,發現無名只匯出了七百多篇文章,留言全都不見了。

假設不是該網友在下載的過程中遇到特殊問題,那就是無名的備份功能真的有Bug,導致匯出時闕漏了文章和留言。

該位網友去信無名,客服的回應為
會幫他調整Blog,請他清除網路暫存檔試試看
(顯然沒用?)

或許無名是對的:關閉服務是因為他們沒有能力維護。

你還放心住在無名嗎?



(對不起我又老梗了)