captcha

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

CAPTCHA Solver Skill

验证码识别技能

Solve CAPTCHAs programmatically in web automation workflows.
以编程方式在Web自动化工作流中解决验证码问题。

How Token-Based CAPTCHA Solving Works

基于令牌的验证码识别原理

Modern CAPTCHAs (reCAPTCHA, hCaptcha, Turnstile) don't require reading distorted text — they issue a signed token when solved. A third-party service solves the challenge on your behalf and returns that token. You inject it into the page, and the page thinks the user solved it.
Page with CAPTCHA
    ├─ 1. Extract sitekey + page URL
    ├─ 2. Submit to solving service (waits 15–45s)
    ├─ 3. Receive token string
    ├─ 4. Inject token into hidden field on page
    ├─ 5. Trigger the CAPTCHA callback
    └─ 6. Submit form / continue automation
Tokens expire in ~2 minutes — solve immediately before submitting.

现代验证码(reCAPTCHA、hCaptcha、Turnstile)无需识别扭曲文本—— 用户完成验证后会生成一个签名令牌。第三方服务会替你完成验证挑战并返回该令牌。你将令牌注入页面后,页面会认为是用户自行完成了验证。
带有验证码的页面
    ├─ 1. 提取sitekey和页面URL
    ├─ 2. 提交至识别服务(等待15–45秒)
    ├─ 3. 接收令牌字符串
    ├─ 4. 将令牌注入页面隐藏字段
    ├─ 5. 触发验证码回调函数
    └─ 6. 提交表单/继续自动化流程
令牌有效期约为2分钟——请在提交前立即完成识别。

Supported CAPTCHA Types

支持的验证码类型

Type
--type
flag
How to detect on page
reCAPTCHA v2 (checkbox)
recaptcha-v2
.g-recaptcha
div,
[data-sitekey]
attribute
reCAPTCHA v2 invisible
recaptcha-v2 --invisible
No visible widget;
grecaptcha.execute()
call
reCAPTCHA v3
recaptcha-v3
grecaptcha.execute(sitekey, {action:...})
in JS
hCaptcha
hcaptcha
.h-captcha
div or
[data-hcaptcha-sitekey]
Cloudflare Turnstile
turnstile
.cf-turnstile
div
Image/text CAPTCHA
image
<img>
with "captcha" in src or alt
Read
references/captcha-types.md
for per-type detection JS snippets and injection patterns.

类型
--type
参数
页面检测方法
reCAPTCHA v2(复选框)
recaptcha-v2
存在
.g-recaptcha
元素或带有
[data-sitekey]
属性的元素
reCAPTCHA v2 隐形版
recaptcha-v2 --invisible
无可见组件;页面中存在
grecaptcha.execute()
调用
reCAPTCHA v3
recaptcha-v3
JS代码中存在
grecaptcha.execute(sitekey, {action:...})
hCaptcha
hcaptcha
存在
.h-captcha
元素或带有
[data-hcaptcha-sitekey]
属性的元素
Cloudflare Turnstile
turnstile
存在
.cf-turnstile
元素
图片/文本验证码
image
存在src或alt中包含"captcha"的
<img>
元素
查看
references/captcha-types.md
获取各类型的检测JS代码片段和注入模式。

Solving Services

识别服务

You need an API key from a solving service. All three below use the same API format, so the solver script works with any of them:
ServiceFlagSpeedCost/1K
2captcha (recommended)
--service 2captcha
20–40s~$1.00
CapMonster Cloud
--service capmonster
10–30s~$0.60
Anti-Captcha
--service anticaptcha
15–35s~$0.70
Set credentials via environment variables (preferred):
bash
export CAPTCHA_API_KEY=your_key_here
export CAPTCHA_SERVICE=2captcha
Read
references/services.md
for signup links, free trial details, and API compatibility.

你需要从识别服务获取API密钥。以下三款服务使用相同的API格式, 因此识别脚本可兼容任意一款:
服务参数速度每千次成本
2captcha(推荐)
--service 2captcha
20–40秒~1.00美元
CapMonster Cloud
--service capmonster
10–30秒~0.60美元
Anti-Captcha
--service anticaptcha
15–35秒~0.70美元
通过环境变量设置凭据(推荐方式):
bash
export CAPTCHA_API_KEY=your_key_here
export CAPTCHA_SERVICE=2captcha
查看
references/services.md
获取注册链接、免费试用详情和API兼容性说明。

