Posts match “ ruby ” tag:

教与学的思考

Published on:

在今年6月份,很意外的接到了一休的电话,于是我受邀成为了51CTO的Ruby课程(诱人的Ruby)讲师。关于是否接受这个任务,我几乎没做什么思想斗争。从当年认识「小废物」,然后在51CTO上面写博客开始,再到认识一休以及其他的51CTO的其他几个管家,到目前为止已经过了五个年头了, 虽然有些年头不在51CTO博客上写文章了,但是51CTO这个团队给我的感觉,一直是很上进,真正做事的团队,所以在听一休说这个平台上线的时候,我几乎就是立马答应了。自己早年一直有写书的想法,但是一直没有实施,一个是没有好的契机,另一个是自己的懒惰,现在有了这个平台,我也愿意把自己的这个想法去实施了,同时也可以参与并推动51CTO学院这样一个在线教育平台的发展中去。当时,我想通过这件事应该会有属于自己的那份收获,当然,不是指钱。

距写这篇文章,已经过去4个月了。 让我没想到的是,我之前预想的收获,来的这么快,我想给大家分享一下。


上学的时候我就对老师这一行当十分讨厌,当然,不是讨厌具体某位老师,而是单纯的讨厌这个职业。所以在录制视频开始,我就给自己定位,不当老师,只作为一个和大家分享所学的好朋友。但是在录制了几个课时之后,我逐渐的发现,老师这个职业,真的不容易,之前讨厌老师那种想法,也逐渐的淡了。 之前没有「教」过别人,所以无法体会,录制视频之后,感觉,如果想给别人讲好一个知识点, 讲清楚,真的没那么容易, 因为这些东西你自己理解了是一个层次, 要讲出来,而且还要求别人听的懂听的明白,并达到让别人知其所以然,又是另一个层次。要达到这个层次,必然需要花很多精力去备课,去录制,自己先反复的听去修改以达到自己满意的程度。 我开始体会到,成为一个授人以渔的好老师其中的艰辛。

所以,第一个收获,就是尊重每一位老师,尊重每一位给别人分享知识的人。之前老看盗版的电子书,现在我也开始购买正版了。


备课固然艰辛,但是如果你付出这份艰辛之后,就可以从中获取到另一个收获。

知识都是相通的,如果你想讲一个知识点,并把它讲全,你固然会运用到其他支援这个知识点的其他知识点,如果做为一个讲师,你不懂这其中的因果关系,就难以给别人讲通那个你要讲的知识点。 老聃在「道德经」里说过:「道生一,一生二,二生三,三生万物」,这是万物固有的道, 而师者,其实就是这样一个逆向工程,「万物 > 三 > 二 > 一 > 道」,你需要引导学你课程的人,从表象到本质,明白其中的道。

拿我的Ruby课程来说,每个课时的知识点,我都要搞清楚来龙去脉,比如讲线程的时候, 就要告诉学员,线程存在的意义,它的出现是为了解决什么问题,人们怎么用它解决这些问题的?有没有什么最佳实践?而在Ruby中的线程是如何实现的,Ruby这门语言,对于线程所要解决的问题,起到多少助力?和其他语言比有何优劣?这个过程,完全可以用哲学上那三个终极问题来形容:「我是谁?我从哪里来?我到哪里去?」

其实作为讲师的我,在前几年的工作中,虽然也在研究Ruby,但是我的学习方式,从来没有像录制视频课程这么通透, 为了录制一个课时,讲清楚一个知识点,去一个个解开相关问题的因果锁链,我也在学习,但是这种学习效果,带给我的不仅仅是知识,更是一种求知欲,对知识的探索,因为这些问题的因果锁链,越解越多,越多越想解。这种感觉是我在多年的工作中,完全没有体验过的。所以,我也希望通过视频,可以把我这种学习、思维方式带给学员朋友,达到授之以渔的效果。

所以,这是我的第二个收获,掌握了真正的学习之法,学习之道。 除了Ruby视频,我也在筹备我的第一本电子书「通往Ember.js之路」,以完全采用这种思维方式去编写,我希望它会是一部与众不同的技术书。

附我做的封面图:


我目前出了两部课程,计划是出四部,因为我也算一个完美主义者, 要出就出系统,出完整。

入门版,目前是免费的,基础提高篇,收费。播放量也有很多,我也建了一个QQ群,目的是让我的学员能和我多交流。免费入门篇课程,看的人很多,目前近3000多播放量,但是这么多人看,根本没几个人和我反馈,真正和我反馈的只有付费的人。而且,我还发现,很多人不愿意去看视频,不知道是不是觉的免费的东西不值得看还是怎么地,其实免费的课程,我也下了不少精力,看过视频的人,应该能看得出我用的心思。我也在不断的学习国外的视频录制技巧,压缩课程时间,尽力控制时间,让视频内容达到一个精致, 不断调整模板,尽力做到让学员视觉上养眼,还有上面所说视频录制的艰辛过程。有的人在群里问一些我在入门视频讲过的最基础的Ruby语法知识,很明显就是没有看视频,这让我很受伤,让我感觉录制免费入门课程有点浪费时间。

直到我昨天看到一篇文章:「高价如何打败低价」,犹如醍醐灌顶(因为是微信看的文章,所以这里不分享地址了)。 于是我明白了,只有真正想学习Ruby的人才来看你的视频,而不是因为你的视频去学习Ruby。

所以,我的第三个收获就是,产品,来做给真正用它的用户,价格体现价值。 我决定入门篇出完之后,结束其免费,改为付费,只给真正想学习Ruby的人。


有的人说:「要先感受到价值才愿意付费」,由此,我又对于「产品的价值」、「免费与付费」仔细想了一下, 下面简单说说我的观点:

价值,价值,有价才有值。

你花钱买的产品,你才会感觉到它的值或者不值。就像你花1w多买台RMBP,你会说,哇,超值!但是你花1w多买了索尼,联想或者其他的笔记本,你可能会觉的,真烂,根本不值那么多钱嘛。

而对于免费来的产品,比如,蛋糕店试吃的蛋糕,你吃完只会感觉,嗯,真的好吃,或者感觉,不好吃,你不会产生那种值不值的感觉,因为你没有付费。或者去参加RubyConf的时候,大会送的礼物,送的书之类免费的产品, 你也只会给一个好与不好的评价。当然你买了RubyConf大会的票,那么你就会觉的,参加这个大会值了,还有礼物。

所以,对于免费使用,增值付费的产品,免费只是噱头,是一种引导。也就是有人说的,感受到价值的过程。就像你不花钱玩网游一样,装备基本可以用「垃圾」来形容了,你会有值不值的感觉吗? 但是你正好花6块钱,买了个会员,一跃而成为土豪级别,你这个时候才会有值的感觉。

所以只有付费的产品,也就是商品,才有价值,直接定位的就是需要这些产品的人,没有前面的引导过程。而我的视频,从最初就定位在这个类型的产品之上,所以才有了现在的这些感悟。

纯免费的产品,充其量就是慈善。

有的人可能会讲,那开源的产品算什么? 算慈善。至于做这个慈善能给作者带来其他利益,就不在价值的讨论范围了。

所以,价格,是产品价值不可或缺的一部分。 价值,是你对这个产品的真实体验,是你花钱买东西,使用之后的感受。在买东西之前,用户去根据价格+产品的质量来判断是否买的值。而产品的质量,在未购买之前,完全是猜, 当然可以提供试用机会,还有完善的退购,保修策略,包括售后服务。 这才是真正价值。

心中所想,写成文字以后,感觉好多了。

「勿忘初心」TDD,Don't DDT

Published on:
Tags: ruby

偶尔翻到自己五年前写的关于TDD的文章,又看一遍, 感觉当时的自己是走在正确的路上。但是这么多年过去了,BDD也在流行,自己却没有一直沿着这条路走下去,为什么? 老文重拾,感慨良多。

这个过程是我当时在用Ruby改造一个php的遗留系统,具体细节已遗忘。


正文

一直有个疑问,对于遗留系统,我们该如何TDD ?

我个人比较认同TDD是一种设计方法,不能代替真正意义上的测试。是帮助我们设计自己代码的一种方法。对于遗留系统,面对一堆需求文档,面对一陀陀已经难 以继续维护的陈旧代码,你的心是否哇凉哇凉的 ?做为一个使用Rails的开发人员,难道你要把这些代码翻译为Ruby代码吗 ?
答案当然是不!
很多时候,我们虽然很清楚要TDD,但是很多时候我们是在DDT。基本上是根据遗留系统的代码把功能代码写的差不多了,才想起来写测试。写完之后, 用流行的rcov测试一下测试代码覆盖率,不足的地方再加上测试 ? 回头想想,我们引入TDD的初衷是这样的吗 ? 大家都心知肚明,并不是这样的!
重新思考TDD之后,我决定在现做的这个ticket里完全采用了TDD的方式去开发,今天一天下来,感触颇深:
1。 昨天拿到php源码,一看代码,了不得啊,和我功能相关的源码文件之有三个,但代码却有三千多行。我该怎么办 ?我肯定不可能把那三千多行都看完吧。我该如何TDD ?
T代表Test,测试驱动开发,很显然,是先有测试,要不谈何驱动开发。 但是没有功能代码,你测试什么呀? 既然TDD是一种设计方法,那么这个测试,代表的应该是你大脑中对这个功能的自我认知。 如果对需求了解不清楚,你是没有办法对这个功能产生自我的认知的。硬着头皮整理了一下需求文档,在脑中形成一个大概的轮廓之后,就可以动手写测试了。此功 能是要生成一个报表,就是一个create -> show 的过程。一般人想到的先是页面,我是一般人,所以我也是从页面开始的,根据已经明确的需求写下自己对这个页面的幻想:

/spec/views/reports/new.html.erb_spec.rb中:

 require File.dirname(__FILE__) + '/../../spec_helper'

  describe "reports/new.html.erb" do

   ....  ..

   it "should render new.html.erb" do
     render "/reports/new.html.erb"

     response.should have_tag("form[action=/reports/new]") do

         #应该有一个label来表明这个field是company name

         with_tag('label',"company name:")                                                       
         with_tag('select#data_company_id[name=?]',"data[company_id]")         

          #应该有个下拉列表来让用户选择存在的company name

         with_tag('label',"Report Filter by Year:")                                                     

         #应该有个时间列表来让用户选择生成报表数据的时间段。

        with_tag('select#report_filter_by_year[name=?]',"report_filter[by_year]") 
        end
    end
   .... ..

 end

好了,写到这,我脑中只剩下了一点不清楚的需求。也就是说,我如果能把这个测试写完,脑中对这部分的需求应该就明朗了。继续看文档,看php代码,只看一 些sql语句。一个疑问就是这个页面用不用加一个排序的下拉列表 ? 这个疑问得问和我们合作的老外Andy,正好他还未到睡觉时间,就问他:
new page whether should need select down list to sort by some column ? 他回答 : sounds like good .
very good , 我可以把上面这句英语翻译成rspec代码:

   with_tag('label',"Sort by:")
   with_tag('select#sort_by_sorted_on[name=?]',"sort_by[sorted_on]")

这样我的这个测试就全部写完了:如下

require File.dirname(__FILE__) + '/../../spec_helper'
describe "reports/new.html.erb" do

   ....  ..
   it "should render new.html.erb" do
     render "/reports/new.html.erb"

     response.should have_tag("form[action=/reports/new]") do
        with_tag('label',"company name:")                                                       
        with_tag('select#data_company_id[name=?]',"data[company_id]")         
        with_tag('label',"Report Filter by Year:")                                                     
         with_tag('select#report_filter_by_year[name=?]',"report_filter[by_year]") 

        with_tag('label',"Sort by:")
       with_tag('select#sort_by_sorted_on[name=?]',"sort_by[sorted_on]")

      end
    end
   .... ..

 end

打开autotest,运行,报错:没有这个页面。好办啊,建个页面不就得了。建立new.html.erb.
运行测试,报错,没有 , 好办啊,在new.html.erb里加个label不就得了。
根据错误信息的提示,我很快就把页面的基本元素写好了。
但是下拉列表的选项,是需要从数据库里查询来生成options的。那么就在model里写个测试方法了:

   #应该返回一个以年份组成的二维数组

   it "should return a array by year" do   
     year = Entry.get_filter_year
     year.should be_an_instance_of(Array)
     year[0].should be_an_instance_of(Array)   
   end  

这样,运行autotest,会报错,Entry没有定义get_filter_year方法,好办啊,加上不就得了吗。
。。。 。。。
不断的根据错误提示来写功能代码。
。。。。
测试终于全部通过。
我的功能代码也完成了。
我对页面的那点幻想终于成了现实。
这就是TDD。

这个TDD的过程里,我没有开过浏览器。一点不吹牛,信不信由你!

