做网络安全分析或网站访问日志统计时,手里往往握着成千上万条IP地址。光看一串串数字很难发现规律,但把它们转换成经纬度并铺到地图上,访问源集中在哪些城市、哪些区域存在异常扫描行为,一目了然。R语言的leaflet包正是干这件事的利器,它把JavaScript世界广受欢迎的Leaflet地图库移植到了R环境里,配合leaflet.extras插件提供的热力图功能,不需要写一行JavaScript就能得到交互式的地理可视化效果。

准备工作:环境搭建与IP地理解析
开始之前需要安装几个包。leaflet是核心地图引擎,leaflet.extras提供热力图层支持,如果要用 tidyverse 系列工具做数据处理,dplyr 和 readr 也建议一并装上。安装命令很常规:
install.packages(c("leaflet", "leaflet.extras", "dplyr", "readr"))接下来准备IP数据。假设我们有一份访问日志,格式为CSV,包含两列:ip和count(该IP出现的次数)。也可以直接从数据库读取,只要最终能整理成数据框就行。IP地理位置解析有两种思路:一种是在线调用免费接口,例如 ip-api.com 的批量接口,一次最多支持100个IP;另一种是使用 MaxMind 提供的 GeoLite2-City 本地数据库,速度快且不依赖网络。对于几万条以上的日志,强烈建议走本地库方案,在线接口的限流机制会让批量任务变得非常缓慢。
下面演示用 httr 批量请求 ip-api 接口的小函数,适合几百到几千条IP的场景:
library(httr)
library(jsonlite)
library(dplyr)
lookup_ips <- function(ips) {
# ip-api 批量接口,每 100 个一组请求
batches <- split(ips, ceiling(seq_along(ips) / 100))
results <- lapply(batches, function(b) {
url <- "http://ip-api.com/batch"
resp <- POST(url, body = toJSON(unname(b)), encode = "raw",
add_headers(`Content-Type` = "application/json"))
fromJSON(content(resp, "text"))
})
bind_rows(results)
}返回的结果里包含 lat、lon 字段,这就是绘图需要的坐标。注意免费接口限频为每分钟45次请求,批量接口每次算一次,所以上千条数据也要分批等待,代码里可以加 Sys.sleep 做节流,避免被封禁。
数据清洗与坐标异常处理
拿到经纬度之后千万别急着画图,脏数据会毁掉整张热力图。常见的坑有几个:第一,解析失败的IP会返回 lat 为0、lon 为0,这些点会全部堆在几内亚湾的海面上,形成一团莫名的热斑;第二,某些IP会被定位到国家质心而不是具体城市,造成首都地区异常密集的假象;第三,IPv6地址如果没做好兼容处理,可能整批解析失败。
清洗逻辑可以写得简洁一些:
library(dplyr) clean_data <- raw_result %>% filter(lat != 0 | lon != 0) %>% # 去掉解析失败的点 filter(status == "success") %>% filter(!is.na(lat), !is.na(lon)) %>% distinct(lat, lon, .keep_all = TRUE) %>% # 去重,热力图不需要重复坐标 mutate(weight = log10(count + 1)) # 计数取对数,避免极端值主导
这里有个细节值得展开:热力图的权重处理。如果直接用原始 count 做强度,一个被访问十万次的机房IP会把其他所有点都压成冷色。对计数取对数是常见做法,能让数量级差异悬殊的数据在视觉上更均衡。如果业务上更关心绝对量,也可以保留原始值,只是要接受热力图呈现一超多弱的格局。
绘制热力图:核心代码与参数调优
清洗完就可以画图了。leaflet 的基本流程是先创建地图容器,设置初始视野,再叠加底图瓦片,最后用 addHeatmap 渲染热力层:
library(leaflet)
library(leaflet.extras)
leaflet(clean_data) %>%
addProviderTiles("CartoDB.DarkMatter") %>% # 深色底图,热力效果更醒目
setView(lng = 105, lat = 35, zoom = 4) %>% # 初始视野定位到中国区域
addHeatmap(
lng = ~lon,
lat = ~lat,
intensity = ~weight, # 热度强度,取自前面算好的对数权重
blur = 20, # 模糊半径,越大越柔和
max = 5, # 权重归一化的上限
radius = 12, # 单个热点的像素半径
cellSize = 10
)参数调优是热力图好不好看的关键。radius 控制每个点向外扩散的像素范围,值太小会呈现一颗颗孤立的斑点,值太大则糊成一片,通常在8到20之间反复尝试。blur 决定高斯模糊的程度,与 radius 配合使用,一般设为 radius 的1到2倍。max 是归一化基准,超过这个权重的点会以最热颜色渲染,把它设为数据的95分位数往往比设为最大值效果更好,能有效抑制离群点对整体配色的影响。
底图的选择也有讲究。CartoDB.DarkMatter 这类深色底图配暖色热力渐变视觉冲击力最强,而如果是给管理层汇报,换成 CartoDB.Positron 浅色底图会显得更商务。国内用户如果遇到瓦片加载缓慢,可以把底图换成高德或天地图的瓦片服务,加载速度会明显改善,只是要注意坐标系差异,leaflet 默认使用 WGS84,而高德使用 GCJ-02,直接叠加会有几百米的偏移,热力图场景下这个偏差基本可以忽略,但如果要精确定位到街道级就需要做坐标纠偏。
大规模数据与性能优化
当IP数量达到几十万甚至百万级时,直接把全部点丢给浏览器渲染会明显卡顿,因为热力图的计算是在客户端完成的。应对思路有两个方向。第一个是空间聚合:先按城市或经纬度网格把点聚合成组,每组只保留聚合后的中心和总权重,点数能压缩两三个数量级,视觉效果几乎无损。用R做网格聚合很简单:
aggregated <- clean_data %>%
mutate(grid_lat = round(lat * 2) / 2, # 0.5 度网格
grid_lon = round(lon * 2) / 2) %>%
group_by(grid_lat, grid_lon) %>%
summarise(total_weight = sum(weight), .groups = "drop")第二个方向是分层渲染,即低缩放级别只显示聚合热力,放大到一定级别后再加载细粒度数据。leaflet 本身支持图层分组控制,可以结合 leafletProxy 在 Shiny 应用里动态切换图层,实现从全局概览到城市级下钻的交互体验,这种方案在安全态势感知平台上非常常见。
最后提一个容易踩的坑:addHeatmap 返回的图层在地图缩放时不会自动重算热力分布,也就是说 radius 是屏幕像素级的,缩放后热斑覆盖的地理范围会跟着变化。如果希望热力范围保持固定的地理尺度,需要在缩放事件里重新渲染,或者干脆接受这种表现方式,因为大多数分析场景下这并不影响整体判断。掌握这些要点之后,一份IP访问日志从原始文本到一张像样的交互热力图,整个流程不超过五十行R代码,效率相当可观。