Setup

安装配置

Copy the solver script to your project before using it:
bash
undefined
在使用前将识别脚本复制到你的项目中:
bash
undefined

From your project directory — adjust path to match your skill install location

从你的项目目录执行——根据技能安装位置调整路径

cp ~/.claude/plugins/captcha/scripts/solve_captcha.py ./solve_captcha.py

The script uses only Python stdlib — no `pip install` needed.
cp ~/.claude/plugins/captcha/scripts/solve_captcha.py ./solve_captcha.py

该脚本仅使用Python标准库——无需执行`pip install`。

Quick Start: reCAPTCHA v2

快速入门:reCAPTCHA v2

Using the bundled script (recommended)

使用内置脚本(推荐)

bash
python solve_captcha.py \
  --type recaptcha-v2 \
  --sitekey 6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ- \
  --pageurl https://www.google.com/recaptcha/api2/demo \
  --api-key $CAPTCHA_API_KEY
bash
python solve_captcha.py \
  --type recaptcha-v2 \
  --sitekey 6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ- \
  --pageurl https://www.google.com/recaptcha/api2/demo \
  --api-key $CAPTCHA_API_KEY

→ {"success": true, "token": "03AGdBq25...", "type": "recaptcha-v2"}

→ {"success": true, "token": "03AGdBq25...", "type": "recaptcha-v2"}

undefined
undefined

Full Playwright workflow (Python)

完整Playwright工作流(Python)

Key insight: start solving before you open the browser. The service takes 15–40s to return a token. If you kick off the solve call first, that wait happens in the background while you launch the browser and navigate — free parallelism.
python
import json, os, subprocess, sys
from playwright.sync_api import sync_playwright

SOLVER = "solve_captcha.py"   # must be in same directory or on PATH

def solve(captcha_type, sitekey, pageurl, service=None):
    cmd = [sys.executable, SOLVER,
           "--type", captcha_type,
           "--sitekey", sitekey,
           "--pageurl", pageurl]
    if service:
        cmd += ["--service", service]
    result = subprocess.run(cmd, capture_output=True, text=True, env=os.environ)
    data = json.loads(result.stdout)
    if not data["success"]:
        raise RuntimeError(data["error"])
    return data["token"]

PAGE_URL = "https://www.google.com/recaptcha/api2/demo"
SITEKEY  = "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-"
核心技巧:在打开浏览器前启动识别流程。服务返回令牌需要15–40秒。如果先启动识别请求,等待时间会在浏览器启动和页面导航的过程中后台完成——实现免费并行处理。
python
import json, os, subprocess, sys
from playwright.sync_api import sync_playwright

SOLVER = "solve_captcha.py"   # 需位于同一目录或系统PATH中

def solve(captcha_type, sitekey, pageurl, service=None):
    cmd = [sys.executable, SOLVER,
           "--type", captcha_type,
           "--sitekey", sitekey,
           "--pageurl", pageurl]
    if service:
        cmd += ["--service", service]
    result = subprocess.run(cmd, capture_output=True, text=True, env=os.environ)
    data = json.loads(result.stdout)
    if not data["success"]:
        raise RuntimeError(data["error"])
    return data["token"]

PAGE_URL = "https://www.google.com/recaptcha/api2/demo"
SITEKEY  = "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-"

Step 1: start solving (this takes 15–40s) — do it BEFORE opening the browser

步骤1:启动识别流程(耗时15–40秒)——务必在打开浏览器前执行

token = solve("recaptcha-v2", SITEKEY, PAGE_URL)
token = solve("recaptcha-v2", SITEKEY, PAGE_URL)

Step 2: now launch and navigate — solve is already done or finishing

步骤2:启动浏览器并导航——此时识别流程已完成或接近完成