TDD的好处:
1。帮助你理清思维,去把遗留系统的需求逐步的挖掘出来,而不用去把客户给的三千行代码看完。
2。驱动你的开发。整理需求写完测试之后,然后只需要运行autotest来利用failure信息提示完善你的功能代码,这个时候开发已经相当轻松了。
3。 这样写的功能代码用rcov测试test覆盖率的时候绝对是百分百覆盖的。


补:有人问, 能否推荐一个比较优秀的TDD/BDD案例?

大家可以看看这个,2013 Rails Rumble Gem Teardown。 Rails Rumble每年都会举行48小时的创业大赛,参赛的队伍必须在48小时内写出自己的参赛作品,这篇文章通过分析参赛队伍的Gemfile来统计了一些Gem的使用情况。 其中,我们可以发现有些团队是使用了TDD/BDD的Gem的,比如Rspec,但是具体他们有没有写测试代码就没有具体分析了。

我觉的想找出TDD/BDD比较优秀的案例,可以从参赛队伍的作品里去寻找,因为48小时内,还可以把TDD/BDD写的很完善的人,那这个人或团队,真的是在用TDD/BDD去进行开发实践,已经变成他们的编程习惯了, 那么他们的作品就应该是一个优秀的TDD/BDD案例。

「追根溯源」Ruby数组的uniq方法

Published on:
Tags: ruby

昨天,在群(「诱人的Ruby」 QQ学习群号:222359552)里,@赵宇 问了一个问题, 为什么他修改了hash方法和eql?方法,会对uniq方法有影响?

他给出的代码是这样的:

class Item
  attr_reader :item_name, :qty
 
  def initialize(item_name, qty)
    @item_name = item_name
    @qty = qty
  end
 
  def to_s
    "Item (#{@item_name}, #{@qty})"
  end
 
  def hash
    puts "#hash? invoked"
    @item_name.hash ^ @qty.hash
  end

  def eql?(other_item)
    puts "#eql? invoked"
    @item_name == other_item.item_name && @qty == other_item.qty
  end
 
end

p Item.new("abcd", 1).hash
p Item.new("abcd", 1) == Item.new("abcd", 1)

items = [Item.new("abcd", 1), Item.new("abcd", 1), Item.new("abcd", 1)]
p items.uniq

当运行上述代码之后,你会发现, 在Item类里他修改的hash和eql?方法是对items.uniq的结果有了影响。


第一眼看到这个问题,挺不以为然,这不是明摆着的吗,因为Ruby Doc里Array#uniq已经说了, uniq方法会依赖hash和eql?两个方法。

但是@赵宇说,他想知道uniq的内部机制, 于是我把Ruby Doc里显示的uniq方法的源码发在群里,当我发在群里之后,也仔细看了下源码(c代码),才发现,这里并没有调用eql?方法的痕迹, 因为eql?方法在C源码里是rb_ary_eql这个函数, 看到这里,我有点好奇了, 就想探寻一下uniq的工作机制。

我打开了github上Ruby的源码,搜索了一下, rb_ary_uniq,因为这是Ruby的uniq方法在C源码中的函数名, 于是就找到了array.c这个文件里对uniq方法的定义:

static VALUE
rb_ary_uniq(VALUE ary)
{
    VALUE hash, uniq;

    if (RARRAY_LEN(ary) <= 1)
        return rb_ary_dup(ary);
    if (rb_block_given_p()) {
        hash = ary_make_hash_by(ary);
        uniq = rb_hash_values(hash);
    }
    else {
        hash = ary_make_hash(ary);
        uniq = rb_hash_values(hash);
    }
    RBASIC_SET_CLASS(uniq, rb_obj_class(ary));
    ary_recycle_hash(hash);

    return uniq;
}


(比对一下ruby-doc.org上面查看的方法的源码,你就知道哪个更靠谱一些了。)

其实我在「诱人的Ruby - 入门篇」的第19课时,讲过如何用C扩展一个ruby gems, 我们知道比较安全和常用的一种扩展方式就是使用"ruby.h",利用Ruby定义好的库,相当于可以用C来写Ruby代码了,在这个库里,函数的命名都很有规律的。

@赵宇给的代码里,使用uniq后面并没有加block,那么,当uniq执行的时候, 程序的执行会走进else语句里,那么我们重点要考察的就是这两行代码了:

    hash = ary_make_hash(ary);
    uniq = rb_hash_values(hash);

我们先来看看第一行代码:hash = ary_make_hash(ary)


我们看到ary_make_hash这个函数,是ary打头命名的函数,那么就应该是在array.c这个文件中被定义的,所以直接在这个文件里查它的定义:

static VALUE
ary_make_hash(VALUE ary)
{
    VALUE hash = ary_tmp_hash_new();
    return ary_add_hash(hash, ary);
}

很轻松就查到了, 代码就两行,可以看得出来, 这个方法是把uniq方法传入的数组,给变成了Hash。ary_tmp_hash_new(), 这个方法甚至都不用去查,可以想到,这是创建一个临时的Hash结构,然后使用ary_add_hash(hash, ary)这个方法,把数组加入到那个临时的Hash里去

static VALUE
ary_add_hash(VALUE hash, VALUE ary)
{
    long i;

    for (i=0; i<RARRAY_LEN(ary); i++) {
        VALUE elt = RARRAY_AREF(ary, i);
        rb_hash_aset(hash, elt, elt);
    }
    return hash;
}

在ary_add_hash函数里,我们看得到, 传进来的数组被循环的插入到Hash里, 那么最终这个Hash的结构是什么样子的呢?或者说, 数组是按什么规则变成一个Hash的呢? 看来有必要去考察一下这个rb_hash_aset的方法了。

可是无奈的是, 我没有在ruby源码中找到这个方法的定义, 原因是我对C语言不是很熟悉,希望哪位看到文章的大神帮我找一下。

不过,我在查找过程中,发现这个函数的用法是这样的:

rb_hash_aset({}, key, value)
就是说, 这个方法执行以后,会创建一个类似于这样的hash:{key => value}

明白了rb_hash_aset方法的用法之后,就不难了解,在ary_add_hash里, 最终传进来的数组变成了下面Ruby语言描述的Hash结构了:

arr = [1, 2, 3, 4]
hash = {1 => 1,  2 => 2,  3 => 3, 4 => 4}

也就是说,ary_add_hash方法按传进去的数组每个元素自身做key和value,来创建了一个Hash。

这个时候,rb_ary_uniq方法里,

  hash = ary_make_hash(ary)

这句代码算是解释清楚了。

那么还剩下另一行代码,我们看看:

uniq = rb_hash_values(hash);

到目前为止,我们发现一个命名规律:

rb_ary_uniq # 是Ruby语言里uniq方法的内部定义
ary_make_hash # 是array.c文件中,Ruby源码内部调用的函数

所以我们得出一个结论, rb_hash_values,应该是在hash.c文件里定义的, 结果,搜了一下, 果然如此

VALUE
rb_hash_values(VALUE hash)
{
    VALUE values;
    st_index_t size = RHASH_SIZE(hash);

    values = rb_ary_new_capa(size);
    if (size == 0) return values;

    if (ST_DATA_COMPATIBLE_P(VALUE)) {
        st_table *table = RHASH(hash)->ntbl;

        if (OBJ_PROMOTED(values)) rb_gc_writebarrier_remember_promoted(values);
        RARRAY_PTR_USE(values, ptr, {
            size = st_values_check(table, ptr, size, Qundef);
        });
        rb_ary_set_len(values, size);
    }
    else {
        rb_hash_foreach(hash, values_i, values);
    }

    return values;
}

这个方法太长了, 但是幸好有注释啊:

 *     h = { "a" => 100, "b" => 200, "c" => 300 }
 *     h.values   #=> [100, 200, 300]

原来这个方法,就是获取hash的所有values。


于是,我们终于得出结论了, Array#uniq的工作机制,可以用下面Ruby代码描述:

arr = [1, 2, 3, 4, 1, 2]
hash = {1 => 1,  2 => 2,  3 => 3, 4 => 4}
new_arr = hash.values  #=> [1, 2, 3, 4]

原来, uniq方法,就是把数组变成了Hash,然后利用了Hash的Key值不能重复的工作机制来去重复的。


但是到目前为止,我们还是没有解决,为什么修改了Object#hash和Object#eql?方法会对Array#uniq产生影响。但是在了解了uniq方法的工作机制之后,我们会想,难道,是创建Hash的时候, 判断Key值重复的时候,使用了Object#hash和Object#eql?方法? 想到这里,我马上翻开了Object#hash方法的文档看到了以下文字:

The hash value is used along with eql? by the Hash class to determine if two objects reference the same hash key.
翻译过来就是:这个hash值,是在Hash结构使用eql?方法去确认是否有两个相同的对象引用被当作了Hash key的时候被调用的。

最后,我在@赵宇的代码后面加了几行代码去验证上面所说:

item1 = Item.new("abcd", 1) 
item2 = Item.new("abcd", 1)

item3 = Item.new("abcd", 2)

h = {}
h[item1] = item1
h[item2] = item2

到此,迷雾解开:

- uniq方法,就是把数组变成了Hash,然后利用了Hash的Key值不能重复的工作机制来去重复的。

- Hash检查key值是否重复的时候,调用了Object#eql?和Object#hash方法。


最后欢迎关注公众帐号: RubyStudy


REPL Drive Development

Published on:
Tags: ruby Pry

REPL, 全称Read-Eval-Print-Loop, 通俗的说,就是一个代码交互环境,你输入什么,它执行什么,并把结果打印出来。像Ruby的irb,Node.js的node,CoffeeScript的coffee,都是REPL的工具。

REPL Drive Development这个概念是在不久前迈阿密举行的RubyConf大会上由ConradIrwin提出的:
[视频PPT]

我们听过TDD-测试驱动开发,BDD-行为驱动开发([视频PPT]), REPL Drive Development - REPL驱动开发, 我还是头一次听说。 但是看完视频之后,我明白了, REPL Drive Development并不是要取代TDD/BDD,其实REPL Drive Development在我们日常工作中大多数人都会经常用到。我们在开发一段功能代码之前,在没有写测试代码和功能代码之前(假设你是Rubyist),总会打开我们的irb,或者现在最流行的pry,在里面去尝试一下你对这个方法的一些构想,比如你要写一个数组的排序方法,那么你可能会在irb或pry里去测试下each方法的用法:

pry(main)>  arr = [1,2,3,6,5,7]
pry(main)>  arr.each {|i| puts i}
#or

pry(main)>  arr.sort

尝试完之后,我们就可以去写测试方法或者是功能代码。 通过这个简单的例子,我们明白,其实REPL Drive Development,我们一直在实践,只是之前没有人给我们总结出这个概念而已,而ConradIrwin,带给我们的优秀工具-pry, 就是基于这种理念去开发的。

pry,简单的介绍,是替代irb的工具,但实际上,它不仅仅是个REPL, 它可以作为一个贯穿我们整个开发过程的一个基础工具,源码阅读、构想实践(就是我们上述的例子)、TDD/BDD开发和Debug等,pry都会给我们提供强有力的支持。

帮助你构思代码:

你可以使用pry提供的三个简单的命令:

  1. ls [Class/Module] , 用于列出类或模块里自己定义的方法,不包括继承来的方法

    [1] pry(main)> class User
    [1] pry(main)*   def hello
    [1] pry(main)*     puts 'hello'
    [1] pry(main)*   end  
    [1] pry(main)*   def self.world
    [1] pry(main)*     puts  'world'
    [1] pry(main)*   end  
    [1] pry(main)* end  
    => :world
    [2] pry(main)> ls User
    #<Class:User>#methods: world
    
    User#methods: hello
    
  2. $ (或 show-source) 命令,可以显示一个方法或类的源码。

  3. ? (或 show-doc)命令,可以显示一个方法或类的自定义的文档。

帮助你写Test:

可以安装pry-plus这个gem,可以帮助你去写测试:

  gem install pry-plus

然后:

  require 'pry-rescu/minitest'
  #or

  require 'pry-rescu/rspec'

你就可以使用pry,结合minitest和rspec来写测试了。 详细的代码示例,可以参考RubyConf的视频。
pry提供了下列命令,帮助你写test:

> try-again
> break
> step/next
> play
> edit

查找修改Bug:

pry也可以帮助你去debug代码:

# test.rb

require 'pry'

class A
  def hello() puts "hello world!" end
end

a = A.new

# start a REPL session

binding.pry

# program resumes here (after pry session)

puts "program resumes here."

你可以在你的代码中想打开pry的位置放置binding.pry, 当代码执行到这里的时候,就会打开一个pry session:

pry(main)> a.hello
hello world!
=> nil
pry(main)> def a.goodbye
pry(main)*   puts "goodbye cruel world!"
pry(main)* end

pry还给你提供了用于debug的很多犀利的方法:

> binding.pry
> cd
> up/down
> whereami

当然, 你可以在pry里面使用'help'命令,来了解pry给你提供的众多神奇工具。

> help

总结:

使用Pry可以提高我们的开发效率,更有效的帮助我们完成测试代码,减少bug的产生,也更容易帮助我们debug,快把它用起来吧! 我相信已经有很多Rubyist已经在用它了。

以上就是REPL Drive Development与Pry的介绍,大家可以去看RubyConf的视频,更详细的代码demo都在视频里。

如何学习Ruby

Published on:

提示:本文是学习方法论,不喜勿看。

我是从06年底开始关注Ruby的,07年的时候投入到了Ruby的开发中,并持续到今天。我从毕业到现在,除了Ruby还使用过VB、Java、JavaScript, 大学里也学过C/汇编,因为我是学电子专业的,偏硬件一些,所以没有很好的学习算法相关的内容,所以本文也不会涉及算法学习的内容, 这是我作为一个程序员的硬伤,当然我还在学习算法的路上,并未放弃它。

和大多数的Rubyist一样,我也是从学习Rails开始去了解Ruby的,在学习Rails之前,我正在使用JavaEE的SSH框架(struts+spring+hibernate), 当时也算是Java入门的阶段,并没有做的多深,各种XML配置,搞的我眼花缭乱,对Java顿时有点绝望:是不是我以后的编程生涯就和XML打交道了呢? Rails的约定大于配置,DRY(Don't repeat yourself)等概念,深深的吸引了我,使用Rails,再也不用去烦那一大堆XML配置了,我只要专心写我的Ruby代码就可以了,这才是快乐的程序员嘛。呵呵,不知道有多少人和我的经历类似呢? 我想应该也有不少人吧。

说到Rubyist, 我曾经也不理解,为什么叫Rubyist,而不叫Rubyer或者其他? 后来我才明白了, Rubyist,有点Artist的意味。Ruby语言算是编程领域里最具艺术气息的了,所以我们把Ruby程序员叫做Rubyist。但实际上,很多人离真正的Rubyist还很远,包括我自己啦。


懵懂:

正是经历过这个阶段,所以我很了解现在的初学者的心情:「不是15分钟开发一个blog吗? Rails很好学,很好用,很好玩,我要快速精通它」。 很多Rubyist的入门书就是「Agile web development with Rails」和 「Programming Ruby」, 基本上,如果通读这两本书并练习以后,基本就可以上手了。当然现在也涌现出很多学习Rails的优秀书籍,比如 「Ruby on Rails Tutorial」和最近kickstarter上面众筹的「Learn Ruby on Rails」,还有官方的「Ruby on Rails Guides」, 在我开始学习的时候,官方的文档可没有这么好。

有这么丰富的资料,我们对于Rails的入门,基本没有什么困难了。 但是你有没有发现,你已经陷入了一个巨大的细节里面去了。我所说的这个巨大的细节,就是指Rails。

我们最初学习Ruby的目标是掌握Rails, 所以潜意识里,Ruby的学习,放到了第二位, 而Rails成为了第一位的学习目标。在你通读各种参考书入门之后,也许你可以开发一个简单的web站点,也可以把自己的Rails技能运用于一般的工作中,但是你会发现,你终会达到一个学习的瓶颈。 Rails社区的一个好处,就是有很多的Gem,各种各有的Gem让你快速的开发,但是你真正的了解这些Gem吗?开发,并不是一个简单的功能堆砌。

当你听说现在流行Cucumber,所以对自己该选用Cucumber或者是Rspec感到迷惑了起来,MiniTest的崛起,又让你感到更迷惑了,我该用哪个?
当你听说Rails4默认了线程安全,那么你是否知道什么是线程安全?Rails3不也是线程安全的吗?默认的意义何在?
当你听说sidekiq比resque更省内存的时候,就马上想去尝试改用sidekiq,有没有想过,为什么?sidekiq比resque写的好?
。。。
等等

这个时候,我们仅停在了使用阶段!因为你已经陷入了Rails的细节里,你忽视了Ruby。

所以,此时,你仅仅是个Ruby码农,离Rubyist,还差很远。


深入

如你所见,你所用到的一切,都是Ruby构建起来的:Rails、Sinatra、Cucumber、RSpec、Sidekiq、Resque等。
Ruby + Web开发知识 + 架构思想 -> Rails/Sinatra
Ruby + TDD/BDD等敏捷实践 -> Cucumber/Rspec
Ruby + 多线程/多进程并发 -> Sidekiq/Resque
Ruby + Actor并发模型 -> Celluloid
Ruby + Socket+Reactor -> eventmachine/Goliath
...
等等等

所以,你要想深入掌握上述各种工具,你就需要系统的去学习Ruby,并且在学习Ruby的过程中,去学习相关的领域知识,这是一个良性的循环过程。

Ruby是一个面向对象并同时兼有函数式编程特性的神奇语言,所以在你随着Ruby的深入学习,你同时可以深入学习面向对象和函数式编程两种范式的特性。

所以,当我们说深入学习Ruby的时候,不是在说Ruby的语法该如何用,而是在说:

1 Ruby背后的设计哲学及其底层实现。
2 Ruby与系统(包括其他领域,比如web)的交互
3 Ruby如何根据它的面向对象和函数式编程特性、及其元编程能力去构建漂亮的DSL

达到这个阶段,你可能脱离了Ruby码农的层次,达到一个新的境界了,离Rubyist已经很接近了。


哲学

如果说算法是一个合格的程序员必须掌握的,那么哲学,也是一个合格的程序员必须要领悟的。

我不是一个哲学大师,请不要误会。

哲学是我们学习和解决问题的灵魂,我只在这里分享几条我用到的哲学经验:

1. 重是轻的根本,静是躁的主宰.

Ruby,只有基础扎实了,才能用的灵活。
当我们碰到问题的时候,着急没用,静下心来思考,顺藤摸瓜,找出问题。
当我们学习的时候,不要急于求成,静下心来学习,慢就是快。

2. 存在即合理。

对于这句话,我的理解就是, 当程序出现了诡异的bug,或者,当你的项目在你机器上能用,而到服务器上不能用的时候,你应该想想,这个问题并不诡异, 程序是按你的指令执行的,肯定是你哪里出问题了,而且可能是个严重的问题!请认真对待这个问题!
学习的时候,碰到一个概念,应该多想想这个概念产生的合理性,比如TDD/BDD, 有了TDD为什么还会有BDD?

3. 哲学经典三问: 你从哪里来?你是谁? 你要到哪里去?

当我们学习新技术的时候,我们就该问了:
这技术是基于什么情形产生的,或者是这技术是为了解决什么问题而出现的? (你从哪里来?)
这技术是如何实现的?这项技术如何去使用? (你是谁?)
这技术要解决的问题是永久性的吗?未来的发展会如何?和这种技术类似的有哪些技术,它们的发展如何?(你要到哪里去?)


总结:

本文,与其说是「如何学习Ruby」,不如就说是「如何学习」,因为以上方法论,适用于你学习任何东西。


P.S 附带「诱人的Ruby」课程规划简要:


最后,

希望大家关注微信公众帐号:RubyStudy,与我讨论任何问题,包括对本文你自己的见解。

大话Ruby Block

Published on:
Tags: ruby block

这是Ruby中「方法和块(block)之间」的夺魂战!

注:本帖,不讨论Ruby中块(block)的函数式语言特性, 在「诱人的Ruby」视频中已有相关论述。


一、 块(block)是一段「半自由」的代码,其形在方法外,而魂归方法里。

块(block)代码由{...}或do...end界定而成。

   [1, 2, 3].each{|i| i + 1}

块(block)代码不能被单独调用,只能依附于方法,所以称其为魂归方法里。

二、 块(block)魂归自身,获得完全自由的唯一方法就是,让其对象化。

魂归自身,意味着,它完全自由了,可被单独调用了

  Proc.new{ puts "hello"}
  proc{ puts  "hello"}
  proc{|msg| puts "hello #{msg}"}
  
  lambda{puts "hello"}
  lambda{|msg = "world!"| puts "hello #{msg}"}
  
  ->{puts "hello"}
  ->(msg="world!"){puts "hello #{msg}"}

以上就是Ruby提供的所有块(block)对象, 都是Proc类的对象。

三、 用不用块(block),是你的自由,但是方法天生就是块的魂归之地。

Ruby中任何一个方法你都可以传递一个块。

   def test;end
   test{ puts i} 

上面的代码,甚至都不会报错,因为在test方法内部没有去使用这个块(block)。
显然,你已经不能把块(block)当成那个常规的方法参数了,虽然它的行为像参数。

四、 如何使用块(block), 直接决定了它的命运!

A. 在方法内部使用yield关键字去引block的“魂”。

  def test
    yield
  end
  test{puts "hello test!"}
  
  def test(x)
    yield(x)
  end
  test('world!'){|x| puts "hello #{x}"} 

yield关键字不仅可以挂载块(block)代码,而且可以给块传递参数。

B. 魂归自身式:使用「&」让一个传递给方法的块(block)魂归自身。

  def test(&block)
    block.call("world")
  end
  
  test{|msg| puts "hello #{msg}"}

block到了方法内部,已经被&转化为了一个Proc对象。
但是在方法内部,也可以使用「&」给块对象(Proc对象)抽魂:

  def test(&block)
    inner_test(&block)
  end
  
  def inner_test
    yield("haha!")
  end
  
  test{|msg| puts "hello #{msg}"}

test方法传进去的block被转化为了Proc对象,而其内部的inner_test又利用「&」把这个Proc对象转化为了半自由的块(block)
test方法真阴险!有木有?

五、 争风吃醋的block兄弟。

{...}和do ... end,虽然是块(block)孪生兄弟,但是也有个谁先谁后的,使用的时候注意,否则它们会让你知道谁才是老大!

  def test(block)
    block.call
  end
  
  test lambda do 
     puts "hello"
  end

你给test方法传递了一个自由的Proc对象,但是稍有不同,这个Proc对象是用do...end,但是,你执行代码后会发现,这段代码会抛出异常:ArgumentError: tried to create Proc object without a block

  test( lambda do
          puts "hello"
        end
      )

必须要这么修改才可以正常执行。或者使用:

  test lambda{puts "hello"}

这样也可以正常工作。

可以看得出来, do...end 不如 {...}优先级高,{...}亲近离它最近的那个lambda, 而do...end却亲近最远的那个test。
感觉do...end有点混,所以类似于这种把Proc对象传递给方法的时候,一定要注意用括号,或者用{...}

不过由于这个抛出的异常,ArgumentError: tried to create Proc object without a block,我们倒是可以得到一个隐式传递block给方法的写法:

  def test
    proc_obj = Proc.new
    proc_obj.call
  end
  
  test{puts "hello"}

六、 块(block)也有自尊。

  def test
    x = 1
    yield(x)
  end
  
  x = 2
  test{ puts x }

块(block)有个超能力,穿透作用域。 上面例子里,块(block)并没有使用test方法里的变量x=1, 而是把块(block)作用域外的变量x=2抓在了手里。

七、 对于块(block)的自尊, 方法向你(Rubyist)求助!

  def test
    x = 1
    yield(x)
  end
  
  x = 2
  test{ |x| puts x }

你(Rubyist)往块(block)里插入了个参数,顿时霸气侧漏。方法对你感激不尽,你骄傲的昂起了头 。。。

八、 块(block)还是不服!向你(Rubyist)求助,恢复其自由身!

你(Rubyist)帮块(block)实现了自由身,使用lambda对象化了块

  def test
    x = 1
    lambda{ puts x}
  end
  
  lambda_proc_obj = test
  lambda_proc_obj.call

这下块对象(lambda_proc_obj)出气了, lambda_proc_obj想啥时候调用就啥时候调用(lambda_proc_obj.call)。
lambda_proc_obj心想:「哥把你这方法打包了,掌握着你的数据(x=1),再也不看你脸色了。」
噢, 原来如此, 让块(block)完全自由以后变成Proc对象, 它就有闭包的超能力了,好可怕!

九、 两种块对象(Proc对象), 两种命运,可悲可叹!

A. 被方法所奴役 : 必须要执行的代码段

  proc_obj = Proc.new{ return "from proc return"}
  # proc_obj = proc {return  "from proc return"}

  
  def test(block)
    msg = block.call
    puts "hello! #{msg}!"
  end
  
  test(proc_obj)

这段代码会抛出LocalJumpError异常, 用Proc.new和proc{}定义的Proc对象,是不能使用return的。
这就意味着,这个Proc对象,一旦被call,就必须要执行完。

注意这里test方法传递一个本身就是Porc的对象,并没有使用&。

再看看这段代码,看看这俩Proc对象对arity的关心程度(是否检查块对象的参数个数)

def call_with_too_many_args(closure)
  begin
      puts "closure arity: #{closure.arity}"
      closure.call(1,2,3,4,5,6)
      puts "Too many args worked"
  rescue Exception => e
      puts "Too many args threw exception #{e.class}: #{e}"
  end
end
 
def two_arg_method(x,y)
end

puts "Proc.new:"; call_with_too_many_args(Proc.new {|x,y|})
puts "proc:"    ; call_with_too_many_args(proc {|x,y|})

这段代码可以看出来, Proc.new或proc{}定义的proc对象,完全是方法的奴隶啊!
工作必须执行完不说(无法return),参数个数给传多少都不能发出半点怨言啊!僵尸? 奴隶?

B. 一段匿名的方法体

  lambda_proc_obj = lambda{return "return from lambda proc"}
  # lambda_proc_obj = ->{return "return from lambda proc"}


  def test(block)
    msg = block.call
    puts "hello! #{msg}!"
  end
  
  test(lambda_proc_obj)

可以看得出来,lambda proc对象是可以正常返回的.

  puts "lambda:"  ; call_with_too_many_args(lambda {|x,y|})

这段代码,就明白了,lambda{}方式创建的Proc对象,才是真正的自由块对象啊!
工作想休息的时候就休息(可以return), 给传的参数多了,也可以发出怨言。
自由的空气,真好。

看来lambda proc和Method对象,有点类似:

  puts "Method:"  ; call_with_too_many_args(method(:two_arg_method))

十、 你(Rubyist)有四种方式对可怜的块对象呼来喝去。

  lm_proc_obj = lambda{puts  "hello world!"}
  
  lm_proc_obj.call
  lm_proc_obj.() #无参数必须给一个空的括号

  lm_proc_obj[] #无参数必须给一个空的中括号

  
  lm_proc_obj = lambda{|msg| puts  "hello #{msg}!"}
  
  lm_proc_obj.call("world")
  lm_proc_obj.("world") 
  lm_proc_obj["world"]
  lm_proc_obj === "world"

看到第四种方式,你应该想到,你可以在case语句里使用proc对象了吧?

  def response_code?(code)
    ->(response) { response.code == code }
  end

  case response
  when response_code?(200) then 'OK'
  when response_code?(404) then 'Not found'
  else 'Unknown code'
  end

总结:

可怜的块(block)和块对象(Proc对象),就这样被你(Rubyist)和方法欺负着。如果你能善心大发,在开发中尽量还块对象以自由身(lambda proc 对象),你会得到好报的。


相关视频:

「诱人的Ruby」 入门篇 - 「Ruby中的Block」
「诱人的Ruby」基础提高篇 - 「块与闭包」。

参考

closures-in-ruby
lambdas-slash-procs-in-case-expressions

如果你喜欢本文,想看更多内容,请关注微信公众平台:RubyStudy

「Ruby」 命名之争: singleton_class? meta_class? eigen_class?

Published on:
  class << self; self; end

这段代码,每个Rubyist应该都很熟悉。
但是这里面的self是什么? singleton_class? meta_class? eigen_class?

在早期,确切来说应该是Ruby1.9.2发布之前,Ruby社区关于以上这段代码的命名,有很大的争议,后来Ruby团队准备在1.9.2中增加一个方法,来消除这种命名的争议。 这个名字除了Ruby官方的统一之外,更重要的是为了方便Rubyist去使用。比如,1.9.2之前的代码:

(class << self; self; end).class_eval do
  define_method meth do |*args|
    subject.send meth, *args
  end
end

或者有更优雅的写法:

def metaclass
  class << self; self; end
end

metaclass.class_eval do
  define_method meth do |*args|
    subject.send meth, *args
  end
end

这个metaclass是社区里其中一个命名,还有命名为eigen_class的, 在Ruby内部,被命名为virtual class, 有代码为证:

#ruby 1.8.7  

class User
  class << self
    puts self
    puts self.new
  end
end
#=> #<Class:User>

#=> TypeError: can't create instance of virtual class

报错的信息显示,Ruby内部称之为:virtual class。

当然,在Ruby1.9.2之后, 确定了名字,叫做singleton_class, 代码为证:

class User
  class << self
    puts self
    puts self.new
  end
end
#=> #<Class:User>

#=> TypeError: can't create instance of singleton class

并且提供了一个方法,Object#singleton_class, 这是一个短形式,相当于你直接使用(class << self; self; end)

Object.instance_methods.grep(/class|singleton|eigen|meta/)
#=> [:class, :singleton_class, :singleton_methods, :define_singleton_method]

关于这个命名,社区里还是有一些争议的, 可以看这个帖子

有的人说用metaclass, metaclass翻译成中文就是元类, 意思就是,一个类自己的类,但是明显不符合事实,因为在Ruby中,对于类还可以用这个命名,但是对象呢?每个对象都有这样一个类(class << self; self; end), 自然,metaclass就被否决了。

有的人说用eigen_class,翻译为中文就是本质类,也可以叫固有类,有点偏语义、偏哲学层面,用这个名字其实也可以。但是,在Ruby里已经有了一个方法,叫singleton_methods,专门用于查看eigen_class这样的类中定义的方法, 难道要把singleton_methods改称eigen_methods吗? 或者叫eigenclass? eigenmethods? 就这样Ruby核心团队陷入了深深的困扰中。

其实最受欢迎的命名还是singleton_class, 但是这个命名有很大的歧义,那就是容易让人联想到设计模式 - 单例模式上面去。不知道这个设计模式的还好,知道这个设计模式的,第一印象就以为是单例模式,尤其是国内,把singleton_class翻译为单例类让人更加困扰,但实际上,这个singleton_class和单例模式没有什么关系, 我们只能从这个命名的英文含义去理解, 个人认为中文翻译为单例类不太好,应该翻译为单类,因为singleton_class是指Ruby中每个对象都对应的那个特殊的类,就像影子一样跟着Ruby中的每一个对象,形单影只,所以有人也把它叫做影子类。 所以,这些因素都是Ruby团队需要考虑的。

但是最后的决定,还是用了singleton_class,因为Ruby团队不想去改已经实现的singleton_methods等带着singleton_前缀的方法了。

这里提一下,「Ruby元编程」中用的是eigenclass,作者说对于这个名字,官方并未定论,按理说在他写这本书之前,应该1.9.2还未发布,但是2011年这本书出版的时候,Ruby1.9.2应该早出版了,singleton_class应该就是官方对于(class << self; self; end)的定论了。

值得一提的是,Ruby标准库里有一个Singleton Moudle,这个才是真正用于去实现单例模式的。

那么现在,我们要动态的创建一个类或对象的方法,可以方便的去使用singleton_class了:

singleton_class.class_eval do
  define_method meth do |*args|
    subject.send meth, *args
  end
end

关于singleton class的更多示例:

class User
  def self.name
    puts "hello"
  end
  
  class << self
    def age
      puts  "18 years old"
    end
  end
end
User.singleton_methods #=> [:name, :age]


user = User.new
def user.lover
  puts "kitty"
end

user.singleton_methods #=> [:lover]


other_user = User.new
other_user.singleton_methods #=> []


class Admin < User
end

Admin.singleton_methods #=> [:name, :age]

其实关于Singleton Class这样的对象模型设计,是属于Ruby语言首创。至于Ruby语言为什么这样设计? 我们以后再谈。


如果你喜欢本文,想看更多内容,请关注微信公众平台:RubyStudy

「诱人的Ruby」入门篇调查报告总结

Published on:

「诱人的Ruby」是我录制的一套Ruby学习课程,一共分成了「入门」 - 「基础提高」 - 「进阶」 - 「高级」四大篇章。 去年(2013)在入门篇课时完结之后,我发起一个调查报告,一共有97人参与调查,为了感谢参与调查的朋友,每个参与调查的朋友关注我的RubyStudy微信帐号,告诉我你们的51cto id,开始赠送学分!

下面我把调查结果公开,顺便做一份总结:

Q1:您是从哪里知道此视频课程的?(单选题)

结果:

结果排序后可以看出:

  • 51cto的宣传 39.18%
  • Ruby China 31.96%
  • 搜索 13.4%
  • 其他方式 12.37%
  • 别人介绍 2.06%
  • 贴吧 1.03%

我在「51cto」和「Ruby China」上的宣传,比较有效果,当然「搜索」的结果,应该也得益于51cto和Ruby China的SEO了。 「其他方式」,我估计就是微博,QQ群的宣传,别人介绍,也应该是走微博和QQ群这种社交工具。「贴吧」是指百度贴吧,百度贴吧可能做技术的,尤其是Ruby很少混迹在上面,所以比例比较少。

Q2:您是在职还是学生?(单选题)

结果:

在这个问题里,学生分国外和国内,之所以这么问,是因为我创建的QQ群(222359552)里通过交流发现有在国外留学的几个学生,这几个学生有在欧洲,有在美国,英国,据他们所说,Ruby在国外的大学里是一门专业课,相比较之下,国内的大学就很落后了,Ruby在国外的火热,也许得再过几年国内才会感受到。

调查结果排序以后:

  • 工作2年+ 52.58%
  • 工作1-2年 25.77%
  • 在读专/本科「国内」 19.59%
  • 在读专/本科「国外」 2.06%

通过这个结果,可以看出, 工作两年以上的,占一大部分, 这个不一定是做Ruby开发两年以上,反正是有两年工作经验,也可能是做别的。总的来说,工作的人群占了80%, Ruby在国内的学校里普及率还是比较低(以我的样本来说)。

Q3:您的编程基础如何?(单选题)

结果:

结果排序以后:

  • 业余爱好,只为了了解Ruby 30.93%
  • 其他语言转Ruby(刚开始学,并打算投入使用) 29.9%
  • 从零开始 17.53%
  • Ruby开发已经有一段时间了 14.43%
  • 运维转开发 7.22%

从这个数据看呢,Ruby还是吸引了不少人的兴趣, 也有差不多比例的人群有准备使用Ruby工作的打算。 由于近几年DevOps的发展,很多自动化运维工具用了Ruby,比如chef,Puppet等,所以也有一部分运维对Ruby也感兴趣。零基础的人群比例也不算少,从零开始就学Ruby,我觉的是个好事。对于使用Ruby开发了一段时间的这部分人群,可能是做Rails开发的,Ruby基础有点薄弱,更想系统的学习Ruby。

Q4:您看完入门篇课程了吗?(单选题)

结果:

结果排序之后:

  • 看了一半,还打算继续跟着学习 57.73%
  • 没有,看了前面的不想看后面的了 27.84%
  • 看完了全部入门篇课程 14.43%

这个结果我还是比较满意的,也算在意料之中吧。刚开始录的时候,是我第一次录视频,对于工具的使用,还有一些录制技巧不是很好,所以前面几个课时录制的视频声音比较低,体验不是很好,这个后面的课时已经做了改进,我想那27.84%的朋友,可能是受不了这个,所以只看了前面的课时。这部分人群在回答完这个问题之后,就直接跳到最后一个提建议的尾题了,所以剩下的几个调查题,这部分人群没有参与。

看完全部课程的人,应该是属于认真学习的,因为通过QQ群的交流,我发现很多人,都是跳着看的,可能是太浮躁,太急于求成了,我录制视频的内容,都是循序渐进的,如果跳着看,可能会有所遗漏。 其实,如果把心静下来去学习,就没什么困难不能克服。

Q5:「诱人的Ruby」对您学习Ruby起了多大帮助?(单选题)

结果:

结果排序之后:

  • 帮助很大 52.86%
  • 某些课时有帮助 47.14%
  • 没有帮助,挺垃圾的 0.0%

这个结果我也比较满意, 希望能给予入门者帮助,是这门课程的初衷。我出「没有帮助,挺垃圾的」这个选项,也是真心的想了解,因为「教」也是一种学习,我也想把「垃圾」的内容去掉,也是因为看到贴吧里有人发了一句:「这么垃圾的视频,不看也罢」,看来这位朋友没有参与调查了,我很想听你说说哪里垃圾,也帮助我提升课程质量。

Q6: 「诱人的Ruby」入门篇您给打几分?(打分题)

结果:

从结果可以看出:大家对视频声音不满意,所以后面的课时我做了改进,基本声音提高了很多,目前应该没有问题了。对于视频的组织和内容,大家还算满意,视频的长度,很多人跟我反应太长了点,所以最近上传的一些课时,我用分节策略,限制时间在20分钟以内,之前偷懒了,只想一次录完一大块内容,造成体验差了,现在感觉分节录播的效果也不错。

Q7: 就是一些建议了。

大部分的建议还是上面说的问题:声音。 声音大小,我已经做了改进, 语调,语速,我也正在提高改进,希望给大家一个更好的学习体验。

还有一些忠告和对视频内容的建议,比如要求增加一些和Rails框架相关的内容,开源组件,Hacker代码讲解,网络IO,测试等内容, 我也听取了这些建议,在后面的课程会陆续加入, 饭要一口一口吃,学习也应该循序渐进。


最后,在新的一年,我会稳定更新视频, 入门篇的视频,我也会不断加入新的内容, 后面还有基础提高篇,进阶篇。到进阶篇为止, 我对视频内容的规划是,基本覆盖Ruby的95%内容讲解,包括ruby的基础, Ruby的OO对象模型, 元编程。 高级篇的话,就是Ruby在杀手级框架Rails中的应用了, 从实践到Rails源码, 和大家一起,解构Rails。

现在所有的视频都已更新到收费状态,以后也不会再免费, 视频是献给我自己,以及跟我一样真正想精通Ruby的人,让我们一起进步。

「Codewars」IP转换器

Published on:
Tags: CodeWars ruby

CodeWars是个很有意思的编程学习网站,这上面有很多有意思高质量的小题目,之前登陆这个网站的时候感觉很慢,今天又登陆上去看了一下,感觉速度比以前快多了,可能是做了一些优化。这样的话,打算抽空就去做一些小题目,练练脑子。然后把解题思路写成blog,我希望可以做完codewars的所有题目。


今天做的这个题目是:IP转换器。实际上只是实现一个IP转换的方法而已。

题目(难度6kyu:IPv4 to int32):

简单翻译一下:

有一个IPv4地址: 128.32.10.1。 这个地址实际由4个8位的字节组成:

第一个128,它的二进制是: 10000000
第二个32, 它的二进制是: 00100000
第三个10, 它的二进制是: 00001010
第四个1, 它的二进制是: 00000001

所以, 128.32.10.1 相当于 10000000.00100000.00001010.00000001

这样的话,IP地址就是由32位的了,于是我们就根据这32位字节得到一个数字:2149583361。

要求:

写一个函数:ip_to_int32(ip) 可以让这个IPv4的地址返回那个数字:
iptoint32("128.32.10.1") = 2149583361


解题思路:

可以看得出来,codewars的题目,多是来源于实践中的一些基础知识。这里隐含的知识是:

为什么要把IP转换为一个数字呢?
TCP/IP协议规定,IP地址是由32位二进制数组成,简单来说,是为了便于查询或做一些根据IP设置的登陆限制,比如一些IP数据库,里面存储的就是根据32位二进制转化成的数。

在知道这个基础知识的前提下,这个题目才变得有意义。

那么我最初的解题思路是这样的:

  1. 需要把"128.32.10.1"这个字符串里的每一个ip数字转换为二进制。
  2. 把4段二进制拼接在一起形成一个32位二进制串。
  3. 把32位二进制串变成十进制。

于是我得到了下面这个方法:

def ip_to_int32(ip)
  ip.gsub(/(\d+)/){|i|i = i.to_i.to_s(2); "0"*(8-i.size)<< i} \
    .gsub(/\./, "")\
    .to_i(2)
end

测试是成功的:

Test.assert_equals(ip_to_int32("128.32.10.1"), 2149583361)

解释:

  1. gsub方法有一个用法就是可以传递块, 这样就避免了我另外去迭代处理。/(\d)+/,这个正则不太严谨,只是取数字,但没有判断不合法的IP地址,这里就不考虑那么多了。
  2. "0"*(8-i.size)<< i 这段代码是个笨方法,是为了给转换为二进制的IP数字补足8位。
  3. 128.to_s(2)是十进制转二进制的一个方法, "1010".to_i(2)是二进制转十进制的方法。

答案通过以后,就可以看到别人写的答案了,大致看了一下,有几个很有启发:

第一个:使用字符串格式化的思路:

def ip_to_int32(ip)
  ("%02x%02x%02x%02x" % ip.split('.')).to_i(16)
end

解释:

  1. "%02x"是把一个字符串格式化为16进制,2为指定的输出字段的宽度.如果位数小于2,则左端补0
  2. % 操作符, 用于字符串格式化。
  3. to_i(16), 把16进制转换为10进制。

受这个思路的启发,我又改了下我的代码:

def ip_to_int32(ip)
  ("%08b"*4 % ip.split('.')).to_i(2)
end

解释:

  1. 使用2进制、8进制、16进制的指示符分别是(b',o', x',X')
  2. 所以,"%08b"是把一个十进制数格式化为一个二进制数,8位宽度,如果位数小于8,则左端补0.

这种写法比我之前那种简洁多了。

第二个: 使用位操作:
看到有一个人用了位移操作,是这样的:

def ip_to_int32(ip)
  ip.split('.').inject(0) { |total, val| (total << 8) + val.to_i}
end

这是我用他的思路改的代码:

def ip_to_int32(ip)
  ip.split('.').each_with_index.inject(0) do |total, (ip, index)|
    total += (ip.to_i << (8*(3 - index)))
  end
end

解释:
IP地址都是基于256来计算的:

128 * (256)3 + 32 * (256)2 + 10 * (256)1 + 1 * (256)0 = ?
2147483648 + 2097152 + 2560 + 1 = 2149583361

当然,可以直接把这个等式写成代码,也可以求得结果,但是我们用位操作更快:

//  128 << 24
//  32  << 16
//  10  << 8
//  1   << 0

当然还有其他解法,都是一般的数组拼接,我就不罗列了。

从十进制数字转换为IP地址的方法,就是一个逆向过程,我也不多写了。

感兴趣的朋友,可以去玩玩CodeWars。

Dev with Vagrant and Docker

Published on:

前言

为了在团队里搭建统一的本地开发环境,最近花了点时间用了下vagrant和docker,在此做个记录, 这也算一个DevOps的实践。


Vagrant介绍

Vagrant 是一款用来构建虚拟开发环境的工具,非常适合 php/python/ruby/java 这类语言开发 web 应用,“代码在我机子上运行没有问题”这种说辞将成为历史。

我们可以通过 Vagrant 封装一个 Linux 的开发环境,分发给团队成员。成员可以在自己喜欢的桌面系统(Mac/Windows/Linux)上开发程序,代码却能统一在封装好的环境里运行,非常霸气。

以上介绍直接抄自网络,我觉得介绍的很到位。

「注意点:」

vagrant up命令执行后,如果看到下面的错误信息,则需要安装另外一个工具:

[default] The guest additions on this VM do not match the installed version of
VirtualBox! In most cases this is fine, but in rare cases it can
prevent things such as shared folders from working properly. If you see
shared folder errors, please make sure the guest additions within the
virtual machine match the version of VirtualBox you have installed on
your host and reload your VM.

Guest Additions Version: 4.1.18
VirtualBox Version: 4.3

这是因为Guest Additions 和 VirtualBox的版本不一致所导致的,所以需要安装另外一个工具,保持他们的版本自动同步:

$  vagrant plugin install vagrant-vbguest

然后重新vagrant up一下就好了(当然需要先vagrant destroy掉之前的)

Docker介绍

Docker是一种增加了高级API的LinuX Container(LXC)技术,提供了能够独立运行Unix进程的轻量级虚拟化解决方案。它提供了一种在安全、可重复的环境中自动部署软件的方式。Docker使用标准化容器的概念,能够容纳软件组件及其依赖关系——二进制文件、类库、配置文件、脚本、Virtualenv、jar包、gem包、原始码等——而且可以在任何支持cgroups的64位(针对x64)Linux内核上运行。

Linux Container,是一个轻量级系统虚拟化机制。有时被人称为:“chroot on steroids”,你也可以拿chroot来理解这个概念。而Docker是用go语言实现的Linux Container包装。更详细的介绍,可以自行搜索。

Github 示例

我在github创建了一个示例,也可以直接拿来当开发环境,只限于Ruby。

详细

关于Vagrant的安装、命令的意义,网上多的是,这里就不做重复介绍了。 我只把github示例做个讲解吧。

Vagrantfile

# -*- mode: ruby -*-

# vi: set ft=ruby :


Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu"
  config.vm.box_url = "http://files.vagrantup.com/precise64.box"

  # 1024 < port < 49151

  #shipyard

  config.vm.network :forwarded_port, guest: 8005, host: 8005

  #apps

  config.vm.network :forwarded_port, guest: 3000, host: 3000
  #elasticsearch

  config.vm.network :forwarded_port, guest: 9200, host: 9200
  config.vm.network :forwarded_port, guest: 9300, host: 9300

  #memcached

  config.vm.network :forwarded_port, guest: 11211, host: 11211

  #mysql

  config.vm.network :forwarded_port, guest: 49153, host: 3306

  #share folder

  config.vm.synced_folder "./vagrant", "/vagrant_data"

  config.vm.provider :virtualbox do |vb|
    vb.gui = false
    #memory

    vb.customize ["modifyvm", :id, "--memory", "1024"]
  end

  config.vm.provision :puppet do |puppet|
    puppet.manifests_path = 'puppet/manifests'
    # puppet.module_path    = 'puppet/modules'

  end

  config.vm.provision "docker" do |d|
    d.pull_images "blackanger/my-mysql-server"
    # d.pull_images "blackanger/elasticsearch"

    # d.pull_images "blackanger/memcached"

    # build


  end

  config.vm.provision :shell, path: "bootstrap.sh"
end

基本的架构是这样的:

Vagrant <-> box <-> VirtualBox[ docker images[containers] ]

Vagrant操作的VirtualBox里面运行着ubuntu, 然后又在ubuntu里安装了docker, 我们可以使用docker image来运行contianers。大家可以去Docker的官网在线体验一下docker的各个命令。

为什么用Vagrant + Docker呢? 因为我不想在本地开多个Vagrant。

一切从Vagrantfile开始:

vagrant up

当这个命令被执行之后, vagrant会按照Vagrantfile文件所配置的来执行命令。

config.vm.network :forwarded_port, guest: 22, host: 2222

这个命令会指定本地系统和VirtualBox系统的端口映射,也就是说,我在本地系统访问22端口,就是去访问VirtualBox的2222端口。这样才能让我们和VirtualBox通信。

其他配置,大家看vagrant init生成的Vagrantfile默认说明就清楚了。

config.vm.provision :puppet do |puppet|
    puppet.manifests_path = 'puppet/manifests'
end

vagrant支持puppet,来帮你自动安装好VirtualBox里需要的各种lib。 这个例子中,我安装了sqlite、rvm。

config.vm.provision "docker" do |d|
  d.pull_images "blackanger/my-mysql-server"
end

首先,这条命令会帮你在VitualBox里安装docker。 然后 pull_images 命令,会把你上传到index.docker.io的配置好的image pull下来。

config.vm.provision :shell, path: "bootstrap.sh"

最后,执行一个shell脚本, 把VirtualBox里的docker images都run出来相应的Contianer供我们使用。

config.vm.synced_folder "./vagrant", "/vagrant_data"

这是设定VirtualBox和本地系统共享的目录, 我在vagrant目录下面放了apps目录, 然后把本地系统开发的Ruby项目做一个link连到vagrant/apps/ 目录下面 (link不工作,可以把开发的项目放到apps目录下), 就可以直接在VirtualBox里跑我们的Ruby应用了,而且还可以使用我们docker run出来的Contianer。比如我例子里运行的mysql docker container, 就可以在Rails项目里配置好直接使用,感觉就像在vagrant上用docker来做的本地云服务。

Docker的使用事项

- pull images的时候必须要翻墙。
- 你可以把一个container 的 ID commit到一个image中去,
  然后push到index.docker.io的image仓库中,可以随时pull下来使用。 
 (有点和git的用法相似)
- 也可以使用
      sudo docker build -t  <username>/<imagesname> . 
  命令从一个Dockerfile文件来构建image。

总结

还有很多的vagrant和docker命令我都没有说,这些只要去看help就明白了。关于其他使用细节,欢迎大家一起交流。

集成支付宝快捷支付

Published on:

集成支付宝快捷支付小记。


客户端

客户端集成移动端支付宝快捷支付,iOS和Android端很方便,使用支付宝提供的sdk就可以了,要注意的地方就是:

  • 一定要用支付宝提供的工具,生成商户自己的公钥和密钥,密钥要经过pkcs8编码。这些都可以使用支付宝提供的工具来完成。
  • 要把商户的公钥提交到支付宝。

基本上看着文档就可以很快完成集成测试。

当用户手机没有安装支付宝App,则会弹出安装支付宝App的弹窗,点确定就去下载支付宝App,点取消就弹出webview打开网页版支付宝结算。

服务端

服务端是用Ruby。

因为所有的支付请求都由客户端完成了,那么服务端只剩下了实现支付宝异步通知接口的任务。

异步通知接口有两步工作:RSA签名验证、Notify ID验证是否支付宝请求。

RSA签名验证

目前快捷支付的签名类型,只支持RSA, 所以服务端接收异步通知的接口Notify就只能用RSA来验证签名。支付RSA验证的逻辑是这样的(文档里也有描述,理解这个逻辑有助于顺利完成验证):

  • 商户使用支付宝提供的工具生成RSA公钥私钥,并把商户公钥提交给支付宝。 这一步意味着双方互换公钥。
  • 支付宝发给商户的请求是用支付宝的私钥加密的,所以必须用支付宝的公钥解密,而支付宝的公钥在文档里已经提供。
  • 商户发往支付宝的请求,必须是商户自己的私钥加密,而支付宝那边用商户的公钥解密。就是移动客户端sdk完成的工作。所以移动客户端那边一定是用商户自己的私钥,并且是经过pkcs8编码的。

理解了RSA的加密解密逻辑,我们就了解了,服务端后台要验证来自于支付宝的异步通知POST请求,必须用支付宝的公钥了。

复制文档里支付宝的RSA公钥, 换行的部分用\n代替,如下:

-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCnxj/9qwVfgoUh/y2W89L6BkRA\nFljhNhgPdyPuBV64bfQNN1PjbCzkIM6qRdKBoLPXmKKMiFYnkd6rAoprih3/PrQE\nB/VsW8OoM8fxn67UDYuyBTqA23MML9q1+ilIZwBC2AQ2UBVOrFXfFl75p6/B5Ksi\nNG9zpgmLCUYuLkxpLQIDAQAB\n-----END PUBLIC KEY-----

然后,我们使用OpenSSL::PKey::RSA类:

rsa = OpenSSL::PKey::RSA.new(alipay_pub_key)

alipay_pub_key是上面的支付宝公钥。
一定要上面这种格式,或者你使用工具生成符合pem格式的pem文件,否则会报错。

然后我们就可以使用rsa的verify方法验证签名了:

rsa.verify('sha1', Base64.decode64(sign), rsa_string.force_encoding("utf-8"))

注意,这里的sign,是支付宝请求里的签名参数, 这里的sign一定要经过base64解码。rsa_string是根据支付宝文档生成的待签名字符串。

有的人可能不理解'sha1'。 这里sha1是签名算法,RSA是加密手段,加密了签名数据。 通过支付宝提供的java和php demo代码了解,支付宝使用的是sha1算法,所以这里需要用sha1算法来参与认证签名。用一句伪代码表示如下:

  RSA_Private_key(sha1(rsa_string))
  • 先通过RSA公钥解开签名数据,得到一个sha1加密的验证数据: A
  • 同样用sha1加密我们的待签名字符串: B
  • 比较两个值 A和B是否相等

这样,你就可以完成RSA验证了。

Notify ID验证

这个很简单, 带上支付宝文档里提供的参数,给支付验证接口发个请求就可以了。


后记

Ruby推荐使用ActiveMerchant,我把上面的验证过程写了个ActiveMerchant补丁,用起来很方便,毕竟ActiveMerchant帮你做了很多工作,这里就不分享了,大家可以自己去写。

最后,需要注意的地方:

  • 你的异步通知接口必须返回text/plain 格式的success字符,否则支付宝会不断的给你发请求,24小时8次
  • 支付宝在完成支付之后,支付状态首先是「等待付款」,这时候会发一次异步请求, 然后等一段时间之后,支付交易成功之后,又会发一次异步通知。 那么这两次通知,你必须在异步通知接口中都进行判断,并返回success。

UPDATE 2014.04.30

今天把支付宝支付客户端RSA签名验证的过程,移到了服务端,这样做是为了更加安全,RSA的密钥就不必保存到客户端了。

增加一个RSA签名方法:

def rsa_sign(rsa_string)
  pri = OpenSSL::PKey::RSA.new(alipay.private_key)
  sign = pri.sign('sha1', rsa_string.force_encoding("utf-8"))
  signature = CGI.escape(Base64.encode64(sign))
  return signature
end

以及一个生成待签名字串的方法

def create_sign_string(sign_params)
  sign_keys = sign_params.keys
  origin_string = sign_keys.inject(''){|s, key|
    if key.to_s == 'notify_url'
      s += %Q{#{key}="#{URI.encode(sign_params[key], Regexp.new(/:\/\//))}"&}
    else
      s += %Q{#{key}="#{sign_params[key]}"&}
    end
  }
  return origin_string
end

相关阅读:

集成微信支付

「互联网大事记」OpenSSL HeartBleed

Published on:

这几天OpenSSL的HeartBleed漏洞算是互联网的大事,详情


关于这个漏洞我大概了解了一下,做个记录:

  • 只影响使用https的服务
  • 受影响的版本是OpenSSL 1.0.1 版至 1.0.1f 版, 1.0.1之前的版本不受影响,为此,OpenSSL更新到了1.0.1g
  • 官方漏洞名称为:CVE-2014-0160
  • 如果网站配置了一项名为「perfect forward secrecy」的功能,则影响会大幅减少,但还不如升级到最新OpenSSL版本
  • 该漏洞已被利用2年之久。
  • 利用该漏洞可以读取服务器内容中64k数据,可以不断反复获取,获取的内容可能包含cookie、明文帐号密码。
  • 这有一个Ruby脚本,用于检测你的网站是否受此漏洞影响: heartbeat

升级OpenSSL

用本地mac开发机做示例(当然,重要的是你应该在HTTPS服务器上面升级):

brew update
brew install openssl
brew link --force openssl

rm /usr/bin/openssl
ln -s /usr/local/Cellar/openssl/1.0.1g/bin/openssl /usr/local/bin

openssl version -a

Rubyist该怎么办?

Ruby官方已经给出解决方法

执行下面命令来验证链接到Ruby的OpenSSL版本:

ruby -v -ropenssl -rfiddle -e 'puts Fiddle::Function.new(Fiddle.dlopen(nil)["SSLeay_version"], [Fiddle::TYPE_INT], Fiddle::TYPE_VOIDP).call(0)'

执行下面命令验证Ruby 中 OpenSSL 库的版本:

ruby -ropenssl -e 'puts OpenSSL::OPENSSL_VERSION'

如果发现你的Ruby用的是受漏洞影响的OpenSSL,那么你就需要重新编译安装Ruby了。

$ ./configure --with-openssl-dir=/path/to/openssl
$ make
$ make install

很多人服务器上使用了rbenv或rvm, 那么则需要依赖rbenv和rvm来更新Ruby,当然前提是升级到OpenSSL 1.0.1g版本。

rbenv ruby-build
rvm config
phusion-passenger


漏洞攻击原理简要描述

  • 正常情况:程序读取数据长度,根据长度读取数据内容,然后原封不动地把数据内容发回给用户
  • 黑客发一段恶意数据,长度为64k,但是实际的数据内容远小于64k, 比如1byte,这种情况下,server就会把内存后续的[64K-1]长度的数据“原封不动”地发回给黑客
  • 可以不段的反复发送恶意数据,sever发回的数据可能包含其他用户的明文密码

这个过程有点像用沾满蜂蜜的筷子捅向一大堆芝麻粒,每次都能粘很多芝麻回来。各大提供HTTPS服务的网站的内存块,就变成了这样的芝麻。

微信支付 - 「App集成」

Published on:

微信支付分为两个场景:公众帐号内支付、App移动客户端集成微信支付。
本文描述的是App移动客户端集成,记录一下文档坑爹的地方。


申请微信支付:

首先,你得去微信开放平台申请微信支付。经过比较漫长的申请过程,审核通过之后,可以去这里下载文档和demo。

开发

使用方式描述: 当使用App点击微信支付,会直接跳转到微信进行支付。

开发步骤:

  • 获取access token
  • 获取prepayid
  • 得到sign,结合prepayid发起微信支付请求
  • 支付完成

上面的所有步骤,你可以在iOS或者Andriod端完成,但是,为了安全最好在服务端完成前两步。

而我就是在服务端完成了前两步,使用Ruby。

支付逻辑

  • 使用access token 访问支付接口。
  • 预支付订单的生成包含双重验证: a:package为订单数据打包,携带一个md5签名,sign, b:app_signature则为访问接口使用签名验证,sha1。
  • 得到预支付id,生成客户端发起微信支付请求需要的sign,这个c: client_pay_sign是sha1签名。因为你在服务端完成签名,所以移动客户端不需要知道package的具体值了,因为已经从服务端创建预支付订单的时候传过去了,所以这里只需要固定值“Sign=WXPay”
  • 最终,需要把预支付id和签名c(上面那个client_pay_sign)传给客户端,客户端就可以发起微信支付请求了。
  • 整个过程客户端完全不知道appkey,secret,partnerid等敏感数据。

下面是值的注意的地方:

获取access token

POST url = https://api.weixin.qq.com/cgi-
bin/token?grant_type=client_credential

# post params

appid=APPID
secret=APPSECRET

微信支付的文档里写的是用GET,我就不吐槽腾讯的文档了,你用GET能取到的只有错误码。所以这个地方是用POST,才能取到access token。
当然这里你用到的APPID和APPSECRET是你申请成功微信支付后才会有的。

获取prepayid

第一眼看微信支付的文档,感觉是和支付宝快捷支付一样,但是看到这一步的时候,感觉微信支付做的更安全。生成预支付订单完全可以在服务端完成。

POST  https://api.weixin.qq.com/pay/genprepay?access_token=ACCESS_TOKEN

很多人会碰到一个错误:{"errcode":40001,"errmsg":"invalid credential"}
这个时候需要仔细检查你的access token是不是给对了。

然后就是POST Data,一定要是json对象, 要仔细看文档,写对参数名,否则会碰到{"errcode":49004,"errmsg":"not match signature"}的错误。

生成package
这个地方文档描述的不是很正确,不知道是不是写文档的人语文没学好,我来把文档重新纠正一下。

  • A)对所有传入参数按照字段名的 ASCII 码从小到大排序(字典序)后,使用 URL 键值对的格 式(即 key1=value1&key2=value2...)拼接成字符串 string1;
  • B) 在 string1 最后拼接上 key=partnerKey 得到 stringSignTemp 字符串, 并对 stringSignTemp 进行 md5 运算,再将得到的字符串所有字符转换为大写,得到 sign 值 signValue。
  • C)对 string1 中的所有键值对中的 value 进行 urlencode 转码,按照 a 步骤重新拼接成字符 串,得到 string2。对于 js 前端程序,一定要使用函数 encodeURIComponent 进行 urlencode 编码(注意!进行 urlencode 时要将空格转化为%20 而不是+)。
  • D)将 sign=signValue 拼接到 string2 后面得到最终的 package 字符串。

