jsonextractor.class.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. <?php
  2. /**
  3. * JSON字段提取器
  4. * Extract json data by fields with type conversion.
  5. *
  6. * 语法规则说明:
  7. * - 简单字段:field1,field2
  8. * - 嵌套字段:parent:{child1,child2}
  9. * - 数组索引:users[0]:{name,email}, users[*]:{name}
  10. * - 字段别名:field|alias, parent|parentAlias:{child|childAlias}
  11. * - 类型转换:field(类型)|alias,支持类型:array/int/float/bool/string
  12. * 示例:user(array)(map转数组)、age(int)|用户年龄(字符串转整数)
  13. */
  14. class jsonextractor
  15. {
  16. /**
  17. * 错误信息存储
  18. * @var string[]
  19. */
  20. private $errors = array();
  21. /**
  22. * 从JSON中提取字段并应用类型转换
  23. * @param string $json JSON字符串
  24. * @param string $query 查询字符串(支持类型转换语法)
  25. * @return string|false 提取后的JSON字符串,失败返回false
  26. */
  27. public function extract($json, $query)
  28. {
  29. $this->errors = array();
  30. /* 解析原始JSON */
  31. $data = json_decode($json, true);
  32. if(json_last_error() !== JSON_ERROR_NONE)
  33. {
  34. $this->errors[] = "JSON解析错误: " . json_last_error_msg();
  35. return false;
  36. }
  37. /* 解析查询语句结构 */
  38. $queryStructure = $this->parseQuery($query);
  39. if($queryStructure === false) return false;
  40. /* 提取字段并应用类型转换 */
  41. $result = $this->extractFields($data, $queryStructure);
  42. /* 返回格式化JSON */
  43. return json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
  44. }
  45. /**
  46. * 解析查询字符串,生成查询结构数组
  47. * @param string $query 查询字符串
  48. * @return array|bool 查询结构数组,失败返回false
  49. */
  50. private function parseQuery($query)
  51. {
  52. $query = trim($query);
  53. if (empty($query)) {
  54. $this->errors[] = "empty";
  55. return false;
  56. }
  57. try
  58. {
  59. return $this->parseExpression($query, 0)[0];
  60. }
  61. catch(Exception $e)
  62. {
  63. $this->errors[] = "error: " . $e->getMessage();
  64. return false;
  65. }
  66. }
  67. /**
  68. * 递归解析表达式(处理嵌套结构)
  69. * @param string $query 查询字符串
  70. * @param int $start 起始解析位置
  71. * @return array [查询结构数组, 下一个解析位置]
  72. * @throws Exception 语法错误时抛出异常
  73. */
  74. private function parseExpression($query, $start)
  75. {
  76. $result = [];
  77. $i = $start;
  78. $len = strlen($query);
  79. while($i < $len)
  80. {
  81. /* 跳过空白字符 */
  82. while($i < $len && in_array($query[$i], [' ', "\t", "\n", "\r"])) $i++;
  83. if($i >= $len) break;
  84. /* 遇到闭合符,退出当前嵌套层级 */
  85. if($query[$i] === '}') break;
  86. /* 解析字段名、类型、别名、数组索引 */
  87. $fieldInfo = $this->parseFieldName($query, $i);
  88. $fieldName = $fieldInfo['name'];
  89. $fieldAlias = $fieldInfo['alias'];
  90. $arrayIndex = $fieldInfo['index'];
  91. $targetType = $fieldInfo['target_type'];
  92. $i = $fieldInfo['pos'];
  93. /* 跳过空白字符 */
  94. while($i < $len && in_array($query[$i], [' ', "\t", "\n", "\r"])) $i++;
  95. /* 处理嵌套结构(如 parent:{child}) */
  96. if($i < $len && $query[$i] === ':')
  97. {
  98. $i++; // 跳过冒号
  99. /* 跳过空白字符 */
  100. while($i < $len && in_array($query[$i], [' ', "\t", "\n", "\r"])) $i++;
  101. if($i < $len && $query[$i] === '{')
  102. {
  103. $i++; // 跳过左大括号
  104. /* 递归解析嵌套字段 */
  105. list($nestedResult, $newPos) = $this->parseExpression($query, $i);
  106. $i = $newPos;
  107. /* 检查是否有闭合符 */
  108. if($i < $len && $query[$i] === '}')
  109. {
  110. $i++; // 跳过右大括号
  111. }
  112. else
  113. {
  114. throw new Exception("缺少闭合符 '}'");
  115. }
  116. /* 存储嵌套字段结构(包含类型转换信息)*/
  117. $result[$fieldName] = array(
  118. 'type' => 'nested',
  119. 'alias' => $fieldAlias,
  120. 'index' => $arrayIndex,
  121. 'target_type' => $targetType,
  122. 'fields' => $nestedResult
  123. );
  124. }
  125. else
  126. {
  127. throw new Exception("'{' afer ':'");
  128. }
  129. }
  130. else
  131. {
  132. /* 存储简单字段结构(包含类型转换信息)*/
  133. $result[$fieldName] = array(
  134. 'type' => 'simple',
  135. 'alias' => $fieldAlias,
  136. 'index' => $arrayIndex,
  137. 'target_type' => $targetType
  138. );
  139. }
  140. /* 跳过空白字符 */
  141. while($i < $len && in_array($query[$i], [' ', "\t", "\n", "\r"])) $i++;
  142. /* 处理字段分隔符 */
  143. if($i < $len && $query[$i] === ',') $i++; // 跳过逗号
  144. }
  145. return [$result, $i];
  146. }
  147. /**
  148. * 解析字段名、类型转换、别名、数组索引
  149. * @param string $query 查询字符串
  150. * @param int $start 起始解析位置
  151. * @return array 包含字段信息的数组
  152. * @throws Exception 语法错误时抛出异常
  153. */
  154. private function parseFieldName($query, $start)
  155. {
  156. $i = $start;
  157. $len = strlen($query);
  158. $name = '';
  159. $alias = null;
  160. $index = null;
  161. $targetType = null;
  162. /* 1. 读取字段名(可能包含类型转换,如 user(array))*/
  163. while($i < $len && !in_array($query[$i], [':', ',', '}', '[', '|', ' ', "\t", "\n", "\r"]))
  164. {
  165. $name .= $query[$i];
  166. $i++;
  167. }
  168. /* 2. 解析类型转换(格式:字段名(类型))*/
  169. $typePattern = '/^(.+)\((\w+)\)$/';
  170. if(preg_match($typePattern, $name, $matches))
  171. {
  172. $name = trim($matches[1]);
  173. $targetType = trim($matches[2]);
  174. /* 验证支持的类型 */
  175. $allowedTypes = array('array', 'int', 'float', 'bool', 'string');
  176. if(!in_array($targetType, $allowedTypes))
  177. {
  178. throw new Exception("Not support '{$targetType}', only " . implode(',', $allowedTypes));
  179. }
  180. }
  181. /* 3. 解析别名(格式:字段名|别名)*/
  182. if($i < $len && $query[$i] === '|')
  183. {
  184. $i++; // 跳过竖线
  185. $alias = '';
  186. while($i < $len && !in_array($query[$i], [':', ',', '}', '[', ' ', "\t", "\n", "\r"]))
  187. {
  188. $alias .= $query[$i];
  189. $i++;
  190. }
  191. $alias = trim($alias);
  192. /* 别名不能为空 */
  193. if(empty($alias)) throw new Exception("alias is empty");
  194. }
  195. /* 4. 解析数组索引(格式:字段名[索引])*/
  196. if($i < $len && $query[$i] === '[')
  197. {
  198. $i++; // 跳过左中括号
  199. $indexStr = '';
  200. while($i < $len && $query[$i] !== ']')
  201. {
  202. $indexStr .= $query[$i];
  203. $i++;
  204. }
  205. /* 检查是否有闭合符 */
  206. if($i < $len && $query[$i] === ']')
  207. {
  208. $i++; // 跳过右中括号
  209. /* 处理通配符索引和数字索引 */
  210. if($indexStr === '*')
  211. {
  212. $index = '*';
  213. }
  214. elseif(is_numeric($indexStr))
  215. {
  216. $index = intval($indexStr);
  217. }
  218. else
  219. {
  220. throw new Exception("index error '{$indexStr}', only support number or '*'");
  221. }
  222. }
  223. else
  224. {
  225. throw new Exception("need ']'");
  226. }
  227. }
  228. /* 字段名不能为空 */
  229. if(empty($name)) throw new Exception("field is empty");
  230. return array(
  231. 'name' => $name,
  232. 'alias' => $alias,
  233. 'index' => $index,
  234. 'target_type' => $targetType,
  235. 'pos' => $i
  236. );
  237. }
  238. /**
  239. * 提取字段并应用类型转换
  240. * @param array|object $data 原始数据(数组或对象)
  241. * @param array $queryStructure 查询结构数组
  242. * @return array 提取并转换后的结果数组
  243. */
  244. private function extractFields($data, $queryStructure)
  245. {
  246. /* 非数组结构直接返回(避免后续报错)*/
  247. if(!is_array($queryStructure)) return $this->convertValueType($data, null);
  248. if(!is_array($data) && !is_object($data)) return null;
  249. $result = array();
  250. $dataArray = (array)$data;
  251. foreach($queryStructure as $field => $config)
  252. {
  253. $resultKey = $config['alias'] ?? $field; // 优先使用别名
  254. $targetType = $config['target_type'] ?? null; // 目标转换类型
  255. /* 1. 处理通配符字段(如 *:{name})*/
  256. if($field === '*')
  257. {
  258. foreach($dataArray as $key => $value)
  259. {
  260. if($config['type'] === 'simple')
  261. {
  262. /* 简单通配符字段:直接转换值 */
  263. $result[$key] = $this->convertValueType($value, $targetType);
  264. }
  265. elseif($config['type'] === 'nested')
  266. {
  267. /* 嵌套通配符字段:先提取嵌套字段,再转换 */
  268. $nestedValue = $this->extractFields($value, $config['fields']);
  269. $result[$key] = $this->convertValueType($nestedValue, $targetType);
  270. }
  271. }
  272. continue;
  273. }
  274. /* 字段不存在时跳过 */
  275. if(!array_key_exists($field, $dataArray)) continue;
  276. $fieldData = $dataArray[$field];
  277. /* 2. 处理数组索引(如 users[0]、users[*])*/
  278. if($config['index'] !== null)
  279. {
  280. /* 非数组类型跳过 */
  281. if(!is_array($fieldData)) continue;
  282. /* 2.1 通配符索引(提取所有数组元素)*/
  283. if($config['index'] === '*')
  284. {
  285. $extractedArray = array();
  286. foreach($fieldData as $item)
  287. {
  288. /* 提取元素值(简单/嵌套)*/
  289. $itemValue = $config['type'] === 'simple'
  290. ? $item
  291. : $this->extractFields($item, $config['fields']);
  292. /* 应用类型转换 */
  293. $extractedArray[] = $this->convertValueType($itemValue, $targetType);
  294. }
  295. $result[$resultKey] = $extractedArray;
  296. }
  297. /* 2.2 特定数字索引(提取指定位置元素)*/
  298. elseif(isset($fieldData[$config['index']]))
  299. {
  300. $item = $fieldData[$config['index']];
  301. /* 提取元素值(简单/嵌套)*/
  302. $itemValue = $config['type'] === 'simple'
  303. ? $item
  304. : $this->extractFields($item, $config['fields']);
  305. /* 应用类型转换并保留索引*/
  306. $result[$resultKey] = array(
  307. $config['index'] => $this->convertValueType($itemValue, $targetType)
  308. );
  309. }
  310. }
  311. /* 3. 无数组索引(普通字段)*/
  312. else
  313. {
  314. /* 提取字段值(简单/嵌套)*/
  315. $fieldValue = $config['type'] === 'simple'
  316. ? $fieldData
  317. : $this->extractFields($fieldData, $config['fields']);
  318. /* 应用类型转换 */
  319. $result[$resultKey] = $this->convertValueType($fieldValue, $targetType);
  320. }
  321. }
  322. return $result;
  323. }
  324. /**
  325. * 按目标类型转换值
  326. * @param mixed $value 原始值
  327. * @param string|null $targetType 目标类型(array/int/float/bool/string)
  328. * @return mixed 转换后的值
  329. */
  330. private function convertValueType($value, $targetType)
  331. {
  332. /* 无目标类型时返回原始值 */
  333. if($targetType === null) return $value;
  334. switch($targetType)
  335. {
  336. /* 转换为数组:关联数组→索引数组,非数组→包装为数组 */
  337. case 'array':
  338. if(is_array($value)) return array_values($value);
  339. return [$value];
  340. /* 转换为整数:仅对数值/数值字符串有效 */
  341. case 'int':
  342. return is_numeric($value) ? intval($value) : $value;
  343. /* 转换为浮点数:仅对数值/数值字符串有效 */
  344. case 'float':
  345. return is_numeric($value) ? floatval($value) : $value;
  346. /* 转换为布尔值:处理字符串"true"/"false"和数字1/0 */
  347. case 'bool':
  348. if(is_string($value))
  349. {
  350. $lowerValue = strtolower($value);
  351. return $lowerValue === 'true' || $lowerValue === '1';
  352. }
  353. return boolval($value);
  354. /* 转换为字符串:直接强制转换 */
  355. case 'string':
  356. return strval($value);
  357. /* 未知类型返回原始值 */
  358. default:
  359. return $value;
  360. }
  361. }
  362. /**
  363. * 获取错误信息列表
  364. * @return string[] 错误信息数组
  365. */
  366. public function getErrors()
  367. {
  368. return $this->errors;
  369. }
  370. }