Python JSONDecodeError 排查:NaN、不完整结构、数字字符串的常见坑
JSONDecodeError 定位 json.loads() 报错时,JSONDecodeError 对象携带精确位置信息: import jsontry: data = json.loads(content) except json.JSONDecodeError as e: print(f"错误信息: {e.msg}") print(f"行号: {e.lineno}, 列号: {e.colno}") print(f"字符偏移: {e.pos}") print(f"出错内容: {content[max(0, e.pos-50):e.pos+50]}")原因一:裸 NaN / Infinity JSON 规范不允许 NaN 和 Infinity 作为数值字面量,只有加了引号的字符串 "NaN" 才合法: # 合法 json.loads('{"value": "NaN"}') # OK,返回字符串# 非法 json.loads('{"value": NaN}') # JSONDecodeError: Expecting value json.loads('{"value": Infinity}') # JSONDecodeError如果数据源返回了裸 NaN,需要预处理: import recleaned = re.sub(r'\bNaN\b', '"NaN"', content) data = json.loads(cleaned)原因二:内容不是完整 JSON 常见错误是将多个 JSON 对象直接拼接(不用数组包裹): # 非法:两个对象并排 content = '{"a": 1}, {"b": 2}' json.loads(content) # JSONDecodeError# 合法:用数组包裹 content = '[{"a": 1}, {"b": 2}]' json.loads(content) # OK另一种情况是只收到了响应的一部分(网络截断),pos 错误位置通常在字符串末尾。 原因三:数字存为字符串 JSON 中数字加了引号,解析后得到字符串而非数值: {"x": "1228.8650628969294", "z": "NaN"}data = json.loads(content) type(data["x"]) # str,不是 float# 需要手动转换 x = float(data["x"])# z 是 "NaN" 字符串,转 float 得到 nan z = float(data["z"]) # float('nan') import math math.isnan(z) # True原因四:编码问题 非 UTF-8 编码的文件用 json.loads 读取会报错: # 读取文件时指定编码 with open('data.json', 'r', encoding='utf-8') as f: data = json.load(f)# 如果来源是 bytes data = json.loads(raw_bytes.decode('utf-8'))快速调试模板 import json, tracebackdef safe_parse(content: str): try: return json.loads(content), None except json.JSONDecodeError as e: context_start = max(0, e.pos - 30) context_end = min(len(content), e.pos + 30) snippet = content[context_start:context_end] marker = ' ' * (e.pos - context_start) + '^' print(f"JSONDecodeError at line {e.lineno}, col {e.colno}:") print(snippet) print(marker) return None, e
Python JSONDecodeError 排查清单:从 NaN 到编码 BOM
json.loads 抛 JSONDecodeError 时,异常信息里有行号、列号、字节偏移量——先把这三个印出来,问题基本就定位了: import jsontry: data = json.loads(content) except json.JSONDecodeError as e: print(e) print("line:", e.lineno, "col:", e.colno, "pos:", e.pos) print(content[max(0, e.pos - 40): e.pos + 40])最后一行把出错位置前后 40 个字符打出来,肉眼就能看出问题。以下是几种常见踩坑。 1. 根本不是合法 JSON(最常见) 抓了一段"对象序列"贴进来: { "id": 1 }, { "id": 2 }这不是合法 JSON,缺个数组外壳。要么改成: [ { "id": 1 }, { "id": 2 } ]要么按字段包一层: { "objects": [...] }2. NaN / Infinity 没引号 标准 JSON 不支持 NaN、Infinity。这样合法: {"z": "NaN"} ← 字符串这样非法: {"z": NaN} ← Python json.loads 会崩服务端如果是 Python json.dumps(..., allow_nan=True) 输出的,可能就是裸 NaN。解决办法: import json data = json.loads(content, parse_constant=lambda x: None) # 或 data = content.replace("NaN", "null") data = json.loads(data)3. 结尾多逗号 也是非标准 JSON: { "a": 1, "b": 2, ← 这个逗号 Python 不接受 }用 json5 或者预处理正则去掉最后一个逗号: import re content = re.sub(r",\s*([}\]])", r"\1", content)4. UTF-8 BOM 从 Windows 记事本另存的 JSON 常带 BOM,第一个字符是 : json.loads(content) # JSONDecodeError: Expecting value: line 1 column 1 (char 0)读文件时用 utf-8-sig: with open("data.json", encoding="utf-8-sig") as f: data = json.load(f)5. 数字全被当字符串了 不会抛异常,但会让下游算错: {"x": "1228.86"}type(obj["x"]) 是 str,要计算得先转: x = float(obj["x"])批量转的话用一个小函数: def to_float_fields(d, fields): for f in fields: if f in d: d[f] = float(d[f]) return d6. 单引号不是 JSON 从 Python print(dict) 复制的字符串是单引号: {'id': 1, 'name': 'Tom'}这不是 JSON,是 Python repr。要么请对方 json.dumps 输出,要么本地 ast.literal_eval: import ast data = ast.literal_eval("{'id': 1, 'name': 'Tom'}")literal_eval 只解析字面量,比 eval 安全得多。 一句话总结 JSONDecodeError 第一步永远是把 e.pos 附近的原文打出来。九成情况能在 30 秒内定位到具体字符是啥。
Windows 查看端口被哪个进程占用:netstat / PowerShell / 资源监视器
cmd 方式(最常用) 打开命令提示符(管理员权限更好),查看指定端口: netstat -ano | findstr :9000输出示例: TCP 0.0.0.0:9000 0.0.0.0:0 LISTENING 12345末尾的 12345 是进程 PID,再查进程名: tasklist | findstr 12345输出: php-cgi.exe 12345 Console 1 25,200 Kphp-cgi.exe 就是占用 9000 端口的进程。 PowerShell 方式 直接查指定端口: Get-NetTCPConnection -LocalPort 9000输出包含 OwningProcess(PID),再查进程: Get-Process -Id (Get-NetTCPConnection -LocalPort 9000).OwningProcess这一条命令可以直接得到进程名,不需要中间步骤。 资源监视器(图形界面) 运行 resmon,进入「网络」→「监听端口」选项卡,直接看各进程占用的端口列表,也可以在「CPU」→「关联的句柄」里搜索端口号。 释放端口(结束进程) 拿到 PID 后,cmd: taskkill /F /PID 12345PowerShell: Stop-Process -Id 12345 -Force/F 或 -Force 表示强制终止,不等进程自行退出。 常见场景现象 原因启动服务报 Address already in use 端口被其他程序占用多个服务共用同一端口 需要改端口配置php-cgi / node / java 没停干净 上次启动的进程残留找到 PID 之后,先确认进程名再决定是否结束——别误杀系统进程。
用 Composer 从零搭建 PHP 框架:Router、IoC 容器、Middleware 到 ServiceProvider
自己搭一个最小 PHP 框架是理解 Laravel 架构的最好方式,比直接读 Laravel 源码有效得多。 项目结构 my-framework/ ├── app/ │ └── Controller/ │ └── HomeController.php ├── public/ │ └── index.php ├── src/ │ ├── Router.php │ ├── Container.php │ └── Middleware.php ├── vendor/ └── composer.json第一步:Composer 初始化 mkdir my-framework && cd my-framework composer initcomposer.json: { "name": "me/framework", "autoload": { "psr-4": { "App\\": "app/", "Framework\\": "src/" } } }生成自动加载文件: composer dump-autoload第二步:最小 Router src/Router.php: <?php namespace Framework;class Router { private array $routes = []; public function get(string $uri, $action): void { $this->routes['GET'][$uri] = $action; } public function post(string $uri, $action): void { $this->routes['POST'][$uri] = $action; } public function dispatch(): void { $method = $_SERVER['REQUEST_METHOD']; $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); if (isset($this->routes[$method][$uri])) { call_user_func($this->routes[$method][$uri]); return; } http_response_code(404); echo "404 Not Found"; } }入口文件 public/index.php: <?php require '../vendor/autoload.php';use Framework\Router; use App\Controller\HomeController;$router = new Router(); $router->get('/', [HomeController::class, 'index']); $router->dispatch();启动: php -S localhost:8000 -t public第三步:IoC 容器(依赖注入) Laravel 的核心是 Container,不是 Router。 src/Container.php: <?php namespace Framework;use ReflectionClass;class Container { private array $bindings = []; public function bind(string $abstract, callable $factory): void { $this->bindings[$abstract] = $factory; } public function make(string $class): object { if (isset($this->bindings[$class])) { return ($this->bindings[$class])($this); } // 用反射自动解析构造函数依赖 $ref = new ReflectionClass($class); $constructor = $ref->getConstructor(); if (!$constructor) { return new $class(); } $params = array_map( fn($p) => $this->make($p->getType()->getName()), $constructor->getParameters() ); return $ref->newInstanceArgs($params); } }这样 $container->make(HomeController::class) 会自动实例化其所有依赖: class HomeController { public function __construct( private UserService $userService ) {} }第四步:Middleware 请求流程:Request → Middleware → Controller → Response <?php namespace Framework;class Middleware { private array $stack = []; public function add(callable $middleware): void { $this->stack[] = $middleware; } public function run(callable $handler): void { $runner = array_reduce( array_reverse($this->stack), fn($next, $mw) => fn() => $mw($next), $handler ); $runner(); } }示例认证中间件: $middleware->add(function (callable $next) { if (!isset($_SESSION['user'])) { http_response_code(401); echo '请先登录'; return; } $next(); });第五步:ServiceProvider <?php namespace Framework;abstract class ServiceProvider { public function __construct( protected Container $app ) {} abstract public function register(): void; }示例: class DatabaseServiceProvider extends ServiceProvider { public function register(): void { $this->app->bind('db', fn($app) => new Database( config('database.host'), config('database.name') )); } }注册: (new DatabaseServiceProvider($container))->register();学习路线Router + 自动加载 → 理解 HTTP 分发 Container + ReflectionClass → 理解依赖注入 Middleware → 理解 Laravel Pipeline ServiceProvider → 理解 Laravel 服务注册 自己实现后再看 Laravel 源码,对照理解完成前 3 步后基本就能看懂 Laravel 入口流程了。
用 Composer 从零搭一个 PHP mini 框架:路由 + PSR-4 + 容器
想理解 Laravel、Symfony 是怎么跑起来的,别一头扎进它们的源码——十几万行随便一个 ServiceProvider 就把人绕晕。自己用 Composer 手写一个 100 行的 mini 框架,把路由、自动加载、IoC 弄明白,再去看大框架就轻松了。 目录结构 my-framework/ ├── app/ │ └── Controller/ │ └── HomeController.php ├── public/ │ └── index.php ├── src/ │ ├── Router.php │ ├── Container.php │ └── Response.php ├── vendor/ # composer install 后生成 └── composer.jsonapp/ 是业务代码、src/ 是框架核心、public/ 是入口。 composer.json 与 PSR-4 mkdir my-framework && cd my-framework composer init改 composer.json: { "name": "you/mini-framework", "type": "project", "require": { "php": ">=8.1" }, "autoload": { "psr-4": { "App\\": "app/", "Framework\\": "src/" } } }生成自动加载器: composer dump-autoloadPSR-4 的规则很简单:类的命名空间路径 = 文件相对路径。App\Controller\HomeController 就在 app/Controller/HomeController.php。 Router:50 行的路由 src/Router.php: <?php namespace Framework;class Router { private array $routes = []; public function get(string $path, callable|array $handler): void { $this->routes['GET'][$path] = $handler; } public function post(string $path, callable|array $handler): void { $this->routes['POST'][$path] = $handler; } public function dispatch(): void { $method = $_SERVER['REQUEST_METHOD']; $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); $handler = $this->routes[$method][$path] ?? null; if (!$handler) { http_response_code(404); echo "404 Not Found"; return; } // 支持 [Controller::class, 'method'] if (is_array($handler)) { [$class, $method] = $handler; $instance = new $class(); echo $instance->$method(); return; } // 支持匿名函数 echo $handler(); } }入口 public/index.php: <?php require __DIR__ . '/../vendor/autoload.php';use Framework\Router; use App\Controller\HomeController;$router = new Router();$router->get('/', fn() => "Hello Framework"); $router->get('/home', [HomeController::class, 'index']);$router->dispatch();Controller: <?php namespace App\Controller;class HomeController { public function index(): string { return "home page"; } }跑起来: php -S localhost:8000 -t public访问 http://localhost:8000/ 和 /home 都能看到内容。 到这里,Laravel 最核心的三件事你都实现了:Composer PSR-4 加载、路由、控制器分发。 第二步:加个 IoC 容器 Laravel 的灵魂其实不是路由,是容器(Container / Service Container)。它做两件事:帮你 new 对象,自动注入构造参数 让你把接口绑定到具体实现,实现依赖倒置最简版本: <?php namespace Framework;use ReflectionClass;class Container { private array $bindings = []; public function bind(string $abstract, callable|string $concrete): void { $this->bindings[$abstract] = $concrete; } public function make(string $class): object { // 1. 有绑定就走绑定 if (isset($this->bindings[$class])) { $c = $this->bindings[$class]; return is_callable($c) ? $c($this) : $this->make($c); } // 2. 反射构造函数依赖,递归 make $ref = new ReflectionClass($class); $ctor = $ref->getConstructor(); if (!$ctor) return new $class(); $args = []; foreach ($ctor->getParameters() as $p) { $type = $p->getType()?->getName(); $args[] = $type ? $this->make($type) : null; } return $ref->newInstanceArgs($args); } }用法: class Logger { public function log(string $msg): void { file_put_contents('log', $msg); } }class UserService { public function __construct(public Logger $logger) {} }$c = new Container(); $user = $c->make(UserService::class); // 自动注入 Logger $user->logger->log("hello");这 40 行代码就是 Laravel Container 的核心思路。真正的 Laravel 加了:方法注入、上下文绑定、单例、tag、event、缓存反射等等,本质还是这个。 后续可以往上加 按需实现,每加一个都是一次深度理解:Middleware — 前后置拦截 Request/Response 对象 View 层(模板引擎或 Blade 简版) ORM(先用 PDO 封个 Query Builder) Event / Listener CLI 命令(symfony/console 或自己写) Config 加载 环境变量(vlucas/phpdotenv)一步步加,你会发现 Laravel 每个模块都能对应上。 一句话总结 Composer PSR-4 + Router + Container,三件套加起来 100 行代码,就是一个能跑的 mini 框架。学 Laravel 前先自己写一遍,再去看源码会很轻松。