with sync_playwright() as p: browser = p.chromium.launch(headless=False) page = browser.new_page() page.goto(PAGE_URL)
# Step 3: inject token — use json.dumps() for safe JS interpolation
import json as _json
token_js = _json.dumps(token)
page.evaluate(f"""
    const token = {token_js};
    const el = document.querySelector('[name="g-recaptcha-response"]');
    if (el) {{ el.value = token; }}
    if (window.___grecaptcha_cfg) {{
        Object.values(window.___grecaptcha_cfg.clients || {{}}).forEach(c => {{
            Object.keys(c).forEach(k => {{
                if (c[k]?.callback) c[k].callback(token);
            }});
        }});
    }}
""")

page.click('#recaptcha-demo-submit')
browser.close()

> **Important:** Always use `json.dumps(token)` when embedding a token in JS — never bare f-string interpolation like `'{token}'`. Token strings are base64url-safe today but the pattern is fragile.

---
with sync_playwright() as p: browser = p.chromium.launch(headless=False) page = browser.new_page() page.goto(PAGE_URL)
# 步骤3:注入令牌——使用json.dumps()确保JS插值安全
import json as _json
token_js = _json.dumps(token)
page.evaluate(f"""
    const token = {token_js};
    const el = document.querySelector('[name="g-recaptcha-response"]');
    if (el) {{ el.value = token; }}
    if (window.___grecaptcha_cfg) {{
        Object.values(window.___grecaptcha_cfg.clients || {{}}).forEach(c => {{
            Object.keys(c).forEach(k => {{
                if (c[k]?.callback) c[k].callback(token);
            }});
        }});
    }}
""")

page.click('#recaptcha-demo-submit')
browser.close()

> **重要提示:** 将令牌嵌入JS时务必使用`json.dumps(token)`——绝不要使用`'{token}'`这类裸字符串插值。当前令牌字符串是base64url安全的,但这种模式非常脆弱。

---

Step-by-Step Workflow

分步工作流

Step 1 — Detect CAPTCHA type and extract sitekey

步骤1 — 检测验证码类型并提取sitekey

Run this JS in the browser (via
page.evaluate
or
browser_evaluate
):
javascript
(() => {
  const q = s => document.querySelector(s);
  if (q('.g-recaptcha, [data-sitekey]:not(.h-captcha):not(.cf-turnstile)'))
    return { type: 'recaptcha-v2', sitekey: q('[data-sitekey]')?.dataset?.sitekey };
  if (q('.h-captcha, [data-hcaptcha-sitekey]'))
    return { type: 'hcaptcha', sitekey: q('[data-sitekey]')?.dataset?.sitekey };
  if (q('.cf-turnstile'))
    return { type: 'turnstile', sitekey: q('.cf-turnstile')?.dataset?.sitekey };
  if (q('img[src*="captcha" i], img[alt*="captcha" i]'))
    return { type: 'image' };
  return { type: null };
})();
For reCAPTCHA v3 (no visible widget), look in the page source for:
grecaptcha.execute('SITEKEY', {action: 'ACTION_NAME'})
在浏览器中运行以下JS代码(通过
page.evaluate
browser_evaluate
):
javascript
(() => {
  const q = s => document.querySelector(s);
  if (q('.g-recaptcha, [data-sitekey]:not(.h-captcha):not(.cf-turnstile)'))
    return { type: 'recaptcha-v2', sitekey: q('[data-sitekey]')?.dataset?.sitekey };
  if (q('.h-captcha, [data-hcaptcha-sitekey]'))
    return { type: 'hcaptcha', sitekey: q('[data-sitekey]')?.dataset?.sitekey };
  if (q('.cf-turnstile'))
    return { type: 'turnstile', sitekey: q('.cf-turnstile')?.dataset?.sitekey };
  if (q('img[src*="captcha" i], img[alt*="captcha" i]'))
    return { type: 'image' };
  return { type: null };
})();
对于reCAPTCHA v3(无可见组件),请在页面源码中查找:
grecaptcha.execute('SITEKEY', {action: 'ACTION_NAME'})

Step 2 — Run the solver script

步骤2 — 运行识别脚本

Always use
solve_captcha.py
for the API call — don't write your own polling loop. The script handles initial wait, retries, error codes, and service switching.
bash
undefined
请始终使用
solve_captcha.py
发起API请求——不要自行编写轮询循环。该脚本会处理初始等待、重试、错误码和服务切换。
bash
undefined

reCAPTCHA v2

reCAPTCHA v2

