基于多种服务的地理位置查询系统
- 本文為掘金投稿,譯文出自 :?掘金翻譯計劃
- 原文鏈接 :?Geolocation using multiple services
- 原文作者 :?wsdookadr,感謝作者對本篇文章的翻譯授權(quán)。
- 譯者 :?emmiter
- 校對者:?a-voyager,?jamweak
- 感謝譯者的辛苦翻譯與校對者的審校,歡迎技術(shù)投稿、約稿,給文章糾錯,請發(fā)送郵件至tangxy@csdn.net。
簡介
我的這篇文章討論了?PostGIS?以及查詢地理數(shù)據(jù)的幾種方法。這篇文章將集中討論構(gòu)建一個免費的地理服務(wù)系統(tǒng),并聚合呈現(xiàn)結(jié)果。
概述
總的來說,我們將會向不同的網(wǎng)絡(luò)服務(wù)(或APIs)發(fā)起請求,對響應(yīng)結(jié)果做反向地理編碼后再聚合展示。
比較?Geonames?和?OpenStreetMap
下表羅列了二者之間的部分差別:
二者用途不同。Geonomes 用于城市/行政區(qū)/國家數(shù)據(jù),可被用于地理編碼。OpenStreetMap 擁有更加詳盡的數(shù)據(jù)(使用者基本上都可以從 OpenStreetMap 中提取出Geonames數(shù)據(jù)),這些數(shù)據(jù)可被用作地理編碼,路線規(guī)劃以及這些和基于 OpenStreetMap 的服務(wù)。
發(fā)送給地理位置服務(wù)的異步請求
我們使用?gevent?庫來向地理位置服務(wù)發(fā)起異步請求。
import gevent import gevent.greenlet from gevent import monkey; gevent.monkey.patch_all()geoip_service_urls=[['geoplugin' , 'http://www.geoplugin.net/json.gp?ip={ip}' ],['ip-api' , 'http://ip-api.com/json/{ip}' ],['nekudo' , 'https://geoip.nekudo.com/api/{ip}' ],['geoiplookup' , 'http://api.geoiplookup.net/?query={ip}' ],]# fetch url in asynchronous mode (makes use of gevent) def fetch_url_async(url, tag, timeout=2.0):data = Nonetry:opener = urllib2.build_opener(urllib2.HTTPSHandler())opener.addheaders = [('User-agent', 'Mozilla/')]urllib2.install_opener(opener)data = urllib2.urlopen(url,timeout=timeout).read()except Exception, e:passreturn [tag, data]# expects req_data to be in this format: [ ['tag', url], ['tag', url], .. ] def fetch_multiple_urls_async(req_data):# start the threads (greenlets)threads_ = []for u in req_data:(tag, url) = unew_thread = gevent.spawn(fetch_url_async, url, tag)threads_.append(new_thread)# wait for threads to finishgevent.joinall(threads_)# retrieve threads return valuesresults = []for t in threads_:results.append(t.get(block=True, timeout=5.0))return resultsdef process_service_answers(location_data):# 1) extract lat/long data from responses# 2) reverse geocoding using geonames# 3) aggregate location data# (for example, one way of doing this would# be to choose the location that most services# agree on)passdef geolocate_ip(ip):urls = []for grp in geoip_service_urls:tag, url = grpurls.append([tag, url.format(ip=ip)])results = fetch_multiple_urls_async(urls)answer = process_service_answers(results)return answer引發(fā)歧義的城市名
同一國家中具有相同名字的城市
同個國家里,有非常多的分屬于不同州或行政區(qū)的同名城市。也有很多同名不同國的城市。例如,根據(jù) Geonames 的數(shù)據(jù)顯示,美國一共有24個名叫 Clinton 的城市(這24個城市共分布在23個州,其中有兩個是在密歇根州)
WITH duplicate_data AS (SELECTcity_name,array_agg(ROW(country_code, region_code)) AS dupesFROM city_region_dataWHERE country_code = 'US'GROUP BY city_name, country_codeORDER BY COUNT(ROW(country_code, region_code)) DESC ) SELECT city_name, ARRAY_LENGTH(dupes, 1) AS duplicity, ( CASE WHEN ARRAY_LENGTH(dupes,1) > 9 THEN CONCAT(SUBSTRING(ARRAY_TO_STRING(dupes,','), 1, 50), '...')ELSE ARRAY_TO_STRING(dupes,',') END ) AS sample FROM duplicate_data LIMIT 5;
同一國家,同一行政區(qū)的同名城市
從全世界范圍來看,即便是在同個國家的同個行政區(qū),都會出現(xiàn)多個名字完全相同的城市。就拿位于美國印第安納州(Indiana)的喬治城(Georgetown)來說,Geonames 表明該州共有3個同名城鎮(zhèn)。維基百科則顯示了更多:
-
喬治城,弗洛伊德縣,印第安納州
-
喬治城小鎮(zhèn),弗洛伊德縣,印第安納州
-
喬治城,卡斯縣,印第安納州
-
喬治城,蘭道夫縣,印第安納州
反向地理編碼
(city_name, country_code),(city_name, country_code, region_name) 這兩個元組都不能唯一地確定一個位置。我們可以使用郵政編碼 (zip codes?或者叫做?postal codes),除非地理位置服務(wù)不提供他們。但是大部分的地理位置服務(wù)卻提供經(jīng)緯度,可以使用這兩者來消除歧義。
PostgreSQL 數(shù)據(jù)庫中的圖形數(shù)據(jù)類型
我深入研究了 PostgreSQL 數(shù)據(jù)庫的文檔,發(fā)現(xiàn)它也擁有幾何數(shù)據(jù)類型和用于2D 幾何(平面幾何)的函數(shù)。你可以使用這些現(xiàn)成的數(shù)據(jù)類型和函數(shù)來模擬點,框,路徑,多邊形和圓并且可以將他們存儲,之后還可以查詢。PostgreSQL 還有一些存在于普通發(fā)布目錄的額外擴展。這些擴展需要大部分 Postgres 安裝后才可以使用。當下的情況,我們對?cube 類型?和earthdistance?擴展感興趣,earthdistance 擴展使用?3-cubes?來存儲向量和表示地球上的點。我們要用到的東西如下所示:
- earth_distance?函數(shù)是可用的,允許你計算球面上兩點之間的最短距離?great-circle-distance
- earth_box?函數(shù)用于檢查對于給定的參考點,和給定的距離,該點是否位于該距離以內(nèi)
- 一個?gist?位于表達式上的索引(expression index),表達式?ll_to_earth(lat,long)?執(zhí)行快速的空間查詢以及尋找附近點。
為城市 & 行政區(qū)數(shù)據(jù)設(shè)計一個視圖
Geonames 數(shù)據(jù)被導(dǎo)入到3個表中:
- geo_geoname?(數(shù)據(jù)來自?cities1000.zip)
- geo_admin1?(數(shù)據(jù)來自?admin1CodesASCII.txt?)
- geo_countryinfo (數(shù)據(jù)來自?countryInfo.txt?)
然后我們來創(chuàng)建一個可以將所有東西拉取到一起的視圖3。現(xiàn)在我們有了人口數(shù)據(jù),城市/行政區(qū)/國家數(shù)據(jù)以及經(jīng)度/維度數(shù)據(jù),都在同個地方了。
CREATE OR REPLACE VIEW city_region_data AS ( SELECTb.country AS country_code,b.asciiname AS city_name,a.name AS region_name,b.region_code,b.population,b.latitude AS city_lat,b.longitude AS city_long,c.name AS country_nameFROM geo_admin1 aJOIN (SELECT *, (country || '.' || admin1) AS country_region, admin1 AS region_codeFROM geo_geonameWHERE fclass = 'P') b ON a.code = b.country_regionJOIN geo_countryinfo c ON b.country = c.iso_alpha2 );設(shè)計一個城市周邊查詢函數(shù)
在大多數(shù)嵌套?SELECT?語句中,我們都確保城市是在以參考點為圓心,以大約23km為半徑的區(qū)域內(nèi),再對結(jié)果應(yīng)用國家過濾器和城市模式過濾器(這兩個過濾器均為可選),最后僅得到接近50個結(jié)果。下一步,我們用人口數(shù)據(jù)對結(jié)果重新排序,因為有時候會在較大城市附近有一些區(qū)和鄰域?4,而 Geonames 不會用特定的方式標記他們,我們只是想選出較大的城市而不是一個區(qū)域(比如說地理位置服務(wù)返回了經(jīng)緯度信息,該信息可被解析為一個較大城市的地區(qū)。于我而言,我比較愿意去把它解析成經(jīng)緯度相對應(yīng)的大城市)。我們也創(chuàng)建了一個 gist 索引(@>?該符號將會使用 gist 索引 ),用于尋找以參照點為圓心,特定半徑范圍內(nèi)的點。這個查詢函數(shù)接受一個點(以緯度和經(jīng)度表示)作為輸入,返回該輸入點相關(guān)聯(lián)的城市,地區(qū)和國家。
CREATE INDEX geo_geoname_latlong_idx ON geo_geoname USING gist(ll_to_earth(latitude,longitude)); CREATE OR REPLACE FUNCTION geo_find_nearest_city_and_region(latitude double precision,longitude double precision,filter_countries_arr varchar[],filter_city_pattern varchar, ) RETURNS TABLE(country_code varchar,city_name varchar,region_name varchar,region_code varchar,population bigint,_lat double precision,_long double precision,country_name varchar,distance numeric) AS $ BEGINRETURN QUERYSELECT *FROM (SELECT*FROM (SELECT *,ROUND(earth_distance(ll_to_earth(c.city_lat, c.city_long),ll_to_earth(latitude, longitude))::numeric, 3) AS distance_FROM city_region_data cWHERE earth_box(ll_to_earth(latitude, longitude), 23000) @> ll_to_earth(c.city_lat, c.city_long) AND(filter_countries_arr IS NULL OR c.country_code=ANY(filter_countries_arr)) AND(filter_city_pattern IS NULL OR c.city_name LIKE filter_city_pattern)ORDER BY distance_ ASCLIMIT 50) dORDER BY population DESC) eLIMIT 1; END; $ LANGUAGE plpgsql;總結(jié)
我們從系統(tǒng)設(shè)計著手,讓這個系統(tǒng)可以查詢多個Geoip 服務(wù),可以收集這些服務(wù)返回的數(shù)據(jù)對其聚合后得到一個更加可靠的結(jié)果。我們首先考慮了唯一確定位置的幾種方式。隨后選取了一種可以在確認位置時消除歧義的方法。第二部分中,我們著眼于構(gòu)建,存儲以及查詢PostgreSQL中地理數(shù)據(jù)的不同方法。然后我們建立了一個視圖和函數(shù),用來找出參考點附近的允許我們用來進行反向編碼的城市。
附注
1?通過使用多種服務(wù)(并且假定這些服務(wù)內(nèi)部使用了不同的數(shù)據(jù)源)聚合后的結(jié)果,將會比我們只使用其中某一種服務(wù)得到的答案更為可靠。
此處還有一點優(yōu)勢就,我們使用了免費服務(wù),不需要什么設(shè)置,也無需關(guān)心更新;因為這些服務(wù)都是由各自的擁有者在維護。
然而,比起查詢一個本地的 geoip(基于 IP 查詢的地理位置)數(shù)據(jù)結(jié)構(gòu),查詢這些網(wǎng)絡(luò)地理位置服務(wù)則會比較緩慢。好在像城市/國家/行政區(qū)這種定位數(shù)據(jù)庫已經(jīng)有了,例如?MaxMind GeoIP2,?IP2Location?以及?DB-IP?。
2?介紹一篇好文章,講述了使用?earthdistance?模塊來計算附近或更遠處酒吧的距離。
3?Genomes 也有 geonamelds,我們可以使用這些 genomes-specific ids 來精確匹配其位置。
4?Geonames 沒有關(guān)于 城市/鄰域的多邊形數(shù)據(jù),或者城市地區(qū)類型的元數(shù)據(jù)(參考概述中 Geonames 和 OpenStreetMap 差異對照表中 criteria 一列的數(shù)據(jù)),所以你無法查詢包含那個點的所有的城市多邊形(不是指區(qū)域/鄰域)。
總結(jié)
以上是生活随笔為你收集整理的基于多种服务的地理位置查询系统的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一键安装GitLab7在RHEL6.4上
- 下一篇: [译]NGINX 和 ZooKeeper