硬核观察 #1158 马自达封杀了业余爱好者的智能汽车 API 工具
马自达封杀了业余爱好者的智能汽车 API 工具上周之前，马自达汽车的车主如果同时安装了“Home Assistant”应用，就可以为自己的汽车创建一些方便的连接。比如根据能源的动态价格控制充电器；再比如还可以在早晨通勤前检查油量，在天气预报下雨前提醒他们车窗是否已关好，以及在寒冷条件下远程解锁和启动汽车等等。这些功能可以扩展的范围非常大，据称已经超出了马自达官方应用程序的功能范围。然而，马自达向 GitHub 发出了下架该仓库的要求，指控该软件包含 “侵犯版权所有权” 的代码；使用 “某些马自达信息，包括专有 API 信息来创建代码和信息”；包含提供与目前马自达应用程序中“相同功能”的代码。该软件作者出于“即使我相信我所做的事情在道德上是正确的，在法律上是受保护的，但法律程序仍然需要付出经济代价”的考虑，向“Home Assistant”核心项目提出了拉取请求，删除了马自达集成。消息来源：Ars Technica老王点评：重施甲骨文故技，我的 API 就是不能给别人用，模仿都不行。高通发布首款面向大众市场的 RISC-V 安卓 SoC去年，谷歌宣布在安卓系统中正式支持 RISC-V，并计划将其打造成与 Arm 同等重要的一级平台。而在硬件方面，高通宣布推出有史以来第一款面向大众市场的 RISC-V 安卓 SoC，它计划 “在全球（包括美国）商业化基于 RISC-V 的可穿戴设备解决方案”。RISC-V 架构是开源的，因此比 Arm 架构更便宜、更灵活，也是回避 Arm 存在的各种问题的一种方法。消息来源：Ars Technica老王点评：有望摆脱 Arm 对移动计算的统治。Reddit 正在逐步停止基于区块链的社区积分Reddit 社区积分于 2020 年首次推出，奖励给积极参与特定子区的用户，以激励更好的内容和对话。这些积分本质上是以太坊通证，存储在 Reddit 的加密货币钱包中。Reddit 称，他们“看到了社区积分未来的一些机会，但不幸的是，所需的资源配置过高，无法证明其合理性。” Rdddit 正在转而优先考虑那些不那么难以扩展的奖励项目。消息来源：Tech Crunch老王点评：对区块链的美好幻想逐一破碎，生不逢时。
采用 Apache APISIX 的 API 主导架构。API 网关是一个单一节点，提供对 API 调用入口。网关聚合了所请求的服务，并相应传回合适的响应信息。为了令你的 API 网关有效地工作，设计一个可靠、高效且简洁地 API 至关重要。本文介绍一种设计风格，但只要你理解其中的重点内容，它就能解决你的相关问题。由 API 主导的方法API 主导的方法是将 API 置于应用程序和它们需要访问的业务能力之间的通信核心，从而在所有数字通道上一致地交付无缝功能。API 主导的连接是指使用一种可重用、且设计得当的 API 来连接数据和应用程序的方法。API 主导的架构API 主导的架构是一种架构方法，它着眼于实现重用 API 的最佳方式。它能解决以下问题：保护 API，使外界无法在未授权情况下访问 API确保应用程序能找到正确的 API 端点限制对 API 的请求次数，从而确保持续的可用性支持持续集成、测试、生命周期管理、监控、运维等等防止错误在栈间传播对 API 的实时监测和分析实现可伸缩和灵活的业务能力（例如支持 微服务 架构）API 资源路由实现一个 API 网关，把它作为与所有服务通信的单一入口点，意味着使用者只需要知道 URL 就能使用 API。将请求路由到相应的服务端点，并执行相应的功能是 API 网关的职责。由于客户端应用程序不需要从多个 HTTP 端点调用功能，这个办法就减少了 API 使用者的操作复杂度。对每个服务来说，也不需实现一个单独的层级去实现认证、授权、节流和速度限制。大多数API 网关，如开源的 Apache APISIX，已经包含了这些核心功能。API 基于内容的路由基于内容的路由机制也使用 API 网关根据请求的内容进行路由调用。例如，一个请求可能是基于 HTTP 请求的头部内容或消息体被路由，而不只基于它的目标 URI。考虑这样一个场景：为了将负载在多个数据库实例间均分，需要对数据库进行分区。当记录总数较大，单个数据库实例难以管理负载时，常常会用这个办法。还有一个更好的办法，就是把记录在多个数据库实例间分散开来。然后你实现多个服务，每个不同的数据库都有一个服务，把一个 API 网关作为访问所有服务的唯一入口。然后，你可以配置你的 API 网关，根据从 HTTP 头或有效载荷中获得的密钥，将调用路由到相应的服务。在上面的图表中，一个 API 网关向多个客户服务暴露一个单一的 /customers 资源，每个服务都有对应的不同数据库。API 地理路由API 地理路由解决方案根据 API 调用的来源将其路由到最近的 API 网关。为了防止地理距离导致的延迟问题（例如一个位于亚洲的客户端调用了位于北美地区的 API），你可以在多个地区部署 API 网关。对于一个 API 网关，你可以在每个区域使用不同的子域名，让应用程序基于业务逻辑选择最近的网关。因此 API 网关就提供了内部负载均衡，确保进入的请求分布于可用的实例之间。通常使用 DNS 流量管理服务和 API 网关，针对该区域的负载均衡器解析子域名，定位到距离最近的网关。API 聚合器这项技术对多个服务执行操作（例如查询），并向客户端服务以单个 HTTP 响应的形式返回结果。API 聚合器使用 API 网关在服务器端代表使用者来执行这项工作，而非让客户端程序多次调用 API。假定你有一款移动端 APP，对不同的 API 发起多次调用。这增加了客户端代码的复杂性，导致网络资源的过度使用，而且由于延迟性，用户体验也不好。网关可以接收所有需要的信息，可以要求认证和验证，并理解来自每个 API 的数据结构。它也可以传递响应的有效载荷，因此它们也会作为一个用户需要的统一负载传回移动端。API 集中认证在这种设计中，API 网关就是一个集中式认证网关。作为一个认证者，API 网关在 HTTP 请求头中查找访问凭据（例如不记名的令牌）。然后它借助于身份验证提供方执行验证凭据的业务逻辑。使用 API 网关的集中式身份验证能解决很多问题。它完全取代了应用程序中的用户管理模块，通过对来自客户端应用程序的身份验证请求的快速响应来提高性能。Apache APISIX 提供了一系列插件，支持不同的 API 网关认证方法。API 格式转换API 格式转换是通过同一传输方式将有效载荷从一种格式转换为另一种格式的能力。例如，你可以通过 HTTPS 从 XML/SOAP 格式转换为 JSON 格式，反之亦然。API 网关提供了支持 REST API 的功能，可以有效地进行负载和传输的转换。例如，它可以把消息队列遥测传输（MQTT）转换为 JSON 格式。Apache APISIX 能够接收 HTTP 请求，对其进行代码转换，然后将其转发给 gRPC 服务。它通过 gRPC Transcode 插件获取响应并将其以 HTTP 格式返回给客户端。API 的可观察性现在，你知道 API 网关为进入各种目的地的流量提供了一个中心控制点。但它也可以是一个中心观察点，因为就监控客户端和服务器端的流量来说，它有独特的资格。为了收集监测工具所需要的数据（结构化日志、度量和跟踪），你可以对 API 网关作出调整。Apache APISIX 提供了 预先构建的连接器，因此你可以跟外部监测工具结合使用。你可以利用这些连接器从你的 API 网关收集日志数据，进一步获得有用的指标，并获取完整可见的服务使用情况。API 缓存API 缓存通常在网关内部实现。它可以减少对端点的调用次数，同时通过缓存上游的响应，改进了请求延迟的情况。如果网关缓存对请求资源有一个新副本，它会直接使用这个副本来响应这个请求，而不必对端点发出请求。如果缓存数据不存在，就将请求传到目标上游服务。API 错误处理由于各种原因，API 服务可能会出错。在这种情况下，API 服务需要有一定的弹性，来应对可预见的错误。你也希望确保弹性机制能正常工作。弹性机制包括错误处理代码、断路器、健康检查、回退、冗余等等。新式的 API 网关支持各种常见错误处理功能，包括自动重试和超时设置。API 网关作为一个协调器，它会根据各方面情况来决定如何管理流量、将负载均衡发送到一个健康的节点，还能快速失败。当有异常状况，它也会向你发出警示。API 网关也保证路由和其他网络级组件能协同将请求传给 API 进程。它能帮助你在早期检测出问题，并修复问题。网关的错误注入机制（类似于 Apache APISIX 使用的那种）可用于测试应用程序或微服务 API 在各种错误发生时的弹性。API 版本管理版本管理是指定义和运行多个并发的 API 版本的功能。这点也很重要，因为 API 是随着时间推移不断改进的。如果能对 API 的并发版本进行管理，那么 API 使用者就可以较快地切换到新的版本。这也意味着较老的版本可以被废弃并最终退役。API 也跟其他应用程序类似，无论是开发新功能还是进行错误修复，都存在演变的过程。你可以使用 API 网关来实现 API 版本管理。版本管理可以是请求头，查询参数或路径。APISIX 的网关如果你需要令 API 服务可伸缩，就需要使用 API 网关。Apache APISIX 提供了必要的功能，可以实现健壮的入口，它的好处是显而易见的。它遵循 API 主导的架构，并且有可能改变客户端与托管服务交互的方式。本文经作者许可，从 Apache APISIX 博客改编并转载。（题图：MJ/f1d05345-48f5-4e3e-9c65-a2ba9105614a）via: https://opensource.com/article/23/1/api-gateway-apache-apisix作者：Bobur Umurzokov 选题：lkxed 译者：cool-summer-021 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
硬核观察 #1034 Reddit 威胁换掉继续抗议的子区管理员
Reddit 威胁换掉继续抗议的子区管理员本周初，数千 Reddit 子区 闭版 以抗议 Reddit API 的天价收费，因为这事实上扼杀了管理员们赖以执行管理操作的第三方客户端。Reddit CEO 对员工 表示 不要担心，因为抗议不会影响公司收入。他称，“大部分子区会按计划在周三回归，我们干好自己的工作就行了”。周三，部分子区陆续恢复，但也有很多子区将继续保持关闭 —— 直到 Reddit 改变其政策。目前还有超过一半的子区保持关闭或限制，这包括订阅数超过四千万的 r/funny、超过三千万的 r/aww、r/Music 等。此外，Reddit 还威胁将移除继续抗议的管理员。Reddit 宣称，如果一个子区的管理团队一致同意停止管理社区，它将邀请愿意管理社区的新管理员来管理社区；而如果至少有一名管理员愿意维持社区运作，Reddit 将移除不想管理社区的管理员。消息来源：Hacker News老王点评：真是要硬扛到底啊，这不是移除几个“刺头”管理员就能解决问题的，相信这样的抗议也代表了大部分社区中坚的意见。无论如何，这都是一幕罕见的大规模社区对抗事件，让我们拭目以待。YouTube 通知其开源替代品 Invidious 关闭Invidious 是 YouTube 的一个开源 “替代前端”，允许用户在没有数据跟踪的情况下观看 YouTube 视频，也可以让用户在观看视频时不被 “恼人的广告” 打断。YouTube 声称该软件违反了其 API 政策，称它没有显示 YouTube 服务条款的链接，也没有 “明确” 解释其对用户信息的处理。YouTube 要求它在 7 天内关闭。消息来源：VICE老王点评：看来开放 API 将全面进入关闭时代，各个网站都开始扼杀这些妨碍其广告业务的客户端了。或许在官方看来，这些利用其 API 的第三方客户端就是在对它们吸血。Alphabet 出售了谷歌域名产品Alphabet 正在结束其谷歌域名业务，并将其资产以约 1.8 亿美元的价格出售给 SquareSpace。这些资产包括“在谷歌域名业务上托管的 1000 万个域名，分布在数百万个客户中”。谷歌域名业务于 2014 年推出，在 2022 年退出了测试阶段。它拥有诸多新的顶级域名，是 HTTPS 的主要支持者之一。消息来源：9to5google老王点评：谷歌连域名这么重要的基础设施服务都开始砍掉，以后还能信任谷歌在基础设施方面的持续性么？
结合开放的 API 和 Python 编程语言的力量。围绕 API 创建封装器的开源项目正变得越来越流行。这些项目使开发人员更容易与 API 交互并在他们的应用中使用它们。openshift-python-wrapper 项目是 openshift-restclient-python 的封装器。最初是一个帮助我们的团队使用 OpenShift API 的内部包，后来变成了一个开源项目（Apache 许可证 2.0）。本文讨论了什么是 API 封装器，为什么它很有用，以及封装器的一些例子。为什么要使用 API 封装器？API 封装器是位于应用和 API 之间的一层代码。它通过将一些涉及发出请求和解析响应的复杂性抽象出来，以简化 API 访问过程。封装器还可以提供 API 本身提供的功能之外的附加功能，例如缓存或错误处理。使用 API 封装器使代码更加模块化并且更易于维护。无需为每个 API 编写自定义代码，你可以使用封装器来提供与 API 交互的一致接口。它可以节省时间，避免代码重复，并减少出错的机会。使用 API 封装器的另一个好处是它可以保护你的代码免受 API 变化的影响。如果 API 更改了它的接口，你可以更新封装器代码而无需修改你的应用程序代码。随着时间的推移，这可以减少维护应用程序所需的工作。安装该应用位于 PyPi 上，因此使用 pip 命令 安装 openshift-python-wrapper：$ python3 -m pip install openshift-python-wrapper Python 封装器OpenShift REST API 提供对 OpenShift 平台的许多功能的编程访问。封装器提供了一个简单直观的界面，用于使用 openshift-restclient-python 库与 API 进行交互。它标准化了如何使用集群资源，并提供了统一的资源 CRUD（创建、读取、更新和删除）流程。它还提供额外的功能，例如需要由用户实现的特定于资源的功能。随着时间的推移，封装器使代码更易于阅读和维护。简化用法的一个示例是与容器交互。在容器内运行命令需要使用 Kubernetes 流、处理错误等。封装器处理这一切并提供 简单直观的功能。>>> from ocp_resources.pod import Pod >>> from ocp_utilities.infra import get_client >>> client = get_client() ocp_utilities.infra INFO Trying to get client via new_client_from_config >>> pod = Pod(client=client, name="nginx-deployment-7fb96c846b-b48mv", namespace="default") >>> pod.execute("ls") ocp_resources Pod INFO Execute ls on nginx-deployment-7fb96c846b-b48mv (ip-10-0-155-108.ec2.internal) 'bin\nboot\ndev\netc\nhome\nlib\nlib64\nmedia\nmnt\nopt\nproc\nroot\nrun\nsbin\nsrv\nsys\ntmp\nusr\nvar\n' 开发人员或测试人员可以使用这个封装器，我们的团队在编写代码的同时牢记测试。使用 Python 的上下文管理器可以提供开箱即用的资源创建和删除，并且可以使用继承来扩展特定场景的功能。Pytest fixtures 可以使用代码进行设置和拆卸，不留任何遗留物。甚至可以保存资源用于调试。可以轻松收集资源清单和日志。这是上下文管理器的示例：@pytest.fixture(scope="module") def namespace(): admin_client = get_client() with Namespace(client=admin_client, name="test-ns",) as ns: ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=240) yield ns def test_ns(namespace): print(namespace.name) 生成器遍历资源，如下所示：>>> from ocp_resources.node import Node >>> from ocp_utilities.infra import get_client >>> admin_client = get_client() # This returns a generator >>> for node in Node.get(dyn_client=admin_client): print(node.name) ip-10-0-128-213.ec2.internal 开源社区的开源代码套用一句流行的说法，“如果你热爱你的代码，就应该让它自由。” openshift-python-wrapper 项目最初是作为 OpenShift 虚拟化 的实用模块。随着越来越多的项目从代码中受益，我们决定将这些程序提取到一个单独的仓库中并将其开源。套用另一句俗语，“如果代码不回到你这里，那就意味着它从未属于你。” 一旦这种情况发生，它就真正成为了开源。更多的贡献者和维护者意味着代码属于社区。欢迎大家贡献。（题图：MJ/5ca32a4a-2194-4b36-ade9-053433e79201）via: https://opensource.com/article/23/4/cluster-open-source-python-api-wrapper作者：Ruth Netser 选题：lkxed 译者：geekpi 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
硬核观察 #836 可根据自然语言指令进行《我的世界》游戏的 AI
可根据自然语言指令进行《我的世界》游戏的 AI英伟达最近发布的一篇论文，披露了其为《我的世界》游戏训练的通用 AI 代理 MineDojo。研究人员给它输入了 73 万个 YouTube 视频（转录了超过 22 亿字），抓取了 7000 个维基网页，以及 34 万个 Reddit 帖子和 660 万条评论。人们可以用高级自然语言告诉 MineDojo 代理在游戏中做什么。不同于其它为电子游戏开发的 AI，它们往往只擅长特定的任务，研究人员开发了一个可扩展的训练框架，能够成功执行各种开放式任务的通用代理。消息来源：Ars Technica老王点评：能根据自然语言进行游戏，这距离通用 AI 更进一步了。GPT-3 可以写押韵的诗歌和歌词OpenAI 宣布了 GPT-3 的一个新模型，人们发现它可以生成押韵的歌曲、打油诗和诗歌，而这是 GPT-3 以前无法产生的水平。用机器生成诗歌并不新鲜，甚至早在 1845 年，发明家们就已经在想办法通过自动化来写出有表现力的诗句。但是，专家们特别指出，GPT-3 的最新模型可以将有关各种主题和风格的知识整合到一个能写出连贯文字的模型中。消息来源：Ars Technica老王点评：AI 甚至可以写押韵的诗歌，比那些下三路诗歌强了不止一点。API 安全漏洞成为大量泄露数据的重要风险恶意行为者利用 2021 年 12 月披露的一个 API 漏洞，获得了超过 540 万 Twitter 用户的数据。报告称，在过去 12 个月里，95% 的企业在生产用的 API 中遇到了安全问题，20% 的企业由于 API 中的安全漏洞而遭受数据泄露。API 攻击的一个不幸的现实是，这些系统的漏洞提供了对前所未有的数据量的访问。消息来源：Venture Beat老王点评：API 是一道通向内部的门，而设计一个完善安全的 API 的重要性，往往被人忽视。
单元测试可能令人生畏，但是这些 Python 模块会使你的生活变得更容易。在这个教程中，你将学到如何对执行 HTTP 请求代码的进行单元测试。也就是说，你将看到用 Python 对 API 进行单元测试的艺术。单元测试是指对单个行为的测试。在测试中，一个众所周知的经验法则就是隔离那些需要外部依赖的代码。比如，当测试一段执行 HTTP 请求的代码时，建议在测试过程中，把真正的调用替换成一个假的的调用。这种情况下，每次运行测试的时候，就可以对它进行单元测试，而不需要执行一个真正的 HTTP 请求。问题就是，怎样才能隔离这些代码？这就是我希望在这篇博文中回答的问题！我不仅会向你展示如果去做，而且也会权衡不同方法之间的优点和缺点。要求：Python 3.8pytest-mockrequestsflaskresponsesVCR.py使用一个天气状况 REST API 的演示程序为了更好的解决这个问题，假设你正在创建一个天气状况的应用。这个应用使用第三方天气状况 REST API 来检索一个城市的天气信息。其中一个需求是生成一个简单的 HTML 页面，像下面这个图片：伦敦的天气，OpenWeatherMap。图片是作者自己制作的。为了获得天气的信息，必须得去某个地方找。幸运的是，通过 OpenWeatherMap 的 REST API 服务，可以获得一切需要的信息。好的，很棒，但是我该怎么用呢？通过发送一个 GET 请求到：https://api.openweathermap.org/data/2.5/weather?q={city_name}&appid={api_key}&units=metric，就可以获得你所需要的所有东西。在这个教程中，我会把城市名字设置成一个参数，并确定使用公制单位。检索数据使用 requests 模块来检索天气数据。你可以创建一个接收城市名字作为参数的函数，然后返回一个 JSON。JSON 包含温度、天气状况的描述、日出和日落时间等数据。下面的例子演示了这样一个函数：def find_weather_for(city: str) -> dict: """Queries the weather API and returns the weather data for a particular city.""" url = API.format(city_name=city, api_key=API_KEY) resp = requests.get(url) return resp.json() 这个 URL 是由两个全局变量构成：BASE_URL = "https://api.openweathermap.org/data/2.5/weather" API = BASE_URL + "?q={city_name}&appid={api_key}&units=metric" API 以这个格式返回了一个 JSON：{ "coord": { "lon": -0.13, "lat": 51.51 }, "weather": [ { "id": 800, "main": "Clear", "description": "clear sky", "icon": "01d" } ], "base": "stations", "main": { "temp": 16.53, "feels_like": 15.52, "temp_min": 15, "temp_max": 17.78, "pressure": 1023, "humidity": 72 }, "visibility": 10000, "wind": { "speed": 2.1, "deg": 40 }, "clouds": { "all": 0 }, "dt": 1600420164, "sys": { "type": 1, "id": 1414, "country": "GB", "sunrise": 1600407646, "sunset": 1600452509 }, "timezone": 3600, "id": 2643743, "name": "London", "cod": 200 当调用 resp.json() 的时候，数据是以 Python 字典的形式返回的。为了封装所有细节，可以用 dataclass 来表示它们。这个类有一个工厂方法，可以获得这个字典并且返回一个 WeatherInfo 实例。这种办法很好，因为可以保持这种表示方法的稳定。比如，如果 API 改变了 JSON 的结构，就可以在同一个地方（from_dict 方法中）修改逻辑。其他代码不会受影响。你也可以从不同的源获得信息，然后把它们都整合到 from_dict 方法中。@dataclass class WeatherInfo: temp: float sunset: str sunrise: str temp_min: float temp_max: float desc: str @classmethod def from_dict(cls, data: dict) -> "WeatherInfo": return cls( temp=data["main"]["temp"], temp_min=data["main"]["temp_min"], temp_max=data["main"]["temp_max"], desc=data["weather"][0]["main"], sunset=format_date(data["sys"]["sunset"]), sunrise=format_date(data["sys"]["sunrise"]), ) 现在来创建一个叫做 retrieve_weather 的函数。使用这个函数调用 API，然后返回一个 WeatherInfo，这样就可创建你自己的 HTML 页面。def retrieve_weather(city: str) -> WeatherInfo: """Finds the weather for a city and returns a WeatherInfo instance.""" data = find_weather_for(city) return WeatherInfo.from_dict(data) 很好，我们的 app 现在有一些基础了。在继续之前，对这些函数进行单元测试。1、使用 mock 测试 API根据维基百科， 模拟对象 mock object 是通过模仿真实对象来模拟它行为的一个对象。在 Python 中，你可以使用 unittest.mock 库来 模拟 mock 任何对象，这个库是标准库中的一部分。为了测试 retrieve_weather 函数，可以模拟 requests.get，然后返回静态数据。pytest-mock在这个教程中，会使用 pytest 作为测试框架。通过插件，pytest 库是非常具有扩展性的。为了完成我们的模拟目标，要用 pytest-mock。这个插件抽象化了大量 unittest.mock 中的设置，也会让你的代码更简洁。如果你感兴趣的话，我在 另一篇博文中 会有更多的讨论。好的，言归正传，现在看代码。下面是一个 retrieve_weather 函数的完整测试用例。这个测试使用了两个 fixture：一个是由 pytest-mock 插件提供的 mocker fixture, 还有一个是我们自己的。就是从之前请求中保存的静态数据。@pytest.fixture() def fake_weather_info(): """Fixture that returns a static weather data.""" with open("tests/resources/weather.json") as f: return json.load(f) def test_retrieve_weather_using_mocks(mocker, fake_weather_info): """Given a city name, test that a HTML report about the weather is generated correctly.""" # Creates a fake requests response object fake_resp = mocker.Mock() # Mock the json method to return the static weather data fake_resp.json = mocker.Mock(return_value=fake_weather_info) # Mock the status code fake_resp.status_code = HTTPStatus.OK mocker.patch("weather_app.requests.get", return_value=fake_resp) weather_info = retrieve_weather(city="London") assert weather_info == WeatherInfo.from_dict(fake_weather_info) 如果运行这个测试，会获得下面的输出：============================= test session starts ============================== ...[omitted]... tests/test_weather_app.py::test_retrieve_weather_using_mocks PASSED [100%] ============================== 1 passed in 0.20s =============================== Process finished with exit code 0 很好，测试通过了！但是...生活并非一帆风顺。这个测试有优点，也有缺点。现在来看一下。优点好的，有一个之前讨论过的优点就是，通过模拟 API 的返回值，测试变得简单了。将通信和 API 隔离，这样测试就可以预测了。这样总会返回你需要的东西。缺点对于缺点，问题就是，如果不再想用 requests 了，并且决定回到标准库的 urllib，怎么办。每次改变 find_weather_for 的代码，都得去适配测试。好的测试是，当你修改代码实现的时候，测试时不需要改变的。所以，通过模拟，你最终把测试和实现耦合在了一起。而且，另一个不好的方面是你需要在调用函数之前进行大量设置——至少是三行代码。... # Creates a fake requests response object fake_resp = mocker.Mock() # Mock the json method to return the static weather data fake_resp.json = mocker.Mock(return_value=fake_weather_info) # Mock the status code fake_resp.status_code = HTTPStatus.OK ... 我可以做的更好吗？是的，请继续看。我现在看看怎么改进一点。使用 responses用 mocker 功能模拟 requests 有点问题，就是有很多设置。避免这个问题的一个好办法就是使用一个库，可以拦截 requests 调用并且给它们 打补丁 patch 。有不止一个库可以做这件事，但是对我来说最简单的是 responses。我们来看一下怎么用，并且替换 mock。@responses.activate def test_retrieve_weather_using_responses(fake_weather_info): """Given a city name, test that a HTML report about the weather is generated correctly.""" api_uri = API.format(city_name="London", api_key=API_KEY) responses.add(responses.GET, api_uri, json=fake_weather_info, status=HTTPStatus.OK) weather_info = retrieve_weather(city="London") assert weather_info == WeatherInfo.from_dict(fake_weather_info) 这个函数再次使用了我们的 fake_weather_info fixture。然后运行测试：============================= test session starts ============================== ... tests/test_weather_app.py::test_retrieve_weather_using_responses PASSED [100%] ============================== 1 passed in 0.19s =============================== 非常好！测试也通过了。但是...并不是那么棒。优点使用诸如 responses 这样的库，好的方面就是不需要再给 requests 打补丁 patch 。通过将这层抽象交给库，可以减少一些设置。然而，如果你没注意到的话，还是有一些问题。缺点和 unittest.mock 很像，测试和实现再一次耦合了。如果替换 requests，测试就不能用了。2、使用适配器测试 API如果用模拟让测试耦合了，我能做什么？设想下面的场景：假如说你不能再用 requests 了，而且必须要用 urllib 替换，因为这是 Python 自带的。不仅仅是这样，你了解了不要把测试代码和实现耦合，并且你想今后都避免这种情况。你想替换 urllib，也不想重写测试了。事实证明，你可以抽象出执行 GET 请求的代码。真的吗？怎么做？可以使用 适配器 adapter 来抽象它。适配器是一种用来封装其他类的接口，并作为新接口暴露出来的一种设计模式。用这种方式，就可以修改适配器而不需要修改代码了。比如，在 find_weather_for 函数中，封装关于 requests 的所有细节，然后把这部分暴露给只接受 URL 的函数。所以，这个：def find_weather_for(city: str) -> dict: """Queries the weather API and returns the weather data for a particular city.""" url = API.format(city_name=city, api_key=API_KEY) resp = requests.get(url) return resp.json() 变成这样：def find_weather_for(city: str) -> dict: """Queries the weather API and returns the weather data for a particular city.""" url = API.format(city_name=city, api_key=API_KEY) return adapter(url) 然后适配器变成这样：def requests_adapter(url: str) -> dict: resp = requests.get(url) return resp.json() 现在到了重构 retrieve_weather 函数的时候：def retrieve_weather(city: str) -> WeatherInfo: """Finds the weather for a city and returns a WeatherInfo instance.""" data = find_weather_for(city, adapter=requests_adapter) return WeatherInfo.from_dict(data) 所以，如果你决定改为使用 urllib 的实现，只要换一下适配器：def urllib_adapter(url: str) -> dict: """An adapter that encapsulates urllib.urlopen""" with urllib.request.urlopen(url) as response: resp = response.read() return json.loads(resp) def retrieve_weather(city: str) -> WeatherInfo: """Finds the weather for a city and returns a WeatherInfo instance.""" data = find_weather_for(city, adapter=urllib_adapter) return WeatherInfo.from_dict(data) 好的，那测试怎么做？为了测试 retrieve_weather, 只要创建一个在测试过程中使用的假的适配器：@responses.activate def test_retrieve_weather_using_adapter( fake_weather_info, ): def fake_adapter(url: str): return fake_weather_info weather_info = retrieve_weather(city="London", adapter=fake_adapter) assert weather_info == WeatherInfo.from_dict(fake_weather_info) 如果运行测试，会获得：============================= test session starts ============================== tests/test_weather_app.py::test_retrieve_weather_using_adapter PASSED [100%] ============================== 1 passed in 0.22s =============================== 优点这个方法的优点是可以成功将测试和实现解耦。使用 依赖注入 dependency injection 在测试期间注入一个假的适配器。你也可以在任何时候更换适配器，包括在运行时。这些事情都不会改变任何行为。缺点缺点就是，因为你在测试中用了假的适配器，如果在实现中往适配器中引入了一个 bug，测试的时候就不会发现。比如说，往 requests 传入了一个有问题的参数，像这样：def requests_adapter(url: str) -> dict: resp = requests.get(url, headers=<some broken headers>) return resp.json() 在生产环境中，适配器会有问题，而且单元测试没办法发现。但是事实是，之前的方法也会有同样的问题。这就是为什么不仅要单元测试，并且总是要集成测试。也就是说，要考虑另一个选项。3、使用 VCR.py 测试 API现在终于到了讨论我们最后一个选项了。诚实地说，我也是最近才发现这个。我用 模拟 mock 也很长时间了，而且总是有一些问题。VCR.py 是一个库，它可以简化很多 HTTP 请求的测试。它的工作原理是将第一次运行测试的 HTTP 交互记录为一个 YAML 文件，叫做 cassette。请求和响应都会被序列化。当第二次运行测试的时候，VCT.py 将拦截对请求的调用，并且返回一个响应。现在看一下下面如何使用 VCR.py 测试 retrieve_weather：@vcr.use_cassette() def test_retrieve_weather_using_vcr(fake_weather_info): weather_info = retrieve_weather(city="London") assert weather_info == WeatherInfo.from_dict(fake_weather_info) 天呐，就这样？没有设置？@vcr.use_cassette() 是什么？是的，就这样！没有设置，只要一个 pytest 标注告诉 VCR 去拦截调用，然后保存 cassette 文件。cassette 文件是什么样？好问题。这个文件里有很多东西。这是因为 VCR 保存了交互中的所有细节。interactions: - request: body: null headers: Accept: - '*/*' Accept-Encoding: - gzip, deflate Connection: - keep-alive User-Agent: - python-requests/2.24.0 method: GET uri: https://api.openweathermap.org/data/2.5/weather?q=London&appid=<YOUR API KEY HERE>&units=metric response: body: string: '{"coord":{"lon":-0.13,"lat":51.51},"weather":[{"id":800,"main":"Clear","description":"clearsky","icon":"01d"}],"base":"stations","main":{"temp":16.53,"feels_like":15.52,"temp_min":15,"temp_max":17.78,"pressure":1023,"humidity":72},"visibility":10000,"wind":{"speed":2.1,"deg":40},"clouds":{"all":0},"dt":1600420164,"sys":{"type":1,"id":1414,"country":"GB","sunrise":1600407646,"sunset":1600452509},"timezone":3600,"id":2643743,"name":"London","cod":200}' headers: Access-Control-Allow-Credentials: - 'true' Access-Control-Allow-Methods: - GET, POST Access-Control-Allow-Origin: - '*' Connection: - keep-alive Content-Length: - '454' Content-Type: - application/json; charset=utf-8 Date: - Fri, 18 Sep 2020 10:53:25 GMT Server: - openresty X-Cache-Key: - /data/2.5/weather?q=london&units=metric status: code: 200 message: OK version: 1 确实很多！真的！好的方面就是你不需要留意它。VCR.py 会为你安排好一切。优点现在看一下优点，我可以至少列出五个：没有设置代码。测试仍然是分离的，所以很快。测试是确定的。如果你改了请求，比如说用了错误的 header，测试会失败。没有与代码实现耦合，所以你可以换适配器，而且测试会通过。唯一有关系的东西就是请求必须是一样的。缺点再与模拟相比较，除了避免了错误，还是有一些问题。如果 API 提供者出于某种原因修改了数据格式，测试仍然会通过。幸运的是，这种情况并不经常发生，而且在这种重大改变之前，API 提供者通常会给他们的 API 提供不同版本。另一个需要考虑的事情是 就地 in place 端到端 end-to-end 测试。每次服务器运行的时候，这些测试都会调用。顾名思义，这是一个范围更广、更慢的测试。它们会比单元测试覆盖更多。事实上，并不是每个项目都需要使用它们。所以，就我看来，VCR.py 对于大多数人的需求来说都绰绰有余。总结就这么多了。我希望今天你了解了一些有用的东西。测试 API 客户端应用可能会有点吓人。然而，当武装了合适的工具和知识，你就可以驯服这个野兽。在 我的 Github 上可以找到这个完整的应用。这篇文章最早发表在 作者的个人博客，授权转载via: https://opensource.com/article/21/9/unit-test-python作者：Miguel Brito 选题：lujun9972 译者：Yufei-Yan 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
硬核观察 #694 马斯克称推特的 API 速率限制是他决定放弃收购的核心原因
马斯克称推特的 API 速率限制是他决定放弃收购的核心原因马斯克曾多次公开表示，他希望知道有多少推特账户是由机器人运营的，因为他觉得可疑账户的数量影响了公司的价值。因此他寻求检测和处理虚假账户的方法。当推特最终提供了马斯克明确要求的 8 个开发者 API 的访问权时，这些 API 的速率限制低于推特向其最大企业客户提供的速率限制。马斯克称，这使得他们无法在任何合理的时间内完成数据分析。消息来源：The Register老王点评：看看，对 API 访问提供速率限制有多严重。: D许多老的 X.Org 组件在本周末有新的发布上周末，Oracle 的长期 X.Org 贡献者 Alan Coopersmith 对各种旧的、很少维护的 X.Org 项目发布了许多新的更新。这些软件多年来没有任何更新，新的版本大多只是收集了过去几年中的一些随机修复。总的来说，这些更新并没有什么值得兴奋的改进。消息来源：Phoronix老王点评：这是觉得 X.org 快被放弃了才最后抢救一次？从另外一方面看，可见 X.org 社区有多冷清了。连随手收到的一些修复都懒得发布个新版本。Linode 现在提供 Kali Linux 实例Kali Linux 是一个为渗透测试或黑客而设计的 Linux 发行版。这个基于 Debian 的 Linux 是安全研究人员最喜欢的发行版。它现在可以在 Linode 云上使用了，你可以用它来测试和保护你的生产系统。Linode 云是第一个与 Kali Linux 合作的替代云提供商。消息来源：ZDNet老王点评：这是一个跑在公有云上的黑客套装？总觉得哪里不对。
硬核观察 #686 一家中国公司发布了首款 RISC-V 笔记本电脑？
一家中国公司发布了首款 RISC-V 笔记本电脑？“RISC-V 国际”透露，一家名为“鉴释”的中国公司已经宣布了第一台面向开发者的 RISC-V 笔记本电脑，现已开放预订。据介绍，这个 “ROMA” 开发平台具有四核 RISC-V 处理器，高达 16GB 的内存，最大 256GB 的存储空间，并且可以运行大多数 RISC-V Linux 发行版。有趣的是，这家名为“鉴释”的公司原本是一家开发静态代码分析工具的公司，这个名为“ROMA”的新产品也无法在其中英文网站找到任何链接，只有一个孤儿 预定页，而预定页中提供一个表格，没有确定的价格或预期供应量的信息、甚至连概念图都没有，并且还提到了一些 Web3 和 NFT 的事情。消息来源：Phoronix老王点评：这真不是洋葱新闻？谷歌推出“高级 API 安全”服务谷歌宣布了“高级 API 安全”的预览版，这是一个新产品，将在谷歌云上推出，旨在检测与 API 有关的安全威胁。该服务可以识别 API 错误配置和检测恶意机器人。配置错误的 API 是造成 API 安全事件的主要原因之一。开发者们越来越依赖 API，但它们也越来越成为攻击的目标。根据一份 2018 年的报告，三分之二的组织正在向公众和合作伙伴暴露了不安全的 API。消息来源：Tech Crunch老王点评：一个配置不当的 API，造成的影响可能非常可怕，我想网络安全圈子对这个很有感受。Linux Mint 21 不准备用 oomdLinux Mint 21 正在努力争取在今年夏天发布，它是建立在 Ubuntu 22.04 LTS 之上的。但因 Ubuntu 22.04 在 systemd-oomd 上广受批评，Linux Mint 决定不用这个服务来“屠杀”用户的应用。Canonical 公司的开发人员一直在努力修复这个问题，随着最新提出的 SRU 可能会有所改善。消息来源：Phoronix老王点评：Mint 做了明智的选择。这根本不是 oomd 的策略问题，而是不经过用户同意，甚至用户没有反对的机会，就被杀了应用。
大家好！几天前我写了篇 小型的个人程序 的文章，里面提到了调用没有文档说明的“秘密” API 很有意思，你需要从你的浏览器中把 cookies 复制出来才能访问。有些读者问如何实现，因此我打算详细描述下，其实过程很简单。我们还会谈谈在调用没有文档说明的 API 时，可能会遇到的错误和道德问题。我们用谷歌 Hangouts 举例。我之所以选择它，并不是因为这个例子最有用（我认为官方的 API 更实用），而是因为在这个场景中更有用的网站很多是小网站，而小网站的 API 一旦被滥用，受到的伤害会更大。因此我们使用谷歌 Hangouts，因为我 100% 肯定谷歌论坛可以抵御这种试探行为。我们现在开始！第一步：打开开发者工具，找一个 JSON 响应我浏览了 https://hangouts.google.com，在Firefox的开发者工具中打开“ruby网络rtNetwork/rt/ruby”标签，找到一个 JSON 响应。你也可以使用 Chrome 的开发者工具。打开之后界面如下图：找到其中一条 “ 类型 Type ” 列显示为 json 的请求。为了找一条感兴趣的请求，我找了好一会儿，突然我找到一条 “people” 的端点，看起来是返回我们的联系人信息。听起来很有意思，我们来看一下。第二步：复制为 cURL下一步，我在感兴趣的请求上右键，点击 “复制Copy” -> “ 复制为 cURL Copy as cURL ”。然后我把 curl 命令粘贴到终端并运行。下面是运行结果：$ curl 'https://people-pa.clients6.google.com/v2/people/?key=REDACTED' -X POST ........ （省略了大量请求标头） Warning: Binary output can mess up your terminal. Use "--output -" to tell Warning: curl to output it to your terminal anyway, or consider "--output Warning: <FILE>" to save to a file. 你可能会想 —— 很奇怪，“二进制的输出在你的终端上无法正常显示” 是什么错误？原因是，浏览器默认情况下发给服务器的请求头中有 Accept-Encoding: gzip, deflate 参数，会把输出结果进行压缩。我们可以通过管道把输出传递给 gunzip 来解压，但是我们发现不带这个参数进行请求会更简单。因此我们去掉一些不相关的请求头。第三步：去掉不相关的请求头下面是我从浏览器获得的完整 curl 命令。有很多行！我用反斜杠（\）把请求分开，这样每个请求头占一行，看起来更清晰：curl 'https://people-pa.clients6.google.com/v2/people/?key=REDACTED' \ -X POST \ -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0' \ -H 'Accept: */*' \ -H 'Accept-Language: en' \ -H 'Accept-Encoding: gzip, deflate' \ -H 'X-HTTP-Method-Override: GET' \ -H 'Authorization: SAPISIDHASH REDACTED' \ -H 'Cookie: REDACTED' -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'X-Goog-AuthUser: 0' \ -H 'Origin: https://hangouts.google.com' \ -H 'Connection: keep-alive' \ -H 'Referer: https://hangouts.google.com/' \ -H 'Sec-Fetch-Dest: empty' \ -H 'Sec-Fetch-Mode: cors' \ -H 'Sec-Fetch-Site: same-site' \ -H 'Sec-GPC: 1' \ -H 'DNT: 1' \ -H 'Pragma: no-cache' \ -H 'Cache-Control: no-cache' \ -H 'TE: trailers' \ --data-raw 'personId=101777723309&personId=1175339043204&personId=1115266537043&personId=116731406166&extensionSet.extensionNames=HANGOUTS_ADDITIONAL_DATA&extensionSet.extensionNames=HANGOUTS_OFF_NETWORK_GAIA_GET&extensionSet.extensionNames=HANGOUTS_PHONE_DATA&includedProfileStates=ADMIN_BLOCKED&includedProfileStates=DELETED&includedProfileStates=PRIVATE_PROFILE&mergedPersonSourceOptions.includeAffinity=CHAT_AUTOCOMPLETE&coreIdParams.useRealtimeNotificationExpandedAcls=true&requestMask.includeField.paths=person.email&requestMask.includeField.paths=person.gender&requestMask.includeField.paths=person.in_app_reachability&requestMask.includeField.paths=person.metadata&requestMask.includeField.paths=person.name&requestMask.includeField.paths=person.phone&requestMask.includeField.paths=person.photo&requestMask.includeField.paths=person.read_only_profile_info&requestMask.includeField.paths=person.organization&requestMask.includeField.paths=person.location&requestMask.includeField.paths=person.cover_photo&requestMask.includeContainer=PROFILE&requestMask.includeContainer=DOMAIN_PROFILE&requestMask.includeContainer=CONTACT&key=REDACTED' 第一眼看起来内容有很多，但是现在你不需要考虑每一行是什么意思。你只需要把不相关的行删掉就可以了。我通常通过删掉某行查看是否有错误来验证该行是不是可以删除 —— 只要请求没有错误就一直删请求头。通常情况下，你可以删掉 Accept*、Referer、Sec-*、DNT、User-Agent 和缓存相关的头。在这个例子中，我把请求删成下面的样子：curl 'https://people-pa.clients6.google.com/v2/people/?key=REDACTED' \ -X POST \ -H 'Authorization: SAPISIDHASH REDACTED' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'Origin: https://hangouts.google.com' \ -H 'Cookie: REDACTED'\ --data-raw 'personId=101777723309&personId=1175339043204&personId=1115266537043&personId=116731406166&extensionSet.extensionNames=HANGOUTS_ADDITIONAL_DATA&extensionSet.extensionNames=HANGOUTS_OFF_NETWORK_GAIA_GET&extensionSet.extensionNames=HANGOUTS_PHONE_DATA&includedProfileStates=ADMIN_BLOCKED&includedProfileStates=DELETED&includedProfileStates=PRIVATE_PROFILE&mergedPersonSourceOptions.includeAffinity=CHAT_AUTOCOMPLETE&coreIdParams.useRealtimeNotificationExpandedAcls=true&requestMask.includeField.paths=person.email&requestMask.includeField.paths=person.gender&requestMask.includeField.paths=person.in_app_reachability&requestMask.includeField.paths=person.metadata&requestMask.includeField.paths=person.name&requestMask.includeField.paths=person.phone&requestMask.includeField.paths=person.photo&requestMask.includeField.paths=person.read_only_profile_info&requestMask.includeField.paths=person.organization&requestMask.includeField.paths=person.location&requestMask.includeField.paths=person.cover_photo&requestMask.includeContainer=PROFILE&requestMask.includeContainer=DOMAIN_PROFILE&requestMask.includeContainer=CONTACT&key=REDACTED' 这样我只需要 4 个请求头：Authorization、Content-Type、Origin 和 Cookie。这样容易管理得多。第四步：在 Python 中发请求现在我们知道了我们需要哪些请求头，我们可以把 curl 命令翻译进 Python 程序！这部分是相当机械化的过程，目标仅仅是用 Python 发送与 cUrl 相同的数据。下面是代码实例。我们使用 Python 的 requests 包实现了与前面 curl 命令相同的功能。我把整个长请求分解成了元组的数组，以便看起来更简洁。import requests import urllib data = [ ('personId','101777723'), # I redacted these IDs a bit too ('personId','117533904'), ('personId','111526653'), ('personId','116731406'), ('extensionSet.extensionNames','HANGOUTS_ADDITIONAL_DATA'), ('extensionSet.extensionNames','HANGOUTS_OFF_NETWORK_GAIA_GET'), ('extensionSet.extensionNames','HANGOUTS_PHONE_DATA'), ('includedProfileStates','ADMIN_BLOCKED'), ('includedProfileStates','DELETED'), ('includedProfileStates','PRIVATE_PROFILE'), ('mergedPersonSourceOptions.includeAffinity','CHAT_AUTOCOMPLETE'), ('coreIdParams.useRealtimeNotificationExpandedAcls','true'), ('requestMask.includeField.paths','person.email'), ('requestMask.includeField.paths','person.gender'), ('requestMask.includeField.paths','person.in_app_reachability'), ('requestMask.includeField.paths','person.metadata'), ('requestMask.includeField.paths','person.name'), ('requestMask.includeField.paths','person.phone'), ('requestMask.includeField.paths','person.photo'), ('requestMask.includeField.paths','person.read_only_profile_info'), ('requestMask.includeField.paths','person.organization'), ('requestMask.includeField.paths','person.location'), ('requestMask.includeField.paths','person.cover_photo'), ('requestMask.includeContainer','PROFILE'), ('requestMask.includeContainer','DOMAIN_PROFILE'), ('requestMask.includeContainer','CONTACT'), ('key','REDACTED') ] response = requests.post('https://people-pa.clients6.google.com/v2/people/?key=REDACTED', headers={ 'X-HTTP-Method-Override': 'GET', 'Authorization': 'SAPISIDHASH REDACTED', 'Content-Type': 'application/x-www-form-urlencoded', 'Origin': 'https://hangouts.google.com', 'Cookie': 'REDACTED', }, data=urllib.parse.urlencode(data), ) print(response.text) 我执行这个程序后正常运行 —— 输出了一堆 JSON 数据！太棒了！你会注意到有些地方我用 REDACTED 代替了，因为如果我把原始数据列出来你就可以用我的账号来访问谷歌论坛了，这就很不好了。运行结束！现在我可以随意修改 Python 程序，比如传入不同的参数，或解析结果等。我不打算用它来做其他有意思的事了，因为我压根对这个 API 没兴趣，我只是用它来阐述请求 API 的过程。但是你确实可以对返回的一堆 JSON 做一些处理。curlconverter 看起来很强大有人评论说可以使用 https://curlconverter.com/ 自动把 curl 转换成 Python（和一些其他的语言！），这看起来很神奇 —— 我都是手动转的。我在这个例子里使用了它，看起来一切正常。追踪 API 的处理过程并不容易我不打算夸大追踪 API 处理过程的难度 —— API 的处理过程并不明显！我也不知道传给这个谷歌论坛 API 的一堆参数都是做什么的！但是有一些参数看起来很直观，比如 requestMask.includeField.paths=person.email 可能表示“包含每个人的邮件地址”。因此我只关心我能看懂的参数，不关心看不懂的。（理论上）适用于所有场景可能有人质疑 —— 这个方法适用于所有场景吗？答案是肯定的 —— 浏览器不是魔法！浏览器发送给你的服务器的所有信息都是 HTTP 请求。因此如果我复制了浏览器发送的所有的 HTTP 请求头，那么后端就会认为请求是从我的浏览器发出的，而不是用 Python 程序发出的。当然，我们去掉了一些浏览器发送的请求头，因此理论上后端是可以识别出来请求是从浏览器还是 Python 程序发出的，但是它们通常不会检查。这里有一些对读者的告诫 —— 一些谷歌服务的后端会通过令人难以理解（对我来说是）方式跟前端通信，因此即使理论上你可以模拟前端的请求，但实际上可能行不通。可能会遭受更多攻击的大型 API 会有更多的保护措施。我们已经知道了如何调用没有文档说明的 API。现在我们再来聊聊可能遇到的问题。问题 1：会话 cookie 过期一个大问题是我用我的谷歌会话 cookie 作为身份认证，因此当我的浏览器会话过期后，这个脚本就不能用了。这意味着这种方式不能长久使用（我宁愿调一个真正的 API），但是如果我只是要一次性快速抓取一小组数据，那么可以使用它。问题 2：滥用如果我正在请求一个小网站，那么我的 Python 脚本可能会把服务打垮，因为请求数超出了它们的处理能力。因此我请求时尽量谨慎，尽量不过快地发送大量请求。这尤其重要，因为没有官方 API 的网站往往是些小网站且没有足够的资源。很明显在这个例子中这不是问题 —— 我认为在写这篇文章的过程我一共向谷歌论坛的后端发送了 20 次请求，他们肯定可以处理。如果你用自己的账号身份过度访问这个 API 并导致了故障，那么你的账号可能会被暂时封禁（情理之中）。我只下载我自己的数据或公共的数据 —— 我的目的不是寻找网站的弱点。请记住所有人都可以访问你没有文档说明的 API我认为本文最重要的信息并不是如何使用其他人没有文档说明的 API。虽然很有趣，但是也有一些限制，而且我也不会经常这么做。更重要的一点是，任何人都可以这么访问你后端的 API！每个人都有开发者工具和网络标签，查看你传到后端的参数、修改它们都很容易。因此如果一个人通过修改某些参数来获取其他用户的信息，这不值得提倡。我认为提供公开 API 的大部分开发者们都知道，但是我之所以再提一次，是因为每个初学者都应该了解。: )via: https://jvns.ca/blog/2022/03/10/how-to-use-undocumented-web-apis/作者：Julia Evans 选题：lujun9972 译者：lxbwolf 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
创建一个 API（应用程序接口），我们所要做的远远不止是让它能“正常工作”。如果你正在构建基于 C/S 模型的应用程序，那么你需要一个应用程序接口（API）。API 就是一种非常清晰而又明确的定义，它是一个 进程 process 与另一个进程之间明确定义的边界。Web 应用中我们常见的边界定义就是 REST/JSON API。虽然很多开发者可能主要关注在如何让 API 正常工作（或功能正常），但却还有一些“非功能性”的要求也是需要他们注意的。所有的 API 必须具备 的 4 个非功能性的要求是：安全文档验证测试安全在软件开发中，安全是最基本的要求。对于 API 开发者来说，API 的安全性主要包含以下 4 个方面：HTTPS/SSL 证书跨域资源共享身份认证与 JSON Web 令牌授权与作用域1、HTTPS/SSL 证书Web 应用的黄金标准是使用 SSL 证书的 HTTPS 协议。Let's Encrypt 可以帮你达到这一目的。Let's Encrypt 来自于非营利性的互联网安全研究小组（ISRG），它是一个免费的、自动化的、开放的证书颁发机构。Let's Encrypt 的软件会为你的域（LCTT 译注：包含域名、IP 等信息）生成中央授权证书。而正是这些证书确保了从你的 API 到客户端的数据载荷是点对点加密的。Let's Encrypt 支持证书管理的多种部署方案。我们可以通过查看 文档 来找出最适合自己需要的方案。2、跨域资源共享 跨域资源共享 Cross-origin resource sharing （CORS）是一个针对浏览器的安全策略预检。如果你的 API 服务器与发出请求的客户端不在同一个域中，那么你就要处理 CORS。例如，如果你的服务器运行在 api.domain-a.com 并且接到一个来自 domain-b.com 的客户端的请求，那么 CORS 就会（让浏览器）发送一个 HTTP 预检请求，以便查看你的 API 服务是否会接受来自此客户域的客户端请求。来自 MDN 的定义：“跨域资源共享（CORS）是一种基于 HTTP 头的机制，这种机制允许服务器标记除自身源外的其他任何来源（域、方案或端口）。而对于这些被服务器标识的源，浏览器应该允许我们从它们加载资源。”(MDN文档， CC-BY-SA 2.5)另外，有很多用于 Node.js 的辅助库来 帮助 API 开发者处理 CORS。3、身份认证与 JSON Web 令牌有多种方法可以验证你的 API 中的认证用户，但最好的方法之一是使用 JSON Web 令牌（JWT），而这些令牌使用各种知名的加密库进行签名。当客户端登录时，身份管理服务会向客户端提供一个 JWT。然后，客户端可以使用这个令牌向 API 发出请求，API 收到请求后，从服务器读取公钥或私密信息来验证这个令牌。有一些现有的库，可以帮助我们对令牌进行验证，包括 jsonwebtoken。关于 JWT 的更多信息，以及各种语言中对其的支持库，请查看 JWT.io。import jwt from 'jsonwebtoken' export default function (req, res, next) { // req.headers.authorization Bearer token const token = extractToken(req) jwt.verify(token, SECRET, { algorithms: ['HS256'] }, (err, decoded) => { if (err) { next(err) } req.session = decoded next() }) } 4、授权与作用域认证（或身份验证）很重要，但授权同样很重要。也就是说，经过验证的客户端是否有权限让服务器执行某个请求呢？这就是作用域的价值所在。当身份管理服务器对客户端进行身份认证，且创建 JWT 令牌时，身份管理服务会给当前客户提供一个作用域，这个作用域将会决定当前经过验证的客户的 API 请求能否被服务器执行。这样也就免去了服务器对访问控制列表的一些不必要的查询。作用域是一个文本块（通常以空格分隔），用于描述一个 API 端点的访问能力。一般来说，作用域被分为资源与动作。这种模式对 REST/JSON API 很有效，因为它们有相似的 RESOURCE:ACTION 结构。（例如，ARTICLE:WRITE 或 ARTICLE:READ，其中 ARTICLE 是资源，READ 和 WRITE 是动作）。作用域的划分让我们的 API 能够专注于功能的实现，而不是去考虑各种角色和用户。身份访问管理服务可以将不同的角色和用户分配不同的权限范围，然后再将这些不同的作用域提供给不同的 JWT 验证中的客户。总结当我们开发和部署 API 时，安全应该一直是最重要的要求之一。虽然安全性是一个比较宽泛的话题，但如果能解决上面四个方面的问题，这对于你的 API 来说，在生产环境中将会表现得更好。文档有什么能比没有文档更糟糕？过期的文档。开发者对文档真的是又爱又恨。尽管如此，文档仍然是 API 定义是否完善的一个关键部分。开发者需要从文档中知道如何使用这些 API，且你创建的文档对于开发者如何更好地使用 API 也有着非常巨大的作用。创建 API 文档，我们需要关注下面三个方面：开发者入门文档（自述文件/基本介绍）技术参考（规范/说明书）使用方法（入门和其他指南）1、入门文档在构建 API 服务的时候，你需要明确一些事情，比如：这个 API 是做什么的？如何建立开发者环境？如何测试该服务？如何提交问题？如何部署它？通常我们可以通过自述（README）文件来回答上面的这些问题，自述文件一般放在你的代码库中，用于为开发者提供使用你项目的最基本的起点和说明。自述文件应该包含：API 的描述技术参考与指南的链接如何以开发者的身份设置你的项目如何测试这个项目如何部署这个项目依赖管理代码贡献指南行为准则许可证致谢你的自述文件应该简明扼要；你不必解释每一个方面，但要提供足够的信息，以便开发者在熟悉你的项目后可以进一步深入研究。2、技术参考在 REST/JSON API 中, 每一个具体的 端点 endpoint 都对应一个特定的功能，都需要一个具体的说明文档，这非常重要。文档中会定义 API 的描述，输入和可能的输出，并为各种客户端提供使用示例。OpenAPI 是一个创建 REST/JSON 文档的标准, 它可以指导你完成编写 API 文档所需的各种细节。OpenAPI 还可以为你的 API 生成演示文档。3、使用方法对于 API 的用户来说，仅仅只有技术说明是不够的。他们还需要知道如何在一些特定的情况和场景下来使用这些 API，而大多数的潜在用户可能希望通过你的 API 来解决他们所遇到的问题。向用户介绍 API 的一个好的方法是利用一个“开始”页面。“开始”页面可以通过一个简单的用例引导用户，让他们迅速了解你的 API 能给他们能带来的益处。总结对于任何完善的 API，文档都是一个很关键的组成部分。当你在创建文档时，你需要关注 API 文档中的如何入门、技术参考以及如何快速开始等三个方面，这样你的 API 才算是一个完善的 API。验证API 开发过程中经常被忽视的一个点就是验证。它是一个验证来自外部来源的输入的过程。这些来源可以是客户端发送过来的 JSON 数据，或者是你请求别人的服务收到的响应数据。我们不仅仅要检查这些数据的类型，还要确保这些数据确实是我们要的数据，这样可以消除很多潜在的问题。了解你的边界以及你能控制的和不能控制的东西，对于 API 的数据验证来说是一个很重要的方面。最好的策略是在进入数据逻辑处理之前，在你能控制的边界的边缘处进行数据的验证。当客户端向你的 API 发送数据时，你需要对该数据做出任何处理之前应用你的验证，比如：确保 Email 是真实的邮件地址、日期数据有正确的格式、字符串符合长度要求。这种简单的检查可以为你的应用增加安全性和一致性。还有，当你从某个服务接收数据时，比如数据库或缓存，你需要重新验证这些数据，以确保返回的结果符合你的数据检查。你可以自己手写这些校验逻辑，当然也可以用像 Lodash 或 Ramda 这样的函数库，它们对于一些小的数据对象非常有用。像 Joi、Yup 或 Zod 这样的验证库效果会更好，因为它们包含了一些常见的验证方法，可以节省你的时间和精力。除此，它们还能创建一个可读性强的模式。如果你需要看看与语言无关的东西，请看 JSON Schema。总结数据验证虽然并不显眼和突出（LCTT 译注：跟 API 的功能实现以及其他几个方面比），但它可以帮你节省大量的时间。如果不做验证，这些时间将可能被用于故障排除和编写数据迁移脚本。真的不要相信你的客户端会发送干净的数据给你，也不要让验证不通过的数据渗入到你的业务逻辑或持久数据存储中去。花点时间验证你的 API 收到的数据和请求到的响应数据，虽然在前期你可能会感到一些挫折和不适，但这总比你在后期花大量时间去做各种数据收紧管制、故障排除等要容易得多。测试测试是软件开发中的最佳实践，它应该是最主要的非功能性的要求。对于包括 API 在内的任何项目，确定测试策略都是一个挑战，因为你自始至终都要掌握各种约束，以便相应的来制定你的测试策略。 集成测试 Integration testing 是测试 API 的最有效的方法之一。在集成测试模式中，开发团队会创建一个测试集用来覆盖应用流程中的某些部分，从一个点到另一个点。一个好的集成测试流程包括测试 API 的入口点以及模拟请求到服务端的响应。搞定这两点，你就覆盖了整个逻辑，包括从 API 请求的开始到模拟服务器的响应，并返回数据给 API。虽然使用的是模拟，但这种方法让能我们专注于代码逻辑层，而不需要去依赖后端服务和展示逻辑来进行测试。没有依赖的测试会更加可靠、更容易实现自动化，且更容易被接入持续集成管道流。集成测试的实施中，我会用 Tape、Test-server 和 Fetch-mock。这些库让我们能够从 API 的请求到数据的响应进行隔离测试，使用 Fetch-mock 还可以将出站请求捕获到持久层。总结虽然其他的各种测试和类型检查对 API 也都有很好的益处，但集成测试在流程效率、构建和管理时间方面却有着更大的优势。使用 Fetch-mock 这样的工具，可以在服务边界提供一个干净的模拟场景。专注于基础不管是设计和构建应用程序还是 API，都要确保包含上面的四个基本要素。它们并不是我们唯一需要考虑的非功能性需求，其他的还包括应用监控、日志和 API 管理等。即便如此，安全、文档、验证和测试这四个基本点，对于构建任何使用场景下的完善 API 都是至关重要的关注点。via: https://opensource.com/article/21/5/successful-apis作者：Tom Wilson 选题：lujun9972 译者：ywxgod 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出