python solve_captcha.py --type recaptcha-v2 --sitekey SITEKEY --pageurl URL
python solve_captcha.py --type recaptcha-v2 --sitekey SITEKEY --pageurl URL

reCAPTCHA v3

reCAPTCHA v3

python solve_captcha.py --type recaptcha-v3 --sitekey SITEKEY --pageurl URL --action verify
python solve_captcha.py --type recaptcha-v3 --sitekey SITEKEY --pageurl URL --action verify

hCaptcha

hCaptcha

python solve_captcha.py --type hcaptcha --sitekey SITEKEY --pageurl URL
python solve_captcha.py --type hcaptcha --sitekey SITEKEY --pageurl URL

Cloudflare Turnstile

Cloudflare Turnstile

python solve_captcha.py --type turnstile --sitekey SITEKEY --pageurl URL
python solve_captcha.py --type turnstile --sitekey SITEKEY --pageurl URL

Image CAPTCHA (send screenshot of the captcha image)

图片验证码(发送验证码截图)

python solve_captcha.py --type image --image captcha_screenshot.png
undefined
python solve_captcha.py --type image --image captcha_screenshot.png
undefined

Step 3 — Inject the token

步骤3 — 注入令牌

Each CAPTCHA type has a different injection pattern. Read
references/captcha-types.md
for the exact JS for each type.
reCAPTCHA v2 (most common):
javascript
// 1. Set the hidden response field
document.querySelector('[name="g-recaptcha-response"]').value = TOKEN;

// 2. Trigger the registered callback
if (window.___grecaptcha_cfg) {
  Object.values(window.___grecaptcha_cfg.clients || {}).forEach(client => {
    Object.keys(client).forEach(key => {
      if (client[key]?.callback) client[key].callback(TOKEN);
    });
  });
}

不同类型的验证码有不同的注入模式。查看
references/captcha-types.md
获取各类型的精确JS代码。
reCAPTCHA v2(最常见):
javascript
// 1. 设置隐藏的响应字段
document.querySelector('[name="g-recaptcha-response"]').value = TOKEN;

// 2. 触发注册的回调函数
if (window.___grecaptcha_cfg) {
  Object.values(window.___grecaptcha_cfg.clients || {}).forEach(client => {
    Object.keys(client).forEach(key => {
      if (client[key]?.callback) client[key].callback(TOKEN);
    });
  });
}

Image Grid Challenge (Vision AI)

图片网格挑战(视觉AI)

Sometimes after clicking the reCAPTCHA v2 checkbox a grid appears: "Select all images with traffic lights". This is a separate visual challenge — token solving won't help here. The bundled
solve_image_grid.py
handles it by screenshotting the grid and asking a vision AI (Claude or GPT-4V) which cells to click.
Requirements: pip install anthropic   (or openai as fallback)
Environment:  ANTHROPIC_API_KEY  or  OPENAI_API_KEY
有时点击reCAPTCHA v2复选框后会出现网格:"选择所有包含交通信号灯的图片"。这是一个独立的视觉挑战——令牌识别无法解决此问题。内置的
solve_image_grid.py
通过截取网格截图并请求视觉AI(Claude或GPT-4V)识别应点击的单元格来处理此类挑战。
依赖:pip install anthropic  (或使用openai作为备选)
环境变量:ANTHROPIC_API_KEY 或 OPENAI_API_KEY

How it works

工作原理

Checkbox clicked
    ├─ Grid appeared? → screenshot grid cells → ask vision AI → click matching cells
    │                                                          → click Verify
    │                                                          → new round? → repeat
    └─ No grid (trusted IP) → returns True immediately
点击复选框
    ├─ 出现网格? → 截取网格单元格截图 → 请求视觉AI → 点击匹配的单元格
    │                                                          → 点击验证
    │                                                          → 出现新的一轮? → 重复操作
    └─ 无网格(可信IP) → 立即返回True

Integration with your script

与脚本集成

python
from playwright.sync_api import sync_playwright
from solve_image_grid import solve_image_grid  # copy from scripts/

