Use Cases

使用验证码处理进行法律研究网络抓取

法律检索团队批量抓取判例、监控法院备案时,最先卡住的往往不是页面结构,而是几乎每个法律数据源都挂着的验证码。PACER、州法院系统、SEC EDGAR 大多用 reCAPTCHA v2 或图片验证码拦截自动请求。CaptchaAI 通过 API 自动识别这些验证码,让判例检索、备案监控脚本能持续稳定跑下去。


中国团队为什么也要处理这些验证码

这类验证码不只是欧美律所的问题:

  • 合规团队抓取 SEC EDGAR 上的中概股信息披露备案
  • 出海企业申请美国专利前检索专利数据库确认在先技术
  • 跨境尽调项目核对律师协会名录和案例引用

只采集你有权访问的公开数据,同时满足《网络安全法》《数据安全法》和 PIPL 的要求。


常见法律数据源与验证码类型

来源 验证码类型 数据 用户
PACER reCAPTCHA v2 联邦法院文件 诉讼团队
州法院系统 图片验证码 / reCAPTCHA 各州案件记录 律师
SEC EDGAR reCAPTCHA v2 上市公司备案 合规团队
专利数据库 reCAPTCHA v2 专利记录 知识产权研究人员
监管门户网站 图片验证码 规则与指导文件 合规团队
法律引文数据库 reCAPTCHA v2 案例引用 法律科技团队
律师协会名录 reCAPTCHA v2 律师执业记录 尽职调查团队

抓取踩坑与处理方式

问题 原因 处理方式
图片验证码反复识别失败 文字扭曲严重 换一张验证码图片重试
PACER 拒绝访问 触发速率限制 等待 30 分钟,降低请求频率
案件详情不完整 内容在付费墙后 按需支付逐页费用
搜索无结果返回 实际返回的是验证码页面 解析前先检测是否命中验证码
案卷监控漏掉新备案 检查间隔太长 缩短轮询间隔

案例检索脚本:批量识别 reCAPTCHA v2 与图片验证码

下面的 Python 示例演示判例检索脚本:请求搜索结果页,命中 reCAPTCHA v2 就走 sitekey 流程提交 token,命中图片验证码就转成 base64 上传识别。

import requests
import time
import re
import base64
from bs4 import BeautifulSoup
import csv

CAPTCHAAI_KEY = "YOUR_API_KEY"
CAPTCHAAI_URL = "https://ocr.captchaai.com"


def solve_recaptcha(sitekey, pageurl):
    resp = requests.post(f"{CAPTCHAAI_URL}/in.php", data={
        "key": CAPTCHAAI_KEY, "method": "userrecaptcha",
        "googlekey": sitekey, "pageurl": pageurl, "json": 1,
    })
    task_id = resp.json()["request"]
    for _ in range(60):
        time.sleep(5)
        result = requests.get(f"{CAPTCHAAI_URL}/res.php", params={
            "key": CAPTCHAAI_KEY, "action": "get",
            "id": task_id, "json": 1,
        })
        data = result.json()
        if data["request"] != "CAPCHA_NOT_READY":
            return data["request"]
    raise TimeoutError("Timeout")


def solve_image_captcha(image_bytes):
    img_b64 = base64.b64encode(image_bytes).decode()
    resp = requests.post(f"{CAPTCHAAI_URL}/in.php", data={
        "key": CAPTCHAAI_KEY, "method": "base64",
        "body": img_b64, "json": 1,
    })
    task_id = resp.json()["request"]
    for _ in range(20):
        time.sleep(3)
        result = requests.get(f"{CAPTCHAAI_URL}/res.php", params={
            "key": CAPTCHAAI_KEY, "action": "get",
            "id": task_id, "json": 1,
        })
        data = result.json()
        if data["request"] != "CAPCHA_NOT_READY":
            return data["request"]
    raise TimeoutError("Timeout")


