开发者

How can I improve?

I was wondering if anyone could point out a cleaner better way to write my code which is pasted here. The code scrapes some data from yelp and processes it into a json format. The reason I'm not using hash.to_json is because it throws some sort of stack error which I can only assume is due to the hash being too large (It's not particularly large).

  • Response object = a hash
  • text = the output which saves to file
开发者_StackOverflow社区

Anyways guidance appreciated.

def mineLocation

  client = Yelp::Client.new
  request = Yelp::Review::Request::GeoPoint.new(:latitude=>13.3125,:longitude => -6.2468,:yws_id => 'nicetry')
  response = client.search(request) 
  response['businesses'].length.times do |businessEntry|
    text =""
     response['businesses'][businessEntry].each { |key, value|
        if value.class == Array 
          value.length.times { |arrayEntry|
            text+= "\"#{key}\":["
             value[arrayEntry].each { |arrayKey,arrayValue|
              text+= "{\"#{arrayKey}\":\"#{arrayValue}\"},"
             }
             text+="]"   
          }
        else 
              text+="\"#{arrayKey}\":\"#{arrayValue}\"," 
        end
       }
  end
 end


It looks like all your code is ultimately doing is this:

require 'json'

def mine_location
  client = Yelp::Client.new
  request = Yelp::Review::Request::GeoPoint.new(latitude: 13.3125,
    longitude: -6.2468, yws_id: 'nicetry')
  response = client.search(request)

  return response['businesses'].to_json
end

Which works just fine for me.

If, for whatever reason you really must write your own implementation of a JSON emitter, here's a couple of tips for you.

The number 1 thing you completely ignore in your code is that Ruby is an object-oriented language, more specifically a class-based object-oriented language. This means that problems are solved by constructing a network of objects that communicate with each other via message passing and respond to those messages by executing methods defined in classes to which those objects belong.

This gives us a lot of power: dynamic dispatch, polymorphism, encapsulation and a ton of others. Leveraging those, your JSON emitter would look something like this:

class Object
  def to_json; to_s                                                         end
end

class NilClass
  def to_json; 'null'                                                       end
end

class String
  def to_json; %Q'"#{to_s}"'                                                end
end

class Array
  def to_json; "[#{map(&:to_json).join(', ')}]"                             end
end

class Hash
  def to_json; "{#{map {|k, v| "#{k.to_json}: #{v.to_json}" }.join(', ')}}" end
end

mine_location looks just like above, except obviously without the require 'json' part.

If you want your JSON nicely formatted, you could try something like this:

class Object
  def to_json(*) to_s    end
end

class String
  def to_json(*) inspect end
end

class Array
  def to_json(indent=0)
    "[\n#{'  ' * indent+=1}#{
      map {|el| el.to_json(indent) }.join(", \n#{'  ' * indent}")
    }\n#{'  ' * indent-=1}]"
  end
end

class Hash
  def to_json(indent=0)
    "{\n#{'  ' * indent+=1}#{
      map {|k, v|
        "#{k.to_json(indent)}: #{v.to_json(indent)}"
      }.join(", \n#{'  ' * indent}")
    }\n#{'  ' * indent-=1}}"
  end
end

There's actually nothing Ruby-specific in this code. This is pretty much exactly what the solution would look like in any other class-based object-oriented language like Java, for example. It's just object-oriented design 101.

The only thing which is language-specific is how to "modify" classes and add methods to them. In Ruby or Python, you literally just modify the class. In C# and Visual Basic.NET, you would probably use extension methods, in Scala you would use implicit conversions and in Java maybe the Decorator Design Pattern.

Another huge problem with your code is that you are trying to solve a problem which is obviously recursive without actually ever recursing. This just can't work. The code you wrote is basically Fortran-57 code: procedural with no objects and no recursion. Even just moving one step up from Fortran to, say, Pascal, gives you a nice recursive procedural solution:

def jsonify(o)
  case o
  when Hash
    "{#{o.map {|k, v| "#{jsonify(k)}: #{jsonify(v)}" }.join(', ')}}"
  when Array
    "[#{o.map(&method(:jsonify)).join(', ')}]"
  when String
    o.inspect
  when nil
    'null'
  else
    o.to_s
  end
