2022年8月31日 星期三

Python 學習筆記 : 用 Folium 套件繪製地圖 (一)

這兩天參加 Python 內訓課程時學會如何用 Folium 套件繪製網頁地圖, 以及如何用 GeoCoder 將地標轉成經緯度再傳給 Folium 使用, 以下是課後的測試紀錄. 

Folium 其實是一個以 Javascript 地圖函式庫 Leaflet 為基礎的 Python 網頁地圖套件, 其 API 可讓 Python 使用者傳入經緯度在網頁介面上繪製互動式網頁地圖, 主要應用在搭配 Flask 或 Django 等框架在網頁上繪製地圖, 參考 : 

 
因為 Folium 所繪製的是網頁地圖, 其結果必須在瀏覽器上呈現, 故以下測試是在 JupyterLab 或 Colab 等網頁介面上進行 (在命令列值行只會顯示地圖物件名稱與地址, 無法顯示地圖). 


1. 安裝 Folium : 

如果要在本機的 JupyterLab/Notebook 上顯示地圖須先在命令列安裝 folium : 

C:\Users\User>pip install folium   
Collecting folium
  Downloading folium-0.12.1.post1-py2.py3-none-any.whl (95 kB)
     ---------------------------------------- 95.0/95.0 kB 1.1 MB/s eta 0:00:00
Requirement already satisfied: requests in c:\python37\lib\site-packages (from folium) (2.21.0)
Requirement already satisfied: jinja2>=2.9 in c:\python37\lib\site-packages (from folium) (2.10.1)
Requirement already satisfied: numpy in c:\python37\lib\site-packages (from folium) (1.19.4)
Collecting branca>=0.3.0
  Downloading branca-0.5.0-py3-none-any.whl (24 kB)
Requirement already satisfied: MarkupSafe>=0.23 in c:\python37\lib\site-packages (from jinja2>=2.9->folium) (1.1.1)
Requirement already satisfied: idna<2.9,>=2.5 in c:\python37\lib\site-packages (from requests->folium) (2.8)
Requirement already satisfied: certifi>=2017.4.17 in c:\python37\lib\site-packages (from requests->folium) (2018.11.29)
Requirement already satisfied: chardet<3.1.0,>=3.0.2 in c:\python37\lib\site-packages (from requests->folium) (3.0.4)
Requirement already satisfied: urllib3<1.25,>=1.21.1 in c:\python37\lib\site-packages (from requests->folium) (1.24.1)
Installing collected packages: branca, folium
Successfully installed branca-0.5.0 folium-0.12.1.post1

在 Colab 上安裝 Folium 是在儲存格輸入 !pip install 指令 :

!pip install folium 




安裝好後先來檢視 Folium 套件的成員, 以下使用一個自訂模組 members, 其 list_members() 函式會列出模組或套件中的公開成員 (即屬性與方法), 參考 :

Python 學習筆記 : 檢視物件成員與取得變數名稱字串的方法

使用 Folium 前須先 import folium :

>>> import folium     
>>> import members   
>>> members.list_members(folium)    
Choropleth <class 'type'>
Circle <class 'type'>
CircleMarker <class 'type'>
ClickForMarker <class 'type'>
ColorLine <class 'type'>
ColorMap <class 'type'>
CssLink <class 'type'>
CustomIcon <class 'type'>
Div <class 'type'>
DivIcon <class 'type'>
Element <class 'type'>
FeatureGroup <class 'type'>
Figure <class 'type'>
FitBounds <class 'type'>
GeoJson <class 'type'>
GeoJsonPopup <class 'type'>
GeoJsonTooltip <class 'type'>
Html <class 'type'>
IFrame <class 'type'>
Icon <class 'type'>
JavascriptLink <class 'type'>
LatLngPopup <class 'type'>
LayerControl <class 'type'>
LinearColormap <class 'type'>
Link <class 'type'>
MacroElement <class 'type'>
Map <class 'type'>
Marker <class 'type'>
PolyLine <class 'type'>
Polygon <class 'type'>
Popup <class 'type'>
Rectangle <class 'type'>
RegularPolygonMarker <class 'type'>
StepColormap <class 'type'>
TileLayer <class 'type'>
Tooltip <class 'type'>
TopoJson <class 'type'>
Vega <class 'type'>
VegaLite <class 'type'>
WmsTileLayer <class 'type'>
branca <class 'module'>
elements <class 'module'>
features <class 'module'>
folium <class 'module'>
map <class 'module'>
raster_layers <class 'module'>
sys <class 'module'>
utilities <class 'module'>
vector_layers <class 'module'>

可見 Folium 的功能非常豐富, 但繪製網頁地圖主要會用到 Map 與 Marker 這兩個類別, Map 用來建立地圖物件, 而 Marker 則用來在地圖上打標記, 可透過呼叫其建構函式來建立物件. 


