How to detect visitor’s country using his IP address

Software77 is perhaps the first company which created IP to country database. On their website you can grab the DB in CSV and GeoIP format with a few example scripts in various languages to use the DB.  I couldn’t find anything in PHP to operate on their GeoIP format DB,  so I converted the CSV version to PHP arrays and created a simple script to handle this new format.

Usage example:

$i = new Ip2Country;

//run below function once only. It will parse IpToCountry.csv
//file into PHP files and save them into php_db directory
$i->parseCSV();

//to display countryCode:
echo $i->load('24.24.24.24')->countryCode;

//to display country and countryCode:
$i->load('24.24.24.24');
echo $i->countryCode;
echo $i->country;

Download link to the DB in CSV format:

PHP class download:

Update (04.02.2010):
Function parseCSV() is storing over 99 000 entries in a memory table before saving to files. Each table entry in PHP is taking a lot of space, so the script needs more than 40 MB of RAM. So I added the parseCSV2() function, which does exactly the same, but requires less than 1 MB of RAM. New function is a bit slower, as it is doing more disk operations.

Update (01.06.2010):
Fixed a bug: IPs are now compared as floats not strings. Thanks EdwardRF for spotting the bug!

This entry was posted on Thursday, January 21st, 2010 at 7:41 pm and is filed under PHP. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