end

Of course, you can play the same game with indentations here:

def jsonify(o, indent=0)
  case o
  when Hash
    "{\n#{'  ' * indent+=1}#{
      o.map {|k, v|
        "#{jsonify(k, indent)}: #{jsonify(v, indent)}"
      }.join(", \n#{'  ' * indent}") }\n#{'  ' * indent-=1}}"
  when Array
    "[\n#{'  ' * indent+=1}#{
      o.map {|el| jsonify(el, indent) }.join(", \n#{'  ' * indent}") }\n#{'  ' * indent-=1}]"
  when String
    o.inspect
  when nil
    'null'
  else
    o.to_s
  end
end

Here's the indented output of puts mine_location, produced using either the second (indented) version of to_json or the second version of jsonify, it doesn't really matter, they both have the same output:

[
  {
    "name": "Nickies",
    "mobile_url": "http://mobile.yelp.com/biz/yyqwqfgn1ZmbQYNbl7s5sQ",
    "city": "San Francisco",
    "address1": "466 Haight St",
    "zip": "94117",
    "latitude": 37.772201,
    "avg_rating": 4.0,
    "address2": "",
    "country_code": "US",
    "country": "USA",
    "address3": "",
    "photo_url_small": "http://static.px.yelp.com/bpthumb/mPNTiQm5HVqLLcUi8XrDiA/ss",
    "url": "http://yelp.com/biz/nickies-san-francisco",
    "photo_url": "http://static.px.yelp.com/bpthumb/mPNTiQm5HVqLLcUi8XrDiA/ms",
    "rating_img_url_small": "http://static.px.yelp.com/static/20070816/i/ico/stars/stars_small_4.png",
    "is_closed": false,
    "id": "yyqwqfgn1ZmbQYNbl7s5sQ",
    "nearby_url": "http://yelp.com/search?find_loc=466+Haight+St%2C+San+Francisco%2C+CA",
    "state_code": "CA",
    "reviews": [
      {
        "rating": 3,
        "user_photo_url_small": "http://static.px.yelp.com/upthumb/ZQDXkIwQmgfAcazw8OgK2g/ss",
        "url": "http://yelp.com/biz/yyqwqfgn1ZmbQYNbl7s5sQ#hrid:t-sisM24K9GvvYhr-9w1EQ",
        "user_url": "http://yelp.com/user_details?userid=XMeRHjiLhA9cv3BsSOazCA",
        "user_photo_url": "http://static.px.yelp.com/upthumb/ZQDXkIwQmgfAcazw8OgK2g/ms",
        "rating_img_url_small": "http://static.px.yelp.com/static/20070816/i/ico/stars/stars_small_3.png",
        "id": "t-sisM24K9GvvYhr-9w1EQ",
        "text_excerpt": "So I know gentrification is supposed to be a bad word and all (especially here in SF), but the Lower Haight might benefit a bit from it. At least, I like...",
        "user_name": "Trey F.",
        "mobile_uri": "http://mobile.yelp.com/biz/yyqwqfgn1ZmbQYNbl7s5sQ?srid=t-sisM24K9GvvYhr-9w1EQ",
        "rating_img_url": "http://static.px.yelp.com/static/20070816/i/ico/stars/stars_3.png"
      },
      {
        "rating": 4,
        "user_photo_url_small": "http://static.px.yelp.com/upthumb/Ghwoq23_alkaXawgqj7dBA/ss",
        "url": "http://yelp.com/biz/yyqwqfgn1ZmbQYNbl7s5sQ#hrid:8xTNOC9L5ZXwGCMNYY-pdQ",
        "user_url": "http://yelp.com/user_details?userid=4F2QG3adYIUNXplqqp9ylA",
        "user_photo_url": "http://static.px.yelp.com/upthumb/Ghwoq23_alkaXawgqj7dBA/ms",
        "rating_img_url_small": "http://static.px.yelp.com/static/20070816/i/ico/stars/stars_small_4.png",
        "id": "8xTNOC9L5ZXwGCMNYY-pdQ",
        "text_excerpt": "This place was definitely a great place to chill. The atmosphere is very non-threatening and very neighborly. I thought it was cool that they had a girl dj...",
        "user_name": "Jessy M.",
        "mobile_uri": "http://mobile.yelp.com/biz/yyqwqfgn1ZmbQYNbl7s5sQ?srid=8xTNOC9L5ZXwGCMNYY-pdQ",
        "rating_img_url": "http://static.px.yelp.com/static/20070816/i/ico/stars/stars_4.png"
      },
      {
        "rating": 5,
        "user_photo_url_small": "http://static.px.yelp.com/upthumb/q0POOE3vv2LzNg1qN8MMyw/ss",
        "url": "http://yelp.com/biz/yyqwqfgn1ZmbQYNbl7s5sQ#hrid:pp33WfN_FoKlQKJ-38j_Ag",
        "user_url": "http://yelp.com/user_details?userid=FmcKafW272uSWXbUF2rslA",
        "user_photo_url": "http://static.px.yelp.com/upthumb/q0POOE3vv2LzNg1qN8MMyw/ms",
        "rating_img_url_small": "http://static.px.yelp.com/static/20070816/i/ico/stars/stars_small_5.png",
        "id": "pp33WfN_FoKlQKJ-38j_Ag",
        "text_excerpt": "Love this place!  I've been here twice now and each time has been a great experience.  The bartender is so nice.  When we had questions about the drinks he...",
        "user_name": "Scott M.",
        "mobile_uri": "http://mobile.yelp.com/biz/yyqwqfgn1ZmbQYNbl7s5sQ?srid=pp33WfN_FoKlQKJ-38j_Ag",
        "rating_img_url": "http://static.px.yelp.com/static/20070816/i/ico/stars/stars_5.png"
      }
    ],
    "phone": "4152550300",
    "neighborhoods": [
      {
        "name": "Hayes Valley",
        "url": "http://yelp.com/search?find_loc=Hayes+Valley%2C+San+Francisco%2C+CA"
      }
    ],
    "rating_img_url": "http://static.px.yelp.com/static/20070816/i/ico/stars/stars_4.png",
    "longitude": -122.429926,
    "categories": [
      {
        "name": "Dance Clubs",
        "category_filter": "danceclubs",
        "search_url": "http://yelp.com/search?find_loc=466+Haight+St%2C+San+Francisco%2C+CA&cflt=danceclubs"
      },
      {
        "name": "Lounges",
        "category_filter": "lounges",
        "search_url": "http://yelp.com/search?find_loc=466+Haight+St%2C+San+Francisco%2C+CA&cflt=lounges"
      },
      {
        "name": "American (Traditional)",
        "category_filter": "tradamerican",
        "search_url": "http://yelp.com/search?find_loc=466+Haight+St%2C+San+Francisco%2C+CA&cflt=tradamerican"
      }
    ],
    "state": "CA",
    "review_count": 32,
    "distance": 1.87804019451141
  }
]