字典序,在Ruby里,用sort方法就可以了。
文档里的D)写的是把sign=signValue拼接到string1后面,应该是拼到string2后面,也就是经过url encode的字串。

我被坑到的地方就是,POST Data转成json对象的时候,使用了to_json方法, 把package参数里的&符号转成了\u0026。
应该使用JSON.generate(data)来转换。

把上面所说的搞对,基本就很顺了。

生成app_signature

这里需要注意的是,appkey就是paySignKey, 128位长的字符串, 请不要和app secret、appid搞混。


总结

剩下的根据文档就很简单了。和支付宝一样,会有异步通知到notify_url, 到时候处理好你的业务逻辑就可以了。记得返回success。

相关阅读:

集成支付宝快捷支付

「CodeWars」Don't rely on luck

Published on:

好久没有去Codewars玩题了,今天有人问我CodeWars的一道6kyu的题:Don‘t rely on luck,我也就趁机写一篇blog。


关于题的描述

The test fixture I use for this kata is pre-populated.
It will compare your guess to a random number generated in Ruby by:

(Kernel::rand() * 100 + 1).floor

in Javascript/CoffeeScript by:
Math.floor(Math.random() * 100 + 1)

You can pass by relying on luck or skill but try not to rely on luck.

"The power to define the situation is the ultimate power." - Jerry Rubin

