I am using Geo::IP to perform location lookups on ip addresses. Everything works fine until I come across an ip address which is not in the geo ip lookup database and the program shuts abruptly giving this error
Can't call method "city" on an undefined value at script.pl line 16.
Current code looks like this
$gi = Geo::IP->open("/usr/local/share/GeoIP/GeoLiteCity.dat", GEOIP_STANDARD);
my $record = $gi->record_by_addr($key);
my $city= $record->city;
Any suggestions on how I can by pass this? It works perfectly fine until it hits an ip address that isn't defined within that module.
Looking at the Geo::IP source, if the IP address is not in the database, it returns undef. Therefore, to bypass the problem, you can do:
my $record = $gi->record_by_addr($key);
## check that $record is defined
if ($record) {
my $city= $record->city;
...
}
else {
# issue an error message if wanted
warn "No record found for $key";
}
Relevant code from the Geo::IP source:
The function you're using is record_by_addr. From the source, record_by_addr is an alias for get_city_record_as_hash (see perlref for the syntax used to create an 'alias' for a function):
*record_by_addr = \&get_city_record_as_hash;
The code for get_city_record_as_hash is as follows:
#this function returns the city record as a hash ref
sub get_city_record_as_hash {
my ( $gi, $host ) = #_;
my %gir;
#gir{qw/ country_code country_code3 country_name region city
postal_code latitude longitude dma_code area_code
continent_code region_name metro_code / } =
$gi->get_city_record($host);
return defined($gir{latitude}) ? bless( \%gir, 'Geo::IP::Record' ) : undef;
}
This code runs get_city_record using $host, the IP address you supplied, as the argument. If get_city_record finds a record, the data it returns populates the %gir hash. The last line of the sub uses the [ternary form of if-else] to evaluate whether getting the record was successful, and to return the appropriate result. It checks whether $gir{latitude} is defined, and if it is, it creates and returns a Geo::IP::Record object from it (which you can query with methods like city, etc.). If it isn't, it returns undef.
A simpler way to view the last line would be this:
# is $gir{latitude} defined?
if (defined ($gir{latitude})) {
# yes: create a Geo::IP::Record object with the data in %gir
# return that object
return bless( \%gir, 'Geo::IP::Record' )
}
else {
# no: return undefined.
return undef;
}
I'd suggest that you need Data::Dumper here, to tell you what's going on with $record. I would guess that record_by_addr($key); is the root of your problems, and that because $key is in some way bad, $record is undefined.
This would thus be fixed:
use Data::Dumper;
print Dumper \$record;
I'm guessing $record will be undefined, and therefore:
next unless $record;
will skip it.
Related
My code is to enter an actor name and the program, via the given actor's filmography in IMDB, lists on a hash table all the cinematic genres of the movies he has acted in as well as their frequency. However, I have a problem: When I type a name like "brad pitt" or "bruce willis" after running the program at the prompt, execution takes indefinitely. How do you know what the problem is?
Another problem: when I type "nicolas bedos" (an actor name that I entered from the beginning), it works but it seems that the index is only made for a single movie selected in the #url_links list. Should the look_down function of the TreeBuilder module within a foreach loop be adapted? I was telling myself that the #genres list was overwritten on each iteration so I added a push () but the result remains the same.
use LWP::Simple;
use PerlIO::locale;
use HTML::TreeBuilder;
use WWW::Mechanize;
binmode STDOUT, ':locale';
use strict;
use warnings;
print "Enter the actor's name:";
my $acteur1 = <STDIN>; # the user enters the name of the actor
print "We will analyze the filmography of the actor $actor1 by genre\n";
#we put the link with the given actor in Mechanize variable in order to browse the internet links
my $lien1 = "https://www.imdb.com/find?s=nm&q=$acteur1";
my $mech = WWW::Mechanize->new();
$mech->get($lien1); #we access the search page with the get function
$mech->follow_link( url_regex => qr/nm0/i ); #we access the first result using the follow_link function and the regular expression nm0 which is in the URL
my #url_links= $mech->find_all_links( url_regex => qr/title\/tt/i ); #owe insert in an array all the links having as regular expression "title" in their URL
my $nb_links = #url_links; #we record the number of links in the list in this variable
my $tree = HTML::TreeBuilder->new(); #we create the TreeBuilder module to access a specific text on the page via the tags
my %index; #we create a hashing table
my #genres = (); #we create the genre list to insert all the genres encountered
foreach (#url_links) { #we make a loop to browse all the saved links
my $mech2 = WWW::Mechanize->new();
my $html = $_->url(); #we take the url of the link
if ($html =~ m=^/title=) { #if the url starts with "/title"
$mech2 ->get("https://www.imdb.com$html"); #we complete the link
my $content = $mech2->content; #we take the content of the page
$tree->parse($content); #we access the url and we use the tree to find the strings that interest us
#genres = $tree->look_down ('class', 'see-more inline canwrap', #We have as criterion to access the class = "see-more .."
sub {
my $link = $_[0]->look_down('_tag','a'); #new conditions: <a> tags
$link->attr('href') =~ m{genres=}; #autres conditions: "genres" must be in the URL
}
);
}
}
my #genres1 = (); #we create a new list to insert the words found (the genres of films)
foreach my $e (#genres){ #we create a loop to browse the list
my $genre = $e->as_text; #the text of the list element is inserted into the variable
#genres1 = split(/[à| ]/,$genre); #we remove the unnecessary characters that are spaces, at and | which allow to keep that the terms of genre cine
}
foreach my $e (#genres1){ #another loop to filter listing errors (Genres: etc ..) and add the correct words to the hash table
if ($e ne ("Genres:" or "") ) {
$index{$e}++;
}
}
$tree->delete; #we delete the tree as we no longer need it
foreach my $cle (sort{$index{$b} <=> $index{$a}} keys %index){
print "$cle : $index{$cle}\n"; #we display the hash table with the genres and the number of times that appear in the filmography of the given actor
}
Thank you in advance for your help,
wobot
The IMDB Conditions of Use say this:
Robots and Screen Scraping: You may not use data mining, robots, screen scraping, or similar data gathering and extraction tools on this site, except with our express written consent as noted below.
So you might want to reconsider what you're doing. Perhaps you could look at the OMDB API instead.
I am trying to test the output of the following method:
package ASC::Builder::Error;
sub new {
my ($package, $first_param) = (shift, shift);
if (ref $first_param eq 'HASH') {
my %params = #_;
return bless { message => $first_param->{message}, %params}, $package;
}
else {
my %params = #_;
return bless {message => $first_param, %params}, $package;
}
}
This method is supposed to accept either an error hash or error string. If it accepts a hash it should output the value of the message key from the error hash.
This is the error hash located in ErrorLibrary.pm:
use constant {
CABLING_ERROR => {
code => 561,
message => "cabling is not correct at T1",
tt => { template => 'disabled'},
fatal => 1,
link =>'http://www.e-solution.com/CABLING_ERROR',
},
};
This is the message method along with the other keys of the hash located in Error.pm
package ASC::Builder::Error;
sub message {
return $_[0]->{message};
}
sub tt {
return {$_[0]->{tt} };
}
sub code {
return {$_[0]->{code} };
}
This is my current unit test located in error.t
#input value will either be a String or and Error Message Hash
# error hash
my $error_hash = CABLING_ERROR;
# error string
my $error_string = "cabling is not correct at T1.";
# error hash is passed into new and an error object is outputted
my $error_in = ASC::Builder::Error->new($error_hash);
# checks to see if the output object from new is an Error object
isa_ok($error_in, 'ASC::Builder::Error');
# checking that object can call the message() method
can_ok( $error_in, 'message');
# checks to see if the output message matches the message contained in the error hash(correct)
is($error_in->message(),( $error_string || $error_hash->{message} ), 'Returns correct error message');
And finally the results of my test:
# Failed test 'Returns correct error message'
# at t/67_error_post.t line 104.
# got: 'HASH(0x38b393d490)'
# expected: 'cabling is not correct at T1.'
#
# '
# Looks like you failed 1 test of 3.
t/67_error_post.t .. Dubious, test returned 1 (wstat 256, 0x100)
Failed 1/3 subtests
On my machine
First of, if I run your code I get an error about CABLING_CHECK_TOR_INCORRECT_CABLING_ERROR being not defined. If I replace that with CABLING_ERROR, the test fails with this.
# got: 'cabling is not correct at T1'
# expected: 'cabling is not correct at T1.'
# Looks like you failed 1 test of 3.
Two possible outputs at the same time
Now to what you say the output is.
For some reason, your $error_in->message returns a hashref, which gets stringified by is(), because is() doesn't do data structures. You can use Test::Deep to do this.
use Test::Deep;
cmp_deeply(
$error_in->message,
any(
$error_string,
$error_hash->{message},
),
'Returns correct error message',
);
Here I assumed that your $error_string || $error_hash->{message} is intended to make it check for either one or the other.
But || will just check if $error_string has a true value and return it, or take the value of $error_hash->{message}. It compares the result of that operation to $error_in->message.
Testing clearly
However, this will likely not solve your real problem. Instead of having one test case that checks two possible things, make a dedicated test case for each possible input. That's what unit-testing is all about.
my $error_direct = ASC::Builder::Error->new('foo');
is $error_direct->message, 'foo', 'direct error message gets read correctly';
my $error_indirect = ASC::Builder::Error->new( { message => 'bar' } );
is $error_indirect->message, 'bar', 'indirect error message gets read correctly';
The above code will give you two test cases. One for a direct error string, and another one for an indirect hash.
ok 1 - direct error message gets read correctly
ok 2 - indirect error message gets read correctly
1..2
Don't waste time
At the same time, this also addresses another issue with your approach. In unit tests, you want to test the smallest possible unit. Don't tie them to your other business logic or your business production data.
Your ASC::Builder::Error class doesn't care about the type of error, so don't over-complicate by loading something additonal to give you the exact same error messages you have in real life. Just use simple things that are enough to prove stuff works.
The simpler your unit tests are, the easier it is to maintain them, and the easier it is to add more once you have more cases.
I am trying to write a script using Net::IMAP::Client that outputs the body of a email, but so far every variable i try and output from the module shows up as something like: ARRAY(0x86f5524) or gives an error "Can't use an undefined value as a SCALAR reference."
The module documentation says that
# fetch full messages
my #msgs = $imap->get_rfc822_body([ #msg_ids ]);
print $$_ for (#msgs)
should contain references to a scalar. #msg_id should be an array of numbers for the email number in the inbox, but is also returned as an array reference.
I am unsure how to properly output this data so it is readable.
Here is the module reference: Net::IMAP::Client
and here is a snipit of my code:
use Net::IMAP::Client;
use Net::IMAP;
use Net::SMTP;
use strict;
use warnings;
my $imap = Net::IMAP::Client->new(
server => ,
user => , # i omitted this data for privacy
pass => ,
ssl => ,
port => ,
) or die "could not connect to IMAP server";
$imap->login or die('Login Failed: ' . $imap->last_error);
my $num_messages = $imap->select('[Gmail]/All Mail');
my #msg_id = $imap->search('ALL');
print #msg_id;
print "\n";
my #data = $imap->get_rfc822_body([#msg_id]);
print $$_ for (#data);
EDIT: I used Data::Dumper and got a big block of test containing the email and all the formatting tags. I also know that $imap-search should return something, as the inbox has 4 emails, 2 unread. But so since the variable #data IS holding the emails, i cant figure out the proper way to de-reference it in the output
$imap->search('ALL') returns an array reference not an array. So you need to change
my #msg_id = $imap->search('ALL');
to
my #msg_id = #{$imap->search('ALL')};
It would be better though to check whether the method returned a defined value before dereferencing, in case it fails.
Looking at the code, the proper usage is:
my $msgs = $imap->get_rfc822_body([ #msg_ids ]);
print $$_ for #$msgs;
The get the documented behaviour,
return $wants_many ? \#ret : $ret[0];
should be
return $wants_many ? (wantarray ? #ret : \#ret) : $ret[0];
First, the relevant xkcd comic: http://xkcd.com/979/
Next, the 10-year old thread on PerlMonks: http://www.perlmonks.org/?node_id=210422
Basically, I'm failing in my attempts to use Net::DNS::Update to create a PTR record and I'd like to see how others have manged this.
Below is what I'm trying. $hst is the hostname that I already have an A record for. $rev is the backwards IP address in-addr.arpa thingy.
# Create the update packet:
my $update = Net::DNS::Update->new($OURDOMAIN);
# Add the PTR record:
$update->push(update => rr_add("$rev 3600 PTR $hst"));
# Send the update to the zone's primary master.
my $res = Net::DNS::Resolver->new;
$res->nameservers("$OURNMSERV");
If $OURDOMAIN is your main domain name, you need to know that you can't put PTR records into your own domain, they have to be put in the right .in-addr.arpa zone.
That zone is most likely being run by your ISP, and they're unlikely to support dynamic updates from end users.
Another ten years later, http://xkcd.com/979/ is still relevant. Having read the question and answer and also the linked PerlMonks thread, it was still unclear to me how to properly add PTR records using Net::DNS.
Well, 'when in Rome do as the Romans do' the saying goes. Having noticed that nsupdate can add PTR records to our internal DNS system just fine without me specifying the reverse zone, I was trying to find out what it does to send valid updates. Turned out it actually sends a SOA query first to figure out what the reverse zone is. Copying this behavior seems to be a somewhat robust way to add PTR records via Net::DNS:
use Net::DNS;
my $hostname = 'somehost.example.com.';
my $ip = '10.11.12.13';
my $primary_ns = 'primary.example.com';
# First, construct the reverse name based on the IP address
my #octets = split /\./, $ip;
my #rev_octs = reverse(#octets);
my $rev_name = join(".", #rev_octs) . '.in-addr.arpa.';
# Then, query the SOA record for the constructed reverse name
my $resolver = Net::DNS::Resolver->new();
$resolver->nameservers($primary_ns);
# Note: Need to use 'send' rather than 'query' since we are interested
# in the 'authority section' of the reply only.
my $packet = $resolver->send($rev_name, 'SOA');
my $zone;
if ($packet) {
# We expect only one authority record
my $len = $packet->authority;
if ($len == 0) {
# Server might not be authorative for reverse lookup zone.
warn "No authority section in reply.\n";
exit 1;
}
my $auth = ($packet->authority)[0];
$zone = $auth->name;
} else {
warn 'query failed: ', $resolver->errorstring, "\n";
exit 1;
}
# Armed with this information, we can finally add the PTR records
my $update = Net::DNS::Update->new($zone);
# Make sure that the PTR record does not already exist
$update->push( pre => nxrrset("$rev_name PTR") );
$update->push( update => rr_add("$rev_name 1200 PTR $hostname") );
my $reply = $resolver->send($update);
if ($reply) {
if ($reply->header->rcode eq 'NOERROR') {
print "Successfully added PTR record!\n";
} else {
print 'Update failed: ', $reply->header->rcode, "\n";
}
} else {
print 'Update failed: ', $resolver->errorstring, "\n";
}
I've been banging my head over this issue for about 5 hours now, I'm really frustrated and need some assistance.
I'm writing a Perl script that pulls jobs out of a MySQL table and then preforms various database admin tasks. The current task is "creating databases". The script successfully creates the database(s), but when I got to generating the config file for PHP developers it blows up.
I believe it is an issue with referencing and dereferencing variables, but I'm not quite sure what exactly is happening. I think after this function call, something happens to
$$result{'databaseName'}. This is how I get result: $result = $select->fetchrow_hashref()
Here is my function call, and the function implementation:
Function call (line 127):
generateConfig($$result{'databaseName'}, $newPassword, "php");
Function implementation:
sub generateConfig {
my($inName) = $_[0];
my($inPass) = $_[1];
my($inExt) = $_[2];
my($goodData) = 1;
my($select) = $dbh->prepare("SELECT id FROM $databasesTableName WHERE name = '$inName'");
my($path) = $documentRoot.$inName."_config.".$inExt;
$select->execute();
if ($select->rows < 1 ) {
$goodData = 0;
}
while ( $result = $select->fetchrow_hashref() )
{
my($insert) = $dbh->do("INSERT INTO $configTableName(databaseId, username, password, path)".
"VALUES('$$result{'id'}', '$inName', '$inPass', '$path')");
}
return 1;
}
Errors:
Use of uninitialized value in concatenation (.) or string at ./dbcreator.pl line 142.
Use of uninitialized value in concatenation (.) or string at ./dbcreator.pl line 154.
Line 142:
$update = $dbh->do("UPDATE ${tablename}
SET ${jobStatus}='${newStatus}'
WHERE id = '$$result{'id'}'");
Line 154:
print "Successfully created $$result{'databaseName'}\n";
The reason I think the problem comes from the function call is because if I comment out the function call, everything works great!
If anyone could help me understand what's going on, that would be great.
Thanks,
p.s. If you notice a security issue with the whole storing passwords as plain text in a database, that's going to be addressed after this is working correctly. =P
Dylan
You do not want to store a reference to the $result returned from fetchrow_hashref, as each subsequent call will overwrite that reference.
That's ok, you're not using the reference when you are calling generate_config, as you are passing data in by value.
Are you using the same $result variable in generate_config and in the calling function? You should be using your own 'my $result' in generate_config.
while ( my $result = $select->fetchrow_hashref() )
# ^^ #add my
That's all that can be said with the current snippets of code you've included.
Some cleanup:
When calling generate_config you are passing by value, not by reference. This is fine.
you are getting an undef warning, this means you are running with 'use strict;'. Good!
create lexical $result within the function, via my.
While $$hashr{key} is valid code, $hashr->{key} is preferred.
you're using dbh->prepare, might as well use placeholders.
sub generateConfig {
my($inName, inPass, $inExt) = #_;
my $goodData = 1;
my $select = $dbh->prepare("SELECT id FROM $databasesTableName WHERE name = ?");
my $insert = $dbh->prepare("
INSERT INTO $configTableName(
databaseID
,username
,password
,path)
VALUES( ?, ?, ?, ?)" );
my $path = $documentRoot . $inName . "_config." . $inExt;
$select->execute( $inName );
if ($select->rows < 1 ) {
$goodData = 0;
}
while ( my $result = $select->fetchrow_hashref() )
{
insert->execute( $result->{id}, $inName, $inPass, $path );
}
return 1;
}
EDIT: after reading your comment
I think that both errors have to do with your using $$result. If $result is the return value of fetchrow_hashref, like in:
$result = $select->fetchrow_hashref()
then the correct way to refer to its values should be:
print "Successfully created " . $result{'databaseName'} . "\n";
and:
$update = $dbh->do("UPDATE ${tablename}
SET ${jobStatus}='${newStatus}'
WHERE id = '$result{'id'}'");
OLD ANSWER:
In function generateConfig, you can pass a reference in using this syntax:
generateConfig(\$result{'databaseName'},$newPassword, "php");
($$ is used to dereference a reference to a string; \ gives you a reference to the object it is applied to).
Then, in the print statement itself, I would try:
print "Successfully created $result->{'databaseName'}->{columnName}\n";
indeed, fetchrow_hashref returns a hash (not a string).
This should fix one problem.
Furthermore, you are using the variable named $dbh but you don't show where it is set. Is it a global variable so that you can use it in generateConfig? Has it been initialized when generateConfig is executed?
This was driving me crazy when I was running hetchrow_hashref from Oracle result set.
Turened out the column names are always returned in upper case.
So once I started referencing the colum in upper case, problem went away:
insert->execute( $result->{ID}, $inName, $inPass, $path );