Ruby Zip: Cannot open entry for reading while its open for writing - ruby

I'm trying to write some mail merge code where I open a docx file (as a zip) replace tags with data and then create a new docx file (as a zip) and iterate over the old zip file either adding my new replaced data or pulling the existing file from the old docx file and adding that instead.
The problem I'm getting is anytime I try to access the out.get_output_stream method, I'm getting the following error:
cannot open entry for reading while its open for writing - [Content_Types].xml (StandardError)
[Content_Types].xml happens to be first file in the docx so that's why its bombing on that particular file. What am I doing wrong?
require 'rubygems'
require 'zip' # rubyzip gem
class WordMailMerge
def self.open(path, &block)
self.new(path, &block)
end
def initialize(path, &block)
#replace = {}
if block_given?
#zip = Zip::File.open(path)
yield(self)
#zip.close
else
#zip = Zip::File.open(path)
end
end
def force_settings
#replace["word/settings.xml"] = %{<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:settings xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:sl="http://schemas.openxmlformats.org/schemaLibrary/2006/main"><w:zoom w:percent="100"/></w:settings>}
end
def merge(rec)
xml = #zip.read("word/document.xml")
# replace tags with correct content
#replace["word/document.xml"] = xml
end
def save(path)
Zip::File.open(path, Zip::File::CREATE) do |out|
#zip.each do |entry|
if #replace[entry.name]
# this line creates the error
out.get_output_stream(entry.name).write(#replace[entry.name])
else
# this line also will do it.
out.get_output_stream(entry.name).write(#zip.read(entry.name))
end
end
end
end
def close
#zip.close
end
end
w = WordMailMerge.open("Option_2.docx")
w.force_settings
w.merge({})
w.save("Option_2_new.docx")
The following is the stack trace:
/home/aaron/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/delegate.rb:85:in `call': cannot open entry for reading while its open for writing - [Content_Types].xml (StandardError)
from /home/aaron/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/delegate.rb:85:in `method_missing'
from /home/aaron/.rvm/gems/ruby-2.4.1#appt/gems/rubyzip-1.2.1/lib/zip/streamable_stream.rb:28:in `get_input_stream'
from /home/aaron/.rvm/gems/ruby-2.4.1#appt/gems/rubyzip-1.2.1/lib/zip/streamable_stream.rb:45:in `write_to_zip_output_stream'
from /home/aaron/.rvm/gems/ruby-2.4.1#appt/gems/rubyzip-1.2.1/lib/zip/file.rb:313:in `block (3 levels) in commit'
from /home/aaron/.rvm/gems/ruby-2.4.1#appt/gems/rubyzip-1.2.1/lib/zip/entry_set.rb:38:in `block in each'
from /home/aaron/.rvm/gems/ruby-2.4.1#appt/gems/rubyzip-1.2.1/lib/zip/entry_set.rb:37:in `each'
from /home/aaron/.rvm/gems/ruby-2.4.1#appt/gems/rubyzip-1.2.1/lib/zip/entry_set.rb:37:in `each'
from /home/aaron/.rvm/gems/ruby-2.4.1#appt/gems/rubyzip-1.2.1/lib/zip/file.rb:312:in `block (2 levels) in commit'
from /home/aaron/.rvm/gems/ruby-2.4.1#appt/gems/rubyzip-1.2.1/lib/zip/output_stream.rb:53:in `open'
from /home/aaron/.rvm/gems/ruby-2.4.1#appt/gems/rubyzip-1.2.1/lib/zip/file.rb:311:in `block in commit'
from /home/aaron/.rvm/gems/ruby-2.4.1#appt/gems/rubyzip-1.2.1/lib/zip/file.rb:409:in `block in on_success_replace'
from /home/aaron/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/tmpdir.rb:130:in `create'
from /home/aaron/.rvm/gems/ruby-2.4.1#appt/gems/rubyzip-1.2.1/lib/zip/file.rb:407:in `on_success_replace'
from /home/aaron/.rvm/gems/ruby-2.4.1#appt/gems/rubyzip-1.2.1/lib/zip/file.rb:310:in `commit'
from /home/aaron/.rvm/gems/ruby-2.4.1#appt/gems/rubyzip-1.2.1/lib/zip/file.rb:334:in `close'
from /home/aaron/.rvm/gems/ruby-2.4.1#appt/gems/rubyzip-1.2.1/lib/zip/file.rb:103:in `ensure in open'
from /home/aaron/.rvm/gems/ruby-2.4.1#appt/gems/rubyzip-1.2.1/lib/zip/file.rb:103:in `open'
from zip.rb:34:in `save'
from zip.rb:56:in `<main>'

You need to change your update code to below
def save(path)
Zip::File.open(path, Zip::File::CREATE) do |out|
#zip.each do |entry|
if #replace[entry.name]
# this line creates the error
out.get_output_stream(entry.name){ |f| f.puts #replace[entry.name] }
else
# this line also will do it.
# out.get_output_stream(entry.name).write(#zip.read(entry.name))
out.get_output_stream(entry.name){ |f| f.puts #zip.read(entry.name) }
end
end
end
end
And then the file will get created
Edit-1
Below is the final code that I had used for testing
require 'rubygems'
require 'zip' # rubyzip gem
class WordMailMerge
def self.open(path, &block)
self.new(path, &block)
end
def initialize(path, &block)
#replace = {}
if block_given?
#zip = Zip::File.open(path)
yield(self)
#zip.close
else
#zip = Zip::File.open(path)
end
end
def force_settings
#replace["word/settings.xml"] = %{<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:settings xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:sl="http://schemas.openxmlformats.org/schemaLibrary/2006/main"><w:zoom w:percent="100"/></w:settings>}
end
def merge(rec)
xml = #zip.read("word/document.xml")
# replace tags with correct content
#replace["word/document.xml"] = xml.gsub("{name}", "Tarun lalwani")
end
def save(path)
Zip::File.open(path, Zip::File::CREATE) do |out|
#zip.each do |entry|
if #replace[entry.name]
# this line creates the error
out.get_output_stream(entry.name){ |f| f.puts #replace[entry.name] }
else
# this line also will do it.
# out.get_output_stream(entry.name).write(#zip.read(entry.name))
out.get_output_stream(entry.name){ |f| f.puts #zip.read(entry.name) }
end
end
end
end
def close
#zip.close
end
end
w = WordMailMerge.open("Option_2.docx")
w.force_settings
w.merge({})
w.save("Option_2_new.docx")
Option_2.docx
Option_2_new.doc

Related

Create a tar.gz with contens of a specific path (without chdir) with Ruby

I'm working on method in Ruby that will create a tar.gz file that will archive directories and files under a certain path (cdpath), it is expected to be similar to tar -C cdpath -zcf targzfile srcs, but without changing the CWD (to keep it thread safe). I'm using Gem::Package::TarWriter to create the Tar object and wrap it with Zlib::GzipWriter to compress.
Here's what I came up with (this is just a simple standalone test):
require 'rubygems/package'
require 'zlib'
require 'pathname'
require 'find'
cdpath="/absolute/path/to/some/place"
targzfile="test.tar.gz"
src=["some-dir-name-at-cdpath"]
BLOCKSIZE_TO_READ = 1024 * 1000
path = Pathname.new(cdpath)
raise "path #{cdpath} should be an absolute path" unless path.absolute?
raise "path #{cdpath} should be a directory" unless File.directory? cdpath
raise "Destination tar.gz file #{targzfile} already exists" if File.exist? targzfile
raise "no file or directory to tar" if !src || src.length == 0
src.each { |p| p.sub! /^/, "#{cdpath}/" }
File.open targzfile, 'wb' do |otargzfile|
Zlib::GzipWriter.wrap otargzfile do |gz|
Gem::Package::TarWriter.new gz do |tar|
Find.find *src do |f|
relative_path = f.sub "#{cdpath}/", ""
mode = File.stat(f).mode
if File.directory? f
tar.mkdir relative_path, mode
else
File.open f, 'rb' do |rio|
tar.add_file relative_path, mode do |tio|
tio.write rio.read
end
end
end
end
end
end
end
However, I'm hitting the following exception and I can't seem to figure out what I'm doing wrong.
/usr/lib/ruby/2.1.0/rubygems/package/tar_writer.rb:108:in `add_file': Gem::Package::NonSeekableIO (Gem::Package::NonSeekableIO)
from tartest2.rb:29:in `block (5 levels) in <main>'
from tartest2.rb:28:in `open'
from tartest2.rb:28:in `block (4 levels) in <main>'
from /usr/lib/ruby/2.1.0/find.rb:48:in `block (2 levels) in find'
from /usr/lib/ruby/2.1.0/find.rb:47:in `catch'
from /usr/lib/ruby/2.1.0/find.rb:47:in `block in find'
from /usr/lib/ruby/2.1.0/find.rb:42:in `each'
from /usr/lib/ruby/2.1.0/find.rb:42:in `find'
from tartest2.rb:22:in `block (3 levels) in <main>'
from /usr/lib/ruby/2.1.0/rubygems/package/tar_writer.rb:85:in `new'
from tartest2.rb:21:in `block (2 levels) in <main>'
from tartest2.rb:20:in `wrap'
from tartest2.rb:20:in `block in <main>'
from tartest2.rb:19:in `open'
from tartest2.rb:19:in `<main>'
EDIT: I was able to resolve this, by using TarWriter's add_file_simple instead of add_file, the file size needs to be obtained using File.stat method, details are in the answer below.
As described in the OP, the solution is to use add_file_simple method instead of add_file, this also requires that you obtain the file size using File.stat method.
Here's a working method:
# similar as 'tar -C cdpath -zcf targzfile srcs', the difference is 'srcs' is related
# to the current working directory, instead of 'cdpath'
def self.cdtargz(cdpath, targzfile, *src)
path = Pathname.new(cdpath)
raise "path #{cdpath} should be an absolute path" unless path.absolute?
raise "path #{cdpath} should be a directory" unless File.directory? cdpath
raise "Destination tar.gz file #{targzfile} already exists" if File.exist? targzfile
raise "no file or directory to tar" if !src || src.length == 0
src.each { |p| p.sub! /^/, "#{cdpath}/" }
File.open targzfile, 'wb' do |otargzfile|
Zlib::GzipWriter.wrap otargzfile do |gz|
Gem::Package::TarWriter.new gz do |tar|
Find.find *src do |f|
relative_path = f.sub "#{cdpath}/", ""
mode = File.stat(f).mode
size = File.stat(f).size
if File.directory? f
tar.mkdir relative_path, mode
else
tar.add_file_simple relative_path, mode, size do |tio|
File.open f, 'r' do |rio|
tio.write rio.read
end
end
end
end
end
end
end
end
EDIT: After reviewing the answer in this question, I revised the above slightly to avoid "slurping" the files, in my case 95% of the files are quite small, but few very BIG ones, so this makes a lot of sense. Here's the updated version:
BLOCKSIZE_TO_READ = 1024 * 1000
def self.cdtargz(cdpath, targzfile, *src)
path = Pathname.new(cdpath)
raise "path #{cdpath} should be an absolute path" unless path.absolute?
raise "path #{cdpath} should be a directory" unless File.directory? cdpath
raise "Destination tar.gz file #{targzfile} already exists" if File.exist? targzfile
raise "no file or directory to tar" if !src || src.length == 0
src.each { |p| p.sub! /^/, "#{cdpath}/" }
File.open targzfile, 'wb' do |otargzfile|
Zlib::GzipWriter.wrap otargzfile do |gz|
Gem::Package::TarWriter.new gz do |tar|
Find.find *src do |f|
relative_path = f.sub "#{cdpath}/", ""
mode = File.stat(f).mode
size = File.stat(f).size
if File.directory? f
tar.mkdir relative_path, mode
else
tar.add_file_simple relative_path, mode, size do |tio|
File.open f, 'rb' do |rio|
while buffer = rio.read(BLOCKSIZE_TO_READ)
tio.write buffer
end
end
end
end
end
end
end
end
end

Reading files in a zip archive, without unzipping the archive

I have a directory with 100+ zip files and I need to read the files inside the zip files to do some data processing, without unzipping the archive.
Is there a Ruby library to read the contents of files in zip archives, without unzipping the file?
Using rubyzip gives an error:
require 'zip'
Zip::File.open('my_zip.zip') do |zip_file|
# Handle entries one by one
zip_file.each do |entry|
# Extract to file/directory/symlink
puts "Extracting #{entry.name}"
entry.extract('here')
# Read into memory
content = entry.get_input_stream.read
end
end
Gives this error:
test.rb:12:in `block (2 levels) in <main>': undefined method `read' for Zip::NullInputStream:Module (NoMethodError)
from .gem/ruby/gems/rubyzip-1.1.6/lib/zip/entry_set.rb:42:in `call'
from .gem/ruby/gems/rubyzip-1.1.6/lib/zip/entry_set.rb:42:in `block in each'
from .gem/ruby/gems/rubyzip-1.1.6/lib/zip/entry_set.rb:41:in `each'
from .gem/ruby/gems/rubyzip-1.1.6/lib/zip/entry_set.rb:41:in `each'
from .gem/ruby/gems/rubyzip-1.1.6/lib/zip/central_directory.rb:182:in `each'
from test.rb:6:in `block in <main>'
from .gem/ruby/gems/rubyzip-1.1.6/lib/zip/file.rb:99:in `open'
from test.rb:4:in `<main>'
The Zip::NullInputStream is returned if the entry is a directory and not a file, could that be the case?
Here's a more robust variation of the code:
#!/usr/bin/env ruby
require 'rubygems'
require 'zip'
Zip::File.open('my_zip.zip') do |zip_file|
# Handle entries one by one
zip_file.each do |entry|
if entry.directory?
puts "#{entry.name} is a folder!"
elsif entry.symlink?
puts "#{entry.name} is a symlink!"
elsif entry.file?
puts "#{entry.name} is a regular file!"
# Read into memory
entry.get_input_stream { |io| content = io.read }
# Output
puts content
else
puts "#{entry.name} is something unknown, oops!"
end
end
end
I came across the same issue and checking for if entry.file?, before entry.get_input_stream.read, resolved the issue.
require 'zip'
Zip::File.open('my_zip.zip') do |zip_file|
# Handle entries one by one
zip_file.each do |entry|
# Extract to file/directory/symlink
puts "Extracting #{entry.name}"
entry.extract('here')
# Read into memory
if entry.file?
content = entry.get_input_stream.read
end
end
end

Zlib::BufError when using progressbar/ruby-progressbar gem

I use the following Ruby snippet to download a 8.9MB file.
require 'open-uri'
require 'net/http'
require 'uri'
def http_download_no_progress_bar(uri, filename)
uri.open(read_timeout: 500) do |file|
open filename, 'w' do |io|
file.each_line do |line|
io.write line
end
end
end
end
I want to add the progressbar gem to visualize the download process:
require 'open-uri'
require 'progressbar'
require 'net/http'
require 'uri'
def http_download_with_progressbar(uri, filename)
progressbar = nil
uri.open(
read_timeout: 500,
content_length_proc: lambda { |total|
if total && 0 < total.to_i
progressbar = ProgressBar.new("...", total)
progressbar.file_transfer_mode
end
},
progress_proc: lambda { |step|
progressbar.set step if progressbar
}
) do |file|
open filename, 'w' do |io|
file.each_line do |line|
io.write line
end
end
end
end
However, it now fails with the following error:
/home/user/.rvm/rubies/ruby-2.1.1/lib/ruby/2.1.0/net/http/response.rb:357:in `finish':
buffer error (Zlib::BufError)oooooo | 8.0MB 8.6MB/s ETA: 0:00:00
from /home/user/.rvm/rubies/ruby-2.1.1/lib/ruby/2.1.0/net/http/response.rb:357:in `finish'
from /home/user/.rvm/rubies/ruby-2.1.1/lib/ruby/2.1.0/net/http/response.rb:262:in `ensure in inflater'
from /home/user/.rvm/rubies/ruby-2.1.1/lib/ruby/2.1.0/net/http/response.rb:262:in `inflater'
from /home/user/.rvm/rubies/ruby-2.1.1/lib/ruby/2.1.0/net/http/response.rb:274:in `read_body_0'
from /home/user/.rvm/rubies/ruby-2.1.1/lib/ruby/2.1.0/net/http/response.rb:201:in `read_body'
from /home/user/.rvm/rubies/ruby-2.1.1/lib/ruby/2.1.0/open-uri.rb:328:in `block (2 levels) in open_http'
from /home/user/.rvm/rubies/ruby-2.1.1/lib/ruby/2.1.0/net/http.rb:1415:in `block (2 levels) in transport_request'
from /home/user/.rvm/rubies/ruby-2.1.1/lib/ruby/2.1.0/net/http/response.rb:162:in `reading_body'
from /home/user/.rvm/rubies/ruby-2.1.1/lib/ruby/2.1.0/net/http.rb:1414:in `block in transport_request'
from /home/user/.rvm/rubies/ruby-2.1.1/lib/ruby/2.1.0/net/http.rb:1405:in `catch'
from /home/user/.rvm/rubies/ruby-2.1.1/lib/ruby/2.1.0/net/http.rb:1405:in `transport_request'
from /home/user/.rvm/rubies/ruby-2.1.1/lib/ruby/2.1.0/net/http.rb:1378:in `request'
from /home/user/.rvm/rubies/ruby-2.1.1/lib/ruby/2.1.0/open-uri.rb:319:in `block in open_http'
from /home/user/.rvm/rubies/ruby-2.1.1/lib/ruby/2.1.0/net/http.rb:853:in `start'
from /home/user/.rvm/rubies/ruby-2.1.1/lib/ruby/2.1.0/open-uri.rb:313:in `open_http'
from /home/user/.rvm/rubies/ruby-2.1.1/lib/ruby/2.1.0/open-uri.rb:724:in `buffer_open'
from /home/user/.rvm/rubies/ruby-2.1.1/lib/ruby/2.1.0/open-uri.rb:210:in `block in open_loop'
from /home/user/.rvm/rubies/ruby-2.1.1/lib/ruby/2.1.0/open-uri.rb:208:in `catch'
from /home/user/.rvm/rubies/ruby-2.1.1/lib/ruby/2.1.0/open-uri.rb:208:in `open_loop'
from /home/user/.rvm/rubies/ruby-2.1.1/lib/ruby/2.1.0/open-uri.rb:149:in `open_uri'
from /home/user/.rvm/rubies/ruby-2.1.1/lib/ruby/2.1.0/open-uri.rb:704:in `open'
Meanwhile I also tried the ruby-progressbar gem:
require 'open-uri'
require 'ruby-progressbar'
require 'net/http'
require 'uri'
def http_download_with_ruby_progressbar(uri, filename)
progressbar = nil
uri.open(
read_timeout: 500,
content_length_proc: lambda { |total|
if total && 0 < total.to_i
progressbar = ProgressBar.create(title: filename, total: total)
end
},
progress_proc: lambda { |step|
progressbar.progress = step if progressbar
}
) do |file|
open filename, 'w' do |io|
file.each_line do |line|
io.write line
end
end
end
end
It fails with the same error. Here is the associated issue for the problem.
The problem is the file you are trying to download as every method works with this file: https://androidnetworktester.googlecode.com/files/1mb.txt.
The problem is that your file is larger than it says it is. The content_length_proc says that it is 8549968 bytes (8.15MB) whereas it is 101187668 bytes (96.5MB) (check with ls after downloading the file). Now I have an alternative that does not crash and gives you a progressbar:
def http_download_with_words(uri, filename)
bytes_total = nil
uri.open(
read_timeout: 500,
:content_length_proc => lambda{|content_length|
bytes_total = content_length},
:progress_proc => lambda{|bytes_transferred|
if bytes_total
# Print progress
print("\r#{bytes_transferred}/#{bytes_total}")
else
# We don’t know how much we get, so just print number
# of transferred bytes
print("\r#{bytes_transferred} (total size unknown)")
end
}
) do |file|
open filename, 'w' do |io|
file.each_line do |line|
io.write line
end
end
end
end
http_download_with_words(URI( 'http://data.wien.gv.at/daten/geo?service=WFS&request=GetFeature&version=1.1.0&typeName=ogdwien%3aBAUMOGD&srsName=EPSG:4326' ), 'temp.txt')
which is pretty self-explanatory, (seen here.)
Now the part I haven't been able to figure out is how exactly the progressbar gem is interfering with the ZLib. Most things seem to work fine inside the procs (e.g. having them print random stuff) so I assume both of these progressbars do something odd on completion that somehow messes with the transfer. I'd be very interested if anyone can figure out why that is?
In my testing when this occurred it was due to the raise in #set. As for why it results in an error in Zlib, that's not clear. Perhaps some strange exception handling in there. In my case I did "progbar.set(count) rescue nil" to get rid of the issue.

How to scrape data from list of URLs and save data to CSV with nokogiri

I have a file called bontyurls.csv that looks like this:
http://bontrager.com/model/11383
http://bontrager.com/model/01740
http://bontrager.com/model/09595
I want my script to read that file and then spit out a file like this: bonty_test_urls_results.csv
url,model_names
http://bontrager.com/model/11383,"Road TLR Conversion Kit"
http://bontrager.com/model/01740,"404 File Not Found"
http://bontrager.com/model/09595,"RXL Road"
Here's what I've got so far:
# based on code from here: http://www.andrewsturges.com/2011/09/how-to-harvest-web-data-using-ruby-and.html
require 'nokogiri'
require 'open-uri'
require 'csv'
#urls = Array.new
#model_names = Array.new
urls = CSV.read("bontyurls.csv")
(0..urls.length - 1).each do |index|
puts urls[index][0]
doc = Nokogiri::HTML(open(urls[index][0]))
doc.xpath('//h1').each do |model_name|
#model_name << model_name.content
end
end
# write results to file
CSV.open("bonty_test_urls_results.csv", "wb") do |row|
row << ["url", "model_names"]
(0..#urls.length - 1).each do |index|
row << [
#urls[index],
#model_names[index]]
end
end
That code isn't working. I'm getting this error:
$ ruby bonty_test_urls.rb
http://bontrager.com/model/00310
bonty_test_urls.rb:15:in `block (2 levels) in <main>': undefined method `<<' for nil:NilClass (NoMethodError)
from /home/simon/.rvm/gems/ruby-1.9.3-p194/gems/nokogiri-1.5.5/lib/nokogiri/xml/node_set.rb:239:in `block in each'
from /home/simon/.rvm/gems/ruby-1.9.3-p194/gems/nokogiri-1.5.5/lib/nokogiri/xml/node_set.rb:238:in `upto'
from /home/simon/.rvm/gems/ruby-1.9.3-p194/gems/nokogiri-1.5.5/lib/nokogiri/xml/node_set.rb:238:in `each'
from bonty_test_urls.rb:14:in `block in <main>'
from bonty_test_urls.rb:11:in `each'
from bonty_test_urls.rb:11:in `<main>'
Here is some code that returns the model_name at least. I'm just having trouble getting it to work in the larger script:
require 'open-uri'
require 'nokogiri'
doc = Nokogiri::HTML(open("http://bontrager.com/model/09124"))
doc.xpath('//h1').each do |node|
puts node.text
end
Also, I haven't figured out how to handle the URLs that return a 404.
This is how I'd do it:
require 'csv'
require 'nokogiri'
require 'open-uri'
CSV_OPTIONS = {
:write_headers => true,
:headers => %w[url model_names]
}
CSV.open('bonty_test_urls_results.csv', 'wb', CSV_OPTIONS) do |csv|
csv_doc = File.foreach('bontyurls.csv') do |url|
url.chomp!
begin
doc = Nokogiri.HTML(open(url))
h1 = doc.at('h1').text.strip
h1 = doc.at('title').text.strip.sub(/^Bontrager: /i, '') if (h1.empty?)
csv << [url, h1]
rescue OpenURI::HTTPError => e
csv << [url, e.message]
end
end
end
Which generates a CSV file like:
url,model_names
http://bontrager.com/model/11383,Road TLR Conversion Kit (Model #11383)
http://bontrager.com/model/01740,404 File Not Found
http://bontrager.com/model/09595,RXL Road (Model #09595)
You declare #model_names, but try to push in to #model_name, which is why it's nil.

Ruby error with CSV and <<

Well, I think the code speaks out for itself :)
# book_in_stock.rb
class BookinStock
attr_reader :isbn, :price
def initialize(isbn, price)
#isbn = isbn
#price = Float(price)
end
end
# csv_reader.rb
require 'csv'
class CsvReader
def initialize
#book_in_stock = []
end
def read_in_csv_data(csv_file_name)
CSV.foreach(csv_file_name, headers: true) do |row|
#books_in_stock << BookinStock.new(row["ISBN"], row["Amount"])
end
end
# later we'll see how to use inject to sum a collection
def total_value_in_stock
sum = 0.0
#books_in_stock.each { |book| sum += book.price }
sum
end
def number_of_each_isbn
# ...
end
end
# stock_stats.rb
reader = CsvReader.new()
ARGV.each do |csv_file_name|
STDERR.puts "[+] Processing #{csv_file_name}"
reader.read_in_csv_data(csv_file_name)
end
puts "[+] Total value = #{reader.total_value_in_stock}"
When running I get:
# +search/pickaxe/csv $ ruby1.9.1 test.rb data.csv
# [+] Processing data.csv
# test.rb:23:in `block in read_in_csv_data': undefined method `<<' for nil:NilCla
# ss (NoMethodError)
# from /usr/lib/ruby/1.9.1/csv.rb:1760:in `each'
# from /usr/lib/ruby/1.9.1/csv.rb:1196:in `block in foreach'
# from /usr/lib/ruby/1.9.1/csv.rb:1334:in `open'
# from /usr/lib/ruby/1.9.1/csv.rb:1195:in `foreach'
# from test.rb:22:in `read_in_csv_data'
# from test.rb:46:in `block in <main>'
# from test.rb:44:in `each'
# from test.rb:44:in `<main>'
What've I done wrong?
'#books_in_stock' != '#book_in_stock' # !
(typo)
You have a typo in the initialize function (#book_in_store insteadn of #books_in_store). Is that it?
By the way, the sum of the prices can be done with inject in one line
#books_in_stock.inject(0) { |sum, book| sum + book.price }
If you run the command with ruby -w, it'll turn on warnings, and the interpreter will explain what went wrong:
$ ruby -w book_in_stock.rb
book_in_stock.rb:28: warning: instance variable #books_in_stock not initialized
book_in_stock.rb:28:in `total_value_in_stock': undefined method `each' for nil:NilClass (NoMethodError)
from book_in_stock.rb:47:in `<main>'

Resources