Good luck!

给出的Test Case, 让你写代码通过这段测试。

#This is exactly what the real test fixture looks like.

lucky_number = (Kernel::rand() * 100 + 1).floor
Test.assert_equals(guess, lucky_number, "Sorry. Unlucky this time.")

这道题的目的,是让你每次用 (Kernel::rand() * 100 + 1).floor 得到的随机数能相等,看你能否有办法掌握随机数的值。先给出答案就明白了:

srand(1)
guess = 42
lucky_number = (Kernel::rand() * 100 + 1).floor
Test.assert_equals(guess, lucky_number, "Sorry. Unlucky this time.")

注, 是Ruby1.9.3 版本才可以。 Ruby2.0 lucky_number返回的是100.

下面我们来讲讲为什么。

随机数

随机数分为三种:真随机数、准随机数、伪随机数。

  • 真随机数是属于大自然的,真随机数只能通过随机的物理过程来获取,实际中是很难的。
  • 准随机数来自于严格的数学方法,理论上可行,但实际上不可行。
  • 伪随机数,就是假的随机数。因为它是根据某种已知的方法获取随机数,本质上已经去除了真正随机的可能。这种方法一旦重复,则获取到的随机数也会重复。

产生随机数的方法有很多种,什么斐波那契法、线性同余法、梅森旋转算法Mersenne twister等。 现在最好的随机数产生算法是梅森旋转算法Mersenne twister,我们的大Ruby就是用的这种算法,想了解这种算法可以去维基百科