class LegalResearchScraper:
    def __init__(self, proxy=None):
        self.session = requests.Session()
        if proxy:
            self.session.proxies = {"http": proxy, "https": proxy}
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 Chrome/126.0.0.0 Safari/537.36",
        })

    def search_cases(self, search_url, query, sitekey=None, max_pages=5):
        """Search case law database."""
        all_cases = []

        for page in range(max_pages):
            url = f"{search_url}?q={query}&page={page + 1}"
            resp = self.session.get(url, timeout=30)

            if self._has_captcha(resp.text):
                if sitekey:
                    token = solve_recaptcha(sitekey, url)
                    resp = self.session.post(url, data={
                        "q": query,
                        "g-recaptcha-response": token,
                    })
                else:
                    resp = self._solve_image_and_retry(resp.text, url, query)

            cases = self._parse_cases(resp.text)
            if not cases:
                break

            all_cases.extend(cases)
            print(f"Page {page + 1}: {len(cases)} cases")
            time.sleep(5)

        return all_cases

    def get_case_details(self, case_url):
        """Fetch full case details."""
        resp = self.session.get(case_url, timeout=30)

        if self._has_captcha(resp.text):
            sitekey = self._extract_sitekey(resp.text)
            if sitekey:
                token = solve_recaptcha(sitekey, case_url)
                resp = self.session.post(case_url, data={
                    "g-recaptcha-response": token,
                })

        soup = BeautifulSoup(resp.text, "html.parser")
        return {
            "title": self._text(soup, "h1, .case-title"),
            "citation": self._text(soup, ".citation, .case-cite"),
            "court": self._text(soup, ".court, .jurisdiction"),
            "date": self._text(soup, ".decision-date, .date-decided"),
            "judge": self._text(soup, ".judge, .authored-by"),
            "summary": self._text(soup, ".summary, .headnote"),
            "url": case_url,
        }

    def monitor_docket(self, docket_url, case_number, sitekey=None):
        """Monitor a specific case docket for new filings."""
        resp = self.session.get(docket_url, timeout=30)

        data = {"case_number": case_number}
        if sitekey and self._has_captcha(resp.text):
            token = solve_recaptcha(sitekey, docket_url)
            data["g-recaptcha-response"] = token

        resp = self.session.post(docket_url, data=data)
        return self._parse_docket(resp.text)

    def export_results(self, cases, filename):
        """Export case results to CSV."""
        if not cases:
            return
        with open(filename, "w", newline="", encoding="utf-8") as f:
            writer = csv.DictWriter(f, fieldnames=cases[0].keys())
            writer.writeheader()
            writer.writerows(cases)

    def _has_captcha(self, html):
        return any(tag in html.lower() for tag in [
            'data-sitekey', 'g-recaptcha', 'captcha',
        ])

    def _extract_sitekey(self, html):
        match = re.search(r'data-sitekey="([^"]+)"', html)
        return match.group(1) if match else None

    def _solve_image_and_retry(self, html, url, query):
        match = re.search(r'src="(/captcha[^"]+)"', html)
        if match:
            img_url = url.split("?")[0].rstrip("/") + match.group(1)
            img = self.session.get(img_url)
            answer = solve_image_captcha(img.content)
            return self.session.post(url, data={
                "q": query,
                "captcha": answer,
            })
        return self.session.get(url)

    def _parse_cases(self, html):
        soup = BeautifulSoup(html, "html.parser")
        cases = []
        for item in soup.select(".case-result, .search-result, tr.result"):
            title_el = item.select_one("a, .case-name")
            if title_el:
                cases.append({
                    "title": title_el.get_text(strip=True),
                    "url": title_el.get("href", ""),
                    "citation": self._text(item, ".citation, .cite"),
                    "date": self._text(item, ".date"),
                    "court": self._text(item, ".court"),
                })
        return cases

    def _parse_docket(self, html):
        soup = BeautifulSoup(html, "html.parser")
        entries = []
        for row in soup.select(".docket-entry, tr.filing"):
            entries.append({
                "date": self._text(row, ".date, td:first-child"),
                "entry": self._text(row, ".description, td:nth-child(2)"),
                "filed_by": self._text(row, ".filer, td:nth-child(3)"),
            })
        return entries

    def _text(self, el, selector):
        found = el.select_one(selector)
        return found.get_text(strip=True) if found else ""


# Usage
scraper = LegalResearchScraper(
    proxy="http://user:pass@residential.proxy.com:5000"
)

# Search case law
cases = scraper.search_cases(
    search_url="https://caselaw.example.com/search",
    query="data privacy GDPR",
    max_pages=5,
)

# Get details for relevant cases
for case in cases[:10]:
    if case["url"]:
        details = scraper.get_case_details(case["url"])
        print(f"{details['citation']}: {details['title']}")
        time.sleep(3)

# Export results
scraper.export_results(cases, "gdpr_cases.csv")

脚本依次翻页检索、抓取案件详情、导出 CSV,方便后续在 Excel 或 BI 工具里做进一步筛选和交叉核对。


监管备案监控:自动比对新增文件

很多合规团队需要盯住多个监管门户的新增备案。下面的 RegulatoryMonitor 类复用同一套识别逻辑,对多个信息源做增量比对,只输出没见过的备案:

class RegulatoryMonitor:
    def __init__(self, proxy=None):
        self.scraper = LegalResearchScraper(proxy=proxy)
        self.seen_entries = set()

    def check_new_filings(self, feeds):
        """Check regulatory portals for new filings."""
        new_filings = []

        for feed in feeds:
            try:
                cases = self.scraper.search_cases(
                    feed["url"], feed["query"],
                    sitekey=feed.get("sitekey"),
                    max_pages=2,
                )

                for case in cases:
                    key = case.get("citation") or case.get("title")
                    if key and key not in self.seen_entries:
                        self.seen_entries.add(key)
                        case["source"] = feed["name"]
                        new_filings.append(case)

            except Exception as e:
                print(f"Error checking {feed['name']}: {e}")

            time.sleep(5)

        return new_filings

把这个类接入定时任务(cron 或 APScheduler),就能把“新增备案提醒”做成后台自动运行的流程。


并发抓取与线程数怎么选

如果要同时监控多个法院系统,CaptchaAI 按线程数计费、不按验证码次数计费,同一线程可无限次识别。

ADVANCE 套餐($90/月,50 线程)能覆盖多数中型团队的日常抓取,价格以官网定价页为准。


常见问题

抓取法院和监管网站的判例数据合规吗?

公开的法院记录和监管备案通常允许访问,但 PACER 会按页收费,跟访问方式无关。抓取前先确认服务条款和频率限制,跨境团队还要留意《数据安全法》和 PIPL 的采集范围要求。

CaptchaAI 能同时处理 reCAPTCHA v2 和图片验证码吗?

可以。同一个 API Key 既能提交 reCAPTCHA v2 任务,也能提交图片验证码,脚本按页面实际触发的类型分流调用即可。

请求太快会被法院系统封禁吗?

会。PACER 和多数州法院系统都有速率限制,请求过密会触发封禁或返回验证码页面,建议放慢翻页间隔。

案卷监控多久检查一次比较合适?

取决于案件活跃度:诉讼中的案件建议几小时检查一次,监管门户按天检查即可,间隔太长是漏备案的常见原因。


延伸阅读


简化法律研究——获取你的 CaptchaAI 密钥,把判例检索和备案监控接成自动化流程。

该文章已禁用评论。