Должен ли я объявлять константу, чтобы импортировать данные моего модуля из очищенного файла?

#ruby-on-rails #ruby

#ruby-on-rails #ruby

Вопрос:

Не уверен, что я делаю неправильно. Хочу импортировать данные моего файла scraper scraper.rb в приложение.

Не могу понять, почему я получаю эту ошибку или почему я должен объявлять константу, вызываемую SCRAPER как указано в ошибке.

Puma обнаружила эту ошибку: ожидаемый файл /Users/jmwofford/Desktop/Dev/scratchpad/scratch2_PRIMARY/projects/rails_scraper/scraperProj/app/controllers/scraper. rb, чтобы определить постоянный скребок, но не сделал этого (Zeitwerk::NameError)

Ниже приведен мой код

scraper.rb

 require 'net/http'
require 'uri'
require 'json'
require "awesome_print"
require 'nokogiri'
require 'httparty'
require 'mechanize'

module ScraperFinder
    def scrape_essential_data
        uri = URI.parse("https://buildout.com/plugins/4b4283d94258de190a1a5163c34c456f6b1294a2/inventory")
        request = Net::HTTP::Get.new(uri)
        request.content_type = "application/x-www-form-urlencoded; charset=UTF-8"
        request["Authority"] = "buildout.com"
        request["Accept"] = "application/json, text/javascript, */*; q=0.01"
        request["X-Newrelic-Id"] = "Vg4GU1RRGwIJUVJUAwY="
        request["Dnt"] = "1"
        request["X-Requested-With"] = "XMLHttpRequest"
        request["User-Agent"] = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36"
        request["Origin"] = "https://buildout.com"
        request["Sec-Fetch-Site"] = "same-origin"
        request["Sec-Fetch-Mode"] = "cors"
        request["Sec-Fetch-Dest"] = "empty"
        request["Referer"] = "https://buildout.com/plugins/4b4283d94258de190a1a5163c34c456f6b1294a2/leasespaces.jll.com/inventory/?pluginId=0amp;iframe=trueamp;embedded=trueamp;cacheSearch=trueamp;=undefined"
        request["Accept-Language"] = "en-US,en;q=0.9"
        
        req_options = {
            use_ssl: uri.scheme == "https",
        }
        
        response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
            http.request(request)
        end
        
        json = JSON.parse(response.body)
        
        props = json['inventory']
        
        props.each do |listing|
            
            property = {
                'id' => listing['id'],
                'name' => listing['name'],
                'address' => listing['address_one_line'],
                'description' => listing['id'],
                'property_type' => listing['property_sub_type_name'],
                'attr' => listing['index_attributes'],
                'latitude'=> listing['latitude'],
                'longitude' => listing['longitude'],
                'picture' => listing['photo_url'],
                'sizing' => listing['size_summary'],
                'link' => listing['show_link'],
                'brokerContacts' => listing['broker_contacts']
            }
            
            Property.create(
                name: property.name,
                address: property.address,
                description: property.description,
                property_type: property.property_type,
                lat: property.latitude,
                lon: property.longitude,
                pic: property.picture,
                size: property.sizing,
                link: property.link,
                brokerContact: property.brokerContacts
            )
            p "==========================================================================================="
            # pp property
        end
    end 
end    
  

Пользовательский контроллер

 require_relative ("./scraper.rb")
include ScraperFinder
class UsersController < ApplicationController
    def index
        @scraped = ScraperFinder.scrape_essential_data
    end
end
  

index.html.erb

 <!DOCTYPE html>

<html>
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <title></title>
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="stylesheet" href="">
    </head>
    <body>
        
        <% @scraped.each do |s|%>
           <div class="prop_container"> <%= s %>  </div>   
        <%end%>
        
        <script src="" ></script>
    </body>
</html>
  

Схемы

 create_table "properties", force: :cascade do |t|
    t.string "name"
    t.string "address"
    t.string "description"
    t.string "property_type"
    t.string "attr"
    t.string "lat"
    t.string "lon"
    t.string "pic"
    t.string "size"
    t.string "link"
    t.string "brokerContact"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
end

create_table "users", force: :cascade do |t|
    t.string "name"
    t.string "email"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
end
  

Комментарии:

1. Можете ли вы попробовать переместить этот файл в lib/scraper.rb и затем require Rails.root.join "lib", "scraper.rb"

