OpenCode 实践课:4 分钟用 Python 写出一个命令行 IP 归属地查询工具

项目介绍

你有没有遇过这种情况:服务器日志里冒出一个陌生 IP,想知道它是哪来的?或者想确认自己的公网 IP 到底在哪个城市?今天我们用 OpenCode 写一个命令行 IP 归属地查询工具,零依赖、纯 Python 标准库,4 分钟搞定。

功能清单: 查询任意 IP 的国家、城市、ISP、ASN、时区、经纬度;支持批量查询多个 IP;支持 JSON 原始输出;不填 IP 自动查本机公网地址。数据源是 ip-api.com 免费接口。

准备工作

  • Python 3.6+(系统自带即可)
  • OpenCode 终端(安装方式见 opencode.ai
  • 能正常访问外网(ip-api.com 免费 API 无需注册)

安装后在终端输入 opencode 进入交互界面,就可以开始用中文"下指令"了。

实践过程

第一步:一句话生成核心功能

对 OpenCode 的需求描述越具体,生成结果越精准。我的第一句提示词是这样的:

> 用 Python 写一个命令行 IP 归属地查询工具 ipquery.py。调用 ip-api.com 免费接口(http://ip-api.com/json/{ip}),支持传入 IP 作为参数,不传参数则查本机公网 IP。用 argparse 解析参数,用 urllib 发请求,输出国家、城市、ISP、时区等信息。纯标准库,不要第三方依赖。

OpenCode 几秒钟就生成了完整的 ipquery.py,包含 URL 拼接、请求头设置、异常处理和格式化输出,直接可运行:

#!/usr/bin/env python3
import sys
import json
import argparse
import urllib.request

API_URL = "http://ip-api.com/json/{}"

FIELDS = "status,message,country,regionName,city,isp,org,as,query,timezone,lat,lon"

def query_ip(ip: str, lang: str = "zh-CN"):
    params = f"?lang={lang}&fields={FIELDS}"
    url = API_URL.format(ip) + params
    req = urllib.request.Request(url, headers={"User-Agent": "ipquery/1.0"})
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            data = json.loads(resp.read().decode())
            if data.get("status") == "fail":
                return {"error": data.get("message", "未知错误")}
            return data
    except urllib.error.URLError as e:
        return {"error": f"网络错误: {e.reason}"}
    except Exception as e:
        return {"error": str(e)}

def format_output(data: dict):
    if "error" in data:
        print(f"[错误] {data['error']}")
        return
    label_width = 12
    mapping = [
        ("IP 地址", "query"), ("国家/地区", "country"),
        ("省份/州", "regionName"), ("城市", "city"),
        ("ISP", "isp"), ("组织", "org"),
        ("ASN", "as"), ("时区", "timezone"),
        ("坐标", "lat,lon"),
    ]
    for label, key in mapping:
        if key == "lat,lon":
            value = f"{data.get('lat', '?')}, {data.get('lon', '?')}"
        else:
            value = data.get(key)
        if value:
            print(f"  {label:<{label_width}}: {value}")

def main():
    parser = argparse.ArgumentParser(description="IP 归属地查询工具")
    parser.add_argument("ip", nargs="?", default="", help="要查询的 IP 地址")
    parser.add_argument("-l", "--lang", default="zh-CN", choices=["zh-CN", "en"])
    args = parser.parse_args()

    display = args.ip or "本机公网 IP"
    print(f"查询: {display}\n")
    result = query_ip(args.ip, args.lang)
    format_output(result)

if __name__ == "__main__":
    main()

运行 python ipquery.py 8.8.8.8,输出类似:

查询: 8.8.8.8

  IP 地址      : 8.8.8.8
  国家/地区    : 美国
  省份/州      : Virginia
  城市         : Ashburn
  ISP          : Google LLC
  ASN          : AS15169 Google LLC
  时区         : America/New_York

要点: 描述需求时要明确技术栈、输入输出和约束条件("纯标准库、不要第三方依赖"),OpenCode 就能一次性生成可直接运行的代码。

第二步:追加功能——JSON 原始输出

基础版跑通后,我想加一个 --raw 参数,方便把结果 pipe 给 jq 或其他脚本处理:

> 给 ipquery.py 加一个 -r / --raw 参数,输出原始 JSON 而不是格式化文本。

OpenCode 精准地新增了参数定义和分支逻辑,没有改动其他代码结构:

parser.add_argument("-r", "--raw", action="store_true", help="输出原始 JSON")

# format_output 改为 format_output(data, raw=False)
if raw:
    print(json.dumps(data, ensure_ascii=False, indent=2))
    return

要点: 追加功能时,上下文是关键。OpenCode 会记住之前的完整代码,只做增量修改。你的提示词只需描述"要加什么",不用重复交代项目全貌。

第三步:批量查询

单个 IP 查起来不过瘾,我让它支持一次传多个 IP:

> 把 ip 参数改成可以接收多个 IP 地址(nargs="*"),遍历查询每个地址,之间用空行分隔。

命令变成了 python ipquery.py 8.8.8.8 1.1.1.1 114.114.114.114,一次查三个。

这个改动涉及参数定义、遍历逻辑和输出格式三处,但 OpenCode 一次性全部改好了,没有遗漏:

parser.add_argument("ips", nargs="*", default=[], help="要查询的 IP 地址")

targets = args.ips if args.ips else [""]
for i, ip in enumerate(targets):
    display = ip or "本机公网 IP"
    print(f"=== {display} ===")
    result = query_ip(ip, args.lang)
    format_output(result)
    if i < len(targets) - 1:
        print()

第四步:美化输出

最后一步,让输出更好看——加分隔线,用 emoji 图标标注字段:

> 美化 format_output,用分隔线框起来,每个字段前面加对应的 emoji 图标,比如🌐 IP 地址、📍 国家、🏙 城市、📡 ISP、🕐 时区。

最终输出风格变成这样:

────────────────────────────────────────────────────
  🌐 IP 地址      : 8.8.8.8
  📍 国家/地区    : 美国
  🏛  省份/州      : Virginia
  🏙  城市         : Ashburn
  📡 ISP          : Google LLC
  🏢 组织         : Google LLC
  🔢 ASN          : AS15169 Google LLC
  🕐 时区         : America/New_York
  🗺  坐标         : 39.03, -77.5
────────────────────────────────────────────────────

四轮对话,从零到完整工具,耗时不到 4 分钟。

完整代码

#!/usr/bin/env python3
"""IP 归属地查询工具 - 查询 IP 地址的地理位置、ISP 等信息"""
import sys
import json
import argparse
import urllib.request
import urllib.error

API_URL = "http://ip-api.com/json/{}"
FIELDS = "status,message,country,regionName,city,isp,org,as,query,timezone,lat,lon"

def query_ip(ip: str, lang: str = "zh-CN"):
    params = f"?lang={lang}&fields={FIELDS}"
    url = API_URL.format(ip) + params
    req = urllib.request.Request(url, headers={"User-Agent": "ipquery/1.0"})
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            data = json.loads(resp.read().decode())
            if data.get("status") == "fail":
                return {"error": data.get("message", "未知错误")}
            return data
    except urllib.error.URLError as e:
        return {"error": f"网络错误: {e.reason}"}
    except Exception as e:
        return {"error": str(e)}

def format_output(data: dict, raw: bool = False):
    if raw:
        print(json.dumps(data, ensure_ascii=False, indent=2))
        return
    if "error" in data:
        print(f"❌ {data['error']}")
        return
    label_width = 16
    sep = "─" * 50
    mapping = [
        ("🌐 IP 地址", "query"),
        ("📍 国家/地区", "country"),
        ("🏛  省份/州", "regionName"),
        ("🏙  城市", "city"),
        ("📡 ISP", "isp"),
        ("🏢 组织", "org"),
        ("🔢 ASN", "as"),
        ("🕐 时区", "timezone"),
        ("🗺  坐标", lambda d: f"{d.get('lat', '?')}, {d.get('lon', '?')}"
                     if d.get('lat') else None),
    ]
    print(sep)
    for label, key in mapping:
        if callable(key):
            value = key(data)
        else:
            value = data.get(key)
        if value:
            print(f"  {label:<{label_width}}: {value}")
    print(sep)

def main():
    parser = argparse.ArgumentParser(
        description="IP 归属地查询工具 - 查询 IP 地址的地理位置、ISP 等信息",
        epilog="示例: python ipquery.py 8.8.8.8\n"
               "      python ipquery.py 1.1.1.1 -r\n"
               "      python ipquery.py 8.8.8.8 1.1.1.1",
    )
    parser.add_argument("ips", nargs="*", default=[], help="要查询的 IP 地址")
    parser.add_argument("-r", "--raw", action="store_true", help="输出原始 JSON")
    parser.add_argument("-l", "--lang", default="zh-CN", choices=["zh-CN", "en"],
                        help="返回语言 (默认: zh-CN)")
    args = parser.parse_args()

    targets = args.ips if args.ips else [""]
    for i, ip in enumerate(targets):
        display = ip or "本机公网 IP"
        print(f"🔍 {display}")
        result = query_ip(ip, args.lang)
        format_output(result, args.raw)
        if i < len(targets) - 1:
            print()

if __name__ == "__main__":
    main()

小结

整个开发过程四轮对话、不到 4 分钟,最终代码 78 行,零依赖、可直接运行。如果手写这个工具,从查 API 文档、写 argparse 到调异常处理,至少得 20 分钟。

OpenCode 的核心价值在于:你只管描述"要什么",代码生成、错误处理、参数校验这些脏活累活它全包了。而且增量修改时它能精准定位要改的代码,不会"牵一发而动全身"。

试试下次遇到"缺个小工具"的需求时别自己敲,打开 OpenCode 说句话就完了。