The first thing I notice off the bat is your use of

response['businesses'].length.times do |i|
  # the business you want is response['businesses'][i]
end

for iterating. This can be simplified greatly by using Array.each which provides you with:

response['businesses'].each do |businessEntry|
  # here, businessEntry is the actual object, so forego something like
  # response['business'][businessEntry], just use businessEntry directly
end

You're actually doing the same thing below with your: response['businesses'][businessEntry].each

Note on style, it's often (though no enforced) common style do use do/end if your block is on multiple lines and {} if the block is one line.

Also, not sure why you're building these key/value pairs in a string, just make a hash. Then you can easily convert to json, or as Jorg pointed out, just convert the whole response to json which does EXACTLY what you're doing manually... unless you need to work with the data first (which it doesn't look like you need to do)


I would be interested to see the error you were getting from hash.to_json to see if the cause for that can be found.

In terms of your Ruby code a couple of observations:

  • The use of .length.times do .. is a bit odd when you could just use each e.g. response['businesses'].each do ..

  • In your else case text+="\"#{arrayKey}\":\"#{arrayValue}\"," it looks like arrayKey and arrayValue are out of scope because they are only used as the block variables in the each above.

  • text ="" sets text back to empty string at each iteration of the outer look so as the code stands it looks like the text built up by the previous iterations of the loop is discarded.


I'm no expert on Ruby, but I do know what things that if they appear in my code, my professor yells at me for. Mikej already hit on the major things, especially with the use of #each instead of #length and #times.

If I am ever iterating through a collection of some sort, the only time I ever use something other than #each is when I need to use a custom iterator, and even then you could still use #each, but you would have to have a control statement inside the passed block to make sure that the block does not execute on certain instances. Even if it gets too complicated for that, the only other method of custom iteration I really use is a for i in Range.new( begin, end, optional_exclusion ) statement. And this could still be turned into a conditional in a block for #each, but it saves me code sometimes and makes it more explicit that I am intentionally not executing code on all elements of a collection as well as explicitly showing that I am setting the boundaries of the affected elements at the time of entry into the loop instead of hard-coded values or just the entire collection.

Mikej already pointed out the out the error of scope when calls to arrayKey and arrayValue are made in the else part of the if statement so I won't bother with that. He also already pointed out, you should probably move your text ="" code line up by two lines to get out of that response code block scope.

My only concern after that is some problems not with the code itself, but more sort of coding style and things generally practiced in the Ruby community. So these suggestions, by no means, you have to take. It just makes reading Ruby code generally easier.

So my first suggestion is whenever you call a method you are passing a block to, unless you can fit the proper indentation, object call, method call, block variable declaration, the code block, and the block closing all on the same line, then don't use curly braces. In those situations of multi-line code blocks, use the keywords do and end instead.

My second suggestion is to use proper indentation which in the Ruby language is two spaces instead of the normal four found in the coding styles of many other languages. You had proper indentation for a good chuck of your code and then you messed up and it kind caused some lines down the road to look like they are on a scope level that they are not. Now, this tip is nowhere near a coding style standard, but my general trick to make sure I am on the right scope level is just adding a end-line comment right after the use of the end keyword and put in the name of the scope it just closed. It has saved me from a number of scope bugs but I have never heard of anyone else doing it and it can possibly clutter code with these random end-line comments but it's a method that has served me well.

My third suggestion is to improve your use of strings and symbols. I am almost afraid to say this just because I still need to improve my understanding of symbols in Ruby and I don't recall using the Yelp class in any recent scripts so I am blind on that one. However, it looks like you use the string 'businesses' like a Hash key. A general rule in Ruby is if you are using a string just for what it represents, then you should be using a symbol while if you are using a string because you actually need its character contents, then you should probably stick to using a string. This is just because every time you call a string literal, a new string is made and stored in system memory. So here since you are using 'businesses' inside an each block that is inside and each block, you are potentially allocating that string O(n²) times (although the allocations for the inner block get garbage collected during the next iteration. Whereas if you used a symbol like ':businesses' instead, it only initializes it once. Also in the case of you text ="" line, you should modify that into a single quotation string literal. The Ruby interpreter can parse through single quote string literals faster than double quotes, so generally if you can use a single quoted string literal, do it.

All I got for suggestions. Take them as you need them or want them. Also here would be what your code would look like assuming you took my suggestions and I didn't break anything in the process.

def mineLocation

  client = Yelp::Client.new
  request = Yelp::Review::Request::GeoPoint.new(:latitude=>13.3125,
                                                :longitude => -6.2468,
                                                :yws_id => 'nicetry')
  response = client.search(request)
  text = ''
  response[:businesses].each do |businessEntry|
    response[:businesses][businessEntry].each do |key, value|
      if value.kindOf( Array )
        value.each do |arrayEntry|
          text += "\"#{key}\":["
          value[arrayEntry].each do |arrayKey, arrayValue|
            text += "{\"#{arrayKey}\":\"#{arrayValue}\"},"
          end #each
          text += ']'   
        end #each
      else
        # Didn't fix because I didn't know you intentions here.
        text += "\"#{arrayKey}\":\"#{arrayValue}\"," 
      end #if
    end #each
  end #each

end #def

I didn't replace 'nicetry' with a symbol just because I don't know how the Yelp class works and might explicitly need a string there instead of a symbol. I also didn't know what the intended code effect was since the only time this code is executed is when the variables are out of scope, so I had no way of knowing what you were trying to reference in that line. Especially since at that point your value is also not an array either.

I know this is a long answer but I hope some of this helps!

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