但是,别忘了,Ruby产生的只能是伪随机数,而不管这个随机产生算法多牛逼,终归是需要一个随机数seed。正是这个seed,再加上那些随机数产生算法,才能产生我们可用的随机数,也正是这个seed的存在,本质上造就了它是伪随机数。

我们可以看看Ruby的示例:

pry> rand
#=> 0.6512974212962637

pry> rand
#=> 0.5279022279716274

pry> rand
#=> 0.6377787632102948

...

Ruby默认的rand方法,生成的是0。。1范围内的随机数,它依赖的随机种子是srand方法提供的,如果你没有指定srand的值,则Ruby默认是根据系统时间、进程id、一个可升序的数字来生成随机数种子。

你也可以指定srand的值:

pry> srand(333)
pry> 10.times.map { rand(10) }
#=> [3, 3, 6, 3, 7, 7, 6, 4, 4, 9]

pry> 10.times.map { rand(10) }
#=> [7, 5, 5, 8, 8, 7, 3, 3, 3, 9]

 
pry> srand(333)
pry> 10.times.map { rand(10) }
#=> [3, 3, 6, 3, 7, 7, 6, 4, 4, 9]

pry> 10.times.map { rand(10) }
#=> [7, 5, 5, 8, 8, 7, 3, 3, 3, 9]