with sync_playwright() as p:
    page = p.chromium.launch(headless=False).new_page()
    page.goto("https://www.google.com/recaptcha/api2/demo")

    # Step 1: click the checkbox (inside its iframe)
    page.frame_locator('iframe[title="reCAPTCHA"]') \
        .locator('.recaptcha-checkbox-border').click()

    # Step 2: handle grid if it appears (returns immediately if no grid)
    solved = solve_image_grid(page)

    if solved:
        page.click('#recaptcha-demo-submit')
python
from playwright.sync_api import sync_playwright
from solve_image_grid import solve_image_grid  # 从scripts目录复制

with sync_playwright() as p:
    page = p.chromium.launch(headless=False).new_page()
    page.goto("https://www.google.com/recaptcha/api2/demo")

    # 步骤1:点击复选框(在其iframe内)
    page.frame_locator('iframe[title="reCAPTCHA"]') \
        .locator('.recaptcha-checkbox-border').click()

    # 步骤2:如果出现网格则处理(无网格时立即返回)
    solved = solve_image_grid(page)

    if solved:
        page.click('#recaptcha-demo-submit')

Combined flow (grid + token fallback)

组合流程(网格+令牌备选)

Some pages require the grid challenge AND a token. Handle both:
python
import json, os, subprocess, sys
from solve_image_grid import solve_image_grid

SOLVER = "solve_captcha.py"
SITEKEY = "your-sitekey"
部分页面同时需要网格挑战和令牌。需同时处理两者:
python
import json, os, subprocess, sys
from solve_image_grid import solve_image_grid

SOLVER = "solve_captcha.py"
SITEKEY = "your-sitekey"

1. Click checkbox — may or may not trigger a grid

1. 点击复选框——可能触发网格挑战

page.frame_locator('iframe[title="reCAPTCHA"]')
.locator('.recaptcha-checkbox-border').click()
page.frame_locator('iframe[title="reCAPTCHA"]')
.locator('.recaptcha-checkbox-border').click()

2. Solve grid if it appeared

2. 如果出现网格则解决

grid_ok = solve_image_grid(page)
grid_ok = solve_image_grid(page)

3. If the form still needs a token (check for g-recaptcha-response being empty)

3. 如果表单仍需要令牌(检查g-recaptcha-response是否为空)

token_empty = page.evaluate( "document.querySelector('[name="g-recaptcha-response"]')?.value === ''" ) if grid_ok and token_empty: result = subprocess.run( [sys.executable, SOLVER, "--type", "recaptcha-v2", "--sitekey", SITEKEY, "--pageurl", page.url], capture_output=True, text=True, env=os.environ ) token = json.loads(result.stdout)["token"] token_js = json.dumps(token) page.evaluate(f""" const t = {token_js}; document.querySelector('[name="g-recaptcha-response"]').value = t; if (window.___grecaptcha_cfg) Object.values(window.___grecaptcha_cfg.clients||{{}}).forEach(c=> Object.keys(c).forEach(k=>{{ if(c[k]?.callback) c[k].callback(t) }}) ); """)
undefined
token_empty = page.evaluate( "document.querySelector('[name="g-recaptcha-response"]')?.value === ''" ) if grid_ok and token_empty: result = subprocess.run( [sys.executable, SOLVER, "--type", "recaptcha-v2", "--sitekey", SITEKEY, "--pageurl", page.url], capture_output=True, text=True, env=os.environ ) token = json.loads(result.stdout)["token"] token_js = json.dumps(token) page.evaluate(f""" const t = {token_js}; document.querySelector('[name="g-recaptcha-response"]').value = t; if (window.___grecaptcha_cfg) Object.values(window.___grecaptcha_cfg.clients||{{}}).forEach(c=> Object.keys(c).forEach(k=>{{ if(c[k]?.callback) c[k].callback(t) }}) ); """)
undefined

Notes

