2016年2月23日 星期二

從 IP 查來源國家 (一)

上週六去市圖還書時看到架上這本以前借過, 但沒時間看的 "PHP 最強外掛 100 選 (碁峰翻譯書, 已絕版)", 一時技癢拿下來看, 翻到最後面第 91 個 Pug-in : "利用 IP 取得所在位置", 覺得挺好玩的, 就又再借回來, 想幫 EasyuiCMS 上面的訪客紀錄器添加顯示來源國家的功能.


此書範例的原始碼可在下面連結下載 :

# Plug-in PHP: 100 Power Solutions

下載後解開第 12 章底下的 plugin91.php 與 ips.txt 這兩個檔案, 這個 ips.txt 就是各國的 ip 分布區間, 格式如下 :

984809472,984875007,Australia
984875008,984940543,South Korea
984940544,984956927,South Korea
984956928,984965119,Australia
984965120,984969215,Pakistan
984973312,985006079,Thailand
985006080,985071615,Singapore
985071616,985104383,Japan
985104384,985137151,Japan
985137152,985202687,South Korea
985202688,985235455,Singapore
985235456,985268223,Singapore
985268224,985399295,Viet Nam
985399296,985661439,Japan
985661440,985792511,China
985792512,985923583,China
985923584,986054655,China

其中第一個數字是 IP 下限, 第二個數字是上限, 最後面為所屬國名. 我有改其中的國名, 例如 Russian Federation 改為 Russia, Korea republic of 改為 South Korea 等等, 然後把這個 ipx.txt 放在根目錄的 data 目錄下面. 此檔可從下列連結下載 :

# 下載修改後的 ips.txt

接著參考 plugin91.php 這個範例程式, 修改 /lib 下的 file.php 函式庫, 加入下面的函式 get_country_by_ip() :

/*-----------------------------------------------------------------------------
get_country_by_ip($ip)
功能 :
  此函數依據傳入的 ip 到 /data/ips.txt 查詢其所屬國家英文名稱.
參數 :
  $ip : IP (192.168.1.1)
傳回值 :
  成功傳回英文國名, 失敗傳回 FALSE.
範例 :
  $result=get_country_by_ip("10.11.223.12");
-----------------------------------------------------------------------------*/
function get_country_by_ip($ip) {
  $iptemp=explode('.', $ip);
  $ipdec=$iptemp[0] * 256 * 256 * 256 + $iptemp[1] * 256 * 256 +
         $iptemp[2] * 256 + $iptemp[3];
  $file=read_file("./data/ips.txt");
  if (!strlen($file)) return FALSE;
  $lines=explode("\n", $file);
  foreach($lines as $line) {
    if (strlen($line)) {
      $parts=explode(',', trim($line));
      if ($ipdec >= $parts[0] && $ipdec <= $parts[1]) {return $parts[2];}
      }
    }
  return FALSE;
  }

其原理就是將 IP 以小數點拆分成四個數字, 然後換算成一個十進位整數, 然後讀取 ips.txt 檔, 比對看看落在哪一個區間, 有的話傳回國名, 否則傳回 False.

最後修改 sys.php 中關於訪客紀錄的兩個 case 模組 visitors 與 list_visitors (sys.php 一開頭就已匯入 file.php 了), 在 visitors 模組中, 修改 datagrid 的設定, 加入 country 欄位 :

    $('#sys_visitors').datagrid({
      columns:[[
        {field:'id',title:'id',sortable:true},
        {field:'visit_time',title:'到訪時間',sortable:true},
        {field:'remote_addr',title:'遠端位址',sortable:true},
        {field:'country',title:'國家',sortable:true},
        {field:'remote_port',title:'遠端埠號',align:'right',sortable:true},
        {field:'user_agent',title:'使用者代理',sortable:true}
        ]],
      url:"sys.php",
      queryParams:{op:"list_visitors"},
      fitColumns:true,
      singleSelect:true,
      pagination:true,
      pageSize:10,
      rownumbers:true
      });