上面代码可以看得出来, 随机数生成序列是根据srand指定的随机数seed来生成的,所以对于同一个随机数seed,可以生成同一组序列。这样,回到我们codewars的这道题中, 我们只需要通过srand指定同一个seed,则我们就可以得到相同的随机数:

srand(10)
guess = (Kernel::rand() * 100 + 1).floor
srand(10)
lucky_number = (Kernel::rand() * 100 + 1).floor
Test.assert_equals(guess, lucky_number, "Sorry. Unlucky this time.")

而上面那段代码:

srand(1)
guess = 42
lucky_number = (Kernel::rand() * 100 + 1).floor
Test.assert_equals(guess, lucky_number, "Sorry. Unlucky this time.")

正是因为在Ruby1.9下,srand(1)设置随机数seed为1,则第一个随机数就是42,所以这里自然guess和lucky_number是相等的。

以上的例子充分说明了伪随机数的伪劣性。

关于Codewars的部分就完了,下面是更有意思的部分:


随机数服务

有一个Gem:RealRand,包装了3个真正的随机数服务生成服务:

  • random.org:此网站根据大气噪声来生成随机数
  • FourmiLab(HotBits):使用放射性衰变来生成随机数
  • random.hd.org(EntropyPool):声称使用各种来源来产生随机数,包括本地程序/文件/设备,网页的点击率,以及远程Web站点。

感兴趣的可以玩玩。

安全随机数

Ruby使用SecureRandom来生成UUID/ Session Token之类的随机数来提供安全的随机数。

pry> require 'securerandom'
 
pry> SecureRandom.hex
#=> "bfd2e9f3b995db66440dd5127fca3d73"

 
pry> SecureRandom.base64
#=> "LtslCqGWMFHV6C9ogRM9rA=="

 
pry> SecureRandom.random_bytes
#=> "y\xDCR\xAB\xD4\x98\x8C{,\x98\xEA\x93L\xF6\x93&"

 
pry> SecureRandom.random_number
#=> 0.09625459534287839

基于概率分布的随机数

有一个Gem:Rubystats, 可以去了解下。

如何得到随机字符串:

Stackoverflow 有很多很好的答案。

随机产生英语单词

Gem: Webster

Gem: random-word

随机生成测试数据

Gem: Faker

它可以帮你生成像名字、地址、电话之类的随机测试数据。

随机产生一段话

Gem: Raingrams

「CodeWars」Matrix

Published on:
Tags: ruby CodeWars

本文为「CodeWars」6ku级别的题: Matrix

我在QQ群:222359552 里发起一个每周不定时活动:答题送视频优惠码, 这是我自己的解答。。


关于题的描述

Write a function that accepts two square matrices (nxn two dimensional arrays), and return the sum of the two. Both matrices being passed into the function will be of size nxn (square), containing only integers.

How to sum two matrices:
Take each cell [n][m] from the first matrix, and add it with the [n][m] cell from the second matrix.> This will be cell [n][m] in the solution matrix.

Visualization:
|1 2 3|
|3 2 1|
|1 1 1|
+
|2 2 1|
|3 2 3|
|1 1 3|

result =:

|3 4 4|
|6 4 4|
|2 2 4|

Example function call:
matrixAddition( [ [1, 2, 3], [3, 2, 1,], [1, 1, 1] ], [ [2, 2, 1], [3, 2, 3], [1, 1, 3] ] );
returns [ [3, 4, 4], [6, 4, 4], [2, 2, 4] ]

本题是给出一个矩阵,让你计算矩阵的和。

背景知识

矩阵(Matrix),是数学中最重要的基本概念之一,关于其具体的数学概念及其应用,可以去查维基百科

在Ruby中,可以用一个二维数组来表示矩阵。比如:

[
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
]

而Ruby的标准库中,也实现了一个Matrix类,来帮助你方便的操作矩阵。

而对于这次CodeWars的题,我自己实现了一个简单的矩阵类。

自己实现的矩阵类Matrix

class Matrix
  class CantCreateMatrixError < ::StandardError; end

  attr_accessor :matrix

  def initialize(*arr)
    begin
      arr.each do |a|
        raise CantCreateMatrixError, "Does not meet specifications" if a.size != arr[0].size
      end
      @matrix = arr
    end
  end

  def self.[](*arr)
    new(*arr)
  end

  def +(other)
    other.matrix.each_index.inject([]) do |arr, index|
      matrix_row = self.row(index)
      arr << matrix_row.size.times.inject([]) do |new_row, i|
        new_row << matrix_row[i] + other.row(index)[i]
      end
    end
  end

  def cloumn(index=0)
    matrix.map{|arr| arr[index]}
  end

  def row(index=0)
    matrix[index]
  end

  def cloumns
    s = []
    matrix[0].size.times do |index|
      s << cloumn(index)
    end
    s
  end
end

用法:

# 创建矩阵

matrix1 = Matrix[
                 [1, 2, 3], 
                 [3, 2, 1], 
                 [1, 1, 1]
                ]
matrix2 = Matrix[
                 [2, 2, 1],
                 [3, 2, 3], 
                 [1, 1, 3]
                ]
                
                
# 如果你创建的矩阵不符合规范,则会报错

error_matrix = Matrix[
                        [1, 2],
                        [3, 4, 5],
                        [1, 3, 5, 6]
              ]
#=> Matrix::CantCreateMatrixError: Does not meet specifications



# 获取矩阵行数组

matrix1.row(0)
matrix1.row(1)
matrix1.row(2)

# 获取矩阵列数组

matrix2.cloumn(0)
matrix2.cloumn(1)
matrix2.cloumn(2)

# 按列获取矩阵,返回数组

matrix2.cloumns

# 求两个矩阵之和

matrix1 + matrix2



以上代码可以扩展各种操作, 比如乘除减等。 当然这段代码还有很多改进空间。

至此,Codewars的题就迎刃而解了:

def matrixAddition(a, b)
  Matrix[*a] + Matrix[*b]
end

Test.assert_equals(matrixAddition([[1, 2], [1, 2]], [[2, 3], [2, 3]]),[[3, 5], [3, 5]]);
#=> true

其他解法

Codewars上面很多其他解法,利用了Ruby提供的内建方法,也值得一看:

def matrixAddition(a, b)
  a.zip(b).map { |r| r.transpose.map { |c| c.reduce(:+) } }
end

利用了内建的Array#transpose方法。

def matrixAddition(a, b)
  new_array = []  
  a.each_with_index do |array, index|
    new_array << a[index].zip(b[index]).map{|pair| pair.reduce(&:+)}
  end
  new_array
end
def matrixAddition(a, b)
  (0...a.length).map do |i|
    (0...a[0].length).map do |j|
      a[i][j] + b[i][j]
    end
  end
end
def matrixAddition(a, b)
  b = b.flatten
  sum = a.flatten.map.with_index {|v,i| v + b[i] }.each_slice(a.length).to_a  
end

还有很多, 大体思路都差不多,像我那样写一个类,还是比较少的,大家的目的不同。

不知道你们的解题思路是哪一种呢?

「Linux基础」nice命令探秘

Published on:
Tags: Linux nice ruby

今天打开Ruby5扫了一眼,看到一篇文章:「Ruby & Rails: Making sure rake task won’t slow the site down」,里面谈到了使用nice命令优化rake任务,更详细的可以点进去看看。

这个命令我以前知道,以前订阅rss,某篇文章里提过,但是我只是知道而已,并没有去消化那些内容。 RSS就是有这种缺点,信息轰炸,你可以获取很多消息,眼花缭乱,但是你真正消化的能有多少? 今天还和别人在讨论rss,突然感觉,自从Google Reader停掉以后,我就没这个订阅rss的习惯了,仔细想来,我现在的习惯是自己去寻找需要的资源,而不是被动的获取。我觉得现在的方式很好,跟随自己的求知欲以及学习计划和结构,主动去寻找学习资源,不至于分散精力。这个nice命令就是这样,看过又能咋样,今天遇见还不是不了解? 不过今天的方式不一样,是我主动去寻找的资源,那我就一定得了解它的用法。

nice与进程优先级

nice命令可以改变进程的优先级。

语法:

nice [-n adjustment] [-adjustment] [--adjustment=adjustment] [--help] [--version] [command [arg...]]

说明:

  • -n adjustment, -adjustment, --adjustment=adjustment 皆为将该原有优先序的增加 adjustment
  • --help 显示求助讯息
  • --version 显示版本资讯

示例:设置程序运行时的优先级

# vi &    //后台运行

[1] 15297

# nice vi & //设置默认优先级

[2] 15298
[1]+ Stopped         vi

# nice -n 19 vi & //设置优先级为19

[3] 15299
[2]+ Stopped         nice vi

# nice -n -20 vi & //设置优先级为 -20

[4] 15300
[3]+ Stopped         nice -n 19 vi

// ls 的优先序加 1 并执行

# nice -n 1 ls


// ls 的优先序加 10 并执行
# nice ls

那什么叫进程的优先级呢?

进程cpu资源分配就是指进程的优先级(priority)。优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。

进程观察

使用下面命令,我们可以观察到系统进程的优先级:

# ps -l //显示进程

F S  UID  PID PPID C PRI NI ADDR SZ WCHAN TTY     TIME CMD
4 S   0 15278 15212 0 80  0 - 1208 wait  pts/2  00:00:00 bash
0 T   0 15297 15278 0 80  0 - 2687 signal pts/2  00:00:00 vi
0 T   0 15298 15278 0 90 10 - 2687 signal pts/2  00:00:00 vi
0 T   0 15299 15278 1 99 19 - 2687 signal pts/2  00:00:00 vi
4 T   0 15300 15278 3 60 -20 - 2687 signal pts/2  00:00:00 vi
4 R   0 15301 15278 0 80  0 -  625 -   pts/2  00:00:00 ps

[4]+ Stopped         nice -n -20 vi

# ps -lA //显示全部进程

...

上面有几个重要概念需要说明:

  • UID : 代表执行者的身份
  • PID : 代表这个进程的代号
  • PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
  • PRI :代表这个进程可被执行的优先级,其值越小越早被执行
  • NI :代表这个进程的nice值

跟我们今天的主题相关的,是后面这两个信息:PRI和NI。

  • PRI: 是指进程的优先级(priority), 是指进程被CPU执行的先后顺序,此值越小,那么进程的优先级越高。
  • NI: 是指nice的值,这个值表示的是进程可被执行的优先级的修正数值,注意,nice命令只是用来设置NI的数值,NI的数值一旦改变,就会影响到PRI。

计算公式如下:

PRI(new)=PRI(old)+nice

可以看得出来, nice的值,越小,PRI的值就越小, 进程优先级越高,则越快被执行。

nice的值,也就是NI,可以指定的范围为[-20, 19],其中-20最高,19最低,值得注意的是,只有系统管理者(root)可以设置负数的等级。

应用

这个命令的应用,就是用来优化系统。比如文章开头说的那篇文章中,用nice命令来调整rake后台的优先级:

RAILS_ENV=:environment nice -n 19 bundle exec rake :your_task 

在执行rake命令的时候,带上nice参数,指定了NI值为19, 意思是告诉CPU,这个rake 任务优先级最低,你们先忙别的吧,忙完别的再执行我。

比如我的App后台,我会把队列任务的优先级调低, 这样后台执行任务的时候,就不会跟其他API接口抢占CPU资源。 不过这个后台任务是不那么紧急的。如果你的后台任务很紧急,那么你可以调高优先级。

值得注意的是:

nice命令,只用于启动新的进程,才起作用。

renice命令

如果你想调整已经存在的进程的优先级,那么可以使用 renice命令:

renice -5 -p 5200 #PID为5200的进程nice设为-5

语法:

renice [优先等级][-g <程序群组名称>...][-p <程序识别码>...][-u <用户名称>...]

补充说明:

renice指令可重新调整程序执行的优先权等级。预设是以程序识别码指定程序调整其优先权,您亦可以指定程序群组或用户名称调整优先权等级, 并修改所有隶属于该程序群组或用户的程序的优先权。等级范围[-20,19],只有系统管理者可以改变其他用户程序的优先权,也仅有系统管理者可以设置负数等级。更nice命令类似。

参数说明:

  • -g <程序群组名称>  使用程序群组名称,修改所有隶属于该程序群组的程序的优先权。
  • -p <程序识别码>  改变该程序的优先权等级,此参数为预设值。
  • -u <用户名称>  指定用户名称,修改所有隶属于该用户的程序的优先权。

也可以用top命令更改已存在进程的nice:

$ top

#进入top后按“r”–>输入进程PID–>输入nice值

结语

虽然我们了解了这一技巧,但是建议使用的时候,还要比较谨慎,要结合你的应用具体情况来优化。

发布了一本书「Chef之道」

Published on:
Tags: chef ruby

今天,在Gitbook发布了一本书,叫「Chef之道」。 本书的初衷因为一次对平安科技的企业培训所作。本书适合想了解Chef的Rubyist, 更适合使用Chef但苦于对Ruby没有学习之法的运维人员。

这本书压了两个月时间,直到今天才发布,是因为本不想发布的,正好今天有个朋友问我关于Chef的问题,我就想,先发布吧,虽然细节不够,但是胜在比较系统。

后续我也会持续更新,但是时间有限,会更新比较慢。

希望对大家有所帮助。

所谓正则表达式的技巧

Published on:
Tags: 正则 ruby

今天下午,看到一道正则的题目:最少包含数字和字母, 只能包含._-@这四个特殊字符,其他的字符不允许使用。

我一看, 这不就是某些网站在输入密码时候为了让用户选择安全强度比较高的密码而做的过滤吗? 还比上面那个题目上多了一个条件: 密码最少要求8位,最大32位。

 第一次想简单了:

/\d+|[a-zA-Z]+|[_.@-]+/

这个是没有考虑到题目中「最少包含数字和字母」这个条件, 这个条件的意思是:字符中最少的组合是数字+字符,也就是说数字和字符必须同时存在,单有数字或单有字母,是不能通过验证的。

否定匹配

这样的话就有点难度了。 正则表达式中, 没有直接表示「且」这种同时存在的概念。想表达「且」,有一个办法就是使用否定匹配(?!),比如:

"abcdsss".match(/^(?!abcd).*?$/)
=> nil
"acbcdsss".match(/^(?!abcd).*?$/)
=> #<MatchData "acbcdsss">

但是用在这个题目上,还不太适合,因为你需要找出一个否定条件:「除数字和字母之外的任意字符都不匹配」。 除数字和字母之外的任意字符,太多了, 而且,这个条件也把题目中允许的那四个特殊字符给否定了。

所以这种方法排除。

环视

经过思考, 我确定了一种方法,是可以判断这种情况的, 那就是判断: 「数字旁边是字母, 字母旁边是数字, 字母和数字之间可能有特殊字符,包括允许和不允许的」。 那么,只有通过「环视」功能,才可以做出以上的判断,于是得出如下正则表达式:

# 数字旁边是字母,字母旁边是数字

/[0-9a-zA-Z]*(?<=[a-zA-Z])[0-9a-zA-Z]*(?=[0-9])[a-zA-Z0-9]*/

"1234fsfsfd123123sdfs".match(/[0-9a-zA-Z]*(?<=[a-zA-Z])[a-zA-Z]*(?=[0-9])[a-zA-Z0-9]*/)
=> #<MatchData "234fsfsfd123123sdfs">


"111".match(/[0-9a-zA-Z]*(?<=[a-zA-Z])[a-zA-Z]*(?=[0-9])[a-zA-Z0-9]*/)
=> nil

"abcd".match(/[0-9a-zA-Z]*(?<=[a-zA-Z])[a-zA-Z]*(?=[0-9])[a-zA-Z0-9]*/)
=> nil

看起来靠谱, 思路应该是没错了,可是。。。 这只符合数字和字母的组合, 数字和字母之间还可能有任意的特殊符合,包括允许的和不允许的。最终,沿着这个思路,我得出了下面这段很长很笨的正则:

"sfdsf_@ssf11_@11".match(/(?<must>[0-9a-zA-Z]*(?<spec>[@_.-]*)(?<=[0-9])[0-9a-zA-Z]*(?<spec>[@_.-]*)(?<=[@_-])[0-9a-zA-Z]*(?<spec>[@_.-]*)(?=[a-z])[0-9a-zA-Z]*(?<spec>[@_.-]*))|(?<must>[0-9a-zA-Z]*(?<spec>[@_.-]*)(?<=[a-z])[0-9a-zA-Z]*(?<spec>[@_.-]*)(?<=[@_-])[0-9a-zA-Z]*(?<spec>[@_.-]*)(?=[0-9])[0-9a-zA-Z@_-]*(?<spec>[@_.-]*))/)

=> #<MatchData

 "sfdsf_@ssf11_@11"
 must:nil
 spec:nil
 spec:nil
 spec:nil
 spec:nil
 must:"sfdsf_@ssf11_@11"
 spec:""
 spec:"_@"
 spec:"_@"
 spec:"">

我使用了命名捕获组,must代表必须匹配, spec代表允许匹配的特殊字符。但是这个答案还不是最终的,因为它不完美,它不能满足所有的字符组合。

一定还有更简单的方法。

奇技淫巧

最终是看了一位正则高手给出的答案(也许有更简单的):

(?=^.*?[0-9])(?=^.*?[A-Za-z])^[0-9a-zA-Z_.@\-]{8,32}$

这个非常简洁,思路跟我那个笨办法是一样的,采用了环视,通过位置的判断达到数字与字母「且」的组合, 其中简单的技巧在于:

^.*?

这四个常用的正则元字符, 单个的意思,大家都知道, 但是组合在一起,非常强大。

"ssfds".match(/^.*/)
=> #<MatchData "ssfds">


"ssfds".match(/^.*?/)
=> #<MatchData "">

我们都知道.*的组合是非常强大的, 匹配任意字符, 但是加了问号「?」, 就不一样了。

? (问号),有两层意思:

  1. 匹配前面的子表达式零次或一次。例如,"do(es)?" 可以匹配 "do" 或 "does" 中的"do" 。? 等价于 {0,1}。
  2. 当该字符紧跟在任何一个其他限制符 (*, +, ?, {n}, {n,}, {n,m}) 后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串 "oooo",'o+?' 将匹配单个 "o",而 'o+' 将匹配所有 'o'。也就是懒惰模式。

所以, (^.*?)这个技巧,就是使用了「懒惰模式」, 意思是「匹配任意字符0次,也就是忽视任意字符」。 回到我们的正则表达式中:

(?=^.*?[0-9]) # 正向环视 ,匹配一个右边包含数字(忽视数字之前的任意字符)的位置

(?=^.*?[A-Za-z]) #正向环视, 匹配一个右边包含大小写字母(忽视字母之前的任意字符)的位置

^[0-9a-zA-Z_.@\-]{8,32}$ # 在匹配到上面的位置之后, 再匹配位置右边是否存在8-32位长的包含数字、字母、四位被允许的特殊字符组合。

这就是这个表达式的含义。 (.*?) 这样的用法,估计很多人知道,您看完之后,也许感觉很简单, 但是如果看了我上面的笨办法之后,你应该可以感受得到,对于不经常写正则表达式的人来说,这样的技巧太重要了,也非常值得我们学习。

这就是所谓的技巧,也许你早已知道,但你却不知道何时何地如何使用。

最后再推荐一个在线调试正则的工具:https://www.debuggex.com/

「KSNCTF攻略」Easy Cipher

Published on:

昨天有幸得知一个网站:KSNCTF,是一个解谜过关的网站,但是你过关的时候,需要各种知识,包括简单的密码学、编程等,我目前只过了三关。你要想过关,就必须找到形如 FLAG_123456xyz 这样的过关钥匙, 这把钥匙就隐藏在你要过的关卡中。而第一关,就是个练习, 直接告诉你FLAG_123456xyz,让你输入过关就行。

本篇blog我们来说说 第二关:Easy Cipher


这是一道50分的题,题目如下:

EBG KVVV vf n fvzcyr yrggre fhofgvghgvba pvcure gung ercynprf n yrggre jvgu gur yrggre KVVV yrggref nsgre vg va gur nycunorg. EBG KVVV vf na rknzcyr bs gur Pnrfne pvcure, qrirybcrq va napvrag Ebzr. Synt vf SYNTFjmtkOWFNZdjkkNH. Vafreg na haqrefpber vzzrqvngryl nsgre SYNT.

这就是一堆乱码,再没有其他的说明。

再这堆乱码中想找出形如「FLAG_123456xyz」的钥匙字符串,显然没那么容易。 肉眼看过去,也就数「SYNTFjmtkOWFNZdjkkNH」这段字符比较像。 那么唯一的线索,就是想到,这可能是一段加过密的字符串。那么既然这里是让我们解题, 那么应该是这种对称加密方式,否则,此题无解,因为并没有任何其他的线索(包括查看网页源码寻找隐藏线索,一般的解迷过关游戏都会这样隐藏信息,否则没地方隐藏了)。 所以,接下来就应该思考,这段文字,是用什么样的对称加密方式呢?

在网上搜索了一下,对称加密方式有好几种, 最简单的一种是凯撒移位加密,还有其他的方式。但是除了凯撒移位的加密方法,其他的方式基本都是需要密文配合使用的,也就是所谓的密钥。 但是我们说了,本题再没有任何的线索,那么只有考虑凯撒移位加密方式,你才能把本题进行下去。

凯撒移位加密:

“恺撒密码”据传是古罗马恺撒大帝用来保护重要军情的加密系统。

它是一种置换密码,通过将字母按顺序推后起3位起到加密作用,如将字母A换作字母D,将字母B换作字母E。据说恺撒是率先使用加密函的古代将领之一,因此这种加密方法被称为恺撒密码。

假如有这样一条指令:
明文(小写):easy cipher
用恺撒密码加密后就成为(加入我们移动1位):
密文(小写):fbtz djqifs

那么接下来,我们就需要寻找, 题中给的密文,到底是移动了多少位? 反正就26个英文字母, 我唯一能用的就是暴力破解了:

选定一个单词,然后把每个字母从1-26都移一遍位,看看得出的单词,那个是比较通顺的。

def caesar_cipher(str)
  result = []
  1.upto(25) do  |i|
    arr = str.downcase.each_char.inject([]) do |s, c|   
      if (c.ord+i> 122) 
        s << (c.ord - (26 - i)).chr
      else
        s << (c.ord + i).chr
      end
      s
    end 
    result << arr.join
  end 
  result
end

上面这个方法(我没判断大小写,全部转为小写),用来暴力破解凯撒位移密码,我们输入那个觉得可疑的字符串:

caesar_cipher("SYNTFjmtkOWFNZdjkkNH")

#=>


["tzougknulpxgoaeklloi",
 "uapvhlovmqyhpbflmmpj",
 "vbqwimpwnrziqcgmnnqk",
 "wcrxjnqxosajrdhnoorl",
 "xdsykoryptbkseioppsm",
 "yetzlpszqucltfjpqqtn",
 "zfuamqtarvdmugkqrruo",
 "agvbnrubswenvhlrssvp",
 "bhwcosvctxfowimsttwq",
 "cixdptwduygpxjntuuxr",
 "djyequxevzhqykouvvys",
 "ekzfrvyfwairzlpvwwzt",
 "flagswzgxbjsamqwxxau",
 "gmbhtxahycktbnrxyybv",
 "hnciuybizdlucosyzzcw",
 "iodjvzcjaemvdptzaadx",
 "jpekwadkbfnwequabbey",
 "kqflxbelcgoxfrvbccfz",
 "lrgmycfmdhpygswcddga",
 "mshnzdgneiqzhtxdeehb",
 "ntioaehofjraiuyeffic",
 "oujpbfipgksbjvzfggjd",
 "pvkqcgjqhltckwaghhke",
 "qwlrdhkrimudlxbhiilf",
 "rxmseilsjnvemycijjmg"]

然后我们看看结果, 发现第10个转换后的字符"flagswzgxbjsamqwxxau"很可疑, 因为有flag前缀。那么我们把这个字符按照KSNCTF网站的过关规则,结合密文中的大写小写位置,变化一下这个字符串:

"SYNTFjmtkOWFNZdjkkNH" # 原来的字符

"flagswzgxbjsamqwxxau" # 破解出来的可疑字串

"FLAGSwzgxBJSAMqwxxAU"  # 大小写比对,肉眼变换

"FLAG_SwzgxBJSAMqwxxAU" # 在FLAG后面加下划线


经过上面几步,我们得到了一个过关钥匙串,把他输入到我们的题目中:

Congratulation! Flag FLAG_SwzgxBJSAMqwxxAU is correct.

恭喜你! 过关了。


请期待下一篇: 「KSNCTF攻略」Crawling Chaos