50 Responses to “How to detect visitor’s country using his IP address”

  1. Tadas Says:

    Hi. Where do you store .csv file in server? And where are you creating php_db directory?

  2. admin Says:

    Put them in the same directory as the file that includes the class.
    You can also set output directory this way:
    $i = new Ip2Country;
    $i->dir = ‘mydirectory/subdirectory’;

    To set another path to CSV file:
    $i->parseCSV(‘otherdir/IpToCountry.csv’);

    After you run parseCSV() and have php_db directory filled with files you can delete CSV file.
    Where you are unsure what is your current directory you can run this on Windows:

    print_r(system(‘dir’));

    or this on Linux:

    print_r(system(‘pwd’));

    This will print you the path.

  3. Trey Murray Says:

    I keep getting the following error when trying to run the class:

    Fatal error: Allowed memory size of 33554432 bytes exhausted (tried to allocate 12 bytes) in /home/labdirec/public_html/ip2country/Ip2Country.php on line 289

    Could you help me out?

  4. admin Says:

    It seems there’s not enough RAM for the script.
    Please add this before calling parseCSV():

    ini_set(‘memory_limit’, ‘48M’);

    It will set memory limit to 48 MB, which is enough.

    After you run parseCSV() and have all the small PHP files created you can even set:

    ini_set(‘memory_limit’, ‘4M’);

    for the script.

    I will try to update the script, so it will need less RAM for parseCSV().

  5. Trey Murray Says:

    I’m still getting basically the same error after adding that line.

  6. Trey Murray Says:

    Below is the code

    parseCSV();

    //to display countryCode:
    echo $i->load(‘24.24.24.24′)->countryCode;

    //to display country and countryCode:
    $i->load(‘24.24.24.24′);
    echo $i->countryCode;
    echo $i->country;
    ?>

  7. Trey Murray Says:

    Here is my code, sorry, the above must have pulled out the parts it didn’t like:

    require ‘Ip2Country.php’;

    $i = new Ip2Country;
    ini_set(‘memory_limit’, ‘48M’);

    //run below function once only. It will parse IpToCountry.csv
    //file into PHP files and save them into php_db directory
    $i->parseCSV();

    //to display countryCode:
    echo $i->load(‘24.24.24.24′)->countryCode;

    //to display country and countryCode:
    $i->load(‘24.24.24.24′);
    echo $i->countryCode;
    echo $i->country;

  8. Trey Murray Says:

    Wow, I got it. Moved it to 120M, to be safe. Went straight through. I don’t know what the magic number is, but 120M is safe.

    Thanks.

  9. admin Says:

    I updated the class. It now contains new function- parseCSV2(). You can see details in the post above.

  10. victor Says:

    hello admin, I have .csv – file this format and use function parseCSV2.

    16777216,”33554431″,”AU”,”AUS”,”AUSTRALIA”

    What should be the format .csv-file ?
    Thanks

  11. victor Says:

    if I send you ftp-access, can you install script ?

    Thanks,

  12. admin Says:

    Victor: Your CSV file is incompatible with this script.
    You can get CSV file compatible with this script using the link in this article.

    The file should have lines like this:
    “1159262208″,”1159266303″,”arin”,”1247788800″,”US”,”USA”,”United States”

    (IpFrom, IpTo, X, X, 2LetterCountryCode, X, CountryName)

    Values from columns marked X are ignored, but they have to be there.

    You can edit the script and change this line (#368):

    list($from, $to, $a, $b, $code, $c, $country) = $temp;

    to:

    list($from, $to, $code, $a, $country) = $temp;

    And it should work with your CSV file.

  13. Lazybum Says:

    How to use the updated class??

    after i did parseCSV2() it says

    Warning: fopen(php_db/0.php)xxxxxxx

    i get million lines of errors like this…

    what do i do?

  14. admin Says:

    Does php_db directory exist and is writable? Can you see any files created in this directory?

  15. reverse number lookup Says:

    Thanks for this. I will try it on my on.

  16. DC 2012 Says:

    I don’t usually post but I enjoyed your blog a lot, thanks alot for the great read.

  17. fermmm Says:

    tanks very much for this info

  18. EdwardRF Says:

    Thanks a lot for this handy tool. But I’ve realize there is a bug in the script. The load function is comparing the IP addresses as string, this normally works well, but there are cases this would fail.

    e.g. “419.php”

    <?php $entries = array(
    array('419430400','436207615','GB'),
    array('4194304000','4211081215','ZZ'),
    );

    If the IP int is 4194304100, it would return you GB instead of ZZ.

    I think the simple fix is to use intval

    if (intval($e[0]) = intval($ip))

  19. admin Says:

    Yes, there is a bug, but please note that:
    intval(‘4194304100′) == intval(‘4194304000′)
    so your code will not solve the bug.
    However floatval() does the trick, so I updated the code. Thanks!

  20. EdwardRF Says:

    I didn’t notice that, Thanks, i’ll correct the code too.

  21. Misti Batimon Says:

    Great website. Thanks!

  22. Suresh N Says:

    Can’t we use database instead of using CSV file?
    Will be there any problem in using the database?

  23. admin Says:

    Suresh N: You can use database, but I am not sure if it won’t be slower..

  24. Olivier Says:

    There is a bug:
    Parse error: syntax error, unexpected ‘,’ in /www/php_db/133.php on line 309
    Which is:
    ***************************************
    array(‘1337982976′,’1342177279′,’DE’),
    );array(‘1330642944′,’1331691519′,’FR’),
    ***************************************
    (the comma before the parenthesis)

    Your function parseCSV2() needs to be fixed.

    Thanks.

  25. Olivier Says:

    ********
    To have it working in the meantime, do a replace in files for “);array” to “array”

  26. Olivier Says:

    Apart from the little bug in parseCSV2() this peace of code is reqlly good. Thank you very much!

  27. admin Says:

    Thanks Olivier for pointing the bug and providing a solution. I’ll try to fix the class, but I am terribly busy these days.

  28. Pascal Nobus Says:

    I found a bug.
    If you parse 78.21.104.246 it will convert to 1310025974.
    However 78.21.104.246 is in block 78.21.0.0/19
    (1309933568 – 1310195711), so parseCSV2 only add’s it in 130.php.
    Result if you do a lookup the IP isn’t found.

    Solution:
    If the first 3 digits of the to-IP is not the same 3 digits from the from-IP, just put the array also in the to-array.

    This is the change code in parseCSV2 (++ is added)

    $piece = substr($from, 0, 3);
    ++ $topiece = substr($to, 0, 3);

    $db[$piece][] = array($from, $to, $code);
    $dbSize++;

    ++ if ($topiece > $piece)
    ++ {
    ++ $db[$topiece][] = array($from, $to, $code);
    ++ $dbSize++;
    ++ }

  29. admin Says:

    Thanks, Pascal!

  30. D Ogi Says:

    Oliver, Admin:

    FIND:

    
    fputs($f, ');');
    

    REPLACE:

    
    fputs($f, ');$entries .=');
    
  31. DOgi Says:

    Or try:

    	public function parseCSV($filename = 'IpToCountry.csv')
    	{
    		$f = fopen($filename, 'r');
    		$db = array();
    
    		//parse into array
    		while (!feof($f))
    		{
    			$s = fgets($f);
    			if (substr($s, 0, 1) == '#') continue;
    
    			$temp = explode(',', $s);
    			if (count($temp)<7) continue;
    
    			list($from, $to, $a, $b, $code, $c, $country) = $temp;
    
    			$from = trim($from, '"');
    			$to = trim($to, '"');
    			$code = trim($code, '"');	
    
    			$piece = substr($from, 0, 3);
    
    			$db[$piece][] = array($from, $to, $code);
    		}
    		fclose($f);
    
    		//dump array into many PHP files
    		foreach ($db AS $piece=>$entries)
    		{
    			$f = fopen($this->dir . $piece . '.php', 'w');
    			fputs($f, '<?php $entries = array(' . "\n");
    
    			foreach ($entries AS $from=>$entry)
    			{
    				fputs($f, "array('" . $entry[0] . "','" . $entry[1] . "','" . $entry[2] . "'),\n");
    			}
    
    			fputs($f, ');');
    			fclose($f);
    		}
    	}
    
  32. Jan S. Says:

    DOgi’s fix above worked fine, from what I can tell.

    For any newcomers: Just replace the parseCSV function in the Ip2Country.php with DOgi’s updated one, and then run the example.php to build the database. You might need to create the php_db folder manually if it doesn’t exist in the same directory.

    Thanks to the author for making a simple solution that does not require advanced database lookups, and thanks to everyone who contributed in the thread. You made my day today. :o )

  33. Alton Brewer Says:

    This represents a really cost-effective method of reaching potential prospects.

  34. 5047 Says:

    I get an error : “Parse error: syntax error, unexpected T_STRING, expecting T_OLD_FUNCTION or T_FUNCTION or T_VAR or ‘}’ in Ip2Country.php on line 12″

    And I cannot find any probleme in the file. I just donwloaded it and put it on the webserver, no modifications at all …

    Can you help me please ?

  35. Andre Says:

    Any plans to update this to use the IPv6 database?

  36. SERGI Says:

    Thanks man! your code was super-helpful! :)

    I made my own version hacking your code, of course, but you save me a lot of hours. Mainly i copy from your code the idea of the database splitting using the 3 first digits! great!

    Cheers, from Hermosillo, Mexico.
    SERGI

  37. Rob72 Says:

    A similar project
    Ip2Country without Mysql
    http://rob72.net78.net
    Demo:
    http://rob72.net78.net?demo

  38. deviant2 Says:

    file downloaded : “http://software77.net/geo-ip/?DL=1″

    [quote]
    # IP FROM IP TO REGISTRY ASSIGNED CTRY CNTRY COUNTRY
    # “1346797568″,”1346801663″,”ripencc”,”20010601″,”il”,”isr”,”Israel”
    [/quote]
    1346797568 ?
    is 134.679.756.8
    or 1.346.797.568
    i dont understand, i think max ip is 255 also its grouped into 4 -> like -> 128.123.200.240

    [quote=@line 275]
    “16777216″,”16777471″,”apnic”,”1313020800″,”AU”,”AUS”,”Australia”
    [/quote]
    16777216=16.777.216 ?

    i really dont understand
    please explain it.
    thank u.

  39. Jose Luis Says:

    Yesterday everything worked fine, but now went back to get the syntax error by ); review the files of the arrays and turns out I doubled the lines of code, do not understand why? can anyone help?

  40. admin Says:

    This is 32bit notation. Instead of 4 bytes: A.B.C.D you have 1 32bit Integer = A*256*256*256+B*256*256+C*256+D

  41. admin Says:

    Such editors can’t work from textarea. Please copy html from textarea to a div, use the editor and after you are done with editing- copy it back to textarea.

  42. admin Says:

    Delete cache files (PHPs created from CSV) and run the script again to generate new PHP files.

  43. johannes Says:

    Hi,

    I worked with the script / class and was not satisfied with the import duration. I recoded the import and added one function. Reworked class imports geo-data slightly faster and has a new function to change the data path. The code is here: http://pastebin.com/UfmqGwBa

    Hope, you’ll like it.

    Thanks & best regards.

  44. tom_e_rock Says:

    BUG BUG BUG!!!
    I discovered this after noticing many IPS not working.

    Example IP: 98.247.53.116

    It will register as ? (unknown using this script)

    Reason:
    The IP4 value is 1660368244

    It checks the file 166.php which does exist properly, but has no array of values that contains the interval.

    BUT when I checked it on http://software77.net/geo-ip/ it was registering as a US IP. So I debugged further.

    File 165.php did have the proper interval, but it spread out from 165xxxxxxxxx to 166xxxxxxxxxx.

    Solution, replace the code in ParseCVS with the following.

    $pieceFrom = substr($from, 0, 3);
    $pieceTo = substr($to, 0, 3);

    for ($i=$pieceFrom; $i<=$pieceTo; $i++)
    {
    $db[$i][] = array($from, $to, $code);
    }

  45. misko Says:

    None of the fixes quite get it right. For example, try this IP address: 50.196.142.177 The problem is that the range of decimal IPs covered can be quite large. The above IP falls into a range with over 8,000,000 IP addresses. So just puttig it in 1 or 2 of the php_db files is not sufficient. This fix corrects that, but it does create a few extra files for IP addresses that will never occur.

    *** Ip2Country.php 2012-10-03 05:33:42.412162893 +0000
    — /var/www/html/Ip2Country.php 2012-10-03 03:36:24.535563936 +0000
    ***************
    *** 307,353 ****
    fclose($f);
    }

    ! public function parseCSV($filename = ‘IpToCountry.csv’)
    {
    ! $f = fopen($filename, ‘r’);
    ! $db = array();
    !
    ! //parse into array
    ! while (!feof($f))
    {
    ! $s = fgets($f);
    ! if (substr($s, 0, 1) == ‘#’) continue;
    !
    ! $temp = explode(‘,’, $s);
    ! if (count($temp)$entries)
    - {
    - $f = fopen($this->dir . $piece . ‘.php’, ‘w’);
    - fputs($f, ‘$entry)
    - {
    - fputs($f, “array(‘” . $entry[0] . “‘,’” . $entry[1] . “‘,’” . $entry[2] . “‘),\n”);
    - }
    -
    - fputs($f, ‘);’);
    - fclose($f);
    - }
    }

    public function parseCSV2($filename = ‘IpToCountry.csv’)
    {
    — 307,356 —-
    fclose($f);
    }

    ! public function parseCSV($filename = ‘IpToCountry.csv’)
    ! {
    ! $f = fopen($filename, ‘r’);
    ! $db = array();
    !
    ! //parse into array
    ! while (!feof($f))
    {
    ! $s = fgets($f);
    ! if (substr($s, 0, 1) == ‘#’) continue;
    !
    ! $temp = explode(‘,’, $s);
    ! if (count($temp)<7) continue;
    !
    ! list($from, $to, $a, $b, $code, $c, $country) = $temp;
    !
    ! $from = trim($from, '"');
    ! $to = trim($to, '"');
    ! $code = trim($code, '"');
    ! $fromPiece = substr($from, 0, 3);
    ! $toPiece = substr($to, 0, 3);
    !
    ! for ($piece = $fromPiece; $piece $entries)
    ! {
    ! $f = fopen($this->dir . $piece . ‘.php’, ‘w’);
    ! fputs($f, ‘$entry)
    {
    ! fputs($f, “array(‘” . $entry[0] . “‘,’” . $entry[1] . “‘,’” . $entry[2] . “‘),\n”);
    }
    +
    + fputs($f, ‘);’);
    fclose($f);
    }
    + }
    +

    public function parseCSV2($filename = ‘IpToCountry.csv’)
    {
    ***************
    *** 370,387 ****
    $from = trim($from, ‘”‘);
    $to = trim($to, ‘”‘);
    $code = trim($code, ‘”‘);
    !
    ! $piece = substr($from, 0, 3);
    ! $topiece = substr($to, 0, 3);
    !
    ! $db[$piece][] = array($from, $to, $code);
    ! $dbSize++;
    !
    ! if ($topiece > $piece) {
    ! $db[$topiece][] = array($from, $to, $code);
    $dbSize++;
    }
    !
    if ($dbSize>100)
    {
    $this->appendToFile($db);
    — 373,386 —-
    $from = trim($from, ‘”‘);
    $to = trim($to, ‘”‘);
    $code = trim($code, ‘”‘);
    ! $fromPiece = substr($from, 0, 3);
    ! $toPiece = substr($to, 0, 3);
    !
    ! for ($piece = $fromPiece; $piece 100)
    {
    $this->appendToFile($db);
    ***************
    *** 467,470 ****
    return false;
    }

    ! }

  46. admin Says:

    Thanks. My code method might not be perfect but still it’s quite reliable and no method is 100% perfect.

  47. saurabh Says:

    It is not a good code. Look at the no of files you are creating in the process. A better solution would be to 1) to pick entries from excel and post it in a database
    2)To use xml file.

    But anyways you have shown the way. Good Work

  48. admin Says:

    1) If you want to use a database then there are other solutions. This is file-based one. And I think it can be faster than DB-based solution
    2) XML files are terribly, terribly slow.

  49. admin Says:

    No, sorry. IPv6 is not popular yet.

  50. admin Says:

    The files work just fine. Maybe your PHP is too old?

Leave a Reply

 
TopOfBlogs Web Development & Design Blogs