基于Playwright与FastAPI构建高可用GitHub趋势爬虫API服务

📅 2026/6/23 14:58:01
基于Playwright与FastAPI构建高可用GitHub趋势爬虫API服务
1. 项目概述为什么我们需要一个“高可用”的GitHub趋势爬虫每次想看看GitHub上最近有什么新玩意儿火起来了你是不是也习惯性地打开GitHub Trending页面手动翻看确实直观但如果你想定时追踪、分析数据趋势或者想把它集成到自己的仪表盘、日报系统里手动操作就太原始了。市面上虽然有一些现成的API或爬虫脚本但往往要么不稳定网站结构一变就挂要么功能单一只能爬今天的数据要么就是性能堪忧一跑就把自己IP给封了。所以这个项目的核心目标就是构建一个稳定、灵活、易于集成的GitHub趋势项目信息获取服务。我们不满足于写一个“一次性”的脚本而是要打造一个生产级的解决方案。它需要具备几个关键特性高可靠性能应对GitHub页面的细微改动和反爬机制服务化通过标准的API接口提供数据方便其他应用调用可维护性代码结构清晰易于扩展和部署。为了实现这个目标技术选型上我们放弃了传统的requestsBeautifulSoup组合也绕过了对动态页面处理略显笨重的Selenium而是选择了更现代的Playwright。它是一个由微软开源的浏览器自动化测试框架但用来做爬虫简直是“降维打击”——它支持无头浏览器能完美执行JavaScript模拟真人操作并且速度飞快。后端API服务则选用FastAPI这个Python异步Web框架以高性能和直观的API文档自动生成而闻名非常适合快速构建数据接口。最终我们将得到一个爬虫核心模块和一个包裹它的API服务你可以轻松地通过HTTP请求获取格式化好的GitHub趋势数据甚至可以部署到服务器上供团队内部使用。2. 核心工具链深度解析Playwright与FastAPI为何是绝配2.1 Playwright不只是自动化测试工具很多人第一次听说Playwright都是在测试领域。但如果你只把它当成Selenium的替代品那就太小看它了。在爬虫场景下Playwright有几个碾压级的优势。首先它原生支持多浏览器引擎Chromium, Firefox, WebKit。这意味着你可以选择最合适的浏览器来执行任务甚至可以在同一脚本中切换。对于GitHub这种主流网站使用Chromium通常就能获得最好的兼容性和性能。其次它的自动等待机制是爬虫开发者的福音。传统爬虫需要自己写time.sleep或者显式等待元素出现既不稳定又低效。Playwright的page.wait_for_selector、page.wait_for_load_state等方法能智能地等待页面元素或状态大大减少了因网络波动或页面加载慢导致的爬取失败。最关键的是Playwright处理动态内容的能力。GitHub Trending页面虽然看似静态但它的项目语言颜色标签、部分交互元素都是动态生成的。用requests直接抓取HTML你会丢失这些信息。Playwright启动一个真实的浏览器环境JavaScript执行完毕后你拿到的是完整的、渲染好的DOM树抓取数据准确无误。此外Playwright的网络拦截Route和请求模拟功能允许我们优化爬取过程。比如我们可以拦截并阻止页面加载图片、CSS等非必要资源显著提升爬取速度。也可以轻松地修改请求头模拟更真实的浏览器指纹。2.2 FastAPI为数据接口而生的现代框架爬虫爬到了数据怎么提供出去写个简单的Flask应用可以但FastAPI能做得更优雅、更高效。FastAPI基于Python的异步asyncio库和类型提示构建这正好与Playwright的异步API珠联璧合。性能优势FastAPI的异步特性意味着它能够高效地处理大量并发请求。当我们的爬虫API被频繁调用时异步处理可以避免线程阻塞用更少的资源服务更多的请求。这与Playwright的异步操作模式async/await完美契合整个数据流从爬取到响应都可以在一个高效的异步管道中完成。开发体验FastAPI利用Python类型提示提供了无与伦比的开发体验和代码可靠性。你定义好请求和响应的数据模型使用PydanticFastAPI会自动进行数据验证、序列化并生成交互式的API文档Swagger UI和ReDoc。这意味着你写完爬虫和数据模型一个功能完整、文档齐全的API就基本完成了前后端协作非常顺畅。依赖注入系统FastAPI强大的依赖注入系统让我们能轻松管理爬虫的核心实例。比如我们可以创建一个全局的Playwright浏览器实例通过依赖注入的方式提供给每个API端点使用避免重复启动浏览器的开销实现连接池的效果。2.3 技术栈协同工作流整个系统的工作流非常清晰用户向FastAPI服务发起一个HTTP请求例如GET /trending?langpythonsincedaily。FastAPI接收请求解析参数并调用注入的“爬虫服务”依赖项。爬虫服务内部使用Playwright启动或复用浏览器导航到对应的GitHub Trending URL如https://github.com/trending/python?sincedaily。Playwright模拟浏览器行为加载并渲染完整页面然后通过选择器定位到仓库名、星数、fork数、描述等元素。爬虫服务将从页面提取的原始数据清洗、转换成结构化的Python对象如Pydantic模型。FastAPI将这个对象序列化为JSON并返回给用户。这个流程中Playwright负责“攻”获取数据FastAPI负责“守”提供数据两者通过异步编程模型紧密结合构建出一个响应迅速、稳定可靠的服务。3. 项目实战从零搭建爬虫核心模块3.1 环境准备与依赖安装首先确保你的Python版本在3.8以上。然后我们使用uv或pip来管理依赖。uv是一个用Rust写的极速Python包安装器和解析器速度远超pip强烈推荐。# 使用uv初始化项目并安装核心依赖 uv init github-trending-api cd github-trending-api uv add playwright fastapi uvicorn httpx pydantic # 安装Playwright所需的浏览器这里安装Chromium uv run playwright install chromium这里解释一下依赖playwright: 主库用于浏览器自动化。fastapi: Web框架。uvicorn: ASGI服务器用于运行FastAPI应用。httpx: 可选的异步HTTP客户端可用于健康检查或调用其他API。pydantic: 数据验证和设置管理FastAPI的核心依赖之一。注意playwright install chromium这一步可能会下载几百MB的浏览器二进制文件请确保网络通畅。你也可以通过环境变量PLAYWRIGHT_DOWNLOAD_HOST配置镜像源来加速下载。3.2 设计数据模型Pydantic在写爬虫逻辑之前我们先定义好数据的“形状”。这能让我们的代码更清晰并且FastAPI能自动利用这些模型生成API文档。# schemas.py from typing import Optional, List from pydantic import BaseModel, HttpUrl class Repository(BaseModel): 单个GitHub仓库的模型 rank: int # 排名 name: str # 仓库全名如 “owner/repo” url: HttpUrl # 仓库主页URL description: Optional[str] None # 描述可能为空 language: Optional[str] None # 主要编程语言 language_color: Optional[str] None # GitHub上该语言对应的颜色代码 stars: int # 星标总数 forks: int # Fork总数 stars_today: int # 今日新增星标数 built_by: List[str] [] # 主要贡献者头像列表URL class TrendingResponse(BaseModel): API响应模型 since: str # 时间范围daily, weekly, monthly language: Optional[str] None # 编程语言筛选 repos: List[Repository] # 仓库列表使用HttpUrl类型Pydantic会自动验证字符串是否为有效的URL。Optional字段表示该字段可能为None。定义好模型后后续的爬虫代码目标就是填充这个Repository对象。3.3 编写Playwright爬虫核心类接下来是重头戏爬虫类。我们将采用面向对象的设计封装所有与Playwright交互的细节。# crawler.py import asyncio from typing import Optional, List from playwright.async_api import async_playwright, Browser, Page from schemas import Repository, TrendingResponse import logging logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) class GitHubTrendingCrawler: def __init__(self, headless: bool True): 初始化爬虫 :param headless: 是否使用无头模式。生产环境建议为True。 self.headless headless self.browser: Optional[Browser] None self.playwright_instance None async def start(self): 启动Playwright和浏览器实例。建议在应用启动时调用一次。 self.playwright_instance await async_playwright().start() # 使用Chromium可配置代理或其他启动参数 self.browser await self.playwright_instance.chromium.launch( headlessself.headless, args[--disable-blink-featuresAutomationControlled] # 隐藏自动化特征 ) logger.info(Playwright浏览器实例已启动) async def stop(self): 关闭浏览器和Playwright。在应用关闭时调用。 if self.browser: await self.browser.close() if self.playwright_instance: await self.playwright_instance.stop() logger.info(Playwright资源已释放) async def fetch_trending(self, language: str , since: str daily) - TrendingResponse: 获取GitHub趋势数据 :param language: 编程语言如python, javascript。空字符串表示所有语言。 :param since: 时间范围daily, weekly, monthly。 :return: TrendingResponse对象 if not self.browser: raise RuntimeError(爬虫未启动请先调用start()方法) # 构造URL base_url https://github.com/trending url f{base_url}/{language}?since{since} if language else f{base_url}?since{since} page: Page await self.browser.new_page() # 关键步骤1设置合理的请求头和视口模拟普通浏览器 await page.set_viewport_size({width: 1920, height: 1080}) await page.set_extra_http_headers({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept-Language: en-US,en;q0.9, }) # 关键步骤2拦截并阻止不必要的资源加载大幅提升速度 await page.route(**/*.{png,jpg,jpeg,gif,css,woff,woff2}, lambda route: route.abort()) logger.info(f正在爬取: {url}) try: # 导航到页面等待主要内容加载完成 await page.goto(url, wait_untilnetworkidle) # 等待网络基本空闲 # 显式等待趋势列表容器出现这是更稳健的做法 await page.wait_for_selector(article.Box-row, timeout10000) # 关键步骤3执行页面内JavaScript获取语言颜色如果需要 # GitHub的语言颜色是通过CSS变量定义的我们可以用JS提取 language_color_map await page.evaluate(() { const map {}; const styleSheets document.styleSheets; for (let sheet of styleSheets) { try { const rules sheet.cssRules || sheet.rules; for (let rule of rules) { if (rule.selectorText rule.selectorText.startsWith(.language-color-)) { const lang rule.selectorText.split(-).pop(); map[lang] rule.style.backgroundColor; } } } catch(e) {} } return map; }) # 提取仓库列表 repo_elements await page.query_selector_all(article.Box-row) repos [] for index, repo_element in enumerate(repo_elements): try: repo await self._parse_repo_element(repo_element, index 1, language_color_map) repos.append(repo) except Exception as e: logger.warning(f解析第{index1}个仓库时出错: {e}) continue # 跳过解析失败的单个仓库不影响整体 logger.info(f成功爬取到 {len(repos)} 个仓库) return TrendingResponse(sincesince, languagelanguage if language else None, reposrepos) except Exception as e: logger.error(f爬取过程发生错误: {e}) raise # 将异常向上抛由API层处理 finally: await page.close() # 确保页面被关闭释放资源 async def _parse_repo_element(self, element, rank: int, color_map: dict) - Repository: 解析单个仓库元素内部方法 # 使用Playwright的ElementHandle方法提取数据比纯文本解析更可靠 name_elem await element.query_selector(h2 a) # 获取href属性并补全为完整URL relative_url await name_elem.get_attribute(href) repo_url fhttps://github.com{relative_url} repo_name (await name_elem.text_content()).strip().replace(\n, ).replace( , ) # 描述可能不存在 desc_elem await element.query_selector(p) description (await desc_elem.text_content()).strip() if desc_elem else None # 提取编程语言和颜色 lang_elem await element.query_selector([itempropprogrammingLanguage]) language (await lang_elem.text_content()).strip() if lang_elem else None language_color color_map.get(language.lower()) if language else None # 提取星标、Fork、今日新增星标数 - 这里需要处理复杂的文本 star_link await element.query_selector(a[href$/stargazers]) fork_link await element.query_selector(a[href$/forks]) stars_today_span await element.query_selector(span.d-inline-block.float-sm-right) # 辅助函数从文本中提取数字 def extract_number(text): if not text: return 0 import re # 处理“1.2k”这样的格式 num_text text.strip().replace(,, ) if k in num_text.lower(): return int(float(num_text.lower().replace(k, )) * 1000) match re.search(r(\d), num_text) return int(match.group(1)) if match else 0 stars_text await star_link.text_content() if star_link else 0 forks_text await fork_link.text_content() if fork_link else 0 stars_today_text await stars_today_span.text_content() if stars_today_span else 0 stars extract_number(stars_text) forks extract_number(forks_text) stars_today extract_number(stars_today_text) # 提取贡献者头像可选 avatar_links await element.query_selector_all(a[data-hovercard-typeuser] img.avatar) built_by [await avatar.get_attribute(src) for avatar in avatar_links] return Repository( rankrank, namerepo_name, urlrepo_url, descriptiondescription, languagelanguage, language_colorlanguage_color, starsstars, forksforks, stars_todaystars_today, built_bybuilt_by )这个GitHubTrendingCrawler类封装了完整的生命周期和爬取逻辑。start()和stop()用于管理浏览器实例避免为每个请求都启动/关闭浏览器这是实现高性能的关键。fetch_trending是核心方法它处理URL构建、页面导航、资源拦截、数据提取和解析的全过程。实操心得在page.goto中使用wait_untilnetworkidle是个不错的默认选择但它有时会等待过久。对于GitHub Trending这种页面可以尝试wait_untildomcontentloaded然后结合page.wait_for_selector等待特定元素通常能更快地开始解析。另外通过page.route拦截图片、字体等资源在我的测试中能将页面加载时间减少60%以上对提升爬虫效率至关重要。4. 构建FastAPI API服务与异步集成有了爬虫核心现在我们需要用FastAPI给它包上一层HTTP外衣。4.1 创建FastAPI应用与依赖注入# main.py from contextlib import asynccontextmanager from fastapi import FastAPI, Depends, HTTPException, Query from fastapi.middleware.cors import CORSMiddleware from typing import Optional import asyncio from crawler import GitHubTrendingCrawler from schemas import TrendingResponse # 全局爬虫实例 _crawler: Optional[GitHubTrendingCrawler] None asynccontextmanager async def lifespan(app: FastAPI): 管理应用生命周期启动时初始化爬虫关闭时清理资源 global _crawler # 启动 _crawler GitHubTrendingCrawler(headlessTrue) # 生产环境用无头模式 await _crawler.start() logger.info(GitHub趋势爬虫服务已启动) yield # 关闭 if _crawler: await _crawler.stop() logger.info(GitHub趋势爬虫服务已关闭) app FastAPI( titleGitHub Trending API Service, description一个高可用的GitHub趋势项目爬虫与API服务基于Playwright和FastAPI构建。, version1.0.0, lifespanlifespan # 使用 lifespan 上下文管理器 ) # 添加CORS中间件方便前端调用 app.add_middleware( CORSMiddleware, allow_origins[*], # 生产环境应指定具体域名 allow_credentialsTrue, allow_methods[*], allow_headers[*], ) def get_crawler() - GitHubTrendingCrawler: 依赖注入函数提供爬虫实例 if _crawler is None: raise HTTPException(status_code503, detail爬虫服务未就绪) return _crawler app.get(/, tags[Root]) async def root(): 服务根路径返回基础信息 return { service: GitHub Trending API, status: running, docs: /docs, endpoints: { trending: /trending, trending_with_lang: /trending/{language} } } app.get(/trending, response_modelTrendingResponse, tags[Trending]) async def get_trending( language: Optional[str] Query(None, description筛选编程语言例如python, javascript, go), since: str Query(daily, description时间范围daily每日, weekly每周, monthly每月), crawler: GitHubTrendingCrawler Depends(get_crawler) ): 获取GitHub趋势仓库列表。 - **language**: 按编程语言筛选。留空或省略则返回所有语言。 - **since**: 趋势的时间范围。 # 参数验证 if since not in [daily, weekly, monthly]: raise HTTPException(status_code400, detail参数since必须是 daily, weekly 或 monthly) # 处理语言参数API中None表示不筛选但爬虫需要空字符串 lang_for_crawl language if language else try: # 调用爬虫核心功能 result await crawler.fetch_trending(languagelang_for_crawl, sincesince) return result except Exception as e: # 记录详细错误日志但返回给用户的信息要友好 logger.error(fAPI调用爬虫失败: {e}, exc_infoTrue) raise HTTPException(status_code500, detail获取趋势数据失败请稍后重试或检查服务状态) app.get(/health, tags[Health]) async def health_check(crawler: GitHubTrendingCrawler Depends(get_crawler)): 健康检查端点用于监控服务状态 try: # 尝试快速访问GitHub首页检查网络和浏览器状态 page await crawler.browser.new_page() await page.goto(https://github.com, wait_untildomcontentloaded, timeout10000) title await page.title() await page.close() return {status: healthy, github_accessible: True, page_title: title} except Exception as e: logger.error(f健康检查失败: {e}) return {status: unhealthy, error: str(e)}, 503这个main.py文件构建了完整的API服务。关键点在于lifespan上下文管理器它确保了爬虫浏览器实例在FastAPI应用启动时被创建并在应用关闭时被正确清理这是一种资源管理的推荐模式。get_crawler依赖函数使得我们可以在路由函数中方便地获取到爬虫实例。API设计上我们提供了两个主要端点根路径/用于服务发现/trending是核心数据获取接口/health用于健康检查这在部署后非常重要。所有响应都遵循我们之前定义的TrendingResponse模型FastAPI会自动将其转换为JSON并验证数据。4.2 运行与测试API服务使用Uvicorn运行这个应用uvicorn main:app --host 0.0.0.0 --port 8000 --reload打开浏览器访问http://localhost:8000/docs你会看到FastAPI自动生成的交互式Swagger UI文档。你可以直接在页面上尝试调用/trending接口选择参数点击“Execute”就能看到实时的API响应和格式化后的数据。测试示例GET /trending?sinceweekly获取本周所有语言的热门仓库。GET /trending?languagepythonsincedaily获取今日Python语言的热门仓库。返回的数据会是结构清晰的JSON包含了排名、仓库名、URL、描述、语言、星标数等所有信息完全可以直接被前端或其他服务消费。5. 部署与高可用性增强策略一个能在本地跑的服务还不够我们需要考虑如何将它部署到服务器并确保其稳定、可靠地运行。5.1 使用Docker容器化部署Docker能解决环境一致性问题是部署的首选。我们需要编写Dockerfile和docker-compose.yml。# Dockerfile FROM python:3.11-slim WORKDIR /app # 安装系统依赖包括Playwright所需的库 RUN apt-get update apt-get install -y \ wget \ gnupg \ libnss3 \ libatk-bridge2.0-0 \ libdrm2 \ libxkbcommon0 \ libgbm1 \ libasound2 \ libpangocairo-1.0-0 \ libx11-xcb1 \ libxcomposite1 \ libxcursor1 \ libxdamage1 \ libxi6 \ libxtst6 \ rm -rf /var/lib/apt/lists/* # 使用uv安装Python依赖更快更高效 COPY pyproject.toml uv.lock ./ RUN pip install uv uv pip install --system -r pyproject.toml # 安装Playwright的Chromium浏览器 RUN playwright install chromium --with-deps # 复制应用代码 COPY . . # 暴露端口 EXPOSE 8000 # 启动命令 CMD [uvicorn, main:app, --host, 0.0.0.0, --port, 8000]对应的docker-compose.yml可以方便地定义服务# docker-compose.yml version: 3.8 services: github-trending-api: build: . container_name: github-trending-api ports: - 8000:8000 restart: unless-stopped # 容器意外退出时自动重启 # 可以在这里添加环境变量如设置代理等 # environment: # - HTTP_PROXYhttp://your-proxy:port # - HTTPS_PROXYhttp://your-proxy:port # 挂载卷如果需要持久化日志或缓存 # volumes: # - ./logs:/app/logs使用docker-compose up -d即可在后台启动服务。restart: unless-stopped策略提供了基础的高可用性当容器因未知原因崩溃时会自动重启。5.2 使用Nginx作为反向代理在生产环境我们通常不会让Uvicorn直接对外服务。使用Nginx作为反向代理可以提供负载均衡、SSL/TLS终止、静态文件服务、缓存和更好的安全性。# nginx.conf 部分配置 server { listen 80; server_name your-domain.com; # 你的域名 location / { proxy_pass http://localhost:8000; # 转发到FastAPI服务 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 以下两行对WebSocket或长时间连接可能有帮助 proxy_read_timeout 300s; proxy_connect_timeout 75s; } # 如果你申请了SSL证书可以添加下面这个server块并重定向HTTP到HTTPS # listen 443 ssl http2; # ssl_certificate /path/to/your/cert.pem; # ssl_certificate_key /path/to/your/key.pem; # ... 其他SSL配置 } # 可选的添加一个上游块如果你部署了多个实例做负载均衡 # upstream fastapi_backend { # server 127.0.0.1:8000; # server 127.0.0.1:8001; # }5.3 实现缓存与限流机制直接每次请求都去爬GitHub不仅慢而且对GitHub服务器不友好容易触发反爬。我们必须实现缓存。内存缓存简单方案对于小型或个人服务可以使用lru_cache或cachetools库在内存中缓存结果。# 在main.py中增加缓存 from functools import lru_cache from datetime import datetime, timedelta class CacheItem: def __init__(self, data, expiry): self.data data self.expiry expiry class SimpleCache: def __init__(self): self._cache {} def get(self, key): item self._cache.get(key) if item and datetime.now() item.expiry: return item.data else: self._cache.pop(key, None) # 过期删除 return None def set(self, key, data, ttl_seconds300): # 默认缓存5分钟 expiry datetime.now() timedelta(secondsttl_seconds) self._cache[key] CacheItem(data, expiry) # 在FastAPI应用中初始化缓存 cache SimpleCache() # 修改 /trending 端点 app.get(/trending, response_modelTrendingResponse) async def get_trending( language: Optional[str] Query(None), since: str Query(daily), crawler: GitHubTrendingCrawler Depends(get_crawler) ): cache_key ftrending:{language or all}:{since} cached_data cache.get(cache_key) if cached_data: logger.info(f缓存命中: {cache_key}) return cached_data # ... 原有的参数验证和爬取逻辑 ... result await crawler.fetch_trending(languagelang_for_crawl, sincesince) # 存入缓存根据时间范围设置不同的TTL ttl 300 if since daily else 1800 # daily缓存5分钟weekly/monthly缓存30分钟 cache.set(cache_key, result, ttl_secondsttl) return result更高级的缓存对于生产环境建议使用Redis或Memcached这样的外部缓存服务它们支持分布式、数据持久化和更复杂的过期策略。限流为了防止API被滥用可以使用slowapi或fastapi-limiter等中间件为API添加速率限制。# 使用 slowapi 示例 from slowapi import Limiter, _rate_limit_exceeded_handler from slowapi.util import get_remote_address from slowapi.errors import RateLimitExceeded limiter Limiter(key_funcget_remote_address) app.state.limiter limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) app.get(/trending) limiter.limit(10/minute) # 每个IP每分钟10次 async def get_trending(...): # ...5.4 监控与日志一个健壮的服务离不开监控和日志。我们可以将Python的日志输出配置为JSON格式并集成像Sentry这样的错误监控平台。# logging_config.py import json import logging from pythonjsonlogger import jsonlogger logger logging.getLogger() logHandler logging.StreamHandler() formatter jsonlogger.JsonFormatter(%(asctime)s %(name)s %(levelname)s %(message)s) logHandler.setFormatter(formatter) logger.addHandler(logHandler) logger.setLevel(logging.INFO) # 在main.py中导入此配置对于部署在云上的服务可以利用云平台提供的监控如AWS CloudWatch, Google Cloud Logging或自建ELKElasticsearch, Logstash, Kibana栈来收集和分析日志。6. 常见问题排查与优化技巧实录在实际开发和运行中你肯定会遇到各种问题。这里记录了一些典型问题的排查思路和解决技巧。6.1 Playwright爬取失败或超时问题现象page.goto超时或者页面元素无法找到。可能原因1网络问题或GitHub访问慢。解决方案增加超时时间或在page.goto中使用timeout参数例如timeout60000。考虑为Docker容器配置网络代理。可能原因2GitHub页面结构发生变化。这是爬虫最常遇到的问题。解决方案定期检查并更新CSS选择器。我们的选择器article.Box-row和h2 a是相对稳定的但GitHub也可能改版。建议将选择器字符串定义为配置常量便于统一修改。可以编写一个简单的测试脚本定期运行验证选择器是否有效。可能原因3被检测为自动化脚本。虽然Playwright已经尽力隐藏但高级反爬系统仍可能识别。解决方案尝试使用playwright.chromium.launch时添加更多启动参数来模拟真人浏览器例如args[--disable-blink-featuresAutomationControlled, --start-maximized]。也可以随机化User-Agent和视口大小。6.2 数据解析不准确或为空问题现象能打开页面但repo_elements列表为空或某些字段提取不到。排查步骤手动检查页面用浏览器打开相同的GitHub Trending URL检查元素是否存在。按F12打开开发者工具尝试用document.querySelectorAll(article.Box-row)验证选择器。启用Playwright调试在爬取时截屏或保存HTML这能帮你看到Playwright实际获取到的页面内容。await page.screenshot(pathdebug.png, full_pageTrue) html await page.content() with open(debug.html, w, encodingutf-8) as f: f.write(html)检查等待逻辑page.wait_for_selector可能在你指定的元素出现之前就返回了如果元素是动态插入的。可以尝试更保守的等待比如await page.wait_for_timeout(2000)后再抓取或者使用page.wait_for_function等待特定条件。处理动态内容如果语言颜色等信息是通过JS动态加载的确保在页面完全渲染后再执行提取逻辑。page.evaluate是在页面上下文中执行的确保你访问的DOM元素已经存在。6.3 API服务性能瓶颈问题现象并发请求时响应变慢或者服务器负载过高。瓶颈分析爬虫本身是瓶颈每次API调用都可能触发一次完整的浏览器页面加载和渲染即使有缓存缓存失效后的第一次请求也很慢。优化方案实现一个后台定时任务例如使用APScheduler或Celery定期如每5分钟爬取热门数据并更新缓存。这样API请求几乎总是命中缓存响应速度极快。浏览器实例单点我们的设计是单浏览器实例。虽然Playwright支持多上下文Context但单个浏览器实例处理大量并发页面也可能有压力。优化方案可以创建多个浏览器上下文browser.new_context()甚至启动多个浏览器进程用连接池的方式管理。但这会显著增加内存消耗需要权衡。缓存未命中风暴如果缓存同时过期大量请求会穿透到爬虫导致瞬间高负载。优化方案使用“缓存预热”或“缓存续期”策略。在缓存过期前后台任务就提前更新数据。或者使用互斥锁如asyncio.Lock确保对于同一个缓存键只有一个请求能执行爬取其他请求等待该结果。6.4 部署相关问题Docker构建失败最常见的是playwright install下载浏览器超时或失败。解决使用国内镜像加速。可以在Dockerfile中设置环境变量ENV PLAYWRIGHT_DOWNLOAD_HOSThttps://npmmirror.com/mirrors/playwright RUN playwright install chromium --with-deps或者在构建前将浏览器二进制包预先下载好通过COPY指令放入镜像避免在线安装。服务无故重启或停止可能是内存不足。Playwright的Chromium实例会消耗一定内存。监控使用docker stats或htop查看容器内存使用情况。调整在docker-compose.yml中为服务设置内存限制和保留值并确保宿主机有足够资源。services: github-trending-api: # ... deploy: resources: limits: memory: 1G # 内存限制 reservations: memory: 512M # 内存保留6.5 伦理与合规性提醒虽然我们构建了一个功能强大的爬虫但必须负责任地使用它。尊重robots.txt检查https://github.com/robots.txt。GitHub通常对爬虫比较友好但明确禁止了对某些路径如搜索API的滥用式访问的爬取。我们的爬虫只访问公开的Trending页面且频率很低通过缓存控制这通常是可接受的。设置合理的请求间隔即使在缓存失效后重新爬取也应避免在短时间内高频访问同一页面。我们的缓存机制5-30分钟已经起到了很好的间隔作用。标识你的爬虫在User-Agent中可以考虑加入一个标识例如MyGitHubTrendingBot/1.0 (https://my-api.com)以示友好。虽然我们的示例中使用了普通浏览器的UA但对于公开API服务使用一个独特的、非误导性的UA是更好的实践。关注服务条款定期查看GitHub的服务条款确保你的使用方式没有违反规定。构建这个项目的过程远不止是学会用Playwright和FastAPI写代码。它涉及到了从需求分析、工具选型、核心开发、服务封装到部署运维、性能优化和伦理考量的一整套工程化思维。当你把这个服务跑起来并通过一个简单的GET请求就能拿到结构化的GitHub趋势数据时你会感受到这种自动化、服务化思维带来的巨大效率提升。这个项目骨架具有很强的扩展性你可以很容易地修改爬虫部分去适配其他动态网站或者为FastAPI服务添加更复杂的业务逻辑、用户认证和数据分析功能。