制作将URL字符串转换为JSON的递归算法

问题描述

我发现自己必须处理以下字符串:

foo=bar&foo1=foo%3Dbar%26foo2%3Dfoo%253Dbar

进入:

{
  "foo": "bar","foo1": {
    "foo": "bar","foo2": {
      "foo": "bar"
    }
  }
}

A real input example.

我最大的尝试是:

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'));

,

这似乎是为您的简单示例和您的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参数更有意义吗?

,

Node.js带有一个内置的“ querystring” npm软件包实用程序,但是在这里我使用了一个更好的“ qs”。您可以在数组中指定分隔符,而不是仅在前一个中使用分隔符。

如果要使用内置的“ 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));
,

对我来说,解决方案是Scott Sauyet's answer,其格式出于可读性目的。

function parse(query) {
  if (query.startsWith('{')) {
    return JSON.parse(query);
  }
  
  if (query.includes('&') || query.includes('=')) {
    return Object.fromEntries(
             query.split('&')
             .map(parameter => parameter.split('='))               
             .map(([key,value]) => [key,parse(decodeURIComponent(value))])
           );
  }
  
  if (query.includes(',')) {
    return query.split(',').filter(Boolean).map(parse);
  }
  
  if (isFinite(query)) {
    return Number(query);
  }
  
  if (query.toLowerCase() === "true" || query.toLowerCase() === "false") {
    return query.toLowerCase() === "true";
  }
  
  return 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(response => response.text())
  .then(parse)
  .then(console.log);
.as-console-wrapper { max-height: 100% !important; top: 0; }