2. 呼叫 folium.Map() 繪製地圖 : 

呼叫 Map 類別的建構函式 folium.Map() 會建立一個 Map 物件, 其 API 如下 :

class folium.folium.Map(location=None, width='100%', height='100%', left='0%', top='0%', position='relative', tiles='OpenStreetMap', attr=None, min_zoom=0, max_zoom=18, zoom_start=10, min_lat=- 90, max_lat=90, min_lon=- 180, max_lon=180, max_bounds=False, crs='EPSG3857', control_scale=False, prefer_canvas=False, no_touch=False, disable_3d=False, png_enabled=False, zoom_control=True, **kwargs)

參考 :


其中最常用的參數如下 :
  • location : 用來指定地圖中心點的位置座標, 其值為經度 (longitude) 與緯度 (latitude) 組成的 tuple 或 list, 如果不指定 location 預設 None 會顯示全世界地圖. 
  • zoom_start : 用來指定放大倍數 (預設是 10 倍) : 
常用語法如下 :

map=folium.Map(location=[緯度, 經度], zoom_start=放大倍數)    

這會傳回一個 Map 物件, 可用 members 模組來檢視其成員 : 

>>> import members 
>>> members.list_members(folium.Map)      
add_child <class 'function'>
add_children <class 'function'>
add_to <class 'function'>
choropleth <class 'function'>
default_css <class 'list'>
default_js <class 'list'>
fit_bounds <class 'function'>
get_bounds <class 'function'>
get_name <class 'function'>
get_root <class 'function'>
keep_in_front <class 'function'>
render <class 'function'>
save <class 'function'>
to_dict <class 'function'>
to_json <class 'function'>

其中較常用的方法是 save(), 可以傳入一個 HTML 檔名字串 (含路徑), 例如 "map.htm" 或 "D:\\test\\python\\map.htm", 會將所繪製的地圖存成 Leaflet 原生的網頁檔.   

如果在命令列執行下列指令, 只會顯示 Map 物件的資訊 : 

>>> import folium      
>>> map=folium.Map()  
>>> type(map)    
<class 'folium.folium.Map'>       
>>> map     
<folium.folium.Map object at 0x0000020DB6E492B0>

要在 JupyterLab 或 Colab 執行才會顯示地圖 : 




如果傳入 location 參數指定地圖中心點的經緯度與起始放大倍數 zoom_start 就可以顯示該位置附近的地圖, 例如高雄 '澄清湖' 座標緯度為 22.66, 經度為 120.351 :

import folium      
map=folium.Map(location=[22.66, 120.35], zoom_start=16) 
map 

結果如下 :




繪製地圖後還可以利用 Marker 物件在地圖上的座標位置, 此類別之 API 如下 :

class folium.map.Marker(location=None, popup=None, tooltip=None, icon=None, draggable=False, **kwargs)    

較常用的參數如下 :
  • location : 要打標記的座標位置, 可用串列或元組表示, 例如 [緯度, 經度]
  • popup : 滑鼠移到該標記上方時的彈出訊息 (字串)
  • icon : 是一個 folium.Icon 物件
icon 參數用來指定標記的樣式, 預設為藍色的 Bootstrap 3 的圖示文字, 其 API 如下 :

class folium.map.Icon(color='blue', icon_color='white', icon='info-sign', angle=0, prefix='glyphicon', **kwargs)   

常用參數為 color (設定圖示標記顏色), 參考 : 


首先用 members 模組檢視 Marker 物件之成員 : 

>>> import members 
>>> members.list_members(folium.Marker)     
add_child <class 'function'>
add_children <class 'function'>
add_to <class 'function'>
get_bounds <class 'function'>
get_name <class 'function'>
get_root <class 'function'>
render <class 'function'>
save <class 'function'>
to_dict <class 'function'>
to_json <class 'function'>

其中常用的方法是 add_to(), 傳入參數為 Map 物件, 表示要將此標記添加到該地圖物件上. 其次來檢視 Icon 物件成員 :

>>> import members 
>>> members.list_members(folium.Icon)     
add_child <class 'function'>
add_children <class 'function'>
add_to <class 'function'>
color_options <class 'set'>
get_bounds <class 'function'>
get_name <class 'function'>
get_root <class 'function'>
render <class 'function'>
save <class 'function'>
to_dict <class 'function'>
to_json <class 'function'>

其中的 add_to(marker) 方法也是用來將 Iccon 物件添加到 Marker 物件上, 但通常會直接將 Icon 物件傳入 Marker() 建構函式的 icon 參數中, 常用語法如下 : 

folium.Marker([緯度, 經度] , popup="說明", icon=folium.Icon(color="red")).add_to(map)    