2. Казалось, это что-то дало, но теперь я получаю сообщение об ошибке, NoMethodError (undefined method `name=' for #<Class:0x00007fd2a5421128>) и оно указывает на блок создания кода..

3. Иногда исправление одной ошибки выявляет другую ошибку. Потратьте немного времени на чтение ошибки и посмотрите, из какой строки она возникла. В конце концов вы научитесь распознавать ошибки и сможете мгновенно узнать, как их исправить. Вы не поделились этой информацией, поэтому мне сложно рассказать вам что-нибудь полезное.

Ответ №1:

Zeitwerk (автозагрузчик, используемый в Rails 6 ) предполагает, что вы объявляете константы в файле с тем же именем, что и константа. scraper.rb таким образом, ожидается объявление константы Scraper . Zeitwerk, в отличие от старого автозагрузчика, будет обходить ваши каталоги автозагрузки при запуске и индексировать все файлы, поэтому он жалуется, даже если вы не ссылались на константу Scraper .

Вы можете настроить Zeitwerk на игнорирование определенных папок, но вам действительно нужно просто ознакомиться с программой и настроить свой код на автозагрузчик. Начните с переименования вашего файла scraper_finder.rb , и он не принадлежит каталогу контроллера, поскольку он не является контроллером. Поместите его в app/lib или app/clients или в любом другом месте, действительно более подходящем.

На самом деле это только верхушка айсберга, поскольку этот код довольно неработающий. На самом деле вам нужно что-то вроде:

 # app/lib/scraper_finder.rb
require 'net/http'
# You don't need to require gems as they are required by bundler during startup

module ScraperFinder
  # You need to use self to make the method callable as `ScraperFinder.scrape_essential_data`
  def self.scrape_essential_data
    req_options = {
      use_ssl: uri.scheme == "https",
    }
    response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
      http.request(
        self.get("https://buildout.com/plugins/4b4283d94258de190a1a5163c34c456f6b1294a2/inventory")
      )
    end
    json = JSON.parse(response.body)
    json['inventory'].map do |listing|
      Property.create(extract_attributes(listing))
    end
  end 
  
  private

  def self.extract_attributes(listing)
    listing.slice('id', 'name').symbolize_keys.merge(
      description:    listing['id'],
      property_type:  listing['property_sub_type_name'],
      attr:           listing['index_attributes'],
      lat:            listing['latitude'],
      lon:            listing['longitude'],
      pic:            listing['photo_url'],
      size:           listing['size_summary'],
      link:           listing['show_link'],
      brokerContacts: listing['broker_contacts']
    )
  end
  
  def self.get(uri)
    Net::HTTP::Get.new(URI.parse(uri)).then do |req|
      req.content_type = "application/x-www-form-urlencoded; charset=UTF-8"
      req["Authority"] = "buildout.com"
      req["Accept"] = "application/json, text/javascript, */*; q=0.01"
      req["X-Newrelic-Id"] = "Vg4GU1RRGwIJUVJUAwY="
      req["Dnt"] = "1"
      req["X-Requested-With"] = "XMLHttpRequest"
      req["User-Agent"] = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36"
      req["Origin"] = "https://buildout.com"
      req["Sec-Fetch-Site"] = "same-origin"
      req["Sec-Fetch-Mode"] = "cors"
      req["Sec-Fetch-Dest"] = "empty"
      req["Referer"] = "https://buildout.com/plugins/4b4283d94258de190a1a5163c34c456f6b1294a2/leasespaces.jll.com/inventory/?pluginId=0amp;iframe=trueamp;embedded=trueamp;cacheSearch=trueamp;=undefined"
      req["Accept-Language"] = "en-US,en;q=0.9"
    end
  end
end
  
 class UsersController < ApplicationController
  def index
    @scraped = ScraperFinder.scrape_essential_data
  end
end
  

Хэши в Ruby не похожи на объекты в Javascript или Struct, поэтому ваш код вызовет NoMethodError on property.name — для доступа к свойствам хэша используйте квадратные скобки property['name'] . Но, как вы можете видеть, все это дублирование никогда не было оправдано в первую очередь, поскольку Ruby имеет отличные методы манипулирования хэшем.

Ваш метод также был объявлен как метод экземпляра, но вы вызываете его как ScraperFinder.scrape_essential_data . Чтобы сделать это методом модуля, который вам нужно использовать def self.scrape_essential_data .

Некоторый быстрый рефакторинг также разбивает это громоздкое чудовище на три отдельных метода, о которых легче понять причину.

Вам не нужно вручную запрашивать свой собственный код, если он находится в каталоге приложения, и это приведет к появлению ошибок. Используйте автозагрузчик.

include ScraperFinder копирует все методы из вашего модуля в глобальную область видимости, поскольку вы вызываете его вне модуля / класса! Поскольку ваш модуль, похоже, имеет только одноэлементные методы (методы, вызываемые в самом модуле), вам не нужно никуда его импортировать.

Комментарии:

1. @eyeslandic спасибо! По крайней мере, на этот раз я не назвал это духом времени.

Ответ №2:

tl; dr: Да, для того, чтобы автозагрузка Rails 6 работала, вам необходимо определить класс или модуль, который соответствует имени файла.

  • Поместите файл в соответствующий каталог приложения. Ваш переходит в app/controllers .
  • Поскольку это модуль, а Rails называет их «проблемами», он должен находиться в concerns подкаталоге.
  • Остальная часть пути к файлу должна соответствовать имени класса или модуля. ScraperFinder входит scraper_finder.rb .

Итак, модуль ScraperFinder, который расширяет контроллеры, переходит в app/controllers/concerns/scraper_finder.rb .

Если бы это было Scraper::Finder так, это вошло бы в app/controllers/concerns/scraper/finder.rb .

Rails не имеет значения, в какой именно app подкаталог он попадает, и находится ли он в concerns подкаталоге или нет. Но это хорошо для организации.

Тогда вам не нужно запрашивать файл, Rails загрузится автоматически для вас.


Rails автоматически загрузит файлы для вас. Вам не require app/model/foo нужно использовать Foo модель. Вы просто используете Foo , и Rails загрузит для вас соответствующий файл.

В Rail 6 была представлена новая и улучшенная система автозагрузки под названием Zeitwerk. В Rails 5 это определяло бы имя файла из константы. Теперь он определит константу из имени файла.

Например, в Rails 5, если вы попытаетесь использовать Scraper его, он пойдет и поищет scraper.rb где-нибудь в app . Это может привести к странным результатам.

В Rails 6 Rails сканирует файлы в приложении и предполагает, что они являются константами. Если он видит app/controllers/scraper.rb , он предполагает, что это файл для Scraper и зарегистрируется Scraper для автоматической загрузки : autoload('Scraper', 'app/controllers/scraper.rb') . При Scraper первом упоминании Ruby выполнит эквивалент require 'app/controllers/scraper.rb' . Также требуется, чтобы после загрузки файла Scraper был определен.

autoload это обычный метод Ruby, поэтому мы можем увидеть это в действии.

 # test.rb
p "Hello"

# in irb

> autoload("Test", "./test.rb")
> Test
"Hello"
Traceback (most recent call last):
        4: from /Users/schwern/.rvm/rubies/ruby-2.6.5/bin/irb:23:in `<main>'
        3: from /Users/schwern/.rvm/rubies/ruby-2.6.5/bin/irb:23:in `load'
        2: from /Users/schwern/.rvm/rubies/ruby-2.6.5/lib/ruby/gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
        1: from (irb):4
NameError (uninitialized constant Test)
  

Используя Test loads test.rb , но поскольку он не определил Test класс, мы получаем ошибку NameError.


Обратите внимание, что вы неправильно используете ScraperFinder. Он должен быть включен в класс, чтобы он мог вводить свои методы.

 class UsersController < ApplicationController
    include ScraperFinder

    def index
        @scraped = ScraperFinder.scrape_essential_data
    end
end
  

Но тогда вы используете его как класс, а не как модуль. Модуль вводит свои методы, и вы будете использовать их напрямую.

 class UsersController < ApplicationController
  include ScraperFinder

  def index
    @scraped = scrape_essential_data
  end
end
  

Но это лучше сделать как класс. Они не включены, они просто используются обычно.

Этот тип класса, который обращается к сервису, является классом обслуживания и должен входить app/services . Rails автоматически загрузит все ближайшие подкаталоги app .

 # You don't need to require external libraries
# if they are defined in your Gemfile.

# app/services/scraper_finder.rb
class ScraperFinder
  def self.scrape_essential_data
    ...
  end
end

# app/controller/users_controller.rb
class UsersController < ApplicationController
    def index
      # ScraperFinder will be autoloaded   
      @scraped = ScraperFinder.scrape_essential_data
    end
end
  

Комментарии:

1. Хороший ответ. На самом деле я не хотел спускаться в кроличью нору служебных объектов и вместо этого просто сохранил код.

2. @max и я не хотел вдаваться в подробности кода. Это хорошие, бесплатные ответы.