API Tutorials

构建 CaptchaAI 使用仪表板和监控

这个月账单突然涨了不少,却说不清是哪种验证码类型、哪个时段用量暴增——说明你还没给 CaptchaAI 接入用量监控。本指南用 Python 搭建一套轻量监控系统:记录成功率、响应耗时和余额消耗,自动生成日报和周报。

不需要 Prometheus 或 Grafana,一个 CSV 文件加几个 Python 类就够用。


先明确要监控哪些指标

先跟踪这几个指标:

七个核心指标

  • 解决次数:掌握真实用量
  • 成功率:及时发现识别质量下滑
  • 响应时间:定位变慢的环节
  • 消耗速度:提前控制预算
  • 错误分布:定位具体失败模式
  • 余额:避免任务中途因欠费中断
  • 方法分布:了解各验证码类型的使用占比

第一步:搭建指标采集器

MetricsCollector 只做两件事:把每次识别结果汇总进内存字典,同时逐条写入本地 CSV。加了一把线程锁,多线程并发调用时不会互相覆盖计数。

线程锁为什么必须加

不加锁,并发写会导致计数错乱、CSV 行错位。threading.Lock() 代价很小,换来数字可信。

import time
import csv
import datetime
import threading
from collections import defaultdict


class MetricsCollector:
    """Collect and store CaptchaAI solve metrics."""

    def __init__(self, log_file="captchaai_metrics.csv"):
        self.log_file = log_file
        self.lock = threading.Lock()
        self.session_stats = defaultdict(lambda: {
            "count": 0, "success": 0, "error": 0,
            "timeout": 0, "total_time": 0,
        })
        self._init_log()

    def _init_log(self):
        try:
            with open(self.log_file, "r"):
                pass
        except FileNotFoundError:
            with open(self.log_file, "w", newline="") as f:
                writer = csv.writer(f)
                writer.writerow([
                    "timestamp", "method", "duration_s",
                    "status", "error_code", "task_id",
                ])

    def record(self, method, duration, status, error_code="", task_id=""):
        """Record a solve attempt."""
        with self.lock:
            # Update in-memory stats
            stats = self.session_stats[method]
            stats["count"] += 1
            stats["total_time"] += duration
            if status == "success":
                stats["success"] += 1
            elif status == "timeout":
                stats["timeout"] += 1
            else:
                stats["error"] += 1

            # Write to CSV
            with open(self.log_file, "a", newline="") as f:
                writer = csv.writer(f)
                writer.writerow([
                    datetime.datetime.utcnow().isoformat(),
                    method, f"{duration:.2f}",
                    status, error_code, task_id,
                ])

    def get_session_summary(self):
        """Get current session statistics."""
        summary = {}
        for method, stats in self.session_stats.items():
            avg_time = (
                stats["total_time"] / stats["count"]
                if stats["count"] > 0 else 0
            )
            success_rate = (
                stats["success"] / stats["count"] * 100
                if stats["count"] > 0 else 0
            )
            summary[method] = {
                "total": stats["count"],
                "success": stats["success"],
                "errors": stats["error"],
                "timeouts": stats["timeout"],
                "success_rate": f"{success_rate:.1f}%",
                "avg_time": f"{avg_time:.1f}s",
            }
        return summary

get_session_summary 只是当次会话的内存统计,重启即清零;跨天数据靠的是同一份 CSV,下一步的报表类会直接读它。


第二步:让求解器自动上报指标

不想在每处业务代码里手动打点,就用 MonitoredSolver 把求解逻辑包一层:无论成功、超时还是报错,finally 块都会把耗时和错误码写进 MetricsCollector

finally 兜底的意义

try/except 只覆盖预判到的失败路径,finally 不管走哪条分支都执行,耗时状态照样落盘。

import requests
import time


