WebXR 手部追踪输出的并不是简单的坐标点集合。它会以 25 个手部关节为骨架,持续给出每个关节相对本地参考空间的位置与朝向。用户在执行抓取、点击、旋转这类任务时,手指张开的幅度、食指到达目标点的轨迹、任务之间的停顿节奏都存在个体差异。把这些差异量化后,就能形成一种基于交互剖面的浏览器指纹。对 R 语言爬虫来说,难点在于 WebXR 数据不在 HTML 文档里,也不在普通接口响应里,必须驱动真实浏览器会话,在每一帧动画回调中把关节数据截获下来。

一、WebXR 手部数据从何而来,R 语言如何接入采集链路
WebXR Device API 的 Hand Input 模块会在用户授权后暴露 XRHand 对象。一个完整的手部骨架包含手腕、掌心、五个手指的指节和指尖,共 25 个关节。每个关节的 jointName 是稳定标识,例如 wrist、index-finger-tip、middle-finger-proximal。每一帧通过 frame.getJointPose(joint, referenceSpace) 可以拿到该关节在本地坐标系中的位置和旋转四元数。这些数据不是 DOM 中的静态内容,也不是某个 REST 接口返回的文本,只有在浏览器运行 WebXR 会话时才会产生。
因此基于 httr 或 rvest 的普通请求抓取在这里完全无效。R 语言需要控制一个带调试端口的 Chromium 内核浏览器,常见方案是 chromote 或 RSelenium。chromote 通过 Chrome DevTools Protocol 建立会话,可以直接执行 JavaScript,适合抓取长会话中的实时帧数据。启动时要给 Chrome 加上 WebXR 实验特性参数,并让页面运行在安全上下文里。下面这段代码演示了从启动浏览器到注入脚本采集手部关节的核心过程。
library(chromote)
b <- Chrome$new(
args = c(
"--enable-features=WebXRHandInput",
"--use-fake-ui-for-media-stream"
)
)
page <- b$new_session()
page$Page$navigate("https://ipipp.com/xr-hand-test")
page$Page$loadEventFired()
page$Runtime$evaluate("
window.__handJoints = [];
navigator.xr.requestSession('immersive-vr', { optionalFeatures: ['hand-tracking'] }).then(session => {
session.requestReferenceSpace('local').then(refSpace => {
session.requestAnimationFrame(function onFrame(time, frame) {
if (frame.session && frame.session.inputSources) {
for (const source of frame.session.inputSources) {
if (source.hand) {
const joints = [];
for (const joint of source.hand.values()) {
const pose = frame.getJointPose(joint, refSpace);
joints.push({
name: joint.jointName,
x: pose.transform.position.x,
y: pose.transform.position.y,
z: pose.transform.position.z
});
}
window.__handJoints.push({ t: time, hand: source.handedness, joints: joints });
}
}
}
frame.session.requestAnimationFrame(onFrame);
});
});
});
")
这段脚本会把每一帧的手部关节位置推入 window.__handJoints 数组。采集结束后,R 侧只需要把这个数组序列化为 JSON 并写回本地,就得到了后续特征工程可用的原始帧序列。需要注意的是,navigator.xr.requestSession 返回的是一个 Promise,真正的数据读取必须放在 requestAnimationFrame 回调里,否则拿不到稳定的关节姿态。
二、手部交互剖面指纹的特征构造方法
原始关节坐标不能直接当指纹使用。不同设备的参考空间原点不同,用户和设备的相对位置也会变化,直接比较 x、y、z 会产生大量噪声。交互剖面指纹的核心思路是提取与设备尺度无关、与用户手部解剖结构和运动习惯相关的特征。第一类特征是骨骼比例,例如食指指尖到手腕的距离与无名指指尖到手腕的距离之比。这个比值在不同人手上差异明显,而且对远近缩放不敏感。
第二类特征是动态特征。手部交互不是静止姿态,而是一段连续动作。食指指尖在从起点移动到目标点的过程中,会产生速度曲线、加速度变化和短暂停顿。速度曲线的标准差、总能量、低速帧占比能够刻画用户动作是干脆还是迟疑。第三类特征是节奏特征,例如完成一次点击任务时,手指张开的持续时间、停顿间隔和来回切换频率。把这三类特征拼接起来,就构成一个可比较的交互剖面向量。
下面的 R 代码从之前保存的 hand_joints.json 中读取帧数据,计算指尖到手腕的距离比例,并提取食指指尖的速度统计。
library(jsonlite)
frames <- fromJSON("hand_joints.json", simplifyVector = FALSE)
joint_coords <- function(frame, joint_name) {
hit <- Filter(function(j) j$name == joint_name, frame$joints)
if (length(hit) == 0) return(NULL)
unlist(hit[[1]][c("x", "y", "z")])
}
wrist_dist <- function(frame, joint_name) {
w <- joint_coords(frame, "wrist")
p <- joint_coords(frame, joint_name)
if (is.null(w) || is.null(p)) return(NA_real_)
sqrt(sum((p - w)^2))
}
tip_names <- c("index-finger-tip", "middle-finger-tip", "ring-finger-tip", "pinky-finger-tip")
dist_mat <- t(sapply(frames, function(f) {
sapply(tip_names, function(tip) wrist_dist(f, tip))
}))
index_tip <- do.call(rbind, lapply(frames, function(f) {
idx <- which(sapply(f$joints, function(j) j$name == "index-finger-tip"))
unlist(f$joints[[idx]][c("x", "y", "z")])
}))
vel <- sqrt(rowSums(diff(index_tip)^2))
features <- c(
index_ring_ratio = mean(dist_mat[, 1], na.rm = TRUE) / mean(dist_mat[, 3], na.rm = TRUE),
middle_pinky_ratio = mean(dist_mat[, 2], na.rm = TRUE) / mean(dist_mat[, 4], na.rm = TRUE),
speed_sd = sd(vel),
speed_energy = sum(vel^2),
pause_ratio = mean(vel < 0.005)
)
print(features)
这里用 index_ring_ratio 和 middle_pinky_ratio 表示静态骨骼比例,用 speed_sd 表示速度波动,用 pause_ratio 表示动作中的停顿比例。实际应用中这些特征还会经过标准化,并增加角度、角速度、轨迹曲率等维度。判断一个指纹是否有效,通常看两个指标:同一用户多次采集的特征变异系数是否足够小,不同用户之间的特征距离是否足够大。对 300 到 500 帧的连续任务数据,骨骼比例类特征通常非常稳定,动态特征次之,节奏特征受任务影响较大。
三、R 语言爬虫落地时的关键细节与稳定性处理
第一类问题是 WebXR 会话本身的不稳定性。浏览器可能因为没有 VR 设备、权限被拒绝或实验特性未开启而直接抛错。解决思路是在启动 Chrome 时明确传入 WebXR 相关开关,并在无实体设备环境下使用模拟接口。如果只是采集手部交互特征用于研究,可以在本地安装 WebXR emulator 扩展,或者使用支持模拟手部关节的测试页面。R 侧需要捕获 Runtime$evaluate 的异常,避免某个页面没有定义 window.__handJoints 时脚本中断。
第二类问题是帧数据质量。WebXR 的关节检测偶尔会丢失某一帧,或者返回 NaN 坐标。如果直接把这些值送入特征计算,平均值和标准差都会被污染。落盘前应当过滤掉包含非有限值的帧;如果丢失帧很少,可以做线性插值。下面这段代码展示了等待帧数达到阈值、序列化落盘和基础清洗的流程。
wait_for_frames <- function(page, min_frames = 300, timeout = 30) {
started <- Sys.time()
repeat {
res <- page$Runtime$evaluate("window.__handJoints ? window.__handJoints.length : 0")
n <- res$result$value
if (!is.null(n) && n >= min_frames) break
if (as.numeric(Sys.time() - started) > timeout) stop("Timeout waiting for hand frames")
Sys.sleep(0.5)
}
raw <- page$Runtime$evaluate("JSON.stringify(window.__handJoints)")
writeLines(raw$result$value, "hand_joints.json")
n
}
frame_count <- wait_for_frames(page, min_frames = 300, timeout = 30)
print(frame_count)
clean_frames <- function(path) {
frames <- fromJSON(path, simplifyVector = FALSE)
frames <- frames[!sapply(frames, function(f) any(sapply(f$joints, function(j) any(!is.finite(unlist(j[c("x","y","z")]))))))]
frames
}
经过清洗的数据仍然不等长,因为不同用户的完成时间不同。对于分类模型,可以把时序窗口切分成固定长度的小段,或者对整段动作提取统计量,使每个样本最终变成一个固定长度的特征向量。在 R 中可以使用 zoo 进行缺失值处理,用 pracma 计算快速傅里叶变换,得到速度频谱的主峰位置和能量集中度。
四、应用价值与限制
交互剖面指纹的价值在于它属于行为生物特征,不像 Canvas 指纹、WebGL 指纹那样容易被通用脚本批量伪造。手部交互数据与用户完成任务的方式绑定,即使更换设备或浏览器,只要任务模式一致,骨骼比例和动态节奏仍有较高区分度。对 R 语言使用者来说,这类数据可以用作反欺诈研究、无障碍交互分析和人机行为建模的训练样本。
不过它的限制也非常明显。WebXR 的覆盖率远低于 Canvas 或 AudioContext,大量用户根本没有支持 WebXR 的设备,实际可采集到的样本比例偏低。手部追踪依赖摄像头或手柄,光线、遮挡、用户姿势都会影响数据质量。另一个问题在于合规性。交互指纹属于生物特征相关信息,采集前必须获得明确授权,并且不能用于未经告知的用户追踪。技术上,这类方法更适合实验环境和受控的认证场景,不适合作为常规爬虫的通用反检测手段。
从趋势看,行为生物特征正在从鼠标轨迹、键盘节奏扩展到更丰富的交互通道。R 语言虽然不擅长直接驱动浏览器,但借助 CDP 和成熟的数据处理生态,完全能够完成从 WebXR 会话采集到指纹特征提取的完整闭环。关键是要理解数据产生的层级:DOM 是表层,网络请求是中层,而 WebXR 这类实时交互数据属于运行时深层,只有嵌入真实浏览器执行环境,才能抓取到真正有效的信息。