James Gardner: Home > Blog > 2009 > IP-based Geolocation with WIP-Mania

IP-based Geolocation with WIP-Mania

Posted:2009-03-11 20:01
Tags:Web, Business, PostgreSQL, Hosting, MySQL

There are quite a few online services which can determine a geographic location from an IP address. Even Google Gears now provides the functionality. Most of the web-based APIs have restrictions on the number of API calls which can be made per day so I needed a solution which I could host myself. I only need country based lookups, not specific cities so I was able to use WIP-mania which provides a 460KB zipped SQL file of IP address ranges and their corresponding countries. In this post I'll show you how to import them into a PostgreSQL database and then write a Python API to lookup IPs.

First you need to download the data. It is updated every two months and can be obtained from http://www.wipmania.com/en/base/ under a Creative Commons license which allows commercial use as long as you don't set up a web-based API lookup service. Download the zipped SQL version.

Unzip the code (the zip file doesn't create a directory so it is best to create your own):

mkdir worldip
cd worldip
wget http://static.wipmania.com/static/worldip.sql.zip
unzip worldip.sql.zip

The archive contains the main IP address table in worldip.sql and a set four versions of a table to provide country name lookups against country code, one in English, one in German one in Russian and one in Spanish. I'd like the English one so I use worldip.lands.en.sql.

The SQL files are designed to be used with MySQL so you'll need to tweak them to work with PostgreSQL.

First edit worldip.sql so that these lines at the top:

DROP TABLE IF EXISTS `worldip`;
CREATE TABLE `worldip` (
  `start` int(10) UNSIGNED NOT NULL default '0',
  `end` int(10) UNSIGNED NOT NULL default '0',
  `code` varchar(2) NOT NULL default '',
  PRIMARY KEY (`start`,`end`)
) ENGINE=MyISAM;

LOCK TABLES `worldip` WRITE;

Are replaced with the simpler:

-- DROP TABLE worldip;

CREATE TABLE worldip (
  start_ip BIGINT NOT NULL,
  end_ip BIGINT NOT NULL,
  code CHAR(2) NOT NULL,
  PRIMARY KEY (start_ip, end_ip)
);

Then you need to modify every other line in the file so that it begins with this:

INSERT INTO worldip (start_ip, end_ip, code) VALUES

and ends with a ;. This is because PostgreSQL doesn't support inserting multiple rows at once in the same way MySQL does.

Remove the UNLOCK statement at the end.

I've renamed the columns start_ip and end_ip so that there is no confusion with the pg/sql END keyword. You should also remove the UNLOCK line from the very end of the file.

You can load the data into a database called dcm-dev like this:

$ psql dcm-dev -f worldip.sql
DROP TABLE
psql:worldip.sql:8: NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "worldip_pkey" for table "worldip"
CREATE TABLE
INSERT 0 58143

Next edit worldip.lands.en.sql so the top of the file looks like this:

-- DROP TABLE worldip_land;

SET CLIENT_ENCODING TO 'utf8';

CREATE TABLE worldip_land (
  code CHAR(2) NOT NULL,
  country VARCHAR(255) NOT NULL,
  PRIMARY KEY (code)
);

As before you need to modify every other line in the file so that it begins with this:

INSERT INTO worldip_land (code, country) VALUES

and ends with a ;. i

You can then load the data like this:

$ psql dcm-dev -f worldip.lands.en.sql
psql:worldip.lands.en.sql:1: ERROR:  table "worldip_land" does not exist
SET
psql:worldip.lands.en.sql:9: NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "worldip_land_pkey" for table "worldip_land"
CREATE TABLE
INSERT 0 238

If you later want to re-insert the data from the two files you will have to uncomment the DROP TABLE lines before running the same two commands.

To use the data from Python you need this function which converts an IP address string into an integer:

def ip_number(ip):
    a, b, c, d = ip.split('.')
    return (16777216*int(a)) + (65536*int(b)) + (256*int(c)) + int(d)

You can now lookup a country code against the database like this:

ip = ip_number('84.9.42.195')
cursor = connection.cursor()
cursor.execute(
    """
    SELECT code
    FROM worldip
    WHERE
        start_ip<=%s
        AND
        end_ip>=%s
    """,
    (ip, ip)
)
rows = cursor.fetchall()
code = rows[0][0]

You can lookup a country like this:

cursor = connection.cursor()
cursor.execute(
    """
    SELECT country
    FROM worldip_land
    WHERE code=%s
    """,
    (code,)
)
rows = cursor.fetchall()
country = rows[0][0]

Further reading:

(view source)

James Gardner: Home > Blog > 2009 > IP-based Geolocation with WIP-Mania