0

I am trying to store the selectors in hashes assigned to the appropriate topics which I will then scrape from a webpage. However, when I do so, I am met with an 'undefined method' error for the "css" method.

Example:

@@letters_hash = {
      "a" => {
         uppercase: "A",
         history: css('div.class_1').css('div.class_2').text,
         url: "www.alphabet.com"
      }
}

Is there a way to encapsulate this? Or, if I store it as a string, is there a way to remove the string and get it back to the methods?

Thank you for your time.

  • 1
    Do you mean `css.('div.class_1')` or `css('div.class_1')`? I'm guessing that you're seeing an "undefined method call" exception. – mu is too short Jan 11 '22 at 19:41
  • sorry, the "css." was a typo. – Marc Auciello Jan 11 '22 at 19:46
  • Which call to `css` is giving you the undefined method error? – Schwern Jan 11 '22 at 19:48
  • it is just giving me the line number, so I would assume it is the first one. edit* when I remove the second one, I get the same error, so it is the first one. – Marc Auciello Jan 11 '22 at 19:49
  • 1
    So where is the `css` method supposed to come from? – mu is too short Jan 11 '22 at 19:53
  • it is a method that is part of the Nokogiri gem. I want to be able to interpolate the 'value' from the hash in a scraper method later. *edit: later it will look something like this `parsed_page.css('div.class_1').css('dic.class_2').text` – Marc Auciello Jan 11 '22 at 19:56
  • @MarcAuciello Are you getting ```NoMethodError (undefined method `css' for nil:NilClass)```? – Schwern Jan 11 '22 at 20:26
  • BTW [Class variables probably don't do what you think](https://stackoverflow.com/questions/33762366/are-ruby-class-variables-bad), use class instance variables instead. – Schwern Jan 11 '22 at 20:29
  • @Schwern this is the exact error I am getting `undefined method `css' for Database:Class (NoMethodError)` Also, it's part of a larger class, and it seems to only work if the hash is a class variable. It seems to not play nice if I try to initialize it. But, perhaps I am just doing that incorrectly as well. Thanks for the tip. – Marc Auciello Jan 11 '22 at 20:36

1 Answers1

2

css('div.class_1').css('div.class_2').text is self.css('div.class_1').css('div.class_2').text and self is your Database class. It doesn't have a css method. You need to call the method on something which has a css method like a Nokogiri node.

Callbacks

If you want to store a set of methods to call on some Nokogiri node you'll get later, you make a callback using a little anonymous function called a lambda.

@letters = {
      "a" => {
         uppercase: "A",
         history: ->(node) { node.css('div.class_1').css('div.class_2').text },
         url: "www.alphabet.com"
      }
}

That takes a node as an argument and calls the methods on the node.

Then later when you have a node you can call this function.

@letters_hash[letter][:history].call(node)

Objects

At this point it's getting compliated and should be encapsulated in an object.

class LetterTopic
  def initialize(letter)
    @letter = letter
  end

  def node_history(node)
    node.css('div.class_1').css('div.class_2').text
  end

  def uppercase
    @letter.upcase
  end

  def url
    "www.alphabet.com"
  end
end

letters = {
  "a" => LetterTopic.new("a")
}

node = ...get a Nokogiri node...

letters[letter].node_history(node)

A Note About Class Variables

@@letters_hash does not do what you think. Class variables in Ruby are shared by subclasses. If you subclass Database they will all share a single @@letters_hash variable.

class Database
  @@letters = {}

  def self.letters
    @@letters
  end
end

class Databasement < Database
end

Database.letters[:a] = 'database'
Databasement.letters[:a] = 'databasement'

p Database.letters     # {:a=>"databasement"}
p Databasement.letters # {:a=>"databasement"}

Instead, use Class Instance Variables. Like everything else in Ruby, the Database class is an object and can have its own instance variables.

class Database
  # Everything inside `class << self` works on the class object.
  class << self
    def letters
      @letters ||= {}
    end
  end
end

class Databasement < Database
end

Database.letters[:a] = 'database'
Databasement.letters[:a] = 'databasement'

p Database.letters       # {:a=>"database"}
p Databasement.letters   # {:a=>"databasement"}
Schwern
  • 153,029
  • 25
  • 195
  • 336