class MonitoredSolver:
    """Solver with automatic metric collection."""

    def __init__(self, api_key, metrics=None):
        self.api_key = api_key
        self.base = "https://ocr.captchaai.com"
        self.metrics = metrics or MetricsCollector()

    def solve(self, method, **params):
        start = time.time()
        task_id = ""
        status = "error"
        error_code = ""

        try:
            # Submit
            data = {"key": self.api_key, "method": method, "json": 1}
            data.update(params)
            resp = requests.post(
                f"{self.base}/in.php", data=data, timeout=30,
            )
            result = resp.json()

            if result.get("status") != 1:
                error_code = result.get("request", "UNKNOWN")
                raise RuntimeError(f"Submit error: {error_code}")

            task_id = result["request"]

            # Poll
            token = self._poll(task_id)
            status = "success"
            return token

        except TimeoutError:
            status = "timeout"
            raise
        except Exception as e:
            error_code = str(e)[:50]
            raise
        finally:
            duration = time.time() - start
            self.metrics.record(method, duration, status, error_code, task_id)

    def _poll(self, task_id, timeout=120):
        start = time.time()
        while time.time() - start < timeout:
            time.sleep(5)
            resp = requests.get(f"{self.base}/res.php", params={
                "key": self.api_key, "action": "get",
                "id": task_id, "json": 1,
            }, timeout=15)
            data = resp.json()
            if data["request"] != "CAPCHA_NOT_READY":
                if data.get("status") == 1:
                    return data["request"]
                raise RuntimeError(f"Solve error: {data['request']}")
        raise TimeoutError("Poll timeout")

    def print_summary(self):
        """Print current session metrics."""
        summary = self.metrics.get_session_summary()
        print("\n=== CaptchaAI Usage Summary ===")
        for method, stats in summary.items():
            print(f"\n{method}:")
            for key, value in stats.items():
                print(f"  {key}: {value}")


# Usage
metrics = MetricsCollector()
solver = MonitoredSolver("YOUR_API_KEY", metrics)

# Solve some CAPTCHAs
for i in range(10):
    try:
        token = solver.solve(
            "userrecaptcha",
            googlekey="SITE_KEY",
            pageurl="https://example.com",
        )
    except Exception as e:
        print(f"Failed: {e}")

# Print results
solver.print_summary()

第三步:把 CSV 明细变成可读报表

有了逐条明细,UsageReport 按三个维度切片:按天看趋势、按类型看占比、按错误码看故障集中在哪,不用另外接 BI 工具。

三个切片各自解决什么问题

daily_summary 盯当天波动;method_breakdown 看哪种类型占大头,方便估算账单;error_breakdown 直接定位故障集中在哪个错误码。

import csv
import datetime
from collections import defaultdict


class UsageReport:
    """Generate usage reports from metrics CSV."""

    def __init__(self, log_file="captchaai_metrics.csv"):
        self.log_file = log_file

    def _load_data(self, days=None):
        """Load metrics, optionally filtered by date range."""
        cutoff = None
        if days:
            cutoff = datetime.datetime.utcnow() - datetime.timedelta(days=days)

        records = []
        with open(self.log_file, "r") as f:
            reader = csv.DictReader(f)
            for row in reader:
                ts = datetime.datetime.fromisoformat(row["timestamp"])
                if cutoff and ts < cutoff:
                    continue
                row["_ts"] = ts
                row["_duration"] = float(row["duration_s"])
                records.append(row)
        return records

    def daily_summary(self, days=7):
        """Summarize usage per day."""
        records = self._load_data(days=days)
        by_day = defaultdict(lambda: {"count": 0, "success": 0, "total_time": 0})

        for rec in records:
            day = rec["_ts"].date().isoformat()
            by_day[day]["count"] += 1
            if rec["status"] == "success":
                by_day[day]["success"] += 1
            by_day[day]["total_time"] += rec["_duration"]

        print(f"=== Daily Summary (last {days} days) ===")
        print(f"{'Date':<12} {'Total':>6} {'Success':>8} {'Rate':>7} {'Avg Time':>9}")
        for day in sorted(by_day.keys()):
            stats = by_day[day]
            rate = stats["success"] / stats["count"] * 100 if stats["count"] > 0 else 0
            avg = stats["total_time"] / stats["count"] if stats["count"] > 0 else 0
            print(f"{day:<12} {stats['count']:>6} {stats['success']:>8} {rate:>6.1f}% {avg:>8.1f}s")

    def method_breakdown(self, days=30):
        """Summarize usage by CAPTCHA type."""
        records = self._load_data(days=days)
        by_method = defaultdict(lambda: {"count": 0, "success": 0, "total_time": 0})

        for rec in records:
            method = rec["method"]
            by_method[method]["count"] += 1
            if rec["status"] == "success":
                by_method[method]["success"] += 1
            by_method[method]["total_time"] += rec["_duration"]

        print(f"\n=== Method Breakdown (last {days} days) ===")
        print(f"{'Method':<25} {'Total':>6} {'Success':>8} {'Rate':>7} {'Avg Time':>9}")
        for method in sorted(by_method.keys()):
            stats = by_method[method]
            rate = stats["success"] / stats["count"] * 100
            avg = stats["total_time"] / stats["count"]
            print(f"{method:<25} {stats['count']:>6} {stats['success']:>8} {rate:>6.1f}% {avg:>8.1f}s")

    def error_breakdown(self, days=7):
        """Show error distribution."""
        records = self._load_data(days=days)
        errors = defaultdict(int)

        for rec in records:
            if rec["status"] != "success" and rec["error_code"]:
                errors[rec["error_code"]] += 1

        if errors:
            print(f"\n=== Error Breakdown (last {days} days) ===")
            for error, count in sorted(errors.items(), key=lambda x: -x[1]):
                print(f"  {error}: {count}")