注意事项

  • Element clicks vs pixel coordinates — the script clicks grid cells via
    frame_locator().locator('td').nth(i)
    , not raw pixel coordinates. This is more reliable across different viewport sizes and DPI settings.
  • Multi-round challenges — Google often shows 2–4 rounds. The script loops automatically up to
    max_rounds=6
    .
  • "None of the above" rounds — if the AI returns
    []
    (no matches), the script clicks Verify anyway. Google sometimes accepts this.
  • Accuracy — Claude Opus performs well on clear images (~80–90% first-round accuracy). Blurry or ambiguous grids may need multiple rounds.

  • 元素点击vs像素坐标——脚本通过
    frame_locator().locator('td').nth(i)
    点击网格单元格,而非原始像素坐标。这种方式在不同视口大小和DPI设置下更可靠。
  • 多轮挑战——Google通常会显示2–4轮挑战。脚本会自动循环最多
    max_rounds=6
    次。
  • "无匹配项"轮次——如果AI返回
    []
    (无匹配项),脚本仍会点击验证按钮。Google有时会接受此操作。
  • 准确率——Claude Opus在清晰图片上表现良好(首轮准确率约80–90%)。模糊或歧义网格可能需要多轮尝试。

Using Playwright MCP Tools

使用Playwright MCP工具

If Playwright MCP tools are available (
mcp__plugin_playwright_playwright__*
):
1. browser_navigate → go to the page
2. browser_evaluate → run detection JS to find CAPTCHA type and sitekey
3. Bash → python scripts/solve_captcha.py ... (takes 15-40s)
4. browser_evaluate → inject token + trigger callback
5. browser_click → submit button
6. browser_snapshot → verify success

如果Playwright MCP工具可用(
mcp__plugin_playwright_playwright__*
):
1. browser_navigate → 访问目标页面
2. browser_evaluate → 运行检测JS以确定验证码类型和sitekey
3. Bash → python scripts/solve_captcha.py ...(耗时15-40秒)
4. browser_evaluate → 注入令牌并触发回调函数
5. browser_click → 提交按钮
6. browser_snapshot → 验证操作成功

Troubleshooting

故障排查

ProblemCauseFix
Token injection has no effectResponse field is hidden/disabledMake it visible:
el.style.display='block'; el.removeAttribute('aria-hidden')
CAPTCHA reappears after submitToken expired (>2 min)Solve and inject immediately before submitting
ERROR_ZERO_BALANCE
No credits on serviceTop up account at service dashboard
CAPCHA_NOT_READY
timeout
Solver backed up or reCAPTCHA v3 hardRetry or switch service; for v3 try increasing
--min-score
Callback not triggeredSite uses custom callback nameSearch page source for
.ready(function
or
grecaptcha.render
to find callback
CAPTCHA passes but site still blocksBrowser fingerprint flagged as botRead
references/stealth.md
2captcha returns wrong text (image)Low quality imageCrop to just the CAPTCHA, increase contrast before sending

问题原因解决方法
令牌注入无效果响应字段被隐藏/禁用使其可见:
el.style.display='block'; el.removeAttribute('aria-hidden')
提交后验证码重新出现令牌已过期(超过2分钟)在提交前立即识别并注入令牌
ERROR_ZERO_BALANCE
服务账户无余额在服务控制台充值
CAPCHA_NOT_READY
超时
识别服务繁忙或reCAPTCHA v3难度较高重试或切换服务;对于v3尝试提高
--min-score
参数
回调函数未触发网站使用自定义回调名称在页面源码中搜索
.ready(function
grecaptcha.render
以找到回调函数
验证码通过但网站仍拦截浏览器指纹被标记为机器人查看
references/stealth.md
2captcha返回错误文本(图片验证码)图片质量低仅截取验证码区域,提高对比度后再发送

Reference Files

参考文件

  • scripts/solve_captcha.py
    — CLI solver for token-based CAPTCHAs (stdlib only, no pip install)
  • scripts/solve_image_grid.py
    — Vision AI solver for reCAPTCHA v2 image grid challenges
  • references/captcha-types.md
    — Detection + injection JS for each CAPTCHA type
  • references/services.md
    — Service comparison, signup, pricing, API keys
  • references/stealth.md
    — Avoiding bot detection after bypassing CAPTCHA
  • scripts/solve_captcha.py
    — 基于令牌的验证码CLI识别工具(仅依赖标准库,无需pip安装)
  • scripts/solve_image_grid.py
    — 用于reCAPTCHA v2图片网格挑战的视觉AI识别工具
  • references/captcha-types.md
    — 各验证码类型的检测和注入JS代码
  • references/services.md
    — 服务对比、注册、定价和API密钥说明
  • references/stealth.md
    — 绕过验证码后避免机器人检测的方法