例如高雄澄清湖座標為 [22.66, 120.35], 在 Colab 或 JupyterLab 中輸入如下指令 : 

import folium   
map=folium.Map(location=[22.66, 120.35], zoom_start=16)     
folium.Marker(location=[22.66, 120.35] , popup="澄清湖", icon=folium.Icon(color="red")).add_to(map) 
map   

這樣就會在地圖中心點座標緯度 22.66 經度 120.35 上打上標記, 結果如下 : 




用滑鼠點一下紅色標記就會跳出 "澄清湖" 字樣. 


3. 安裝 GeoCoder : 

上面的範例是已知座標經緯度情況下繪製地圖, 通常可利用 Google 搜尋而得, 但若要在程式中查詢地標或城市知經緯度, 可用 GeoCoder 套件, 參考 :

使用此套件前須用先安裝 : 

C:\Users\User>pip install geocoder    
Collecting geocoder
  Downloading geocoder-1.38.1-py2.py3-none-any.whl (98 kB)
     ---------------------------------------- 98.6/98.6 kB 1.1 MB/s eta 0:00:00
Requirement already satisfied: requests in c:\python37\lib\site-packages (from geocoder) (2.21.0)
Requirement already satisfied: future in c:\python37\lib\site-packages (from geocoder) (0.18.2)
Requirement already satisfied: click in c:\python37\lib\site-packages (from geocoder) (7.1.2)
Collecting ratelim
  Downloading ratelim-0.1.6-py2.py3-none-any.whl (4.0 kB)
Requirement already satisfied: six in c:\python37\lib\site-packages (from geocoder) (1.12.0)
Requirement already satisfied: decorator in c:\python37\lib\site-packages (from ratelim->geocoder) (4.3.2)
Requirement already satisfied: idna<2.9,>=2.5 in c:\python37\lib\site-packages (from requests->geocoder) (2.8)
Requirement already satisfied: certifi>=2017.4.17 in c:\python37\lib\site-packages (from requests->geocoder) (2018.11.29)
Requirement already satisfied: urllib3<1.25,>=1.21.1 in c:\python37\lib\site-packages (from requests->geocoder) (1.24.1)
Requirement already satisfied: chardet<3.1.0,>=3.0.2 in c:\python37\lib\site-packages (from requests->geocoder) (3.0.4)
Installing collected packages: ratelim, geocoder
Successfully installed geocoder-1.38.1 ratelim-0.1.6

在 Colab 安裝 GeoCoder 要用 !pip install geocoder :

!pip install geocoder   




4. 使用 GeoCoder 查詢經緯度 : 

首先用 members 模組來檢視 GeoCoder 套件的成員 : 

>>> import geocoder 
>>> import members 
>>> members.list_members(geocoder)     
absolute_import <class '__future__._Feature'>
api <class 'module'>
arcgis <class 'function'>
arcgis_reverse <class 'module'>
baidu <class 'function'>
baidu_reverse <class 'module'>
base <class 'module'>
bing <class 'function'>
bing_batch <class 'module'>
bing_batch_forward <class 'module'>
bing_batch_reverse <class 'module'>
bing_reverse <class 'module'>
canadapost <class 'function'>
cli <class 'click.core.Command'>
distance <class 'function'>
elevation <class 'function'>
freegeoip <class 'function'>
gaode <class 'function'>
gaode_reverse <class 'module'>
geocodefarm <class 'function'>
geocodefarm_reverse <class 'module'>
geolytica <class 'function'>
geonames <class 'function'>
geonames_children <class 'module'>
geonames_details <class 'module'>
geonames_hierarchy <class 'module'>
get <class 'function'>
gisgraphy <class 'function'>
gisgraphy_reverse <class 'module'>
google <class 'function'>
google_elevation <class 'module'>
google_places <class 'module'>
google_reverse <class 'module'>
google_timezone <class 'module'>
here <class 'function'>
here_reverse <class 'module'>
ip <class 'function'>
ipinfo <class 'function'>
keys <class 'module'>
komoot <class 'function'>
komoot_reverse <class 'module'>
location <class 'function'>
locationiq <class 'function'>
locationiq_reverse <class 'module'>
mapbox <class 'function'>
mapbox_reverse <class 'module'>
mapquest <class 'function'>
mapquest_batch <class 'module'>
mapquest_reverse <class 'module'>
mapzen <class 'function'>
mapzen_reverse <class 'module'>
maxmind <class 'function'>
nokia <class 'function'>
opencage <class 'function'>
opencage_reverse <class 'module'>
osm <class 'function'>
osm_reverse <class 'module'>
ottawa <class 'function'>
places <class 'function'>
reverse <class 'function'>
tamu <class 'function'>
tgos <class 'function'>
timezone <class 'function'>
tomtom <class 'function'>
uscensus <class 'function'>
uscensus_batch <class 'module'>
uscensus_reverse <class 'module'>
w3w <class 'function'>
w3w_reverse <class 'module'>
yahoo <class 'function'>
yandex <class 'function'>
yandex_reverse <class 'module'>

