Showing Commenter IP Locations in WordPress with a Local ip2region Database

Published:

A few days ago, while talking about IP location display, I was reminded of something that is easy to overlook: most free IP lookup services are not especially accurate. For a personal blog, though, perfect precision is usually unnecessary. If the comment area can show roughly which province or city a visitor is from, that is already enough.

That is also why I prefer a local IP database for this kind of feature.

Why use a local IP database?

For displaying IP attribution on a blog, I do not think it is worth using a paid location service. A rough province-level or city-level result is practical enough, and a local database has several obvious advantages:

  • More stable: it does not rely on an external API, so there is no need to worry about a remote service going down.
  • Faster response: local lookup is generally much quicker than online requests.
  • Better privacy: the visitor’s IP address does not have to be sent to a third party.

Moving from the QQWry database to ip2region

The old QQWry dat database had been used for a long time, but it officially stopped updating in September 2024. Although a newer CZDB format is available, it requires applying for a key, which makes it less convenient to use.

The old QQWry database has been around for many years, but the accumulated issues are also noticeable:

  • The data format is not very clean, and typos appear from time to time.
  • IPv6 support is very limited; domestic IPv6 addresses are basically shown only as “China”.
  • It may even require manual cleanup, such as correcting the dat database with tools and spreadsheets.

For later theme updates, switching to ip2region is the more reasonable choice.

Why ip2region?

ip2region released version 3.0 in September this year and began offering real IPv6 support:

  • IPv6 can be located down to prefecture-level cities, although accuracy still has room to improve.
  • The data format is standardized and easier to work with.
  • It is free and open source.
  • The project is still actively maintained.

ip2region generally resolves locations to the city level. In some cases, QQWry may be more detailed and reach districts, counties, or towns, but for displaying comment locations on a blog, city-level information is already sufficient.

Integrating ip2region into WordPress

The basic integration is not complicated. In practice, it only takes a few steps.

1. Download the required files

Download ip2region from GitHub or Gitee. The main files needed are:

data/ip2region_v4.xdb — IPv4 database

data/ip2region_v6.xdb — IPv6 database

binding/php/xdb/Searcher.class.php — core lookup class

2. Create the conversion file

Create a file named ip2region.php in the theme directory and add the following IP conversion code. Pay attention to the file paths referenced in the code.


<?php
/**
 * Ip2region 是一个离线 IP 数据管理框架和定位库,支持 IPv4 和 IPv6。此代码版本支持 IPv4 和 IPv6。
 *
 * 官方社区:https://ip2region.net/
 */

require_once __DIR__ . '/xdb/Searcher.class.php';

use \ip2region\xdb\Util;
use \ip2region\xdb\Searcher;

//初始化,使用向量索引
function init_ip2region_vector($dbFile) {
    if (!file_exists($dbFile)) {
        error_log("IP数据库文件不存在: " . $dbFile);
        return null;
    }

    try {
        // 读取文件头,获取版本信息
        $header = Util::loadHeaderFromFile($dbFile);
        $version = Util::versionFromHeader($header);

        // 加载向量索引
        $vIndex = Util::loadVectorIndexFromFile($dbFile);

        // 创建 Searcher
        return Searcher::newWithVectorIndex($version, $dbFile, $vIndex);
    } catch (Exception $e) {
        error_log("IP数据库初始化失败: " . $e->getMessage());
        return null;
    }
}

// 全局 Searcher,显式初始化为 null
global $ip2region_searcher_v4, $ip2region_searcher_v6;
$ip2region_searcher_v4 = $ip2region_searcher_v4 ?? null;
$ip2region_searcher_v6 = $ip2region_searcher_v6 ?? null;

//获取 IPv4 或 IPv6 Searcher
function get_ip_searcher($ip) {
    global $ip2region_searcher_v4, $ip2region_searcher_v6;

    // 判断 ip 类型
    $isIpv6 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
    $isIpv4 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);

    // 尝试加载 IPv6 searcher
    if ($isIpv6) {
        if ($ip2region_searcher_v6 === null) {
            $dbFile_v6 = __DIR__ . '/data/ip2region_v6.xdb';
            $ip2region_searcher_v6 = init_ip2region_vector($dbFile_v6);
        }
        if ($ip2region_searcher_v6 !== null) {
            return $ip2region_searcher_v6;
        }
        // 如果 IPv6 DB 不可用,继续尝试加载 IPv4(fallback降级)
    }

    // IPv4 路径(或者作为fallback降级)
    if ($ip2region_searcher_v4 === null) {
        $dbFile_v4 = __DIR__ . '/data/ip2region_v4.xdb';
        $ip2region_searcher_v4 = init_ip2region_vector($dbFile_v4);
    }

    return $ip2region_searcher_v4;
}

