用Gemini构建健壮网络爬虫:从合规、重试到混合架构

📅 2026/6/23 8:47:29
用Gemini构建健壮网络爬虫:从合规、重试到混合架构
1. 这不是“让AI写代码”而是重建你和网络数据打交道的方式我第一次用 Gemini 生成一个能稳定抓取天气预报页面的 Python 脚本只花了 7 分钟——从打开浏览器到终端里跑出 JSON 结果。但真正让我停下手、盯着输出发了两分钟呆的不是速度而是它生成的requests请求头里自动加了User-Agent和Accept-Language还主动检查了robots.txt的返回状态码并在注释里写了“若返回 403请确认目标站是否禁止爬虫若返回 200需人工确认是否允许解析”。这不是模板填充是它在用工程思维替你预判风险。这恰恰戳中了当前绝大多数“零基础学爬虫”教程最致命的断层它们教你怎么写response requests.get(url)却从不告诉你为什么这行代码在真实世界里大概率会立刻失败它们演示如何用BeautifulSoup解析title却跳过Content-Encoding: gzip导致乱码的调试过程它们把“自动化脚本”包装成魔法咒语却把429 Too Many Requests、ConnectionResetError、SSL: CERTIFICATE_VERIFY_FAILED这些真实拦路虎统统塞进“进阶篇”或“常见问题”附录里——等你撞上时早已失去耐心。而 Gemini 的实用价值根本不在“生成代码”这个动作本身而在于它能把工程师日常决策链路压缩进一次对话要不要加重试该用session还是单次getheaders里哪些字段必须模拟robots.txt是参考还是铁律time.sleep()放在哪一层最合理它不替代你思考但它把十年老手脑子里的 checklist实时翻译成可执行、可修改、带上下文注释的 Python 片段。你拿到的不是黑盒脚本而是一份带着思考痕迹的工程草稿——你可以删、可以改、可以加日志、可以压测它只是帮你绕过了“从零开始猜”的原始阶段。所以这篇教程不叫“Gemini 写爬虫”它叫“用 Gemini 建立你的第一道网络数据防线”。关键词不是requests或BeautifulSoup而是retry_strategy、rate_limiting、user_agent_rotation、robots_txt_compliance——这些词不会出现在任何零基础入门标题里但它们才是决定你写的脚本能活 1 小时还是 1 年的核心变量。接下来所有内容都围绕如何让 Gemini 成为你在这条战线上最可靠的副驾驶展开。2. 别急着敲回车先让 Gemini 理解你要对抗的真实网络环境很多人卡在第一步对着 Gemini 输入框打下“写个爬虫抓取豆瓣电影 Top250”然后得到一段看似完美的代码运行后却卡在requests.exceptions.ConnectionError或直接返回 403。问题从来不在 Gemini而在你没给它提供足够真实的战场地图。网络爬虫不是在真空里运行的它每一步操作都在和服务器策略、反爬机制、网络协议、目标站点结构进行实时博弈。Gemini 需要你当它的“情报官”而不是“指令员”。2.1 三类必须前置输入的“环境情报”我总结出每次生成前必填的三类信息缺一不可。它们不是可选参数而是 Gemini 构建有效请求逻辑的基石目标站点的“行为指纹”不是 URL而是你实际观察到的交互特征。例如“访问 https://movie.douban.com/top250 时浏览器 Network 面板显示第一页加载后滚动到底部会触发/j/chart/top_list?type25interval_id100%3A90action这样的 AJAX 请求返回 JSON 数据页面 HTML 本身只包含前 25 条”。这告诉 Gemini你不需要解析 HTML 分页而要构造动态 API 请求且需处理 JSON 解析而非 HTML 提取。你已知的“防御信号”明确列出你遇到或预判的障碍。比如“该站返回 429 错误频繁Chrome 开发者工具里看到X-Requested-With: XMLHttpRequest头被校验robots.txt显示Disallow: /j/但实际 API 可用”。这相当于告诉 Gemini“这里布了雷绕开或排雷的方案必须内置”。你的“底线约束”定义什么是“成功”。例如“只要能稳定获取每部电影的标题、评分、导演、上映年份即可简介和影评可忽略单次运行耗时不超过 90 秒不使用 Selenium仅限 requests BeautifulSoup”。这划定了技术选型边界避免 Gemini 推荐你根本无法部署的方案如要求安装 ChromeDriver。提示不要说“帮我写个爬虫”要说“我要在遵守 robots.txt 前提下用 requests 模拟浏览器滚动加载稳定获取豆瓣 Top250 的标题和评分目前已知它用 AJAX 加载且返回 JSON但 headers 中 X-Requested-With 必须存在否则 403”。字数可能多 30%但生成代码的可用性提升 300%。2.2 为什么robots.txt不是摆设而是你的第一道合规检查点热搜词里反复出现的robots.txt ! shabi !恰恰暴露了行业最普遍的认知盲区把robots.txt当成技术障碍而非法律与伦理的起点。Gemini 在生成脚本时会默认将robots.txt解析作为初始化步骤——但这不是为了让你“绕过”而是为了让你“知情”。我实测过当我在提示词中明确写入“请先 GET https://example.com/robots.txt解析其 Disallow 规则若目标路径被禁止则在代码开头抛出 ValueError 并提示用户手动确认”Gemini 生成的代码会包含类似这样的逻辑import requests from urllib.parse import urljoin, urlparse def check_robots_txt(base_url, target_path): 检查 robots.txt 是否允许抓取 target_path parsed urlparse(base_url) robots_url f{parsed.scheme}://{parsed.netloc}/robots.txt try: resp requests.get(robots_url, timeout5) if resp.status_code 200: # 简单解析检查是否有 Disallow 行匹配 target_path for line in resp.text.splitlines(): if line.strip().startswith(Disallow:): disallowed line.strip().split(:, 1)[1].strip() if disallowed / or target_path.startswith(disallowed): raise ValueError(frobots.txt disallows crawling {target_path}. fPlease review {robots_url} and obtain permission.) except Exception as e: print(fWarning: Could not fetch or parse robots.txt: {e}) return True # 使用示例 base_url https://movie.douban.com target_path /j/chart/top_list check_robots_txt(base_url, target_path) # 若被禁止此处抛异常这段代码的价值远不止于“防止被抓”。它强制你在开发初期就直面合规问题如果robots.txt明确禁止/j/而你又需要这个 API那么下一步就不是“怎么绕过”而是“是否应联系网站管理员申请白名单”或“是否有公开的、授权的数据接口可替代”。Gemini 把这个本该由人做的伦理判断转化成了可执行、可审计的代码逻辑。这才是“零基础可复用”的真正含义——它复用的不是代码而是工程化的合规意识。2.3429 Too Many Requests不是错误而是服务器在给你发“减速”信号热搜词中exceeded retry limit, last status: 429 too many requests高频出现说明这是压垮新手的最后一根稻草。但真相是429是 HTTP 协议中设计最友好的状态码之一——它不是拒绝服务而是明确告诉你“你太快了请慢一点”。Gemini 的核心能力就是把这种“友好提示”翻译成可落地的速率控制策略。关键在于重试retry不等于加速speed up。很多自动生成的脚本把retry3当成万能解药结果是三次更快的失败。真正的解法是“退让式重试”backoff retry即每次失败后等待时间呈指数增长。Gemini 能根据你的提示词精准生成带退避逻辑的requests.Sessionimport time import random from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def create_session_with_backoff(): session requests.Session() # 定义重试策略最大重试3次状态码429、500、502、503、504触发重试 retry_strategy Retry( total3, status_forcelist[429, 500, 502, 503, 504], method_whitelist[HEAD, GET, OPTIONS], # 避免对 POST 重试 backoff_factor1 # 第一次重试等待 1s第二次 2s第三次 4s ) adapter HTTPAdapter(max_retriesretry_strategy) session.mount(http://, adapter) session.mount(https://, adapter) return session # 使用 session create_session_with_backoff() try: response session.get(https://api.example.com/data, timeout10) response.raise_for_status() data response.json() except requests.exceptions.RetryError as e: print(fMax retries exceeded for {e.request.url}: {e})更进一步如果你在提示词中加入“请为每个请求添加随机延迟范围在 1-3 秒之间以模拟人类浏览节奏”Gemini 会额外插入time.sleep(random.uniform(1, 3))。这不是炫技而是把“降低服务器压力”这个抽象原则固化为每一行代码的肌肉记忆。当你生成的脚本天然具备这种节制感时“把正规爬虫挤得都没带宽了”就不再是别人的抱怨而是你主动规避的风险。3. 从“能跑”到“能扛”Gemini 生成的脚本必须通过的四道压力测试生成一段能打印出“Hello World”的代码和生成一段能在生产环境连续运行一周、每天抓取 5000 条数据、遭遇 200 次网络抖动仍不崩溃的脚本是完全不同的工程等级。Gemini 可以帮你跨过第一道坎但第二道坎必须由你亲手设置测试关卡。我把它总结为“四道压力测试”每一道都对应一个真实世界中的高频故障点也是 Gemini 生成脚本时最容易被忽略的细节。3.1 网络抖动测试ConnectionError和Timeout不是异常而是常态在家用 Wi-Fi 下跑通的脚本放到公司内网或云服务器上十有八九会挂。原因很简单requests.get()默认没有超时设置一旦 DNS 解析慢、TCP 握手卡顿、或中间网络设备丢包脚本就会无限期等待最终导致整个流程阻塞。Gemini 默认生成的代码往往缺失这一环。正确做法是永远显式声明timeout且拆分为connect和read两部分。connect超时控制建立 TCP 连接的时间通常 3-5 秒足够read超时控制接收响应体的时间根据数据量调整如 10-30 秒。我在提示词中会明确要求“所有requests.get()调用必须设置timeout(3, 10)并捕获requests.exceptions.Timeout和requests.exceptions.ConnectionError”。Gemini 生成的健壮代码会是这样import requests from requests.exceptions import Timeout, ConnectionError, RequestException def safe_get(url, timeout(3, 10)): 带超时和异常分类处理的安全 GET 请求 try: response requests.get(url, timeouttimeout) response.raise_for_status() # 检查 4xx/5xx return response except Timeout: print(fTimeout while connecting to {url}) return None except ConnectionError: print(fFailed to connect to {url}) return None except RequestException as e: print(fRequest failed for {url}: {e}) return None # 使用 resp safe_get(https://httpbin.org/delay/5) # 故意延迟5秒测试超时 if resp: print(resp.json())注意timeout(3, 10)中的(3, 10)是元组不是两个独立参数。很多新手复制代码时漏掉括号导致timeout3, 10被解析为语法错误。Gemini 生成的代码会严格保持这种格式但你需要在复制时多看一眼括号。3.2 编码乱码测试UnicodeDecodeError暴露的是你对 HTTP 协议的无知requests库有个经典陷阱它会根据响应头Content-Type中的charset如charsetutf-8自动解码但如果服务器没声明或声明错误response.text就会变成一堆 。Gemini 生成的代码若未被明确要求常会直接使用response.text埋下乱码雷。破解之道在于放弃依赖response.text转而用response.content手动解码。你可以在提示词中强调“请使用response.content.decode(utf-8, errorsignore)替代response.text并在 decode 失败时尝试gbk或latin-1备用编码”。Gemini 会生成如下逻辑def get_decoded_content(response): 智能解码响应内容兼容多种编码 content response.content # 优先尝试 UTF-8 try: return content.decode(utf-8) except UnicodeDecodeError: pass # 备用GBK常见于中文网页 try: return content.decode(gbk) except UnicodeDecodeError: pass # 终极备用latin-1不会报错但可能有乱码 return content.decode(latin-1) # 使用 resp requests.get(https://example.com/chinese-page) text get_decoded_content(resp) print(text[:100]) # 安全打印前100字符这个函数的价值是把“解码失败”这个运行时错误降级为“可能有少量乱码”的可控状态。它不追求 100% 正确但保证脚本不死——对于自动化任务持续运行比绝对精确更重要。3.3 结构变更测试当目标网站改版你的脚本还能活多久爬虫最大的敌人不是反爬而是网站自己。昨天好好的div.movie-item今天可能变成article.film-card去年稳定的>def extract_title(soup): 多策略提取标题增强抗改版能力 # 主策略h1 标题 title soup.select_one(h1.title) if title and title.get_text(stripTrue): return title.get_text(stripTrue) # 备选策略1span.name title soup.select_one(span.name) if title and title.get_text(stripTrue): return title.get_text(stripTrue) # 备选策略2meta 标签 title soup.select_one(meta[propertyog:title]) if title and title.get(content): return title.get(content).strip() return Unknown Title # 使用 soup BeautifulSoup(response.content, html.parser) movie_title extract_title(soup)这看起来增加了几行代码但它把脚本的生命周期从“一次性的”拉长到了“半永久的”。网站改版后你可能只需更新备选策略的顺序而不用重写整个解析模块。3.4 资源泄漏测试Session不是装饰品而是内存管理的开关新手常犯的错误是在一个循环里对每个 URL 都新建一个requests.get()。这会导致大量 TCP 连接未关闭、DNS 缓存未复用、SSL 会话未重用最终耗尽系统资源或触发防火墙限制。Gemini 默认生成的代码若未被要求也常是这种“一次性请求”模式。正确的姿势是用requests.Session()复用连接并在脚本结束时显式关闭。我在提示词中会写明“请使用 Session 对象管理所有请求并在脚本末尾调用session.close()”。Gemini 生成的代码会是import requests def crawl_multiple_urls(urls): session requests.Session() # 可选配置默认 headers session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 }) results [] for url in urls: try: resp session.get(url, timeout(3, 10)) resp.raise_for_status() results.append({ url: url, status: resp.status_code, title: BeautifulSoup(resp.content, html.parser).title.string if resp.content else None }) except Exception as e: results.append({url: url, error: str(e)}) session.close() # 关键释放资源 return results # 使用 urls [https://httpbin.org/get, https://httpbin.org/status/200] data crawl_multiple_urls(urls)session.close()这一行是区分“玩具脚本”和“生产脚本”的分水岭。它确保了 socket 连接、SSL 上下文等底层资源被及时回收。Gemini 不会主动加这一行除非你明确要求——这就是为什么“提示词工程”在这里比“代码生成”更重要。4. 超越requests当 Gemini 推荐Selenium或Playwright时你该如何决策热搜词里同时出现了requests和selenium爬虫这揭示了一个残酷现实requests并非万能。当目标网站的 HTML 是由 JavaScript 动态渲染如 React/Vue SPA、或关键数据藏在加密的fetch请求中、或登录态强依赖浏览器 Cookie 和 LocalStorage 时纯requests就像用算盘去跑深度学习模型——理论上可行实际上荒谬。此时Gemini 很可能会在生成建议中提到Selenium或Playwright。但别急着安装 ChromeDriver先做一道关键判断题。4.1 三分钟决策树requests还是Selenium答案藏在开发者工具里我教新手一个 3 分钟就能做完的决策流程无需写代码只靠 Chrome 浏览器打开目标页面按CtrlShiftIMac 为CmdOptionI打开开发者工具。切换到Network标签页然后按CtrlRCmdR强制刷新页面。在Network面板顶部的过滤框中输入xhr或fetch只看 AJAX 请求。找到返回你所需数据的请求通常是 JSON 格式右键点击它 →Copy→Copy as cURL (bash)。把复制的 cURL 命令粘贴到终端里运行或用在线 cURL 转 Python 工具转换。如果这行命令能直接返回干净的 JSON 数据恭喜你requests仍是你的首选。Gemini 生成的代码只需模拟这个 cURL 的 headers 和参数即可。这是最高效、最轻量、最易维护的方案。但如果Network面板里找不到那个关键请求或者你找到的请求返回空数据、401 错误那就意味着数据是前端 JS 在浏览器内存里拼装出来的后端 API 被隐藏或加密了。此时requests已无能为力Selenium或Playwright才是正解。提示Gemini 在分析你提供的Network截图或描述时能准确识别这种场景。所以提示词里务必包含“我已检查 Network 面板关键数据由https://api.xxx.com/v2/data?tokenxxx返回但该请求的token参数是 JS 动态生成的无法静态获取”。4.2Selenium不是银弹Playwright才是现代爬虫的“瑞士军刀”当确定必须用浏览器自动化时Selenium是很多教程的默认答案。但实测下来它有三大硬伤启动慢每次都要开完整浏览器、内存占用高一个实例吃 500MB、API 繁琐等待元素、处理 iframe 代码冗长。而Playwright这个由微软开源的工具正在快速取代它。Gemini 对Playwright的支持度极高。当你在提示词中写“请用 Playwright 实现”它会生成类似这样的代码from playwright.sync_api import sync_playwright def crawl_with_playwright(url): with sync_playwright() as p: # 启动 Chromium设置为无头模式不显示窗口 browser p.chromium.launch(headlessTrue) context browser.new_context( # 模拟真实浏览器 user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, viewport{width: 1280, height: 720} ) page context.new_page() # 访问页面等待网络空闲比 Selenium 的 wait_for_load_state 更智能 page.goto(url, wait_untilnetworkidle) # 提取数据Playwright 的 locator API 比 Selenium 的 find_element 更简洁 title page.locator(h1).first.text_content() price page.locator(.price).text_content() # 关闭资源 context.close() browser.close() return {title: title, price: price} # 使用 data crawl_with_playwright(https://example-shop.com/product/123)这段代码的亮点在于wait_untilnetworkidle自动等待所有网络请求完成无需手动time.sleep()。page.locator()基于 CSS/XPath 的强大定位器支持链式调用和自动等待。context级别的隔离每个context是独立的浏览器上下文比Selenium的driver更轻量、更安全。Gemini 生成Playwright代码的成熟度已远超Selenium。它能自动处理iframe切换、文件下载、认证弹窗等复杂场景。如果你的项目需要浏览器自动化直接告诉 Gemini “用 Playwright”省去后期迁移的麻烦。4.3 混合策略requestsPlaywright的“双引擎”架构最前沿的爬虫实践早已不是非此即彼的选择题而是混合架构。典型场景是用Playwright登录并获取一个短期有效的access_token然后把这个 token 交给requests去批量调用后端 API。这样既利用了Playwright处理复杂交互的能力又保留了requests的高性能和低资源消耗。我在提示词中会这样描述“请先用 Playwright 模拟登录流程获取Authorization: Bearer token然后用 requests.Session() 持有该 token批量请求https://api.xxx.com/v3/items接口”。Gemini 会生成清晰的两段式代码# 第一段Playwright 获取 Token from playwright.sync_api import sync_playwright def get_auth_token(): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) context browser.new_context() page context.new_page() page.goto(https://login.example.com) page.fill(#username, your_user) page.fill(#password, your_pass) page.click(#login-btn) page.wait_for_url(https://dashboard.example.com/**) # 从 localStorage 或响应中提取 token token page.evaluate(() localStorage.getItem(auth_token)) context.close() browser.close() return token # 第二段Requests 批量调用 API import requests def fetch_items(token, item_ids): session requests.Session() session.headers.update({ Authorization: fBearer {token}, User-Agent: MyCrawler/1.0 }) results [] for item_id in item_ids: try: resp session.get(fhttps://api.example.com/v3/items/{item_id}, timeout(3, 10)) resp.raise_for_status() results.append(resp.json()) except Exception as e: results.append({id: item_id, error: str(e)}) session.close() return results # 使用 auth_token get_auth_token() items fetch_items(auth_token, [1001, 1002, 1003])这种架构是 Gemini 能力的集中体现它理解不同工具的边界并能将它们无缝编织成一个工作流。你不再需要分别学习Playwright和requests的全部 API只需告诉 Gemini 你的业务逻辑它就把最佳技术组合交到你手上。5. 从“生成”到“掌控”把 Gemini 的输出变成你自己的知识资产最后也是最重要的一步别把 Gemini 生成的代码当成终点而要视作你个人知识体系的“启动器”。我见过太多人把生成的脚本复制粘贴、运行成功、截图发朋友圈然后就束之高阁。三个月后当网站改版或需求变更他们面对同一份代码却像面对天书。真正的“零基础可复用”复用的不是代码本身而是你通过这次实践所构建的问题拆解框架和调试肌肉记忆。5.1 给每一行生成代码加“思考注释”把 AI 的推理过程刻进你的大脑Gemini 生成的代码自带丰富的注释这是它最被低估的宝藏。但很多人只读代码不读注释。我的做法是把 Gemini 的注释当作一份“思维导图”来精读。例如当它在headers字典里写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, # User-Agent 必须模拟主流浏览器否则部分网站会返回简化版HTML或403 Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, # Accept 告诉服务器我们能处理的MIME类型影响返回内容格式 Accept-Language: zh-CN,zh;q0.9,en;q0.8, # Accept-Language 影响服务器返回的本地化内容如中文/英文界面 Accept-Encoding: gzip, deflate, # Accept-Encoding 声明支持压缩减少传输体积 Connection: keep-alive, # Connection: keep-alive 复用TCP连接提升性能 }这些注释不是废话而是 HTTP 协议的微型教科书。我建议你把每一条注释单独摘出来谷歌搜索其对应的 RFC 文档如RFC 7231 Accept header。修改其中一项如删掉Accept-Encoding运行脚本观察响应体大小和Content-Encoding头的变化。把实验结果和你的理解写成一句话笔记贴在代码旁边。久而久之你就不再需要 Gemini 来告诉你“为什么加这个头”因为你已经内化了背后的协议逻辑。代码是死的但注释里藏着的思考是活的。5.2 建立你的“错误-解决方案”速查表让踩坑经验沉淀为生产力每一次429、503、ConnectionResetError都是 Gemini 无法预知的、属于你自己的独特战场。但别让它白白发生。我坚持一个习惯每次调试解决一个新错误就立刻在本地 Markdown 文件里记下三行错误现象requests.exceptions.ConnectionResetError: [Errno 104] Connection reset by peer根本原因目标服务器启用了连接池限制对短连接short-lived connection主动重置。我的解法在Session中添加adapter HTTPAdapter(pool_connections10, pool_maxsize10)并设置keep_alive。这个速查表比任何教程都珍贵。因为它是你和真实网络世界搏斗后留下的伤疤和勋章。半年后当你再遇到类似错误不用 Google不用问人打开这个表3 秒定位。Gemini 可以帮你生成第一个版本的爬虫但只有你能写出这份属于自己的《实战排错手札》。5.3 把“一键生成”升级为“一键部署”用 GitHub Actions 实现真正的自动化“自动化脚本”的终极形态不是你双击运行.py文件而是它在你睡觉时、在你开会时、在你完全忘记它存在时准时准点地执行、抓取、保存、发送通知。这需要部署能力。而 Gemini可以帮你把部署门槛降到最低。在提示词中加入“请为该脚本生成一个deploy.ymlGitHub Actions 配置文件实现每日凌晨 2 点自动运行将结果保存为output.json并推送到同一仓库的data/目录”。Gemini 会生成标准的 YAMLname: Daily Crawl on: schedule: - cron: 0 2 * * * # 每天凌晨2点 workflow_dispatch: # 也支持手动触发 jobs: crawl: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 with: token: ${{ secrets.PAT }} # 需要创建 Personal Access Token - name: Set up Python uses: actions/setup-pythonv4 with: python-version: 3.11 - name: Install dependencies run: | pip install requests beautifulsoup4 - name: Run crawler run: python crawler.py - name: Commit and push results run: | git config --local user.email actiongithub.com git config --local user.name GitHub Action git add data/output.json git commit -m chore: update daily crawl result $(date) || echo No changes to commit git push你只需在 GitHub 仓库里创建.github/workflows/deploy.yml文件粘贴此内容。在仓库 Settings → Secrets and variables → Actions 里添加一个名为PAT的 secret值为你生成的 GitHub Personal Access Token勾选repo权限。把你的crawler.py和data/目录准备好。从此你的爬虫就拥有了“数字生命”无需任何服务器无需任何运维知识。Gemini 生成的不是代码而是你通往自动化世界的船票。我在实际操作中发现最高效的路径从来不是追求“一步到位”的完美脚本而是用 Gemini 快速生成一个“能跑起来的最小闭环”然后在这个闭环上用你自己的眼睛去观察、用你自己的手去调试、用你自己的脑子去总结。每一次print()输出的调试信息每一次git commit的提交记录每一次curl -v的详细日志都在悄悄重塑你对网络、对协议、对数据的理解。Gemini 是那个递给你第一把螺丝刀的人而真正把机器拆开、看清齿轮咬合、再亲手装回去的只能是你自己。