10月12日突然對Ruby產生興趣了,於是就找了本書《Programming Ruby 1.9》來看,結果被它迷上了。長期以來我一直認為我知道的各種編程語言都不夠好,一直想自己設計一門語言。看了Ruby之后,發現的它的語法和語義正是我想要的,尤其是它的Mix in機制,是我尋找了好久的一個功能,Ruby有了,而且實現的很好,這一點很令人興奮。唯一不同的是我想設計的是靜態類型的語言,而Ruby是動態類型的,一旦引入類型的聲明,語法就會很復雜,比如Scala,各有優劣,Scala也是個不錯的語言。
上次學Lua的時候,兩天學會,然后兩天做了一個練習《繪制任意二元不等式的圖像表示》。Ruyb比Lua復雜很多,要想學會估計至少需要兩個星期。連着看了四五天書以后,每天一段時間看書,一段時間做練習。這次做什么練習呢?離線瀏覽代理服務器。
老早以前使用離線瀏覽的目的有兩個:一是網速很慢,提前把網頁下載好后,就可以飛速瀏覽;二是做電子書,把把網頁下載到文件里,適當地做一些編輯,然后編譯成CHM格式的電子書。現在做離線瀏覽則有兩個新的目的:一是保存有時效的內容,比如cnBeta網站的新聞評論在文章發布24小時之后就不再顯示了,保存到本地之后則可以不受此限制;二是可以內建一個搜索引擎,檢索自己需要的內容。根據這兩個目的,確定的方案是把網頁下載保存到數據庫里,然后做一個代理服務器,當請求的網址在數據庫中存在時從數據庫返回內容,不存在時再從外網獲取內容。以下是我的實現:
webclient.rb 訪問網址,返回響應
1require 'net/http' 2 3module Proxies 4 Fiddler = Net::HTTP::Proxy('localhost', 8888) 5end 6 7class WebClient 8 def initialize(proxy = nil, user_agent = 'WebSpider') 9 @http = if proxy then proxy else Net::HTTP end 10 @user_agent = user_agent 11 end 12 13 def visit(url, params = nil, rest = nil) 14 uri = URI.parse(URI.encode(url)) 15 uri.query = URI.encode_www_form(params) if params 16 puts "Get #{uri}" 17 @http.start(uri.host, uri.port) do |http| 18 request = Net::HTTP::Get.new uri.request_uri 19 request["User-Agent"] = @user_agent 20 response = http.request request 21 22 puts "#{response.code}#{response.message}" 23 puts "wait #{sleep rest}" if rest 24 response 25 end 26 end 27end
mongodoc.rb 把要保存到數據庫的數據轉成hash表
website.rb 定義網站和網頁類
1require_relative 'mongodoc' 2 3class Website 4 attr_accessor :domain, :encoding 5 def initialize(domain, encoding) 6 @domain = domain 7 @encoding = encoding 8 end 9end 10 11class Webpage 12 include MongoDoc::Document 13 attr_accessor :url, :data 14 def initialize(url, data) 15 @url = url 16 @data = data 17 end 18end
database.rb 創建數據庫連接
websites.rb 針對特定的網站定義需要保存的網址列表,從網頁中提取信息和處理網頁的方法
1require 'nokogiri' 2require 'date' 3require_relative 'webclient' 4require_relative 'website' 5require_relative 'database' 6 7module Websites 8 Cnbeta = Website.new('cnbeta.com', Encoding::GBK) 9 def Cnbeta.page_list(client) 10 latest = get_latest(client).to_i 11 recent = get_recent().to_i 12 if recent == 0 then recent = latest - 200 end 13 for id in recent.upto(latest) 14 yield "http://www.cnbeta.com/articles/#{id}.htm" 15 end 16 end 17 18 def Cnbeta.get_latest(client) 19 response = client.visit("http://www.cnbeta.com/") 20 if response.is_a? Net::HTTPOK 21 text = response.body.force_encoding(encoding) 22 doc = Nokogiri::HTML(text) 23 url = doc.css("div.newslist>dl>dt>a").first["href"] 24 id = url.match(/\/(?<id>\d+)\.htm/)["id"] 25 end 26 end 27 28 def Cnbeta.get_recent() 29 website = WebsitesDb.collection(domain) 30 if webpage = website.find(pubDate:{"$gt" => DateTime.now.prev_day.to_time}).sort(pubDate:"asc").next 31 id = webpage["url"].match(/\/(?<id>\d+)\.htm/)["id"] 32 end 33 end 34 35 def Cnbeta.process_page(client, webpage) 36 puts "process #{webpage["url"]}" 37 id = webpage["url"].match(/\/(?<id>\d+)\.htm/)["id"] 38 text = webpage["data"].to_s.force_encoding(encoding) 39 40 begin 41 if text =~ /^<meta http-equiv="refresh"/ 42 return nil 43 end 44 rescue ArgumentError => error 45 puts error.message 46 end 47 48 doc = Nokogiri::HTML(text) 49 title = doc.css("#news_title").inner_html.encode("utf-8") 50 time = doc.css("#news_author > span").inner_html.encode("utf-8").match(/\u53D1\u5E03\u4E8E (?<time>.*)\|/)["time"] 51 52 time = DateTime.parse(time + " +0800") 53 if time < DateTime.now.prev_day 54 puts "skip" 55 return nil 56 end 57 58 if g_content = doc.css("#g_content").first 59 comments = client.visit("http://www.cnbeta.com/comment/g_content/#{id}.html", nil, 0.2).body.force_encoding("utf-8") 60 g_content.inner_html = comments.encode(encoding) 61 end 62 63 if normal = doc.css("#normal").first 64 comments = client.visit("http://www.cnbeta.com/comment/normal/#{id}.html", nil, 0.2).body.force_encoding("utf-8") 65 normal.inner_html = comments.encode(encoding) 66 end 67 68 webpage["title"] = title 69 webpage["pubDate"] = time.to_time 70 if g_content and normal 71 webpage["data"] = BSON::Binary.new(doc.to_html) 72 else 73 puts "Comment on #{id} skipped" 74 end 75 puts "done" 76 return webpage 77 end 78end
webspider.rb 運行此文件下載、處理和保存網頁
1require_relative 'websites' 2 3class WebSpider 4 def initialize(proxy = nil) 5 @client = WebClient.new proxy 6 end 7 8 def collect(site) 9 website = WebsitesDb.collection(site.domain) 10 site.page_list(@client) do |pageUrl| 11 if webpage = website.find_one(url:pageUrl) 12 if processed = site.process_page(@client, webpage) 13 website.save(processed) 14 end 15 elsif webpage = collect_page(pageUrl) 16 webpage.data = BSON::Binary.new(webpage.data) 17 webpage = webpage.to_hash 18 if processed = site.process_page(@client, webpage) 19 website.insert(processed) 20 else 21 website.insert(webpage) 22 end 23 end 24 end 25 end 26 27 def collect_page(pageUrl) 28 response = @client.visit(pageUrl, nil, 0.5) 29 case response 30 when Net::HTTPOK 31 webpage = Webpage.new pageUrl, response.body 32 else 33 return 34 end 35 end 36end 37 38# spider = WebSpider.new Proxies::Fiddler 39spider = WebSpider.new 40spider.collect Websites::Cnbeta
proxyserver.rb 運行此文件啟動代理服務,並且掛載離線搜索頁面
1# encoding: utf-8 2require "webrick" 3require "webrick/httpproxy" 4require_relative 'database' 5 6class OfflineProxyServer < WEBrick::HTTPProxyServer 7 def do_GET(req, res) 8 uri = req.request_uri.to_s 9 WebsitesDb.collections.each do |site| 10 if page = site.find_one(url:uri) 11 @logger.info("Found #{uri}") 12 res['connection'] = "close" 13 res.status = 200 14 res.body = page["data"].to_s 15 return 16 end 17 end 18 super 19 end 20end 21 22def search_db(keyword) 23 keyword.force_encoding("utf-8") 24 site = WebsitesDb.collection("cnbeta.com") 25 site.find(title:/#{keyword}/).sort(pubDate:"desc").limit(100).to_a 26end 27 28search = WEBrick::HTTPServlet::ProcHandler.new ->(req, resp) do 29 keyword = req.query["keyword"].to_s 30 result = if keyword.empty? then [] else search_db(keyword) end 31 resp['Content-Type'] = "text/html" 32 resp.body = %{ 33 <html> 34 <head><meta charset="utf-8"><title>離線搜索</title><style>form{margin-left:30px}li{margin:10px}</style></head> 35 <body> 36 <form> 37 <em>cnBeta</em> 38 <input type="text" name="keyword" value="#{keyword}"><input type="submit" value="搜索"> 39 </form> 40 <ul> 41#{ 42 result.map{|page|"<li><a href=#{page['url']} target=_blank>#{page['title']}</a> #{page['pubDate']}</li>"}.join 43 } 44 </ul> 45 </body></html> 46} 47end 48 49server = OfflineProxyServer.new(Port: 9999) 50server.mount("/search", search) 51Signal.trap(:INT){ server.shutdown } 52server.start
先運行webspider.rb,之后啟動proxyserver.rb,在瀏覽器中把代理服務器設置成localhost:9999,就可以離線瀏覽已經在數據庫保存過的網站。打開http://localhost:9999/search,就可以看到離線搜索頁面。 來張圖,看一下離線搜索的效果
如果你也想運行這個程序,需要以下步驟:
- 安裝MongoDB 2.2.0,啟動數據庫服務
- 安裝Ruby 1.9.3,安裝程序中所用到的gems
- 安裝Fiddler,非必需,可以不裝
- 保存以上代碼到相應的文件中,運行
C語言關注的是底層功能,Ruby關注的是高層功能,程序員應該把Ruby當作日常語言。通過這個練習,大家也可以看到Ruby寫的代碼簡短易讀,功能強大。整個程序全部代碼加起來只有231行,很有說服力。當然了,這只是一個初始的版本,肯定有不足之處,請高手指正。在這個基礎還可以增加新的功能,比如過濾廣告,全文檢索等等,發揮你的想象力!