然後在 list_visitors 模組中加入呼叫 get_country_by_ip() 的程式碼 :

  case "list_visitors" : {
    $page=isset($_REQUEST['page']) ? intval($_REQUEST['page']) : 1;
    $rows=isset($_REQUEST['rows']) ? intval($_REQUEST['rows']) : 10;
    $sort=isset($_REQUEST['sort']) ? $_REQUEST['sort'] : 'visit_time';
    $order=isset($_REQUEST['order']) ? $_REQUEST['order'] : 'desc';
    if (isset($_REQUEST['search_field'])) { //有 search
      $where="WHERE ".$_REQUEST['search_field']." LIKE '%".
             $_REQUEST['search_what']."%'";
      }
    else {$where="";} //無 search
    $start=($page-1) * $rows;  //本頁第一個列索引 (0 起始)
    $SQL="SELECT COUNT(*) FROM `sys_visitors`";
    $RS=run_sql($SQL);
    $total=$RS[0][0]; //紀錄總筆數
    $SQL="SELECT * FROM sys_visitors ".$where." ORDER BY ".
         $sort." ".$order." LIMIT ".$start.",".$rows;
    $RS=run_sql($SQL);
    $visitors=Array();
    if (is_array($RS)) {
      for ($i=0; $i<count($RS); $i++) {
        $country=get_country_by_ip($RS[$i]["remote_addr"]);
        if (!$country) {$country="";}  //傳回 false 設為空字串
        $visitors[$i]=Array("id" => $RS[$i]["id"],
                            "visit_time" => $RS[$i]["visit_time"],
                            "remote_addr" => $RS[$i]["remote_addr"],
                            "country" => $country,
                            "remote_port" => $RS[$i]["remote_port"],
                            "user_agent" => $RS[$i]["user_agent"]
                            );
        }
      }
    $arr=array("total" => $total, "rows" => $visitors);
    echo json_encode($arr);
    break;
    }

這樣就會增加國家這個欄位了 :



但顯然這個 ips.txt 不是很完整, 某些 IP 查不到所屬國家. 如果要加註中文國名, 那麼需要準備一張中英國名對照表. 這張表在目前還在用的舊工作日誌的 ACCESS 資料庫中就有, 我先將其匯出後, 剔除不需要的欄位如時區等, 並將英文國名與上面的 ips.txt 一致化, 此檔可從下列連結下載 :

下載中英國名對照表 country.txt

在整理這張表時遇到排序的問題, 我在 ACCESS 2003 上將其按英文國名排序後, 按 "檔案/匯出" 輸出為 country.txt, 但它卻不是按照英文字母排序 :

ACCESS 中已排序好

以逗號分隔欄位

去除雙引號

沒有按照字母排序

解決之道就是把這 .txt 檔改成 .csv 副檔名, 然後用 EXCEL 開啟後排序, 再回存為 country.txt 檔即可 :


參考 :

跑出來的順序不是照我ACCESS上所看到的順序

接下來去修改 file.php 裡的 get_country_by_ip() 函式, 在查到英文國名後, 再去開啟 country.txt, 用同樣的邏輯查詢其中文國名 :

/*-----------------------------------------------------------------------------
get_country_by_ip($ip)
功能 : 
  此函數依據傳入的 ip 到 /data/ips.txt 查詢其所屬國家英文名稱.
參數 :  
  $ip : IP (192.168.1.1) 
傳回值 :
  成功傳回英文國名, 失敗傳回 FALSE. 
範例 :
  $result=get_country_by_ip("10.11.223.12"); 
-----------------------------------------------------------------------------*/  
function get_country_by_ip($ip) {
  $iptemp=explode('.', $ip);
  $ipdec=$iptemp[0] * 256 * 256 * 256 + $iptemp[1] * 256 * 256 +
         $iptemp[2] * 256 + $iptemp[3];
  $file=read_file("./data/ips.txt");
  if (!strlen($file)) return FALSE; //沒檔案
  $lines=explode("\n", $file);  //拆出各列
  foreach($lines as $line) {  //拜訪每一列
    if (strlen($line)) {  //非空白列
      $parts=explode(',', trim($line));  //以逗號拆分
      if ($ipdec >= $parts[0] && $ipdec <= $parts[1]) {  //比對 ip
        //return $parts[2];
        $country=trim($parts[2]);  //英文國名
        $file2=read_file("./data/country.txt");
        if (!strlen($file2)) return FALSE;  //沒檔案
        $lines2=explode("\n", $file2);  //拆出各列
        foreach($lines2 as $line2) {  //拜訪每一列
          if (strlen($line2)) {  //非空白列
            $parts2=explode(',', trim($line2));  //以逗號拆分
            if ($parts2[0]==$country) { //比對英文國名
              return $country." (".$parts2[1].")";
              }  
            }
          }
        }
      }
    }
  return FALSE;
  }


這樣就會顯示中英國名了.


沒有留言 :