Skill: shuxiangfanclaw/scrapling-fetch-basic (version 1.0.0)

--- name: scrapling-fetch-basic version: 1.0.0 description: 基础网页抓取工具，支持绕过反爬系统、自动定位正文区域、HTML 转 Markdown。适合抓取博客、新闻、公告等静态页面。 keywords: - 网页抓取 - 爬虫 - web scraping - 反爬绕过 - markdown - scrapling --- # Scrapling Fetch Basic 基础版网页抓取工具，快速高效，适合大多数场景。 ## 主要功能 ### 🌐 网页内容抓取 - **智能正文提取**：自动识别并提取网页正文内容，无需手动指定选择器 - **Markdown 输出**：将 HTML 自动转换为干净的 Markdown 格式 - **字符数控制**：支持自定义最大输出字符数（默认 30000） ### 🔓 反爬绕过 - **Cloudflare Turnstile**：stealth 模式可绕过 Cloudflare 反爬验证 - **浏览器指纹伪装**：隐身模式下模拟真实浏览器 ### 🎯 模式选择 - **basic 模式**：快速 HTTP 抓取，适合静态页面（默认） - **stealth 模式**：隐身浏览器抓取，适合有反爬保护的网站 ## 快速开始 ```bash # 基础抓取 python3 scripts/scrapling_fetch.py https://example.com/article # 指定字符数 python3 scripts/scrapling_fetch.py https://example.com/article 50000 # 绕过反爬保护 python3 scripts/scrapling_fetch.py https://protected-site.com --mode stealth # JSON 输出 python3 scripts/scrapling_fetch.py https://example.com --json ``` ## 正文选择器（11个） 按优先级自动尝试： 1. `article` - HTML5 article 元素 2. `main` - HTML5 main 主元素 3. `.post-content` - 博客常见内容区域 4. `.article-content` - 新闻常见内容区域 5. `.entry-content` - WordPress 常见 6. `.post-body` - 文章正文 7. `[class*='body']` - 包含 "body" 的类名 8. `[class*='content']` - 包含 "content" 的类名 9. `#content` - content ID 10. `#main` - main ID 11. `body` - 最后回退 ## 依赖 | 包名 | 用途 | |------|------| | scrapling | 爬虫核心框架 | | html2text | HTML 转 Markdown | | playwright | 浏览器自动化（stealth 模式） | ## 使用场景 - ✅ 抓取博客文章 - ✅ 抓取新闻页面 - ✅ 抓取公告文档 - ✅ 绕过基础反爬保护 - ⚠️ 微信公众号文章（支持有限，建议使用专业版） ## 对比专业版 | 特性 | 基础版 | 专业版 | |------|--------|--------| | 抓取模式 | basic / stealth | basic / stealth / **auto** | | 选择器数量 | 11 个 | **16 个** | | 微信公众号 | ⚠️ 有限支持 | ✅ **完整支持** | | 噪音清理 | ❌ | ✅ **微信专用清理** | | 自动检测 | ❌ | ✅ **智能模式选择** | --- **版本**: 1.0.0 **作者**: OpenClaw

path: _meta.json
content: {
  "name": "scrapling-fetch-basic",
  "version": "1.0.0",
  "displayName": "Scrapling Fetch Basic",
  "description": "基础网页抓取工具，支持绕过反爬系统、自动定位正文区域、HTML 转 Markdown",
  "tags": ["latest"],
  "acceptLicenseTerms": true
}


path: scripts/scrapling_fetch.py
content: #!/usr/bin/env python3
"""
Scrapling Fetch Basic - 基础网页抓取工具 v1.0.0

用法:
    python3 scrapling_fetch.py <url> [max_chars] [--mode basic|stealth] [--json] [--debug]

示例:
    python3 scrapling_fetch.py "https://example.com/article"
    python3 scrapling_fetch.py "https://example.com" 10000
    python3 scrapling_fetch.py "https://protected.com" --mode stealth
"""

import argparse
import json
import re
import sys
from typing import Tuple

# 基础版正文选择器优先级（11个）
CONTENT_SELECTORS = [
    "article",
    "main",
    ".post-content",
    ".article-content",
    ".entry-content",
    ".post-body",
    "[class*='body']",
    "[class*='content']",
    "#content",
    "#main",
    "body",
]


def fetch_basic(url: str, debug: bool = False) -> Tuple[str, str]:
    """
    基础 HTTP 抓取模式
    
    Returns:
        Tuple[html, selector]: 页面内容、命中的选择器
    """
    from scrapling.fetchers import Fetcher
    
    if debug:
        print(f"[DEBUG] 使用 Fetcher.get() 抓取: {url}", file=sys.stderr)
    
    page = Fetcher.get(url)
    html = page.html_content
    
    # 尝试选择器
    for selector in CONTENT_SELECTORS:
        try:
            elements = page.css(selector)
            if elements and len(elements) > 0:
                elem = elements[0]
                content = elem.get() if hasattr(elem, 'get') else str(elem)
                if content and len(content) > 500:
                    if debug:
                        print(f"[DEBUG] 命中选择器: {selector} (长度: {len(content)})", file=sys.stderr)
                    return content, selector
        except Exception as e:
            if debug:
                print(f"[DEBUG] 选择器 {selector} 失败: {e}", file=sys.stderr)
            continue
    
    if debug:
        print(f"[DEBUG] 未命中选择器，使用完整页面", file=sys.stderr)
    return html, "full_page"


