用Ruby写的离线浏览代理服务器,只有两百多行代码

后端开发   发布日期:2025年06月26日   浏览次数:102

10月12日突然对Ruby产生兴趣了,于是就找了本书《Programming Ruby 1.9》来看,结果被它迷上了。长期以来我一直认为我知道的各种编程语言都不够好,一直想自己设计一门语言。看了Ruby之后,发现的它的语法和语义正是我想要的,尤其是它的Mix in机制,是我寻找了好久的一个功能,Ruby有了,而且实现的很好,这一点很令人兴奋。唯一不同的是我想设计的是静态类型的语言,而Ruby是动态类型的,一旦引入类型的声明,语法就会很复杂,比如Scala,各有优劣,Scala也是个不错的语言。

上次学Lua的时候,两天学会,然后两天做了一个练习《绘制任意二元不等式的图像表示》。Ruyb比Lua复杂很多,要想学会估计至少需要两个星期。连着看了四五天书以后,每天一段时间看书,一段时间做练习。这次做什么练习呢?离线浏览代理服务器。

老早以前使用离线浏览的目的有两个:一是网速很慢,提前把网页下载好后,就可以飞速浏览;二是做电子书,把把网页下载到文件里,适当地做一些编辑,然后编译成CHM格式的电子书。现在做离线浏览则有两个新的目的:一是保存有时效的内容,比如cnBeta网站的新闻评论在文章发布24小时之后就不再显示了,保存到本地之后则可以不受此限制;二是可以内建一个搜索引擎,检索自己需要的内容。根据这两个目的,确定的方案是把网页下载保存到数据库里,然后做一个代理服务器,当请求的网址在数据库中存在时从数据库返回内容,不存在时再从外网获取内容。以下是我的实现:

webclient.rb 访问网址,返回响应

  1. 1require 'net/http'
  2. 2
  3. 3module Proxies
  4. 4 Fiddler = Net::HTTP::Proxy('localhost', )
  5. 5end
  6. 6
  7. 7class WebClient
  8. 8 def initialize(proxy = nil, user_agent = 'WebSpider')
  9. 9 @http = if proxy then proxy else Net::HTTP end
  10. 10 @user_agent = user_agent
  11. 11 end
  12. 12
  13. 13 def visit(url, params = nil, rest = nil)
  14. 14 uri = URI.parse(URI.encode(url))
  15. 15 uri.query = URI.encode_www_form(params) if params
  16. 16 puts "Get #{uri}"
  17. 17 @http.start(uri.host, uri.port) do |http|
  18. 18 request = Net::HTTP::Get.new uri.request_uri
  19. 19 request["User-Agent"] = @user_agent
  20. 20 response = http.request request
  21. 21
  22. 22 puts "#{response.code}#{response.message}"
  23. 23 puts "wait #{sleep rest}" if rest
  24. 24 response
  25. 25 end
  26. 26 end
  27. 27end

mongodoc.rb 把要保存到数据库的数据转成hash表

  1. 1module MongoDoc
  2. 2 module Document
  3. 3 def to_hash
  4. 4 hash = {}
  5. 5 instance_variables.each do |var|
  6. 6 key = var.to_s.delete('@')
  7. 7 hash[key] = instance_variable_get(var)
  8. 8 end
  9. 9 hash
  10. 10 end
  11. 11 end
  12. 12end

website.rb 定义网站和网页类

  1. 1require_relative 'mongodoc'
  2. 2
  3. 3class Website
  4. 4 attr_accessor :domain, :encoding
  5. 5 def initialize(domain, encoding)
  6. 6 @domain = domain
  7. 7 @encoding = encoding
  8. 8 end
  9. 9end
  10. 10
  11. 11class Webpage
  12. 12 include MongoDoc::Document
  13. 13 attr_accessor :url, :data
  14. 14 def initialize(url, data)
  15. 15 @url = url
  16. 16 @data = data
  17. 17 end
  18. 18end

database.rb 创建数据库连接

  1. 1require 'mongo'
  2. 2
  3. 3DbServer = "mongodb://localhost:27017"
  4. 4WebsitesDb = Mongo::Connection.from_uri(DbServer).db("Websites")