//判断 IP 类型
function get_ip_type($ip) {
    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
        return 'ipv4';
    } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
        return 'ipv6';
    }
    return false;
}

//判断内网IP并返回显示文本
function is_private_ip($ip) {
    $ip_type = get_ip_type($ip);

    if ($ip_type === 'ipv4') {
        if (filter_var(
            $ip,
            FILTER_VALIDATE_IP,
            FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
        ) === false) {
            return '内网IP';
        }
    } elseif ($ip_type === 'ipv6') {
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE) === false) {
            return '内网IP';
        }
        if ($ip === '::1') {
            return '内网IP';
        }
        if (preg_match('/^fe80:/i', $ip)) {
            return '内网IP';
        }
    }
    return false;
}

//IP转换函数
function convertip($ip, $withIsp = false, $simpleMode = false) {
    if (!$ip) return '火星';

    // 检查内网IP
    $private_result = is_private_ip($ip);
    if ($private_result !== false) {
        return $private_result;
    }

    // 获取 searcher(延迟加载、并可降级)
    $searcher = get_ip_searcher($ip);
    if ($searcher === null) {
        return '火星'; // 数据库不存在或加载失败
    }

    try {
        $region = $searcher->search($ip);
    } catch (Exception $e) {
        return '火星';
    }

    $parts = explode('|', $region);
    $country = ($parts[0] !== '0') ? $parts[0] : '';
    $province = ($parts[1] !== '0') ? $parts[1] : '';
    $city = ($parts[2] !== '0') ? $parts[2] : '';
    $isp = ($parts[3] !== '0') ? $parts[3] : '';

    $resultParts = [];

    // 处理 "中国|0|0|ISP" 特殊情况
    if ($country === '中国' && $province === '' && $city === '') {
        $resultParts[] = '中国';
        if ($withIsp && $isp) {
            $resultParts[] = ' ' . $isp;
        }
        return implode('', $resultParts);
    }

    // 处理中国的专属逻辑
    if ($country === '中国') {
        // 直辖市列表
        $municipalities = ['北京', '上海', '天津', '重庆'];

        // 直辖市逻辑
        if (in_array($province, $municipalities) || in_array($city, $municipalities)) {
            $resultParts[] = $city ?: $province;
        } else {
            // 普通省份逻辑
            if ($province === $city) {
                $resultParts[] = $city;
            } else {
                if ($province) $resultParts[] = $province;
                if (!$simpleMode && $city) $resultParts[] = $city;
            }
        }
    } else {
        // 国外逻辑
        if ($country) $resultParts[] = $country;
        if ($province) $resultParts[] = $province;
        if (!$simpleMode && $city) $resultParts[] = $city;
    }

    // 可选显示网络ISP
    if ($withIsp && $isp) {
        $resultParts[] = ' ' . $isp;
    }

    // 兜底,防止结果为空
    if (empty($resultParts)) {
        return $country ?: '火星';
    }

    return implode('', $resultParts);
}

//简版 - 国内只显示省,国外显示国家 + 省
function convertipsimple($ip, $withIsp = false) {
    return convertip($ip, $withIsp, true);
}

?>

3. Load it in the theme

Add the following line to function.php, again making sure the referenced path matches your theme structure:


require get_template_directory() . '/ip2region.php';

4. Call it in the comment template

Once the file is loaded, the comment location can be displayed with these functions:


// 显示省市(无运营商)
echo convertip(get_comment_author_IP());

// 显示省市 + 运营商
echo convertip(get_comment_author_IP(), true);

// 只显示省份
echo convertipsimple(get_comment_author_IP());

// 只显示省份 + 运营商,
echo convertipsimple(get_comment_author_IP(), true);

//如果是国外,以上都会加上国家,只有中国时会过滤一下

After this, WordPress comments can show the commenter’s approximate geographic location in a simple and direct way. With a small adjustment to the way the IP address is obtained, the same logic can also be used in other PHP-based blog systems.

Showing the location only in the admin comment list

Not every blogger wants to show IP locations on the front end. A more restrained approach is to display the location only in the WordPress admin comment list. That keeps the public comment area clean while still letting the site administrator see where visitors are roughly from.

After completing steps 1, 2, and 3 above, add this code to the theme’s function.php:


//后台评论管理新增地理位置信息
function my_comments_columns( $columns ){
    $columns[ 'location' ] = __( '位置' );
    return $columns;
}
add_filter( 'manage_edit-comments_columns', 'my_comments_columns' );
function output_my_comments_columns(){
    echo convertip(get_comment_author_ip()); //可以使用第4步其他方式
}
add_action( 'manage_comments_custom_column', 'output_my_comments_columns', 10, 2 );

IPv4-only version

If the blog server is not configured for IPv6, then comments will not normally contain IPv6 addresses. In that case, there is no need to load the IPv6 database at all. The other steps remain the same; simply replace the code from step 2 with the IPv4-only version below.


<?php
/**
 * Ip2region 是一个离线 IP 数据管理框架和定位库,支持 IPv4 和 IPv6。此代码版本只支持 IPv4 。
 *
 * 官方社区:https://ip2region.net/
 */

require_once __DIR__ . '/xdb/Searcher.class.php';

use \ip2region\xdb\Util;
use \ip2region\xdb\Searcher;

// 全局 Searcher
global $ip2region_searcher_v4;
$ip2region_searcher_v4 = null;

// 初始化 IPv4 Searcher(向量索引模式)
function init_ip2region_vector($dbFile) {
    if (!file_exists($dbFile)) {
        error_log("IP数据库文件不存在: " . $dbFile);
        return null;
    }
    try {
        // 读取文件头,获取版本信息
        $header = Util::loadHeaderFromFile($dbFile);
        $version = Util::versionFromHeader($header);

        // 加载向量索引
        $vIndex = Util::loadVectorIndexFromFile($dbFile);

        // 创建 Searcher
        return Searcher::newWithVectorIndex($version, $dbFile, $vIndex);
    } catch (Exception $e) {
        error_log("IP数据库初始化失败: " . $e->getMessage());
        return null;
    }
}

// 获取 IPv4 searcher
function get_ip_searcher() {
    global $ip2region_searcher_v4;
    if ($ip2region_searcher_v4 === null) {
        $dbFile_v4 = __DIR__ . '/data/ip2region_v4.xdb';
        $ip2region_searcher_v4 = init_ip2region_vector($dbFile_v4);
    }
    return $ip2region_searcher_v4;
}

// 检查内网 IPv4
function is_private_ip($ip) {
    return filter_var(
        $ip,
        FILTER_VALIDATE_IP,
        FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
    ) === false;
}

// 主转换函数
function convertip($ip, $withIsp = false, $simpleMode = false) {
    if (!$ip) return '火星';
    if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
        return '火星';
    }

    // 内网IP
    if (is_private_ip($ip)) {
        return '内网IP';
    }

    // 获取 Searcher
    $searcher = get_ip_searcher();
    if ($searcher === null) {
        return '火星';
    }

    // 查询 IP
    try {
        $region = $searcher->search($ip);
    } catch (Exception $e) {
        return '火星';
    }

    $parts = explode('|', $region);
    $country = ($parts[0] !== '0') ? $parts[0] : '';
    $province = ($parts[1] !== '0') ? $parts[1] : '';
    $city = ($parts[2] !== '0') ? $parts[2] : '';
    $isp = ($parts[3] !== '0') ? $parts[3] : '';

    $resultParts = [];

    // 处理 "中国|0|0|ISP" 特殊情况
    if ($country === '中国' && $province === '' && $city === '') {
        $resultParts[] = '中国';
        if ($withIsp && $isp) {
            $resultParts[] = ' ' . $isp;
        }
        return implode('', $resultParts);
    }

    // 处理中国的专属逻辑
    if ($country === '中国') {
        // 直辖市列表
        $municipalities = ['北京', '上海', '天津', '重庆'];

        // 直辖市逻辑
        if (in_array($province, $municipalities) || in_array($city, $municipalities)) {
            $resultParts[] = $city ?: $province;
        } else {
            // 普通省份逻辑
            if ($province === $city) {
                $resultParts[] = $city;
            } else {
                if ($province) $resultParts[] = $province;
                if (!$simpleMode && $city) $resultParts[] = $city;
            }
        }
    } else {
        // 国外逻辑
        if ($country) $resultParts[] = $country;
        if ($province) $resultParts[] = $province;
        if (!$simpleMode && $city) $resultParts[] = $city;
    }

    // 可选显示网络ISP
    if ($withIsp && $isp) {
        $resultParts[] = ' ' . $isp;
    }

    // 兜底,防止结果为空
    if (empty($resultParts)) {
        return $country ?: '火星';
    }

    return implode('', $resultParts);
}

//简版 - 国内只显示省,国外显示国家 + 省
function convertipsimple($ip, $withIsp = false) {
    return convertip($ip, $withIsp, true);
}

?>