# Usage
report = UsageReport()
report.daily_summary(days=7)
report.method_breakdown(days=30)
report.error_breakdown(days=7)

第四步:跟踪余额,提前预警

BalanceDashboard 定期调用 getbalance 记录余额,两次采样相减就是这段时间的实际消耗速度。接进定时任务后,余额异常下降能在耗尽前被发现,而不是等任务批量失败才排查。

怎么用这段代码报警

单独调用 record() 没什么用,要配合定时任务定期跑,再用 get_spending 算区间消耗,超阈值就通知。

import requests
import time
import csv
import datetime


class BalanceDashboard:
    """Track balance over time for spending analysis."""

    def __init__(self, api_key, log_file="balance_history.csv"):
        self.api_key = api_key
        self.log_file = log_file

    def record(self):
        resp = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": self.api_key,
            "action": "getbalance",
            "json": 1,
        })
        balance = float(resp.json()["request"])

        with open(self.log_file, "a", newline="") as f:
            writer = csv.writer(f)
            writer.writerow([
                datetime.datetime.utcnow().isoformat(),
                f"{balance:.4f}",
            ])
        return balance

    def get_spending(self, hours=24):
        """Calculate spending over time period."""
        cutoff = datetime.datetime.utcnow() - datetime.timedelta(hours=hours)
        balances = []

        try:
            with open(self.log_file, "r") as f:
                reader = csv.reader(f)
                for row in reader:
                    ts = datetime.datetime.fromisoformat(row[0])
                    if ts > cutoff:
                        balances.append(float(row[1]))
        except FileNotFoundError:
            return 0

        if len(balances) < 2:
            return 0
        return balances[0] - balances[-1]

get_spending 的结果接一个 webhook,推到企业微信或飞书群机器人,异常时直接在群里提醒,比人工刷仪表板可靠。


常见故障排查

先按下面的思路排查,不用一上来就怀疑是 CaptchaAI 出了问题:

快速自查清单

  • CSV 文件越来越大 —— 后台长期运行,按天或按周轮换日志文件
  • 解决记录缺失 —— 有调用没接入 MonitoredSolver,统一用它包装所有求解调用
  • 统计数字和账单对不上 —— 部分错误没被记录,确认 finally 块每次都执行了写入
  • 仪表板错误率偏高 —— API 参数传错了,查看错误分布报表定位错误码

常见问题

监控数据要留多久?

明细数据留 30 天,用来排查近期问题;汇总数据留 90 天看长期趋势,更早的建议归档。

能不能把余额异常接到企业微信或飞书机器人?

可以,get_spending 返回具体数值,超过阈值时调用对应机器人的 webhook 接口推送消息即可。

监控脚本会不会拖慢识别速度?

不会,每次写入 CSV 的开销在 1 毫秒以内,相比一次识别请求本身可以忽略不计。

生产环境有必要跑这套监控吗?

有必要,记录成本几乎为零,却能在账单异常或成功率下滑造成实际影响之前提前发现。

多进程同时跑监控,会不会把 CSV 写坏?

threading.Lock() 只在同一进程内有效。多进程各跑一份 MonitoredSolver 时,给每个进程用独立日志文件名,报表阶段再合并。


相关指南

延伸阅读


先把这些数字摸清楚,账单异常才不会等到月底才发现。

立即为 CaptchaAI 接入用量监控

该文章已禁用评论。