可見 GeoCoder 套件功能非常多, 但此處只會用到其中的 osm() 函式來查詢地標名稱之座標經緯度, 其傳入參數為地標名稱字串, 它會傳回一個 OsmQuery 物件, 根據官網文件說明, 此物件的 latlng 屬性即為查詢之結果 (是一個 [緯度, 經度] 串列), 例如 : 

>>> import geocoder   
>>> osm=geocoder.osm('澄清湖')      
>>> type(osm)      
<class 'geocoder.osm.OsmQuery'>       
>>> osm.latlng    
[22.660662549999998, 120.35109204721456]     

前者 22.66 為緯度, 後者 120.35 為經度. 

上面的程式碼可以改成如下用 GeoCoder 查詢經緯度的版本 (地標改為台北101) : 

import folium    
import geocoder      
place='台北101'    
latlng=geocoder.osm(place).latlng      
map=folium.Map(location=latlng, zoom_start=16)        
folium.Marker(location=latlng , popup=place, icon=folium.Icon(color="red")).add_to(map)        
map      

結果如下 :




不過很奇怪的是, 我用 members 模組或 dir() 去檢視 OsmQuery 物件的成員, 卻沒有發現 latlng 這個屬性, 非常奇怪 : 

>>> import geocoder 
>>> import members 
>>> osm=geocoder.osm('台北101')
>>> members.list_members(osm)    
add <class 'method'>
append <class 'method'>
clear <class 'method'>
count <class 'method'>
current_result <class 'geocoder.osm.OsmResult'>
debug <class 'method'>
encoding <class 'str'>
error <class 'bool'>
extend <class 'method'>
geojson <class 'dict'>
headers <class 'dict'>
index <class 'method'>
insert <class 'method'>
location <class 'str'>
method <class 'str'>
ok <class 'bool'>
one_result <class 'type'>
params <class 'collections.OrderedDict'>
pop <class 'method'>
provider <class 'str'>
proxies <class 'str'>
rate_limited_get <class 'method'>
remove <class 'method'>
response <class 'requests.models.Response'>
reverse <class 'method'>
session <class 'requests.sessions.Session'>
set_default_result <class 'method'>
status <class 'str'>
status_code <class 'int'>
timeout <class 'float'>
url <class 'str'>

原以為是 members 模組遺漏了, 但用 dir() 去檢視也沒看到 latlng 屬性, 太奇怪了. 

順便檢視了 OsmQuery 物件的幾個屬性如下 :

>>> osm.location   
'台北101'   
>>> osm.current_result   
[台北101, 7, 信義路五段, 西村里, 信義區, 信義商圈, 臺北市, 11049, 臺灣]


5. 利用 Sellenium 將地圖存成 png 圖片 : 

由於 Folium 所繪製的是以 Leafty 為基礎的網頁地圖, 其 API 並未提供將網頁地圖匯出為圖檔的函式或方法, 只能透過螢幕截圖的方式手動處理. 如果要在程式中自動處理存成圖檔的動作, 可以利用 Selenium 的網頁快照 (save_screenshot) 功能, 參考下面這個影片 :





關於 Selenium 用法參考 : 


延續上面澄清湖的地圖物件 map, 首先要將網頁地圖物件輸出存成 htm 檔案, 參考上面檢視 Map 物件的成員, 可知它有一個 save() 方法, 呼叫此方法並傳入檔名 (含路徑) 字串, 例如 "map.htm" 或 "D:\\test\\python\\map.htm" 即可將此地圖存成網頁 :

>>> map   
<folium.folium.Map object at 0x0000020DCA803780>
>>> map.save('map.htm')       

這時在目前目錄底下就會出現一個檔案 map.htm, 用瀏覽器開啟如下 :




檢視原始碼可知此網頁地圖使用了 Leaflet, jQuery, 以及 Bootstrapt 等函式庫 : 




接下來要從 sellenium 匯入 webdriver, 呼叫 Chrome() 建立瀏覽器物件, 呼叫其 get() 方法開啟上面所輸出的網頁後再呼叫 save_screenshot() 擷取網頁快照儲存為 PNG 圖檔 :

>>> from selenium import webdriver      
>>> browser=webdriver.Chrome()    
>>> browser.get('d:\\test\python\\map.htm')   
>>> browser.save_screenshot("map.png")       
True

傳回 True 表示存檔成功, 這時工作目錄下就會出現 map.png 檔了 :



 
參考 : 

沒有留言 :