問題描述
對於一份ip數據,我們希望使用開放API來獲得ip地址對應的地理地址。
ip查詢API選擇
互聯網上開放的ip查詢API有很多,但是,它們各有優缺點。筆者在短暫的實踐中了解了一下兩個API。
這兩個API都是免費開放。淘寶的API的訪問限制是10qps,百度API的訪問限制是6000/min。百度對於未認證用戶一天的訪問限制次數是10萬次(認證后可以提高配額)。
通過少量數據的比對,淘寶的api可以返回縣地址,而百度的api基本沒有返回縣地址。但是百度的api訪問速度,並發量都比淘寶要大的多。考慮到淘寶api的統計數據中,縣數據的覆蓋率也只有6.72%。所以我們選擇百度的api。
讀者可以查看上述的兩個鏈接,進一步了解判斷,根據自己的情況做出決定。
API接口了解
http://api.map.baidu.com/location/ip?ak=yourak&ip=targetIp
百度的api存在兩種認證模式,一種是ip白名單,一種是sn校驗算。不過筆者在nodejs下發起請求時,一直沒有辦法算對sn值,在網絡上也沒有找到相應的資料,最后轉而使用ip白名單模式,其實對於數據處理的應用,使用ip白名單模式就可以了。
設置ip白名單后就可以直接發起API請求了。
只有IP白名單內的服務器才能成功發起調用
格式: 202.198.16.3,202.198.0.0/16 填寫IP地址或IP前綴網段,英文半角逗號分隔
如果不想對IP做任何限制,請設置為0.0.0.0/0
請求的格式如上,詳細請直接查看鏈接。
代碼
代碼的細節就不多討論,讀者可以直接閱讀源代碼來了解。這里提及一下使用到的npm包以及使用該包的原因和相應的技術文檔,
- 數據處理在nodejs下必然包含很多異步操作比如文件讀寫,網絡請求等。所以我們使用了流程控制庫
co
如果你不了解co請參看這個鏈接
Generator 函數的異步應用
你也可以使用其它流程控制庫。 co-parallel
和async
。co
只能讓異步的流程同步化。但是一些細節的控制還是要依賴其它一些庫。筆者在這里沒有過多調研。簡單查閱資料后,使用co-parallel
來做並發控制,使用async.retry
在請求失敗的時候重試。superagent
http請求庫
下面是源代碼。
const bluebird = require('bluebird')
const fs = bluebird.promisifyAll(require('fs'))
const co = require('co')
const os = require('os')
const superagent = require('superagent')
const parallel = require('co-parallel')
const async = require('async')
// 計數連接失敗的個數
let failCount = 0
//百度IP地址庫
const urltaoip = 'http://api.map.baidu.com/location/ip?ak=yourak&ip='
// 輸入文件名字
let inputFileName = process.argv[2] || 'xxx.csv'
// 輸出文件名
let outputFileName = process.argv[3] || 'result.csv'
// 輸出文件流
const writer = fs.createWriteStream(outputFileName)
// 把supsuperagent 包裝為promise 供co使用
// 使用async.retry 在發生錯誤的情況下重試5次
function getUrlPromise(url) {
return new Promise((resolve, reject) => {
async.retry(5, (cb) => {
superagent.get(url)
.end((err, res) => {
if (err) {
cb(err)
} else {
cb(null, res)
}
})
}, (err, res) => {
if (err) {
resolve('{}') // 重連5次后,仍然錯誤, 也不要拋出錯誤,避免程序終止
} else {
resolve(res.text)
}
})
})
}
co(function* () {
// 讀取文件
let content = yield fs.readFileAsync(inputFileName, 'utf-8')
// 分割得到行
let lineArr = content.split(os.EOL)
// 從行中得到ip
let ipArr = []
for(line of lineArr) {
let itemArr = line.split(',') // 確保ip在最后一列
let ip = itemArr.pop()
ipArr.push(ip)
}
// 構造請求列表
let reqs = ipArr.map(function* (ip) {
let url = `${urltaoip}${ip}`
console.log(url)
return yield getUrlPromise(url)
})
// 並發不超過100 來請求, 百度限制 一分鍾 6000個並發請求
let res = yield parallel(reqs, 100)
// 輸出結果
for(let i = 0; i < res.length; i++) {
let ipObjStr = res[i]
let ipObj = JSON.parse(ipObjStr)
let address =ipObj && ipObj.content && ipObj.content.address_detail
if (!address) {
failCount++;
address = {
province: '獲取失敗',
city: '獲取失敗',
district: '獲取失敗'
}
}
console.log(address.province)
console.log(address.city)
console.log(address.district)
writer.write(lineArr[i])
writer.write(`,${address.province || '無數據'},${address.city || '無數據'},${address.district || '無數據'}${os.EOL}`)
}
})
.catch(err => {
console.log(err)
})
.then(() => {
console.log('失敗個數' + failCount)
})