playwright-拖拽验证码 一、有如下一个拖拽验证码demo实现自动拖拽html代码如下!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title滑动验证码/title style * { margin: 0; padding: 0; box-sizing: border-box; } body { display: flex; justify-content: center; align-items: center; min-height: 100vh; background: #f0f2f5; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif; } .captcha-container { background: #fff; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.1); padding: 24px; width: 360px; } .captcha-title { font-size: 14px; color: #333; margin-bottom: 16px; font-weight: 500; } .image-wrapper { position: relative; width: 100%; height: 200px; background: #e8e8e8; border-radius: 4px; overflow: hidden; margin-bottom: 16px; user-select: none; } .image-wrapper img { width: 100%; height: 100%; object-fit: cover; display: block; } /* 缺口 */ .gap { position: absolute; width: 50px; height: 50px; border-radius: 4px; pointer-events: none; box-shadow: 0 0 0 2px rgba(255,255,255,0.7), inset 0 0 0 2px rgba(255,255,255,0.7); } /* 滑块 */ .slider-piece { position: absolute; width: 50px; height: 50px; border-radius: 4px; top: 0; left: 0; cursor: grab; box-shadow: 0 0 0 2px #409eff, inset 0 0 0 2px #409eff; background: rgba(64,158,255,0.3); z-index: 10; transition: left .05s linear; } .slider-piece.dragging { cursor: grabbing; } /* 底部滑轨 */ .slider-track { position: relative; width: 100%; height: 40px; background: #e8e8e8; border-radius: 4px; margin-bottom: 12px; } .slider-track-bg { position: absolute; top: 0; left: 0; bottom: 0; width: 0; background: linear-gradient(90deg, #409eff, #79bbff); border-radius: 4px 0 0 4px; transition: width .05s linear; } .slider-track-bg.success { background: linear-gradient(90deg, #67c23a, #95d475); width: 100% !important; border-radius: 4px; transition: background .3s, border-radius .3s; } .slider-track-bg.fail { background: linear-gradient(90deg, #f56c6c, #f89898); width: 100% !important; border-radius: 4px; transition: background .3s, border-radius .3s; } .slider-btn { position: absolute; top: -4px; left: 0; width: 48px; height: 48px; background: #fff; border: 1px solid #d9d9d9; border-radius: 6px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); cursor: grab; display: flex; align-items: center; justify-content: center; z-index: 20; transition: box-shadow .2s; } .slider-btn:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.15); } .slider-btn:active { cursor: grabbing; } .slider-btn .arrow { display: inline-block; width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 8px solid #999; transition: border-top-color .2s; } .slider-btn.active .arrow { border-top-color: #409eff; } .slider-track.success .slider-btn { background: #67c23a; border-color: #67c23a; } .slider-track.success .slider-btn .arrow { border-top-color: #fff; } .slider-track.fail .slider-btn { background: #f56c6c; border-color: #f56c6c; } .slider-track.fail .slider-btn .arrow { border-top-color: #fff; } .hint { font-size: 12px; color: #999; text-align: center; min-height: 18px; line-height: 18px; transition: color .2s; } .hint.success { color: #67c23a; } .hint.fail { color: #f56c6c; } /style /head body div classcaptcha-container div classcaptcha-title安全验证/div div classimage-wrapper idimageWrapper div classgap idgap/div div classslider-piece idsliderPiece/div /div div classslider-track idsliderTrack div classslider-track-bg idsliderTrackBg/div div classslider-btn idsliderBtn span classarrow/span /div /div div classhint idhint请按住滑块拖拽到缺口处/div /div script (function () { const TRACK_WIDTH 310; // 滑轨可拖动像素 const GAP_SIZE 50; const TOLERANCE 5; // 允许误差像素 const wrapper document.getElementById(imageWrapper); const gap document.getElementById(gap); const sliderPiece document.getElementById(sliderPiece); const track document.getElementById(sliderTrack); const bg document.getElementById(sliderTrackBg); const btn document.getElementById(sliderBtn); const hint document.getElementById(hint); let gapX 0; // 缺口 left let isDragging false; let startX 0; let currentX 0; let verified false; /* ---- 工具 ---- */ function rand(min, max) { return Math.floor(Math.random() * (max - min 1)) min; } /* ---- 重置缺口与滑块位置 ---- */ function resetChallenge() { const wrapperW wrapper.clientWidth; gapX rand(GAP_SIZE, wrapperW - GAP_SIZE * 2); // 留出左右余量 gap.style.left gapX px; gap.style.top rand(0, wrapper.clientHeight - GAP_SIZE) px; // 滑块 piece 与缺口同 Y sliderPiece.style.top gap.style.top; sliderPiece.style.left 0px; // 复位 UI btn.style.left 0px; bg.style.width 0px; bg.className slider-track-bg; track.className slider-track; btn.className slider-btn; hint.textContent 请按住滑块拖拽到缺口处; hint.className hint; verified false; currentX 0; } /* ---- 验证 ---- */ function verify(offsetX) { const diff Math.abs(offsetX - gapX); const passed diff TOLERANCE; if (passed) { bg.className slider-track-bg success; track.className slider-track success; btn.className slider-btn; hint.textContent 验证通过; hint.className hint success; // 滑块 piece 同步到最终位置 sliderPiece.style.left gapX px; verified true; } else { bg.className slider-track-bg fail; track.className slider-track fail; btn.className slider-btn; hint.textContent 验证失败请重试; hint.className hint fail; // 弹回 setTimeout(resetChallenge, 800); } } /* ---- 拖动逻辑 ---- */ function onPointerDown(e) { if (verified) return; isDragging true; startX e.clientX - currentX; btn.classList.add(active); btn.setPointerCapture(e.pointerId); } function onPointerMove(e) { if (!isDragging) return; let offset e.clientX - startX; offset Math.max(0, Math.min(offset, TRACK_WIDTH)); currentX offset; btn.style.left offset px; bg.style.width offset px; sliderPiece.style.left offset px; } function onPointerUp(e) { if (!isDragging) return; isDragging false; btn.classList.remove(active); btn.releasePointerCapture(e.pointerId); if (!verified) verify(currentX); } /* ---- 事件绑定 ---- */ btn.addEventListener(pointerdown, onPointerDown); btn.addEventListener(pointermove, onPointerMove); btn.addEventListener(pointerup, onPointerUp); btn.addEventListener(pointercancel, onPointerUp); /* ---- 阻止页面选中/拖拽图片等默认行为 ---- */ wrapper.addEventListener(dragstart, e e.preventDefault()); /* ---- 初始化 ---- */ resetChallenge(); // 演示用点击图片随机重置 wrapper.addEventListener(click, () { if (verified) resetChallenge(); }); })(); /script /body /html二、playwright 拖拽测试类import com.microsoft.playwright.*; import com.microsoft.playwright.options.BoundingBox; import util.CaptchaSolver; import util.MouseTracker; public class TestCaptcha { public static void main(String[] args) { try (Playwright playwright Playwright.create()) { Browser browser playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(false)); Page page browser.newPage(); //打开测试页面 page.navigate(file:///E:/OPENCODE/captcha.html); CaptchaSolver.solveSlider(page); //停2秒 page.waitForTimeout(10000); browser.close(); } } }工具类CaptchaSolverpackage util; import com.microsoft.playwright.Page; import com.microsoft.playwright.options.BoundingBox; public class CaptchaSolver { public static void solveSlider(Page page) { String leftStr page.evaluate(document.getElementById(gap).style.left).toString(); double targetX Double.parseDouble(leftStr.replace(px, )); BoundingBox btnBox page.locator(#sliderBtn).boundingBox(); if (btnBox null) throw new RuntimeException(Slider button not found); double fromX btnBox.x btnBox.width / 2; double toX btnBox.x btnBox.width / 2 targetX; double y btnBox.y btnBox.height / 2; int steps (int) Math.max(Math.abs(toX - fromX) / 2, 10); steps Math.min(steps, 30); MouseTracker.drag(page, fromX, y, toX, y, steps); } }工具类MouseTrackerpackage util; import com.microsoft.playwright.Locator; import com.microsoft.playwright.Page; import com.microsoft.playwright.options.BoundingBox; public class MouseTracker { public static void inject(Page page) { page.evaluate(() { const dot document.createElement(div); dot.id __pw_mouse_tracker__; dot.style.cssText position:fixed;width:10px;height:10px; background:red;border-radius:50%;z-index:99999; pointer-events:none;transform:translate(-50%,-50%); document.body.appendChild(dot); }); } public static void moveTo(Page page, double x, double y) { page.evaluate((x, y) { const d document.getElementById(__pw_mouse_tracker__); if (d) { d.style.left x px; d.style.top y px; } }); page.mouse().move(x, y); } public static void drag(Page page, double fromX, double fromY, double toX, double toY, int steps) { moveTo(page, fromX, fromY); page.mouse().down(); for (int i 1; i steps; i) { double x fromX (toX - fromX) * i / steps; double y fromY (toY - fromY) * i / steps; moveTo(page, x, y); } page.mouse().up(); } public static void dragSlider(Page page, String selector, double targetPercent) { Locator slider page.locator(selector); BoundingBox box slider.boundingBox(); if (box null) throw new RuntimeException(Element not found or not visible: selector); double fromX box.x box.width * 0.5; double toX box.x box.width * targetPercent; double y box.y box.height / 2; drag(page, fromX, y, toX, y, 10); } public static void remove(Page page) { page.evaluate(() { document.getElementById(__pw_mouse_tracker__)?.remove(); }); } }三、执行效果如下