websites.rb 针对特定的网站定义需要保存的网址列表,从网页中提取信息和处理网页的方法

  1. 1require 'nokogiri'
  2. 2require 'date'
  3. 3require_relative 'webclient'
  4. 4require_relative 'website'
  5. 5require_relative 'database'
  6. 6
  7. 7module Websites
  8. 8 Cnbeta = Website.new('cnbeta.com', Encoding::GBK)
  9. 9 def Cnbeta.page_list(client)
  10. 10 latest = get_latest(client).to_i
  11. 11 recent = get_recent().to_i
  12. 12 if recent == then recent = latest - end
  13. 13 for id in recent.upto(latest)
  14. 14 yield "http://www.cnbeta.com/articles/#{id}.htm"
  15. 15 end
  16. 16 end
  17. 17
  18. 18 def Cnbeta.get_latest(client)
  19. 19 response = client.visit("http://www.cnbeta.com/")
  20. 20 if response.is_a? Net::HTTPOK
  21. 21 text = response.body.force_encoding(encoding)
  22. 22 doc = Nokogiri::HTML(text)
  23. 23 url = doc.css("div.newslist>dl>dt>a").first["href"]
  24. 24 id = url.match(/\/(?<id>\d+)\.htm/)["id"]
  25. 25 end
  26. 26 end
  27. 27
  28. 28 def Cnbeta.get_recent()
  29. 29 website = WebsitesDb.collection(domain)
  30. 30 if webpage = website.find(pubDate:{"$gt" => DateTime.now.prev_day.to_time}).sort(pubDate:"asc").next
  31. 31 id = webpage["url"].match(/\/(?<id>\d+)\.htm/)["id"]
  32. 32 end
  33. 33 end
  34. 34
  35. 35 def Cnbeta.process_page(client, webpage)
  36. 36 puts "process #{webpage["url"]}"
  37. 37 id = webpage["url"].match(/\/(?<id>\d+)\.htm/)["id"]
  38. 38 text = webpage["data"].to_s.force_encoding(encoding)
  39. 39
  40. 40 begin
  41. 41 if text =~ /^<meta http-equiv="refresh"/
  42. 42 return nil
  43. 43 end
  44. 44 rescue ArgumentError => error
  45. 45 puts error.message
  46. 46 end
  47. 47
  48. 48 doc = Nokogiri::HTML(text)
  49. 49 title = doc.css("#news_title").inner_html.encode("utf-8")
  50. 50 time = doc.css("#news_author > span").inner_html.encode("utf-8").match(/\u53D1\u5E03\u4E8E (?<time>.*)\|/)["time"]
  51. 51
  52. 52 time = DateTime.parse(time + " +0800")
  53. 53 if time < DateTime.now.prev_day
  54. 54 puts "skip"
  55. 55 return nil
  56. 56 end
  57. 57
  58. 58 if g_content = doc.css("#g_content").first
  59. 59 comments = client.visit("http://www.cnbeta.com/comment/g_content/#{id}.html", nil, 0.2).body.force_encoding("utf-8")
  60. 60 g_content.inner_html = comments.encode(encoding)
  61. 61 end
  62. 62
  63. 63 if normal = doc.css("#normal").first
  64. 64 comments = client.visit("http://www.cnbeta.com/comment/normal/#{id}.html", nil, 0.2).body.force_encoding("utf-8")
  65. 65 normal.inner_html = comments.encode(encoding)
  66. 66 end
  67. 67
  68. 68 webpage["title"] = title
  69. 69 webpage["pubDate"] = time.to_time
  70. 70 if g_content and normal
  71. 71 webpage["data"] = BSON::Binary.new(doc.to_html)
  72. 72 else
  73. 73 puts "Comment on #{id} skipped"
  74. 74 end
  75. 75 puts "done"
  76. 76 return webpage
  77. 77 end
  78. 78end

