[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M7EVAF7W979PVC2MNX9S7TEB":3,"skill-same:01M7EVAF7W979PVC2MNX9S7TEB":46},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":24,"commitSha":25,"skillDirectory":26,"forksCount":27,"repositoryHtmlUrl":28,"repositoryHomepageUrl":6,"ownerHtmlUrl":29,"ownerBlogUrl":6,"ownerDescription":30,"updatedAt":31,"install":32,"resources":45},"01M7EVAF7W979PVC2MNX9S7TEB","web-scraping",null,"Web scraping with anti-bot bypass, content extraction, undocumented APIs and poison pill detection. Use when extracting content from websites, handling paywalls, implementing scraping cascades or processing social media. Covers requests, trafilatura, Playwright with stealth mode, yt-dlp and instaloader patterns.","lza6\u002FArmor-piercing-system-prompts","lza6","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F128880206?v=4","01M7EB3K7WPX5T28EJ2NVHE6EG","Chinese-tingfeng","01M7EB3K97E1KQ16QWKJMFNGX8",11,"MIT","https:\u002F\u002Fgithub.com\u002Flza6\u002FArmor-piercing-system-prompts\u002Fblob\u002Fmain\u002F04-安全渗透\u002Fweb-scraping\u002FSKILL.md",[],[],"2026-11-15 14:19:17",false,"---\nname: web-scraping\ndescription: Web scraping with anti-bot bypass, content extraction, undocumented APIs and poison pill detection. Use when extracting content from websites, handling paywalls, implementing scraping cascades or processing social media. Covers requests, trafilatura, Playwright with stealth mode, yt-dlp and instaloader patterns.\n---\n\n# Web scraping methodology\n\nPatterns for reliable, ethical web scraping with fallback strategies and anti-bot handling.\n\n## Scraping cascade architecture\n\nImplement multiple extraction strategies with automatic fallback:\n\n```python\nfrom abc import ABC, abstractmethod\nfrom typing import Optional\nimport requests\nfrom bs4 import BeautifulSoup\nimport trafilatura\n\n#for .py files\nfrom playwright.sync_api import sync_playwright\nfrom playwright_stealth import stealth_sync\n\n#for .ipynb files\nimport asyncio\nfrom playwright.async_api import async_playwright\n\nclass ScrapingResult:\n    def __init__(self, content: str, title: str, method: str):\n        self.content = content\n        self.title = title\n        self.method = method  # Track which method succeeded\n\nclass Scraper(ABC):\n    @abstractmethod\n    def fetch(self, url: str) -> Optional[ScrapingResult]: ...\n\nclass TrafilaturaСscraper(Scraper):\n    \"\"\"Fast, lightweight extraction for standard articles.\"\"\"\n\n    def fetch(self, url: str) -> Optional[ScrapingResult]:\n        try:\n            downloaded = trafilatura.fetch_url(url)\n            if not downloaded:\n                return None\n\n            content = trafilatura.extract(\n                downloaded,\n                include_comments=False,\n                include_tables=True,\n                favor_recall=True\n            )\n\n            if not content or len(content) \u003C 100:\n                return None\n\n            # Extract title separately\n            soup = BeautifulSoup(downloaded, 'html.parser')\n            title = soup.find('title')\n            title_text = title.get_text() if title else ''\n\n            return ScrapingResult(content, title_text, 'trafilatura')\n        except Exception:\n            return None\n\nclass RequestsScraper(Scraper):\n    \"\"\"HTTP requests with rotating user agents.\"\"\"\n\n    USER_AGENTS = [\n        'Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\u002F537.36',\n        'Mozilla\u002F5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\u002F537.36',\n        'Mozilla\u002F5.0 (X11; Linux x86_64) AppleWebKit\u002F537.36',\n    ]\n\n    def fetch(self, url: str) -> Optional[ScrapingResult]:\n        import random\n\n        headers = {\n            'User-Agent': random.choice(self.USER_AGENTS),\n            'Accept': 'text\u002Fhtml,application\u002Fxhtml+xml',\n            'Accept-Language': 'en-US,en;q=0.9',\n        }\n\n        try:\n            response = requests.get(url, headers=headers, timeout=30)\n            response.raise_for_status()\n\n            soup = BeautifulSoup(response.text, 'html.parser')\n\n            # Remove script\u002Fstyle elements\n            for element in soup(['script', 'style', 'nav', 'footer', 'aside']):\n                element.decompose()\n\n            # Find main content\n            main = soup.find('main') or soup.find('article') or soup.find('body')\n            content = main.get_text(separator='\\n', strip=True) if main else ''\n\n            title = soup.find('title')\n            title_text = title.get_text() if title else ''\n\n            if len(content) \u003C 100:\n                return None\n\n            return ScrapingResult(content, title_text, 'requests')\n        except Exception:\n            return None\n\nclass PlaywrightScraper(Scraper):\n    \"\"\"Heavy JavaScript rendering with stealth mode for anti-bot bypass.\"\"\"\n\n    def fetch(self, url: str) -> Optional[ScrapingResult]:\n        try:\n            with sync_playwright() as p:\n                browser = p.chromium.launch(headless=True)\n                context = browser.new_context(\n                    viewport={'width': 1920, 'height': 1080},\n                    user_agent='Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\u002F537.36'\n                )\n                page = context.new_page()\n\n                # Apply stealth to avoid detection\n                stealth_sync(page)\n\n                page.goto(url, wait_until='networkidle', timeout=60000)\n\n                # Wait for content to load\n                page.wait_for_timeout(2000)\n\n                # Extract content\n                content = page.evaluate('''() => {\n                    const article = document.querySelector('article, main, .content, #content');\n                    return article ? article.innerText : document.body.innerText;\n                }''')\n\n                title = page.title()\n\n                browser.close()\n\n                if len(content) \u003C 100:\n                    return None\n\n                return ScrapingResult(content, title, 'playwright')\n        except Exception:\n            return None\n\nclass PlaywrightScraperAsync:\n    \"\"\"Async Playwright scraper for Jupyter notebooks (.ipynb files).\n    \n    Jupyter notebooks run their own event loop, so sync Playwright won't work.\n    Use this async version with `await` in notebook cells.\n    \"\"\"\n\n    async def fetch(self, url: str) -> Optional[ScrapingResult]:\n        try:\n            async with async_playwright() as p:\n                browser = await p.chromium.launch(headless=True)\n                context = await browser.new_context(\n                    viewport={'width': 1920, 'height': 1080},\n                    user_agent='Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\u002F537.36'\n                )\n                page = await context.new_page()\n\n                # Note: playwright-stealth async version\n                # from playwright_stealth import stealth_async\n                # await stealth_async(page)\n\n                await page.goto(url, wait_until='networkidle', timeout=60000)\n\n                # Wait for content to load\n                await page.wait_for_timeout(2000)\n\n                # Extract content\n                content = await page.evaluate('''() => {\n                    const article = document.querySelector('article, main, .content, #content');\n                    return article ? article.innerText : document.body.innerText;\n                }''')\n\n                title = await page.title()\n\n                await browser.close()\n\n                if len(content) \u003C 100:\n                    return None\n\n                return ScrapingResult(content, title, 'playwright_async')\n        except Exception:\n            return None\n\n# Usage in Jupyter notebook cells:\n# scraper = PlaywrightScraperAsync()\n# result = await scraper.fetch('https:\u002F\u002Fexample.com')\n\nclass ScrapingCascade:\n    \"\"\"Try multiple scrapers in order until one succeeds.\"\"\"\n\n    def __init__(self):\n        self.scrapers = [\n            TrafilaturaСscraper(),\n            RequestsScraper(),\n            PlaywrightScraper(),\n        ]\n\n    def fetch(self, url: str) -> Optional[ScrapingResult]:\n        for scraper in self.scrapers:\n            result = scraper.fetch(url)\n            if result:\n                return result\n        return None\n```\n\n## Undocumented APIs\n\n### Finding undocumented APIs\n\nUse browser developer tools to discover APIs:\n\n1. **Open developer tools** (right-click → Inspect, or F12)\n2. **Go to the Network tab** to monitor all requests\n3. **Filter by Fetch\u002FXHR** to show only API calls\n4. **Trigger the action** you want to capture (search, scroll, click)\n5. **Analyze the response** — usually JSON with key-value pairs\n6. **Copy as cURL** (right-click the request)\n7. **Convert to code** using [curlconverter.com](https:\u002F\u002Fcurlconverter.com\u002F)\n\n### Stripping down API requests\n\nWhen you copy a cURL from dev tools, it includes many parameters. Strip it down by:\n\n1. **Remove unnecessary cookies** — test without them first\n2. **Keep authentication tokens** if required\n3. **Identify the input parameters** you can modify (like `prefix` for search terms)\n4. **Test parameter values** — some expire, so periodically verify\n\n### Example: Reverse-engineering an autocomplete API\n\n```python\nimport requests\nimport time\n\ndef search_suggestions(keyword: str) -> dict:\n    \"\"\"\n    Get autocompleted search suggestions from an undocumented API.\n    Stripped down from browser dev tools capture.\n    \"\"\"\n    headers = {\n        'User-Agent': 'Mozilla\u002F5.0 (Macintosh; Intel Mac OS X 10.15; rv:100.0) Gecko\u002F20100101 Firefox\u002F100.0',\n        'Accept': 'application\u002Fjson, text\u002Fjavascript, *\u002F*; q=0.01',\n        'Accept-Language': 'en-US,en;q=0.5',\n    }\n\n    params = {\n        'prefix': keyword,\n        'suggestion-type': ['WIDGET', 'KEYWORD'],\n        'alias': 'aps',\n        'plain-mid': '1',\n    }\n\n    response = requests.get(\n        'https:\u002F\u002Fcompletion.amazon.com\u002Fapi\u002F2017\u002Fsuggestions',\n        params=params,\n        headers=headers\n    )\n    return response.json()\n\n# Collect suggestions for multiple keywords\nkeywords = ['a', 'b', 'cookie', 'sock']\ndata = []\n\nfor keyword in keywords:\n    suggestions = search_suggestions(keyword)\n    suggestions['search_word'] = keyword  # track seed keyword\n    time.sleep(1)  # rate limit yourself\n    data.extend(suggestions.get('suggestions', []))\n```\n*Source: [Leon Yin, \"Finding Undocumented APIs,\" Inspect Element](https:\u002F\u002Finspectelement.org\u002Fapis.html), 2023*\n\n## Poison pill detection\n\nDetect paywalls, anti-bot pages, and other failures:\n\n```python\nfrom dataclasses import dataclass\nfrom enum import Enum\nimport re\n\nclass PoisonPillType(Enum):\n    PAYWALL = 'paywall'\n    CAPTCHA = 'captcha'\n    RATE_LIMIT = 'rate_limit'\n    CLOUDFLARE = 'cloudflare'\n    LOGIN_REQUIRED = 'login_required'\n    NOT_FOUND = 'not_found'\n    NONE = 'none'\n\n@dataclass\nclass PoisonPillResult:\n    detected: bool\n    type: PoisonPillType\n    confidence: float\n    details: str\n\nclass PoisonPillDetector:\n    PATTERNS = {\n        PoisonPillType.PAYWALL: [\n            r'subscribe to continue',\n            r'subscription required',\n            r'become a member',\n            r'sign up to read',\n            r'you\\'ve reached your limit',\n            r'article limit reached',\n        ],\n        PoisonPillType.CAPTCHA: [\n            r'verify you are human',\n            r'captcha',\n            r'robot verification',\n            r'prove you\\'re not a robot',\n        ],\n        PoisonPillType.RATE_LIMIT: [\n            r'too many requests',\n            r'rate limit exceeded',\n            r'slow down',\n            r'429',\n        ],\n        PoisonPillType.CLOUDFLARE: [\n            r'checking your browser',\n            r'cloudflare',\n            r'ddos protection',\n            r'please wait while we verify',\n        ],\n        PoisonPillType.LOGIN_REQUIRED: [\n            r'sign in to continue',\n            r'log in required',\n            r'create an account',\n        ],\n    }\n\n    PAYWALL_DOMAINS = {\n        'nytimes.com': PoisonPillType.PAYWALL,\n        'wsj.com': PoisonPillType.PAYWALL,\n        'washingtonpost.com': PoisonPillType.PAYWALL,\n        'ft.com': PoisonPillType.PAYWALL,\n        'bloomberg.com': PoisonPillType.PAYWALL,\n    }\n\n    def detect(self, url: str, content: str, status_code: int = 200) -> PoisonPillResult:\n        # Check status code\n        if status_code == 429:\n            return PoisonPillResult(True, PoisonPillType.RATE_LIMIT, 1.0, 'HTTP 429')\n        if status_code == 403:\n            return PoisonPillResult(True, PoisonPillType.CLOUDFLARE, 0.8, 'HTTP 403')\n        if status_code == 404:\n            return PoisonPillResult(True, PoisonPillType.NOT_FOUND, 1.0, 'HTTP 404')\n\n        # Check known paywall domains\n        from urllib.parse import urlparse\n        domain = urlparse(url).netloc.replace('www.', '')\n        for paywall_domain, pill_type in self.PAYWALL_DOMAINS.items():\n            if paywall_domain in domain:\n                # Check if content is suspiciously short (paywall truncation)\n                if len(content) \u003C 500:\n                    return PoisonPillResult(True, pill_type, 0.9, f'Short content from {domain}')\n\n        # Pattern matching\n        content_lower = content.lower()\n        for pill_type, patterns in self.PATTERNS.items():\n            for pattern in patterns:\n                if re.search(pattern, content_lower):\n                    return PoisonPillResult(True, pill_type, 0.7, f'Pattern match: {pattern}')\n\n        return PoisonPillResult(False, PoisonPillType.NONE, 0.0, '')\n```\n\n## Social media scraping\n\n### YouTube with yt-dlp\n\n```python\nimport yt_dlp\nfrom pathlib import Path\n\ndef download_video_metadata(url: str) -> dict:\n    \"\"\"Extract metadata without downloading video.\"\"\"\n    ydl_opts = {\n        'skip_download': True,\n        'quiet': True,\n        'no_warnings': True,\n    }\n\n    with yt_dlp.YoutubeDL(ydl_opts) as ydl:\n        info = ydl.extract_info(url, download=False)\n        return {\n            'title': info.get('title'),\n            'description': info.get('description'),\n            'duration': info.get('duration'),\n            'upload_date': info.get('upload_date'),\n            'view_count': info.get('view_count'),\n            'channel': info.get('channel'),\n            'thumbnail': info.get('thumbnail'),\n        }\n\ndef download_video(url: str, output_dir: Path, audio_only: bool = False) -> Path:\n    \"\"\"Download video or audio.\"\"\"\n    output_template = str(output_dir \u002F '%(title)s.%(ext)s')\n\n    ydl_opts = {\n        'outtmpl': output_template,\n        'quiet': True,\n    }\n\n    if audio_only:\n        ydl_opts['format'] = 'bestaudio\u002Fbest'\n        ydl_opts['postprocessors'] = [{\n            'key': 'FFmpegExtractAudio',\n            'preferredcodec': 'mp3',\n        }]\n\n    with yt_dlp.YoutubeDL(ydl_opts) as ydl:\n        info = ydl.extract_info(url, download=True)\n        filename = ydl.prepare_filename(info)\n        if audio_only:\n            filename = filename.rsplit('.', 1)[0] + '.mp3'\n        return Path(filename)\n\ndef get_transcript(url: str) -> list[dict]:\n    \"\"\"Extract auto-generated or manual subtitles.\"\"\"\n    ydl_opts = {\n        'skip_download': True,\n        'writesubtitles': True,\n        'writeautomaticsub': True,\n        'subtitleslangs': ['en'],\n        'quiet': True,\n    }\n\n    with yt_dlp.YoutubeDL(ydl_opts) as ydl:\n        info = ydl.extract_info(url, download=False)\n\n        # Check for subtitles\n        subtitles = info.get('subtitles', {})\n        auto_captions = info.get('automatic_captions', {})\n\n        # Prefer manual subtitles over auto-generated\n        subs = subtitles.get('en') or auto_captions.get('en')\n        if not subs:\n            return []\n\n        # Get the vtt or json format\n        for sub in subs:\n            if sub['ext'] in ['vtt', 'json3']:\n                # Download and parse subtitle file\n                # ... implementation depends on format\n                pass\n\n        return []\n```\n\n### Instagram with instaloader\n\n```python\nimport instaloader\nfrom pathlib import Path\n\nclass InstagramScraper:\n    def __init__(self, username: str = None, session_file: str = None):\n        self.loader = instaloader.Instaloader(\n            download_videos=True,\n            download_video_thumbnails=False,\n            download_geotags=False,\n            download_comments=False,\n            save_metadata=True,\n            compress_json=False,\n        )\n\n        if session_file and Path(session_file).exists():\n            self.loader.load_session_from_file(username, session_file)\n\n    def get_profile_posts(self, username: str, limit: int = 50) -> list[dict]:\n        \"\"\"Get recent posts from a profile.\"\"\"\n        profile = instaloader.Profile.from_username(self.loader.context, username)\n        posts = []\n\n        for i, post in enumerate(profile.get_posts()):\n            if i >= limit:\n                break\n\n            posts.append({\n                'shortcode': post.shortcode,\n                'url': f'https:\u002F\u002Finstagram.com\u002Fp\u002F{post.shortcode}\u002F',\n                'caption': post.caption,\n                'timestamp': post.date_utc.isoformat(),\n                'likes': post.likes,\n                'comments': post.comments,\n                'is_video': post.is_video,\n                'video_url': post.video_url if post.is_video else None,\n            })\n\n        return posts\n\n    def download_post(self, shortcode: str, output_dir: Path):\n        \"\"\"Download a single post's media.\"\"\"\n        post = instaloader.Post.from_shortcode(self.loader.context, shortcode)\n        self.loader.download_post(post, target=str(output_dir))\n```\n\n### TikTok with yt-dlp\n\n```python\ndef scrape_tiktok_profile(username: str, output_dir: Path, limit: int = 50) -> list[dict]:\n    \"\"\"Scrape TikTok profile videos.\"\"\"\n    profile_url = f'https:\u002F\u002Ftiktok.com\u002F@{username}'\n\n    ydl_opts = {\n        'quiet': True,\n        'extract_flat': True,  # Don't download, just get info\n        'playlistend': limit,\n    }\n\n    with yt_dlp.YoutubeDL(ydl_opts) as ydl:\n        info = ydl.extract_info(profile_url, download=False)\n        videos = []\n\n        for entry in info.get('entries', []):\n            videos.append({\n                'id': entry.get('id'),\n                'title': entry.get('title'),\n                'url': entry.get('url'),\n                'timestamp': entry.get('timestamp'),\n                'view_count': entry.get('view_count'),\n            })\n\n        return videos\n\ndef download_tiktok_video(url: str, output_dir: Path) -> Path:\n    \"\"\"Download a single TikTok video.\"\"\"\n    ydl_opts = {\n        'outtmpl': str(output_dir \u002F '%(id)s.%(ext)s'),\n        'quiet': True,\n    }\n\n    with yt_dlp.YoutubeDL(ydl_opts) as ydl:\n        info = ydl.extract_info(url, download=True)\n        return Path(ydl.prepare_filename(info))\n```\n\n## Request patterns\n\n### Rotating user agents and headers\n\n```python\nimport random\nfrom fake_useragent import UserAgent\n\nclass RequestManager:\n    def __init__(self):\n        self.ua = UserAgent()\n        self.session = requests.Session()\n\n    def get_headers(self) -> dict:\n        return {\n            'User-Agent': self.ua.random,\n            'Accept': 'text\u002Fhtml,application\u002Fxhtml+xml,application\u002Fxml;q=0.9,*\u002F*;q=0.8',\n            'Accept-Language': 'en-US,en;q=0.5',\n            'Accept-Encoding': 'gzip, deflate, br',\n            'DNT': '1',\n            'Connection': 'keep-alive',\n            'Upgrade-Insecure-Requests': '1',\n        }\n\n    def fetch(self, url: str, retry_count: int = 3) -> requests.Response:\n        for attempt in range(retry_count):\n            try:\n                response = self.session.get(\n                    url,\n                    headers=self.get_headers(),\n                    timeout=30\n                )\n                response.raise_for_status()\n                return response\n            except requests.RequestException as e:\n                if attempt == retry_count - 1:\n                    raise\n                time.sleep(2 ** attempt)  # Exponential backoff\n```\n\n### Respectful scraping with delays\n\n```python\nimport time\nimport random\nfrom urllib.parse import urlparse\n\nclass PoliteRequester:\n    def __init__(self, min_delay: float = 1.0, max_delay: float = 3.0):\n        self.min_delay = min_delay\n        self.max_delay = max_delay\n        self.last_request_per_domain = {}\n\n    def wait_for_domain(self, url: str):\n        domain = urlparse(url).netloc\n        last_request = self.last_request_per_domain.get(domain, 0)\n\n        elapsed = time.time() - last_request\n        delay = random.uniform(self.min_delay, self.max_delay)\n\n        if elapsed \u003C delay:\n            time.sleep(delay - elapsed)\n\n        self.last_request_per_domain[domain] = time.time()\n```\n\n## Ethical considerations\n\n- Always check `robots.txt` before scraping\n- Respect rate limits and add delays between requests\n- Don't scrape personal data without consent\n- Cache responses to avoid redundant requests\n- Identify yourself with a descriptive User-Agent when appropriate\n- Stop if you receive explicit blocking signals\n","# Web scraping methodology\n\nPatterns for reliable, ethical web scraping with fallback strategies and anti-bot handling.\n\n## Scraping cascade architecture\n\nImplement multiple extraction strategies with automatic fallback:\n\n```python\nfrom abc import ABC, abstractmethod\nfrom typing import Optional\nimport requests\nfrom bs4 import BeautifulSoup\nimport trafilatura\n\n#for .py files\nfrom playwright.sync_api import sync_playwright\nfrom playwright_stealth import stealth_sync\n\n#for .ipynb files\nimport asyncio\nfrom playwright.async_api import async_playwright\n\nclass ScrapingResult:\n    def __init__(self, content: str, title: str, method: str):\n        self.content = content\n        self.title = title\n        self.method = method  # Track which method succeeded\n\nclass Scraper(ABC):\n    @abstractmethod\n    def fetch(self, url: str) -> Optional[ScrapingResult]: ...\n\nclass TrafilaturaСscraper(Scraper):\n    \"\"\"Fast, lightweight extraction for standard articles.\"\"\"\n\n    def fetch(self, url: str) -> Optional[ScrapingResult]:\n        try:\n            downloaded = trafilatura.fetch_url(url)\n            if not downloaded:\n                return None\n\n            content = trafilatura.extract(\n                downloaded,\n                include_comments=False,\n                include_tables=True,\n                favor_recall=True\n            )\n\n            if not content or len(content) \u003C 100:\n                return None\n\n            # Extract title separately\n            soup = BeautifulSoup(downloaded, 'html.parser')\n            title = soup.find('title')\n            title_text = title.get_text() if title else ''\n\n            return ScrapingResult(content, title_text, 'trafilatura')\n        except Exception:\n            return None\n\nclass RequestsScraper(Scraper):\n    \"\"\"HTTP requests with rotating user agents.\"\"\"\n\n    USER_AGENTS = [\n        'Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\u002F537.36',\n        'Mozilla\u002F5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\u002F537.36',\n        'Mozilla\u002F5.0 (X11; Linux x86_64) AppleWebKit\u002F537.36',\n    ]\n\n    def fetch(self, url: str) -> Optional[ScrapingResult]:\n        import random\n\n        headers = {\n            'User-Agent': random.choice(self.USER_AGENTS),\n            'Accept': 'text\u002Fhtml,application\u002Fxhtml+xml',\n            'Accept-Language': 'en-US,en;q=0.9',\n        }\n\n        try:\n            response = requests.get(url, headers=headers, timeout=30)\n            response.raise_for_status()\n\n            soup = BeautifulSoup(response.text, 'html.parser')\n\n            # Remove script\u002Fstyle elements\n            for element in soup(['script', 'style', 'nav', 'footer', 'aside']):\n                element.decompose()\n\n            # Find main content\n            main = soup.find('main') or soup.find('article') or soup.find('body')\n            content = main.get_text(separator='\\n', strip=True) if main else ''\n\n            title = soup.find('title')\n            title_text = title.get_text() if title else ''\n\n            if len(content) \u003C 100:\n                return None\n\n            return ScrapingResult(content, title_text, 'requests')\n        except Exception:\n            return None\n\nclass PlaywrightScraper(Scraper):\n    \"\"\"Heavy JavaScript rendering with stealth mode for anti-bot bypass.\"\"\"\n\n    def fetch(self, url: str) -> Optional[ScrapingResult]:\n        try:\n            with sync_playwright() as p:\n                browser = p.chromium.launch(headless=True)\n                context = browser.new_context(\n                    viewport={'width': 1920, 'height': 1080},\n                    user_agent='Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\u002F537.36'\n                )\n                page = context.new_page()\n\n                # Apply stealth to avoid detection\n                stealth_sync(page)\n\n                page.goto(url, wait_until='networkidle', timeout=60000)\n\n                # Wait for content to load\n                page.wait_for_timeout(2000)\n\n                # Extract content\n                content = page.evaluate('''() => {\n                    const article = document.querySelector('article, main, .content, #content');\n                    return article ? article.innerText : document.body.innerText;\n                }''')\n\n                title = page.title()\n\n                browser.close()\n\n                if len(content) \u003C 100:\n                    return None\n\n                return ScrapingResult(content, title, 'playwright')\n        except Exception:\n            return None\n\nclass PlaywrightScraperAsync:\n    \"\"\"Async Playwright scraper for Jupyter notebooks (.ipynb files).\n    \n    Jupyter notebooks run their own event loop, so sync Playwright won't work.\n    Use this async version with `await` in notebook cells.\n    \"\"\"\n\n    async def fetch(self, url: str) -> Optional[ScrapingResult]:\n        try:\n            async with async_playwright() as p:\n                browser = await p.chromium.launch(headless=True)\n                context = await browser.new_context(\n                    viewport={'width': 1920, 'height': 1080},\n                    user_agent='Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\u002F537.36'\n                )\n                page = await context.new_page()\n\n                # Note: playwright-stealth async version\n                # from playwright_stealth import stealth_async\n                # await stealth_async(page)\n\n                await page.goto(url, wait_until='networkidle', timeout=60000)\n\n                # Wait for content to load\n                await page.wait_for_timeout(2000)\n\n                # Extract content\n                content = await page.evaluate('''() => {\n                    const article = document.querySelector('article, main, .content, #content');\n                    return article ? article.innerText : document.body.innerText;\n                }''')\n\n                title = await page.title()\n\n                await browser.close()\n\n                if len(content) \u003C 100:\n                    return None\n\n                return ScrapingResult(content, title, 'playwright_async')\n        except Exception:\n            return None\n\n# Usage in Jupyter notebook cells:\n# scraper = PlaywrightScraperAsync()\n# result = await scraper.fetch('https:\u002F\u002Fexample.com')\n\nclass ScrapingCascade:\n    \"\"\"Try multiple scrapers in order until one succeeds.\"\"\"\n\n    def __init__(self):\n        self.scrapers = [\n            TrafilaturaСscraper(),\n            RequestsScraper(),\n            PlaywrightScraper(),\n        ]\n\n    def fetch(self, url: str) -> Optional[ScrapingResult]:\n        for scraper in self.scrapers:\n            result = scraper.fetch(url)\n            if result:\n                return result\n        return None\n```\n\n## Undocumented APIs\n\n### Finding undocumented APIs\n\nUse browser developer tools to discover APIs:\n\n1. **Open developer tools** (right-click → Inspect, or F12)\n2. **Go to the Network tab** to monitor all requests\n3. **Filter by Fetch\u002FXHR** to show only API calls\n4. **Trigger the action** you want to capture (search, scroll, click)\n5. **Analyze the response** — usually JSON with key-value pairs\n6. **Copy as cURL** (right-click the request)\n7. **Convert to code** using [curlconverter.com](https:\u002F\u002Fcurlconverter.com\u002F)\n\n### Stripping down API requests\n\nWhen you copy a cURL from dev tools, it includes many parameters. Strip it down by:\n\n1. **Remove unnecessary cookies** — test without them first\n2. **Keep authentication tokens** if required\n3. **Identify the input parameters** you can modify (like `prefix` for search terms)\n4. **Test parameter values** — some expire, so periodically verify\n\n### Example: Reverse-engineering an autocomplete API\n\n```python\nimport requests\nimport time\n\ndef search_suggestions(keyword: str) -> dict:\n    \"\"\"\n    Get autocompleted search suggestions from an undocumented API.\n    Stripped down from browser dev tools capture.\n    \"\"\"\n    headers = {\n        'User-Agent': 'Mozilla\u002F5.0 (Macintosh; Intel Mac OS X 10.15; rv:100.0) Gecko\u002F20100101 Firefox\u002F100.0',\n        'Accept': 'application\u002Fjson, text\u002Fjavascript, *\u002F*; q=0.01',\n        'Accept-Language': 'en-US,en;q=0.5',\n    }\n\n    params = {\n        'prefix': keyword,\n        'suggestion-type': ['WIDGET', 'KEYWORD'],\n        'alias': 'aps',\n        'plain-mid': '1',\n    }\n\n    response = requests.get(\n        'https:\u002F\u002Fcompletion.amazon.com\u002Fapi\u002F2017\u002Fsuggestions',\n        params=params,\n        headers=headers\n    )\n    return response.json()\n\n# Collect suggestions for multiple keywords\nkeywords = ['a', 'b', 'cookie', 'sock']\ndata = []\n\nfor keyword in keywords:\n    suggestions = search_suggestions(keyword)\n    suggestions['search_word'] = keyword  # track seed keyword\n    time.sleep(1)  # rate limit yourself\n    data.extend(suggestions.get('suggestions', []))\n```\n*Source: [Leon Yin, \"Finding Undocumented APIs,\" Inspect Element](https:\u002F\u002Finspectelement.org\u002Fapis.html), 2023*\n\n## Poison pill detection\n\nDetect paywalls, anti-bot pages, and other failures:\n\n```python\nfrom dataclasses import dataclass\nfrom enum import Enum\nimport re\n\nclass PoisonPillType(Enum):\n    PAYWALL = 'paywall'\n    CAPTCHA = 'captcha'\n    RATE_LIMIT = 'rate_limit'\n    CLOUDFLARE = 'cloudflare'\n    LOGIN_REQUIRED = 'login_required'\n    NOT_FOUND = 'not_found'\n    NONE = 'none'\n\n@dataclass\nclass PoisonPillResult:\n    detected: bool\n    type: PoisonPillType\n    confidence: float\n    details: str\n\nclass PoisonPillDetector:\n    PATTERNS = {\n        PoisonPillType.PAYWALL: [\n            r'subscribe to continue',\n            r'subscription required',\n            r'become a member',\n            r'sign up to read',\n            r'you\\'ve reached your limit',\n            r'article limit reached',\n        ],\n        PoisonPillType.CAPTCHA: [\n            r'verify you are human',\n            r'captcha',\n            r'robot verification',\n            r'prove you\\'re not a robot',\n        ],\n        PoisonPillType.RATE_LIMIT: [\n            r'too many requests',\n            r'rate limit exceeded',\n            r'slow down',\n            r'429',\n        ],\n        PoisonPillType.CLOUDFLARE: [\n            r'checking your browser',\n            r'cloudflare',\n            r'ddos protection',\n            r'please wait while we verify',\n        ],\n        PoisonPillType.LOGIN_REQUIRED: [\n            r'sign in to continue',\n            r'log in required',\n            r'create an account',\n        ],\n    }\n\n    PAYWALL_DOMAINS = {\n        'nytimes.com': PoisonPillType.PAYWALL,\n        'wsj.com': PoisonPillType.PAYWALL,\n        'washingtonpost.com': PoisonPillType.PAYWALL,\n        'ft.com': PoisonPillType.PAYWALL,\n        'bloomberg.com': PoisonPillType.PAYWALL,\n    }\n\n    def detect(self, url: str, content: str, status_code: int = 200) -> PoisonPillResult:\n        # Check status code\n        if status_code == 429:\n            return PoisonPillResult(True, PoisonPillType.RATE_LIMIT, 1.0, 'HTTP 429')\n        if status_code == 403:\n            return PoisonPillResult(True, PoisonPillType.CLOUDFLARE, 0.8, 'HTTP 403')\n        if status_code == 404:\n            return PoisonPillResult(True, PoisonPillType.NOT_FOUND, 1.0, 'HTTP 404')\n\n        # Check known paywall domains\n        from urllib.parse import urlparse\n        domain = urlparse(url).netloc.replace('www.', '')\n        for paywall_domain, pill_type in self.PAYWALL_DOMAINS.items():\n            if paywall_domain in domain:\n                # Check if content is suspiciously short (paywall truncation)\n                if len(content) \u003C 500:\n                    return PoisonPillResult(True, pill_type, 0.9, f'Short content from {domain}')\n\n        # Pattern matching\n        content_lower = content.lower()\n        for pill_type, patterns in self.PATTERNS.items():\n            for pattern in patterns:\n                if re.search(pattern, content_lower):\n                    return PoisonPillResult(True, pill_type, 0.7, f'Pattern match: {pattern}')\n\n        return PoisonPillResult(False, PoisonPillType.NONE, 0.0, '')\n```\n\n## Social media scraping\n\n### YouTube with yt-dlp\n\n```python\nimport yt_dlp\nfrom pathlib import Path\n\ndef download_video_metadata(url: str) -> dict:\n    \"\"\"Extract metadata without downloading video.\"\"\"\n    ydl_opts = {\n        'skip_download': True,\n        'quiet': True,\n        'no_warnings': True,\n    }\n\n    with yt_dlp.YoutubeDL(ydl_opts) as ydl:\n        info = ydl.extract_info(url, download=False)\n        return {\n            'title': info.get('title'),\n            'description': info.get('description'),\n            'duration': info.get('duration'),\n            'upload_date': info.get('upload_date'),\n            'view_count': info.get('view_count'),\n            'channel': info.get('channel'),\n            'thumbnail': info.get('thumbnail'),\n        }\n\ndef download_video(url: str, output_dir: Path, audio_only: bool = False) -> Path:\n    \"\"\"Download video or audio.\"\"\"\n    output_template = str(output_dir \u002F '%(title)s.%(ext)s')\n\n    ydl_opts = {\n        'outtmpl': output_template,\n        'quiet': True,\n    }\n\n    if audio_only:\n        ydl_opts['format'] = 'bestaudio\u002Fbest'\n        ydl_opts['postprocessors'] = [{\n            'key': 'FFmpegExtractAudio',\n            'preferredcodec': 'mp3',\n        }]\n\n    with yt_dlp.YoutubeDL(ydl_opts) as ydl:\n        info = ydl.extract_info(url, download=True)\n        filename = ydl.prepare_filename(info)\n        if audio_only:\n            filename = filename.rsplit('.', 1)[0] + '.mp3'\n        return Path(filename)\n\ndef get_transcript(url: str) -> list[dict]:\n    \"\"\"Extract auto-generated or manual subtitles.\"\"\"\n    ydl_opts = {\n        'skip_download': True,\n        'writesubtitles': True,\n        'writeautomaticsub': True,\n        'subtitleslangs': ['en'],\n        'quiet': True,\n    }\n\n    with yt_dlp.YoutubeDL(ydl_opts) as ydl:\n        info = ydl.extract_info(url, download=False)\n\n        # Check for subtitles\n        subtitles = info.get('subtitles', {})\n        auto_captions = info.get('automatic_captions', {})\n\n        # Prefer manual subtitles over auto-generated\n        subs = subtitles.get('en') or auto_captions.get('en')\n        if not subs:\n            return []\n\n        # Get the vtt or json format\n        for sub in subs:\n            if sub['ext'] in ['vtt', 'json3']:\n                # Download and parse subtitle file\n                # ... implementation depends on format\n                pass\n\n        return []\n```\n\n### Instagram with instaloader\n\n```python\nimport instaloader\nfrom pathlib import Path\n\nclass InstagramScraper:\n    def __init__(self, username: str = None, session_file: str = None):\n        self.loader = instaloader.Instaloader(\n            download_videos=True,\n            download_video_thumbnails=False,\n            download_geotags=False,\n            download_comments=False,\n            save_metadata=True,\n            compress_json=False,\n        )\n\n        if session_file and Path(session_file).exists():\n            self.loader.load_session_from_file(username, session_file)\n\n    def get_profile_posts(self, username: str, limit: int = 50) -> list[dict]:\n        \"\"\"Get recent posts from a profile.\"\"\"\n        profile = instaloader.Profile.from_username(self.loader.context, username)\n        posts = []\n\n        for i, post in enumerate(profile.get_posts()):\n            if i >= limit:\n                break\n\n            posts.append({\n                'shortcode': post.shortcode,\n                'url': f'https:\u002F\u002Finstagram.com\u002Fp\u002F{post.shortcode}\u002F',\n                'caption': post.caption,\n                'timestamp': post.date_utc.isoformat(),\n                'likes': post.likes,\n                'comments': post.comments,\n                'is_video': post.is_video,\n                'video_url': post.video_url if post.is_video else None,\n            })\n\n        return posts\n\n    def download_post(self, shortcode: str, output_dir: Path):\n        \"\"\"Download a single post's media.\"\"\"\n        post = instaloader.Post.from_shortcode(self.loader.context, shortcode)\n        self.loader.download_post(post, target=str(output_dir))\n```\n\n### TikTok with yt-dlp\n\n```python\ndef scrape_tiktok_profile(username: str, output_dir: Path, limit: int = 50) -> list[dict]:\n    \"\"\"Scrape TikTok profile videos.\"\"\"\n    profile_url = f'https:\u002F\u002Ftiktok.com\u002F@{username}'\n\n    ydl_opts = {\n        'quiet': True,\n        'extract_flat': True,  # Don't download, just get info\n        'playlistend': limit,\n    }\n\n    with yt_dlp.YoutubeDL(ydl_opts) as ydl:\n        info = ydl.extract_info(profile_url, download=False)\n        videos = []\n\n        for entry in info.get('entries', []):\n            videos.append({\n                'id': entry.get('id'),\n                'title': entry.get('title'),\n                'url': entry.get('url'),\n                'timestamp': entry.get('timestamp'),\n                'view_count': entry.get('view_count'),\n            })\n\n        return videos\n\ndef download_tiktok_video(url: str, output_dir: Path) -> Path:\n    \"\"\"Download a single TikTok video.\"\"\"\n    ydl_opts = {\n        'outtmpl': str(output_dir \u002F '%(id)s.%(ext)s'),\n        'quiet': True,\n    }\n\n    with yt_dlp.YoutubeDL(ydl_opts) as ydl:\n        info = ydl.extract_info(url, download=True)\n        return Path(ydl.prepare_filename(info))\n```\n\n## Request patterns\n\n### Rotating user agents and headers\n\n```python\nimport random\nfrom fake_useragent import UserAgent\n\nclass RequestManager:\n    def __init__(self):\n        self.ua = UserAgent()\n        self.session = requests.Session()\n\n    def get_headers(self) -> dict:\n        return {\n            'User-Agent': self.ua.random,\n            'Accept': 'text\u002Fhtml,application\u002Fxhtml+xml,application\u002Fxml;q=0.9,*\u002F*;q=0.8',\n            'Accept-Language': 'en-US,en;q=0.5',\n            'Accept-Encoding': 'gzip, deflate, br',\n            'DNT': '1',\n            'Connection': 'keep-alive',\n            'Upgrade-Insecure-Requests': '1',\n        }\n\n    def fetch(self, url: str, retry_count: int = 3) -> requests.Response:\n        for attempt in range(retry_count):\n            try:\n                response = self.session.get(\n                    url,\n                    headers=self.get_headers(),\n                    timeout=30\n                )\n                response.raise_for_status()\n                return response\n            except requests.RequestException as e:\n                if attempt == retry_count - 1:\n                    raise\n                time.sleep(2 ** attempt)  # Exponential backoff\n```\n\n### Respectful scraping with delays\n\n```python\nimport time\nimport random\nfrom urllib.parse import urlparse\n\nclass PoliteRequester:\n    def __init__(self, min_delay: float = 1.0, max_delay: float = 3.0):\n        self.min_delay = min_delay\n        self.max_delay = max_delay\n        self.last_request_per_domain = {}\n\n    def wait_for_domain(self, url: str):\n        domain = urlparse(url).netloc\n        last_request = self.last_request_per_domain.get(domain, 0)\n\n        elapsed = time.time() - last_request\n        delay = random.uniform(self.min_delay, self.max_delay)\n\n        if elapsed \u003C delay:\n            time.sleep(delay - elapsed)\n\n        self.last_request_per_domain[domain] = time.time()\n```\n\n## Ethical considerations\n\n- Always check `robots.txt` before scraping\n- Respect rate limits and add delays between requests\n- Don't scrape personal data without consent\n- Cache responses to avoid redundant requests\n- Identify yourself with a descriptive User-Agent when appropriate\n- Stop if you receive explicit blocking signals\n",{"name":5,"description":7},"main","d69096e239f2a89aabc2cd078c47664a9d3b1e11","04-安全渗透\u002Fweb-scraping",3,"https:\u002F\u002Fgithub.com\u002Flza6\u002FArmor-piercing-system-prompts","https:\u002F\u002Fgithub.com\u002Flza6","个人vx：Tf00798\r\n个人✈：@TFGY999","2026-10-08 14:29:22",{"source":8,"skillName":5,"commands":33,"prompt":43,"zipAvailable":44},[34,37,40],{"tool":35,"command":36},"npx","npx skills add lza6\u002FArmor-piercing-system-prompts --skill web-scraping",{"tool":38,"command":39},"bunx","bunx skills add lza6\u002FArmor-piercing-system-prompts --skill web-scraping",{"tool":41,"command":42},"pnpm","pnpm dlx skills add lza6\u002FArmor-piercing-system-prompts --skill web-scraping","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Flza6\u002FArmor-piercing-system-prompts\u002Fblob\u002Fmain\u002F04-安全渗透\u002Fweb-scraping\u002FSKILL.md",true,[],[47,54,61,68,76,83],{"skillNo":48,"name":49,"nameZh":6,"description":50,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":51,"categories":52,"keywords":53,"publishedAt":19},"01M7EXRM12046G0GM1WGMMFXCN","ci-cd-and-automation","Automates CI\u002FCD pipeline setup. Use when setting up or modifying build and deployment pipelines. Use when you need to automate quality gates, configure test runners in CI, or establish deployment strategies.","https:\u002F\u002Fgithub.com\u002Flza6\u002FArmor-piercing-system-prompts\u002Fblob\u002Fmain\u002F05-技能与代理开发\u002Fci-cd-and-automation\u002FSKILL.md",[],[],{"skillNo":55,"name":56,"nameZh":6,"description":57,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":58,"categories":59,"keywords":60,"publishedAt":19},"01M7EXRFYYSJ3Y15AGPDDEWWQD","chrome-devtools","Uses Chrome DevTools via MCP for efficient debugging, troubleshooting and browser automation. Use when debugging web pages, automating browser interactions, analyzing performance, or inspecting network requests. This skill does not apply to `--slim` mode (MCP configuration).","https:\u002F\u002Fgithub.com\u002Flza6\u002FArmor-piercing-system-prompts\u002Fblob\u002Fmain\u002F05-技能与代理开发\u002Fchrome-devtools\u002FSKILL.md",[],[],{"skillNo":62,"name":63,"nameZh":6,"description":64,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":65,"categories":66,"keywords":67,"publishedAt":19},"01M7EXR6EYPE4X5X1BTAE6QJRV","chrome-cdp","Drive a real Chrome via the Chrome DevTools Protocol (CDP). Ensures a\ndebuggable Chrome is running (launching it if needed) so the agent can\nnavigate, read page content, inspect the DOM, run JavaScript, and take\nscreenshots — without the user typing any launch command. Includes a\nfirst-class \"review a live running app\" flow: point Chrome at the app under\ndevelopment and inspect\u002Fscreenshot the REAL page (what a static-artifact tool\ncannot do). Backed by Glimpse and the chrome-devtools MCP server. Use when\nasked to \"use chrome\", \"open chrome\", \"drive my browser\", \"read the page in\nchrome\", \"review the running app\", \"screenshot the app\", or \"\u002Fchrome-cdp\".","https:\u002F\u002Fgithub.com\u002Flza6\u002FArmor-piercing-system-prompts\u002Fblob\u002Fmain\u002F05-技能与代理开发\u002Fchrome-cdp\u002FSKILL.md",[],[],{"skillNo":69,"name":70,"nameZh":6,"description":71,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":72,"licenseFileUrl":6,"sourceHtmlUrl":73,"categories":74,"keywords":75,"publishedAt":19},"01M7EXR1Y7E79KQKG4J798XGH6","check-identity-pack","Run an AFP 100-point or AUSTRAC safe-harbour identity check over a set of documents, and report exactly what's missing. Use when the user asks to check identity documents, verify someone's ID for onboarding, or assess whether a document pack satisfies Australian identity requirements.","Apache-2.0","https:\u002F\u002Fgithub.com\u002Flza6\u002FArmor-piercing-system-prompts\u002Fblob\u002Fmain\u002F05-技能与代理开发\u002Fcheck-identity-pack\u002FSKILL.md",[],[],{"skillNo":77,"name":78,"nameZh":6,"description":79,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":80,"categories":81,"keywords":82,"publishedAt":19},"01M7EXQXW7C7PGREJW9ZZTZ16H","changelog","Auto-generates a changelog from git commits, sprint data, and design documents. Produces both internal and player-facing versions.","https:\u002F\u002Fgithub.com\u002Flza6\u002FArmor-piercing-system-prompts\u002Fblob\u002Fmain\u002F05-技能与代理开发\u002Fchangelog\u002FSKILL.md",[],[],{"skillNo":84,"name":85,"nameZh":6,"description":86,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":87,"categories":88,"keywords":89,"publishedAt":19},"01M7EXQMH2ZME3ANN8Q8MEWPFB","cellxgene-census","Query the CZ CELLxGENE Census programmatically for versioned public single-cell and spatial transcriptomics data. Use when you need population-scale cell metadata, gene expression slices, Census summary counts, source H5AD URIs\u002Fdownloads, embeddings, spatial Census data, or reference atlas comparisons across organisms, tissues, diseases, assays, and cell types. For analyzing your own local single-cell data use scanpy, anndata, or scvi-tools.","https:\u002F\u002Fgithub.com\u002Flza6\u002FArmor-piercing-system-prompts\u002Fblob\u002Fmain\u002F05-技能与代理开发\u002Fcellxgene-census\u002FSKILL.md",[],[]]