#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 и я не хотел вдаваться в подробности кода. Это хорошие, бесплатные ответы.