webspider.rb 运行此文件下载、处理和保存网页

  1. 1require_relative 'websites'
  2. 2
  3. 3class WebSpider
  4. 4 def initialize(proxy = nil)
  5. 5 @client = WebClient.new proxy
  6. 6 end
  7. 7
  8. 8 def collect(site)
  9. 9 website = WebsitesDb.collection(site.domain)
  10. 10 site.page_list(@client) do |pageUrl|
  11. 11 if webpage = website.find_one(url:pageUrl)
  12. 12 if processed = site.process_page(@client, webpage)
  13. 13 website.save(processed)
  14. 14 end
  15. 15 elsif webpage = collect_page(pageUrl)
  16. 16 webpage.data = BSON::Binary.new(webpage.data)
  17. 17 webpage = webpage.to_hash
  18. 18 if processed = site.process_page(@client, webpage)
  19. 19 website.insert(processed)
  20. 20 else
  21. 21 website.insert(webpage)
  22. 22 end
  23. 23 end
  24. 24 end
  25. 25 end
  26. 26
  27. 27 def collect_page(pageUrl)
  28. 28 response = @client.visit(pageUrl, nil, 0.5)
  29. 29 case response
  30. 30 when Net::HTTPOK
  31. 31 webpage = Webpage.new pageUrl, response.body
  32. 32 else
  33. 33 return
  34. 34 end
  35. 35 end
  36. 36end
  37. 37
  38. 38# spider = WebSpider.new Proxies::Fiddler
  39. 39spider = WebSpider.new
  40. 40spider.collect Websites::Cnbeta

proxyserver.rb 运行此文件启动代理服务,并且挂载离线搜索页面

  1. 1# encoding: utf-8
  2. 2require "webrick"
  3. 3require "webrick/httpproxy"
  4. 4require_relative 'database'
  5. 5
  6. 6class OfflineProxyServer < WEBrick::HTTPProxyServer
  7. 7 def do_GET(req, res)
  8. 8 uri = req.request_uri.to_s
  9. 9 WebsitesDb.collections.each do |site|
  10. 10 if page = site.find_one(url:uri)
  11. 11 @logger.info("Found #{uri}")
  12. 12 res['connection'] = "close"
  13. 13 res.status =
  14. 14 res.body = page["data"].to_s
  15. 15 return
  16. 16 end
  17. 17 end
  18. 18 super
  19. 19 end
  20. 20end
  21. 21
  22. 22def search_db(keyword)
  23. 23 keyword.force_encoding("utf-8")
  24. 24 site = WebsitesDb.collection("cnbeta.com")
  25. 25 site.find(title:/#{keyword}/).sort(pubDate:"desc").limit(100).to_a
  26. 26end
  27. 27
  28. 28search = WEBrick::HTTPServlet::ProcHandler.new ->(req, resp) do
  29. 29 keyword = req.query["keyword"].to_s
  30. 30 result = if keyword.empty? then [] else search_db(keyword) end
  31. 31 resp['Content-Type'] = "text/html"
  32. 32 resp.body = %{
  33. 33 <html>
  34. 34 <head><meta charset="utf-8"><title>离线搜索</title><style>form{margin-left:30px}li{margin:10px}</style></head>
  35. 35 <body>
  36. 36 <form>
  37. 37 <em>cnBeta</em>
  38. 38 <input type="text" name="keyword" value="#{keyword}"><input type="submit" value="搜索">
  39. 39 </form>
  40. 40 <ul>
  41. 41#{
  42. 42 result.map{|page|"<li><a href=#{page['url']} target=_blank>#{page['title']}</a> #{page['pubDate']}</li>"}.join
  43. 43 }
  44. 44 </ul>
  45. 45 </body></html>
  46. 46}
  47. 47end
  48. 48
  49. 49server = OfflineProxyServer.new(Port: )
  50. 50server.mount("/search", search)
  51. 51Signal.trap(:INT){ server.shutdown }
  52. 52server.start

先运行webspider.rb,之后启动proxyserver.rb,在浏览器中把代理服务器设置成localhost:9999,就可以离线浏览已经在数据库保存过的网站。打开http://localhost:9999/search,就可以看到离线搜索页面。 来张图,看一下离线搜索的效果

如果你也想运行这个程序,需要以下步骤:

  1. 安装MongoDB 2.2.0,启动数据库服务
  2. 安装Ruby 1.9.3,安装程序中所用到的gems
  3. 安装Fiddler,非必需,可以不装
  4. 保存以上代码到相应的文件中,运行

C语言关注的是底层功能,Ruby关注的是高层功能,程序员应该把Ruby当作日常语言。通过这个练习,大家也可以看到Ruby写的代码简短易读,功能强大。整个程序全部代码加起来只有231行,很有说服力。当然了,这只是一个初始的版本,肯定有不足之处,请高手指正。在这个基础还可以增加新的功能,比如过滤广告,全文检索等等,发挥你的想象力!

以上就是用Ruby写的离线浏览代理服务器,只有两百多行代码的详细内容,更多关于用Ruby写的离线浏览代理服务器,只有两百多行代码的资料请关注九品源码其它相关文章!