制作将 URL 字符串转换为 JSON 的递归算法
Making a recursive algorithm for converting URL string into JSON
我发现自己必须处理如下字符串:
foo=bar&foo1=foo%3Dbar%26foo2%3Dfoo%253Dbar
进入:
{
"foo": "bar",
"foo1": {
"foo": "bar",
"foo2": {
"foo": "bar"
}
}
}
我最好的尝试是:
function parse(input) {
try {
const parsed = JSON.parse(input);
return parseJSON(parsed);
} catch (err) {
const decodedInput = decodeURIComponent(input);
if (input.includes("&") && input.includes("=")) {
return input.split("&").reduce((json, part) => {
const [key, value] = part.split("=");
const decodedValue = decodeURIComponent(value);
return { ...json, [key]: parsePrimitive(decodedValue) };
}, {});
}
return decodedInput;
}
}
function parsePrimitive(input) {
if (!isNaN(input)) {
return Number(input);
}
if (input === "true" || input === "false") {
return input === "true";
}
return parse(input);
}
function parseJSON(input) {
return Object.entries(input).reduce((json, [key, value]) => {
let object = {};
if (typeof value === "object") {
if (Array.isArray(value)) {
object[key] = value;
} else {
object[key] = parseJSON(value);
}
} else {
const decodedValue = decodeURIComponent(value);
if (decodedValue.includes("&") && decodedValue.includes("=")) {
object[key] = parse(decodedValue);
} else {
object[key] = parsePrimitive(decodedValue);
}
}
return { ...json, ...object };
}, {});
}
如果你尝试 运行 它,你应该调用 parse(input)
但是,它确实对某些输入失败
我怎样才能为这类问题做出完美的递归算法?
谢谢!
您可以通过检查编码的 =
符号来采用递归方法。
const getValues = string => string.split('&')
.reduce((r, pair) => {
let [key, value] = pair.split('=');
value = decodeURIComponent(value);
r[key] = value.includes('=')
? getValues(value)
: value;
return r;
}, {});
console.log(getValues('foo=bar&foo1=foo%3Dbar%26foo2%3Dfoo%253Dbar'));
Node.js 附带了一个 built-in “querystring” npm 包实用程序,但在这里我使用了一个更好的名为“qs”的实用程序。您可以在数组中指定分隔符,而不是只使用一个数组分隔符。
如果你想使用built-in "querystring" 包,你需要在调用 parse 时删除定界符数组并检查字符串以查看使用了什么定界符 - 你提供的示例文件使用几个不同的。
所以试试这个:
const qs = require("qs");
let params = `foo=bar&foo1=foo%3Dbar%26foo2%3Dfoo%253Dbar`;
const isObject = (param) => {
try {
let testProp = JSON.parse(param);
if (typeof testProp === "object" && testProp !== null) {
return true;
}
return false;
} catch (e) {
return false;
}
};
const isURL = (value) => {
try {
new URL(value);
} catch (e) {
return false;
}
return true;
};
const isQueryString = (value) => {
if (/[/&=]/.test(value) && !isURL(value)) {
return true;
} else {
return false;
}
};
const parseData = (data, parsed = false) => {
if (isQueryString(data) && !parsed) {
return parseData(qs.parse(data, { delimiter: /[;,/&]/ }), true);
} else if (isObject(data) || parsed) {
for (let propertyName in data) {
if (isObject(data[propertyName])) {
data[propertyName] = parseData(JSON.parse(data[propertyName]), true);
} else {
data[propertyName] = parseData(data[propertyName]);
}
}
return data;
} else {
return data;
}
};
let s = parseData(params);
console.log(JSON.stringify(s, null, 2));
这似乎适用于您的简单示例和您的 more complex one(现在 更新 以处理数字和布尔值):
const parse = (query) =>
query .startsWith ('{')
? JSON .parse (query)
: query .includes ('&') || query .includes ('=')
? Object .fromEntries (
query .split ('&')
.map (p => p .split ('='))
.map (([k, v]) => [k, parse (decodeURIComponent (v))])
)
: query .includes (',')
? query .split (',') .filter (Boolean) .map (parse)
: isFinite (query)
? Number (query)
: query .toLowerCase () == "true" || query .toLowerCase () == "false"
? query .toLowerCase () == "true"
: // else
query
const q = 'foo=bar&foo1=foo%3Dbar%26foo2%3Dfoo%253Dbar'
console .log (parse(q))
console.log('fetching larger example...')
fetch ('https://gist.githubusercontent.com/avi12/cd1d6728445608d64475809a8ddccc9c/raw/030974baed3eaadb26d9378979b83b1d30a265a3/url-input-example.txt')
.then (res => res .text ())
.then (parse)
.then (console .log)
.as-console-wrapper {max-height: 100% !important; top: 0}
有两部分值得关注。
首先,这对逗号做了一个假设:它们代表数组元素之间的分隔。而且,进一步,它假设空字符串不是预期的,转向
watermark=%2Chttps%3A%2F%2Fs.ytimg.com%2Fyts%2Fimg%2Fwatermark%2Fyoutube_watermark-vflHX6b6E.png
%2Chttps%3A%2F%2Fs.ytimg.com%2Fyts%2Fimg%2Fwatermark%2Fyoutube_hd_watermark-vflAzLcD6.png
进入这个:
watermark: [
"https://s.ytimg.com/yts/img/watermark/youtube_watermark-vflHX6b6E.png",
"https://s.ytimg.com/yts/img/watermark/youtube_hd_watermark-vflAzLcD6.png"
]
原件以编码逗号 (%2C
) 开头,这将导致初始空字符串,因此我们使用 .filter (Boolean)
将其删除。
其次,对代表JSON的字符串的测试非常幼稚,只做了.startsWith ('{')
。你可以用你需要的任何东西来代替它,但这会导致一个意图问题。我不确定我们能否以这种方式完全通用地编写它。
不过,我认为它很接近。而且代码相当干净。
不过,我确实想知道 为什么 。如此多的数据将 运行 放入各种 url 大小限制中。在这一点上,将其放入请求正文而不是 url 参数不是更有意义吗?
我使用 Object.fromEntries(new URLSearchParams()) 修改了算法。
function parse(query) {
try {
return JSON.parse(query);
} catch {
if (!isNaN(query)) {
return Number(query);
}
if (typeof query !== "string") {
const obj = {};
for (const queryKey in query) {
if (query.hasOwnProperty(queryKey)) {
obj[queryKey] = parse(query[queryKey]);
}
}
return obj;
}
if (!query) {
return "";
}
if (query.toLowerCase().match(/^(true|false)$/)) {
return query.toLowerCase() === "true";
}
const object = Object.fromEntries(new URLSearchParams(query));
const values = Object.values(object);
if (values.length === 1 && values[0] === "") {
return query;
}
return parse(object);
}
}
const q = 'foo=bar&foo1=foo%3Dbar%26foo2%3Dfoo%253Dbar';
console.log(parse(q));
console.log('fetching larger example...');
fetch('https://gist.githubusercontent.com/avi12/cd1d6728445608d64475809a8ddccc9c/raw/030974baed3eaadb26d9378979b83b1d30a265a3/url-input-example.txt')
.then(response => response.text())
.then(parse)
.then(console.log);
.as-console-wrapper { max-height: 100% !important; top: 0; }
我发现自己必须处理如下字符串:
foo=bar&foo1=foo%3Dbar%26foo2%3Dfoo%253Dbar
进入:
{
"foo": "bar",
"foo1": {
"foo": "bar",
"foo2": {
"foo": "bar"
}
}
}
我最好的尝试是:
function parse(input) {
try {
const parsed = JSON.parse(input);
return parseJSON(parsed);
} catch (err) {
const decodedInput = decodeURIComponent(input);
if (input.includes("&") && input.includes("=")) {
return input.split("&").reduce((json, part) => {
const [key, value] = part.split("=");
const decodedValue = decodeURIComponent(value);
return { ...json, [key]: parsePrimitive(decodedValue) };
}, {});
}
return decodedInput;
}
}
function parsePrimitive(input) {
if (!isNaN(input)) {
return Number(input);
}
if (input === "true" || input === "false") {
return input === "true";
}
return parse(input);
}
function parseJSON(input) {
return Object.entries(input).reduce((json, [key, value]) => {
let object = {};
if (typeof value === "object") {
if (Array.isArray(value)) {
object[key] = value;
} else {
object[key] = parseJSON(value);
}
} else {
const decodedValue = decodeURIComponent(value);
if (decodedValue.includes("&") && decodedValue.includes("=")) {
object[key] = parse(decodedValue);
} else {
object[key] = parsePrimitive(decodedValue);
}
}
return { ...json, ...object };
}, {});
}
如果你尝试 运行 它,你应该调用 parse(input)
但是,它确实对某些输入失败
我怎样才能为这类问题做出完美的递归算法?
谢谢!
您可以通过检查编码的 =
符号来采用递归方法。
const getValues = string => string.split('&')
.reduce((r, pair) => {
let [key, value] = pair.split('=');
value = decodeURIComponent(value);
r[key] = value.includes('=')
? getValues(value)
: value;
return r;
}, {});
console.log(getValues('foo=bar&foo1=foo%3Dbar%26foo2%3Dfoo%253Dbar'));
Node.js 附带了一个 built-in “querystring” npm 包实用程序,但在这里我使用了一个更好的名为“qs”的实用程序。您可以在数组中指定分隔符,而不是只使用一个数组分隔符。
如果你想使用built-in "querystring" 包,你需要在调用 parse 时删除定界符数组并检查字符串以查看使用了什么定界符 - 你提供的示例文件使用几个不同的。
所以试试这个:
const qs = require("qs");
let params = `foo=bar&foo1=foo%3Dbar%26foo2%3Dfoo%253Dbar`;
const isObject = (param) => {
try {
let testProp = JSON.parse(param);
if (typeof testProp === "object" && testProp !== null) {
return true;
}
return false;
} catch (e) {
return false;
}
};
const isURL = (value) => {
try {
new URL(value);
} catch (e) {
return false;
}
return true;
};
const isQueryString = (value) => {
if (/[/&=]/.test(value) && !isURL(value)) {
return true;
} else {
return false;
}
};
const parseData = (data, parsed = false) => {
if (isQueryString(data) && !parsed) {
return parseData(qs.parse(data, { delimiter: /[;,/&]/ }), true);
} else if (isObject(data) || parsed) {
for (let propertyName in data) {
if (isObject(data[propertyName])) {
data[propertyName] = parseData(JSON.parse(data[propertyName]), true);
} else {
data[propertyName] = parseData(data[propertyName]);
}
}
return data;
} else {
return data;
}
};
let s = parseData(params);
console.log(JSON.stringify(s, null, 2));
这似乎适用于您的简单示例和您的 more complex one(现在 更新 以处理数字和布尔值):
const parse = (query) =>
query .startsWith ('{')
? JSON .parse (query)
: query .includes ('&') || query .includes ('=')
? Object .fromEntries (
query .split ('&')
.map (p => p .split ('='))
.map (([k, v]) => [k, parse (decodeURIComponent (v))])
)
: query .includes (',')
? query .split (',') .filter (Boolean) .map (parse)
: isFinite (query)
? Number (query)
: query .toLowerCase () == "true" || query .toLowerCase () == "false"
? query .toLowerCase () == "true"
: // else
query
const q = 'foo=bar&foo1=foo%3Dbar%26foo2%3Dfoo%253Dbar'
console .log (parse(q))
console.log('fetching larger example...')
fetch ('https://gist.githubusercontent.com/avi12/cd1d6728445608d64475809a8ddccc9c/raw/030974baed3eaadb26d9378979b83b1d30a265a3/url-input-example.txt')
.then (res => res .text ())
.then (parse)
.then (console .log)
.as-console-wrapper {max-height: 100% !important; top: 0}
有两部分值得关注。
首先,这对逗号做了一个假设:它们代表数组元素之间的分隔。而且,进一步,它假设空字符串不是预期的,转向
watermark=%2Chttps%3A%2F%2Fs.ytimg.com%2Fyts%2Fimg%2Fwatermark%2Fyoutube_watermark-vflHX6b6E.png
%2Chttps%3A%2F%2Fs.ytimg.com%2Fyts%2Fimg%2Fwatermark%2Fyoutube_hd_watermark-vflAzLcD6.png
进入这个:
watermark: [
"https://s.ytimg.com/yts/img/watermark/youtube_watermark-vflHX6b6E.png",
"https://s.ytimg.com/yts/img/watermark/youtube_hd_watermark-vflAzLcD6.png"
]
原件以编码逗号 (%2C
) 开头,这将导致初始空字符串,因此我们使用 .filter (Boolean)
将其删除。
其次,对代表JSON的字符串的测试非常幼稚,只做了.startsWith ('{')
。你可以用你需要的任何东西来代替它,但这会导致一个意图问题。我不确定我们能否以这种方式完全通用地编写它。
不过,我认为它很接近。而且代码相当干净。
不过,我确实想知道 为什么 。如此多的数据将 运行 放入各种 url 大小限制中。在这一点上,将其放入请求正文而不是 url 参数不是更有意义吗?
我使用 Object.fromEntries(new URLSearchParams()) 修改了算法。
function parse(query) {
try {
return JSON.parse(query);
} catch {
if (!isNaN(query)) {
return Number(query);
}
if (typeof query !== "string") {
const obj = {};
for (const queryKey in query) {
if (query.hasOwnProperty(queryKey)) {
obj[queryKey] = parse(query[queryKey]);
}
}
return obj;
}
if (!query) {
return "";
}
if (query.toLowerCase().match(/^(true|false)$/)) {
return query.toLowerCase() === "true";
}
const object = Object.fromEntries(new URLSearchParams(query));
const values = Object.values(object);
if (values.length === 1 && values[0] === "") {
return query;
}
return parse(object);
}
}
const q = 'foo=bar&foo1=foo%3Dbar%26foo2%3Dfoo%253Dbar';
console.log(parse(q));
console.log('fetching larger example...');
fetch('https://gist.githubusercontent.com/avi12/cd1d6728445608d64475809a8ddccc9c/raw/030974baed3eaadb26d9378979b83b1d30a265a3/url-input-example.txt')
.then(response => response.text())
.then(parse)
.then(console.log);
.as-console-wrapper { max-height: 100% !important; top: 0; }