def fetch_stealth(url: str, debug: bool = False) -> Tuple[str, str]:
    """
    隐身浏览器抓取模式（绕过反爬）
    
    Returns:
        Tuple[html, selector]: 页面内容、命中的选择器
    """
    from scrapling.fetchers import StealthyFetcher
    
    if debug:
        print(f"[DEBUG] 使用 StealthyFetcher 抓取: {url}", file=sys.stderr)
    
    try:
        page = StealthyFetcher.fetch(
            url,
            headless=True,
            network_idle=True,
            wait=3,
        )
    except Exception as e:
        if debug:
            print(f"[DEBUG] StealthyFetcher 失败: {e}，回退到 Fetcher", file=sys.stderr)
        return fetch_basic(url, debug)
    
    html = page.html_content
    
    # 尝试选择器
    for selector in CONTENT_SELECTORS:
        try:
            elements = page.css(selector)
            if elements and len(elements) > 0:
                elem = elements[0]
                content = elem.get() if hasattr(elem, 'get') else str(elem)
                if content and len(content) > 500:
                    if debug:
                        print(f"[DEBUG] 命中选择器: {selector} (长度: {len(content)})", file=sys.stderr)
                    return content, selector
        except Exception as e:
            if debug:
                print(f"[DEBUG] 选择器 {selector} 失败: {e}", file=sys.stderr)
            continue
    
    if debug:
        print(f"[DEBUG] 未命中有效选择器，使用完整页面", file=sys.stderr)
    return html, "full_page"


def html_to_markdown(html: str, base_url: str = "") -> str:
    """将 HTML 转换为 Markdown"""
    from html2text import HTML2Text
    
    h = HTML2Text()
    h.ignore_links = False
    h.ignore_images = False
    h.ignore_emphasis = False
    h.body_width = 0
    h.unicode_snob = True
    h.skip_internal_links = False
    
    if base_url:
        h.baseurl = base_url
    
    return h.handle(html)


def clean_content(text: str) -> str:
    """清理内容中的多余空行"""
    text = re.sub(r'\n{3,}', '\n\n', text)
    return text.strip()


def extract_title(html: str) -> str:
    """从 HTML 中提取标题"""
    from scrapling.fetchers import Fetcher
    
    try:
        page = Fetcher.parse(html)
        
        # 标准 title 标签
        title = page.css("title::text").get()
        if title:
            return title.strip()
        
        # h1 标签
        h1 = page.css("h1::text").get()
        if h1:
            return h1.strip()
    except:
        pass
    
    return ""


def main():
    parser = argparse.ArgumentParser(
        description="Scrapling Fetch Basic v1.0.0",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument("url", help="目标网页 URL")
    parser.add_argument("max_chars", nargs="?", type=int, default=30000,
                        help="最大输出字符数（默认: 30000）")
    parser.add_argument("--mode", choices=["basic", "stealth"], default="basic",
                        help="抓取模式: basic（快速）/ stealth（隐身，绕过反爬）")
    parser.add_argument("--json", action="store_true", help="JSON 格式输出")
    parser.add_argument("--debug", action="store_true", help="显示调试信息")
    
    args = parser.parse_args()
    
    try:
        # 抓取页面
        if args.mode == "stealth":
            html, selector = fetch_stealth(args.url, args.debug)
        else:
            html, selector = fetch_basic(args.url, args.debug)
        
        # 提取标题
        title = extract_title(html)
        
        # 转换为 Markdown
        markdown = html_to_markdown(html, args.url)
        
        # 清理内容
        markdown = clean_content(markdown)
        
        # 截断
        original_len = len(markdown)
        truncated = len(markdown) > args.max_chars
        if truncated:
            markdown = markdown[:args.max_chars] + "\n\n... [内容已截断]"
        
        # 输出
        if args.json:
            result = {
                "url": args.url,
                "title": title,
                "content": markdown,
                "selector": selector,
                "length": original_len,
                "truncated": truncated,
                "mode": args.mode,
            }
            print(json.dumps(result, ensure_ascii=False, indent=2))
        else:
            if title:
                print(f"# {title}\n")
            print(markdown)
            
            if args.debug:
                print(f"\n---", file=sys.stderr)
                print(f"[DEBUG] 选择器: {selector}", file=sys.stderr)
                print(f"[DEBUG] 原始长度: {original_len}", file=sys.stderr)
                print(f"[DEBUG] 截断: {truncated}", file=sys.stderr)
    
    except Exception as e:
        if args.json:
            print(json.dumps({
                "error": str(e),
                "url": args.url,
            }, ensure_ascii=False))
        else:
            print(f"[error] fetch failed: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)