【问题标题】:How to filter deeply nested json by multiple attributes with vue/javascript如何使用 vue/javascript 通过多个属性过滤深度嵌套的 json
【发布时间】:2021-04-02 20:42:12
【问题描述】:

我有一些具有以下结构的 JSON:

{
  "root": {
    "Europe": {
      "children": [
        {
          "name": "Germany"
        },
        {
          "name": "England",
          "children": [
            {
              "name": "London",
              "search_words": ["city", "capital"],
              "children": [
                {
                  "name": "Westminster",
                  "search_words": ["borough"]
                }
              ]
            },
            {
              "name": "Manchester",
              "search_words": ["city"]
            }
          ]
        },
        {
          "name": "France",
          "children": [
            {
              "name": "Paris",
              "search_words": ["city", "capital"]
            }
          ]
        }
      ]
    },
    "North America": {
      "children": [
        {
          "name": "Canada",
          "children": [
            {
              "name": "Toronto"
            },
            {
              "name": "Ottawa"
            }
          ]
        },
        {
          "name": "United States"
        }
      ]
    }
  }
}

我想根据文本搜索过滤 JSON。我应该可以通过name 和任何search_words 进行搜索。最后一个问题是 JSON 可以任意深,因此它需要能够搜索所有级别。

我还需要循环并打印出 HTML 中的 JSON(使用 Vue),并根据搜索进行更新。目前不清楚如何在不知道 JSON 将进入多少层的情况下做到这一点?

任何帮助将不胜感激!

【问题讨论】:

标签: javascript json vue.js recursion


【解决方案1】:

我最近回复了a similar question。我在这里分享它是因为我认为它为这篇文章提供了相关的基础。不过,在开始之前,我们必须先解决输入数据的不规则形状 -

const data2 =
  { name:"root"
  , children:
      Array.from
        ( Object.entries(data.root)
        , ([ country, _ ]) =>
            Object.assign({ name:country }, _)
        )
  }

console.log(JSON.stringify(data2, null, 2))

现在我们可以看到data2 是一个统一的{ name, children: [ ... ]} 形状-

{
  "name": "root",
  "children": [
    {
      "name": "Europe",
      "children": [
        { "name": "Germany" },
        {
          "name": "England",
          "children": [
            {
              "name": "London",
              "search_words": [ "city", "capital" ],
              "children": [
                {
                  "name": "Westminster",
                  "search_words": [ "borough" ]
                }
              ]
            },
            {
              "name": "Manchester",
              "search_words": [ "city" ]
            }
          ]
        },
        {
          "name": "France",
          "children": [
            {
              "name": "Paris",
              "search_words": [ "city", "capital" ]
            }
          ]
        }
      ]
    },
    {
      "name": "North America",
      "children": [
        {
          "name": "Canada",
          "children": [
            { "name": "Toronto" },
            { "name": "Ottawa" }
          ]
        },
        { "name": "United States" }
      ]
    }
  ]
}

现在我们编写一个通用的深度优先遍历函数,dft -

function* dft (t, path = [])
{ for (const _ of t.children ?? [])
    yield* dft(_, [...path, t.name ])
  yield [path, t]
}

dft 函数为我们输入树中的每个元素 e 提供了一个 path t -

["root","Europe"]
{"name":"Germany"}

["root","Europe","England","London"]
{name:"Westminster", search_words:["borough"]}

["root","Europe","England"]
{name:"London", search_words:["city","capital"], children:[...]}

["root","Europe","England"]
{name:"Manchester", search_words:["city"]}

["root","Europe"]
{name:"England", children:[...]}

["root","Europe","France"]
{name:"Paris", search_words:["city","capital"]}

["root","Europe"]
{name:"France", children:[...]}

["root"]
{name:"Europe", children:[...]}

["root","North America","Canada"]
{name:"Toronto"}

现在我们知道每个节点的路径,我们可以创建一个index,它使用path 和任何search_words 链接回节点 -

const index = t =>
  Array.from
    ( dft(t)
    , ([path, e]) =>
        [ [...path, e.name, ...e.search_words ?? [] ] // all words to link to e
        , e                                           // e
        ]
    )
    .reduce
      ( (m, [ words, e ]) =>
          insertAll(m, words, e) // update the index using generic helper
      , new Map
      )

这取决于通用助手 insertAll -

const insertAll = (m, keys, value) =>
  keys.reduce
    ( (m, k) =>
        m.set(k, [ ...m.get(k) ?? [], value ])
    , m
    )

index 完成后,我们有办法为任何搜索词创建快速查找 -

const myIndex = 
  index(data2)

console.log(myIndex)
Map 
{ "Europe" =>
    [{"name":"Germany"},{"name":"Westminster",...},{"name":"London",...},{"name":"Manchester",...},{"name":"England"...},{"name":"Manchester",...}]},{"name":"Paris",...},{"name":"France"...},{"name":"Europe"...},{"name":"Manchester",...}]},{"name":"France"...}]}]

, "Germany" => 
    [{"name":"Germany"}]

, "England" =>
    [{"name":"Westminster",...},{"name":"London",...},{"name":"Manchester",...},{"name":"England"...},{"name":"Manchester",...}]}]

, "London" =>
    [{"name":"Westminster",...},{"name":"London",...}]

, "Westminster" =>
    [{"name":"Westminster",...}]

, "borough" =>
    [{"name":"Westminster",...}]

, "city" =>
    [{"name":"London",...},{"name":"Manchester",...},{"name":"Paris",...}]

, "capital" =>
    [{"name":"London",...},{"name":"Paris",...}]

, "Manchester" =>
    [{"name":"Manchester",...}]

, "France" =>
    [{"name":"Paris",...},{"name":"France"...}]

, "Paris" =>
    [{"name":"Paris",...}]

, "North America" =>
    [{"name":"Toronto"},{"name":"Ottawa"},{"name":"Canada"...},{"name":"United States"},{"name":"North America"...},
    {"name":"United States"}]}]

, "Canada" =>
    [{"name":"Toronto"},{"name":"Ottawa"},{"name":"Canada"...}]

, "Toronto" =>
    [{"name":"Toronto"}]

, "Ottawa" =>
    [{"name":"Ottawa"}]

, "United States" =>
    [{"name":"United States"}]   
}

这应该会突出显示数据中剩余的不一致之处。例如,您有一些节点嵌套在citycapitalborough 下。另外值得注意的是,我们可能应该在所有索引键上使用s.toLowerCase(),以便查找可以不区分大小写。这是留给读者的练习。

创建index 很简单,您只需一次 -

const myIndex = 
  index(data2)

您的索引可以根据需要重复用于多次查找 -

console.log(myIndex.get("Toronto") ?? [])
console.log(myIndex.get("France") ?? [])
console.log(myIndex.get("Paris") ?? [])
console.log(myIndex.get("Canada") ?? [])
console.log(myIndex.get("Zorp") ?? [])
[{"name":"Toronto"}]
[{"name":"Paris",...},{"name":"France"...}]
[{"name":"Paris",...}]
[{"name":"Toronto"},{"name":"Ottawa"},{"name":"Canada"...}]
[]

在你的 Vue 应用程序中插入结果留给你。

【讨论】:

  • 我有点同意反向索引是更方便的方法。
【解决方案2】:

正如Thankyou 所指出的,您不一致的数据格式使得为此编写漂亮的代码变得更加困难。我的方法略有不同。我没有转换您的数据,而是为我的通用函数编写了一个包装器,以便以更有用的方式处理此输出。

我们从函数collect 开始,它将递归地处理{name?, search_words?, children?, ...rest} 对象,返回与给定谓词匹配的节点并在子节点上循环。我们用函数search 调用它,它接受一个搜索词并从中生成一个谓词。 (这里我们测试name 或任何search_term 是否与该术语匹配;这对于部分匹配、不区分大小写等很容易修改。)

然后我们编写我提到的包装器searchLocations。它下降到.root 节点,然后映射并组合在每个根值上调用search 的结果。

const collect = (pred) => ({children = [], ...rest}) => [
  ... (pred (rest) ? [rest] : []),
  ... children .flatMap (collect (pred))
]
  
const search = (term) => 
  collect (({name = '', search_words = []}) => name == term || search_words .includes (term))

const searchLocations = (locations, term) => 
  Object.values (locations .root) .flatMap (search (term))

const locations = {root: {Europe: {children: [{name: "Germany"}, {name: "England", children: [{name: "London", search_words: ["city", "capital"], children: [{name: "Westminster", search_words: ["borough"]}]}, {name: "Manchester", search_words: ["city"]}]}, {name: "France", children: [{name: "Paris", search_words: ["city", "capital"]}]}]}, "North America": {children: [{name: "Canada", children: [{name: "Toronto"}, {name: "Ottawa"}]}, {name: "United States"}]}}}

console .log ('Toronto', searchLocations (locations, 'Toronto'))
console .log ('borough', searchLocations (locations, 'borough'))
console .log ('capital', searchLocations (locations, 'capital'))
.as-console-wrapper {max-height: 100% !important; top: 0}

如果您想要的,就像听起来一样,是与输入相同的结构,只保留包含匹配项所需的节点,那么我们应该能够从树过滤函数开始做类似的事情。我会在假期后尝试看看。

更新

我确实再次查看了这个,希望将树过滤为树。代码并不难。但这一次,我确实使用了convert 函数将您的数据转换为更一致的递归结构。因此,整个对象变成了一个数组,根有两个元素,一个是name“Europe”,另一个是name“North America”,每个都有现有的children节点。这使得所有进一步的处理变得更容易。

这里有两个关键功能:

第一个是通用的deepFilter 函数,它接受一个谓词和一个可能具有children 节点的项目数组,其结构类似于它们的父节点,并返回一个包含与谓词匹配的任何内容的新版本及其完全祖先。它看起来像这样:

const deepFilter = (pred) => (xs) =>
  xs .flatMap (({children = [], ...rest}, _, __, kids = deepFilter (pred) (children)) =>
    pred (rest) || kids.length
      ? [{...rest, ...(kids.length ? {children: kids} : {})}]
      : []
  )

第二个专门针对这个问题:searchLocation。它调用deepFilter,使用由搜索词和已经讨论过的转换结构构造的谓词。它使用convert 帮助器作为结构,并使用search 帮助器将搜索词转换为谓词,在名称和所有搜索词上查找(不区分大小写)部分匹配项。

const searchLocations = (loc, locations = convert(loc)) => (term) =>
  term.length ? deepFilter (search (term)) (locations) : locations

这通过一个用户界面来演示,该界面在嵌套的<UL>s 中显示位置,并带有一个实时过滤位置的搜索框。

例如,如果您在搜索框中只输入“w”,您将得到

Europe
  England
    London (city, capital)
      Westminster (borough)
North America
  Canada
    Ottawa

因为“威斯敏斯特”和“渥太华”是唯一的匹配项。

如果你输入“城市”你会得到

Europe
  England
    London (city, capital)
    Manchester (city)
  France
    Paris (city, capital)

您可以在这个 sn-p 中看到这一点:

// utility function
const deepFilter = (pred) => (xs) =>
  xs .flatMap (({children = [], ...rest}, _, __, kids = deepFilter (pred) (children)) =>
    pred (rest) || kids.length
      ? [{...rest, ...(kids.length ? {children: kids} : {})}]
      : []
  )

// helper functions
const search = (t = '', term = t.toLowerCase()) => ({name = '', search_words = []}) =>
  term.length &&  (
    name .toLowerCase () .includes (term) ||
    search_words .some (word => word .toLowerCase() .includes (term))
  )

const convert = ({root}) => 
  Object.entries (root) .map (([name, v]) => ({name, ...v}))


// main function
const searchLocations = (loc, locations = convert(loc)) => (term) =>
  term.length ? deepFilter (search (term)) (locations) : locations


// sample data
const myData = { root: { Europe: { children: [{ name: 'Germany' }, { name: 'England', children: [{ name: 'London', search_words: ['city', 'capital'], children: [{ name: 'Westminster', search_words: ['borough'] }] }, { name: 'Manchester', search_words: ['city'] }] }, { name: 'France', children: [{ name: 'Paris', search_words: ['city', 'capital'] }] }] }, 'North America': { children: [{ name: 'Canada', children: [{ name: 'Toronto' }, { name: 'Ottawa' }] }, { name: 'United States' }] } } };


// main function specialized to given data
const mySearch = searchLocations(myData)


// UI demo
const format = (locations, searchTerm) => `<ul>${
  locations.map(loc => `<li>${
    loc.name + 
    (loc.search_words ? ` (${loc.search_words. join(', ')})` : ``) + 
    (loc.children ? format(loc.children, searchTerm) : '')
  }</li>`)
  .join('') 
}</ul>`

const render = (locations, searchTerm) => 
  document .getElementById ('main') .innerHTML = format (locations, searchTerm)

document .getElementById ('search') .addEventListener (
  'keyup',
  (e) => render (mySearch (e.target.value))
)

// show demo
render (mySearch (''))
<div style="float: right" id="control">
  <label>Search: <input type="text" id="search"/></label>
</div>
<div style="margin-top: -1em" id="main"></div>

显然,这并没有使用Vue来生成树,只是一些字符串操作和innerHTML。我把那部分留给你。但它应该显示另一种过滤嵌套结构的方法。

【讨论】:

    【解决方案3】:

    不完全清楚您要从问题中寻找什么,但我猜您需要修改数据以确保在呈现时正确突出显示匹配的数据?

    这是使用object-scan查找匹配对象的解决方案

    // const objectScan = require('object-scan');
    
    const myData = { root: { Europe: { children: [{ name: 'Germany' }, { name: 'England', children: [{ name: 'London', search_words: ['city', 'capital'], children: [{ name: 'Westminster', search_words: ['borough'] }] }, { name: 'Manchester', search_words: ['city'] }] }, { name: 'France', children: [{ name: 'Paris', search_words: ['city', 'capital'] }] }] }, 'North America': { children: [{ name: 'Canada', children: [{ name: 'Toronto' }, { name: 'Ottawa' }] }, { name: 'United States' }] } } };
    // eslint-disable-next-line camelcase
    const mySearchFn = (term) => ({ name, search_words = [] }) => name === term || search_words.includes(term);
    
    const search = (input, searchFn) => objectScan(['**[*]'], {
      filterFn: ({ value, context }) => {
        if (searchFn(value)) {
          const { children, ...match } = value;
          context.push(match);
        }
      }
    })(input, []);
    
    console.log(search(myData, mySearchFn('Toronto')));
    // => [ { name: 'Toronto' } ]
    console.log(search(myData, mySearchFn('borough')));
    // => [ { name: 'Westminster', search_words: [ 'borough' ] } ]
    console.log(search(myData, mySearchFn('capital')));
    // => [ { name: 'Paris', search_words: [ 'city', 'capital' ] }, { name: 'London', search_words: [ 'city', 'capital' ] } ]
    .as-console-wrapper {max-height: 100% !important; top: 0}
    &lt;script src="https://bundle.run/object-scan@13.8.0"&gt;&lt;/script&gt;

    免责声明:我是object-scan的作者

    这就是您可以注入信息的方式,然后您的渲染管道可以获取这些信息

    // const objectScan = require('object-scan');
    
    const myData = { root: { Europe: { children: [{ name: 'Germany' }, { name: 'England', children: [{ name: 'London', search_words: ['city', 'capital'], children: [{ name: 'Westminster', search_words: ['borough'] }] }, { name: 'Manchester', search_words: ['city'] }] }, { name: 'France', children: [{ name: 'Paris', search_words: ['city', 'capital'] }] }] }, 'North America': { children: [{ name: 'Canada', children: [{ name: 'Toronto' }, { name: 'Ottawa' }] }, { name: 'United States' }] } } };
    // eslint-disable-next-line camelcase
    const mySearchFn = (term) => ({ name, search_words = [] }) => name === term || search_words.includes(term);
    
    const search = (input, searchFn) => objectScan(['**[*]'], {
      filterFn: ({ value }) => {
        if (searchFn(value)) {
          value.css = { highlight: true };
          return true;
        } else {
          delete value.css;
          return false;
        }
      },
      rtn: 'count' // return number of matches
    })(input);
    
    console.log(search(myData, mySearchFn('Toronto')));
    // => 1
    console.log(myData);
    // => { root: { Europe: { children: [ { name: 'Germany' }, { name: 'England', children: [ { name: 'London', search_words: [ 'city', 'capital' ], children: [ { name: 'Westminster', search_words: [ 'borough' ] } ] }, { name: 'Manchester', search_words: [ 'city' ] } ] }, { name: 'France', children: [ { name: 'Paris', search_words: [ 'city', 'capital' ] } ] } ] }, 'North America': { children: [ { name: 'Canada', children: [ { name: 'Toronto', css: { highlight: true } }, { name: 'Ottawa' } ] }, { name: 'United States' } ] } } }
    
    console.log(search(myData, mySearchFn('borough')));
    // => 1
    console.log(myData);
    // => { root: { Europe: { children: [ { name: 'Germany' }, { name: 'England', children: [ { name: 'London', search_words: [ 'city', 'capital' ], children: [ { name: 'Westminster', search_words: [ 'borough' ], css: { highlight: true } } ] }, { name: 'Manchester', search_words: [ 'city' ] } ] }, { name: 'France', children: [ { name: 'Paris', search_words: [ 'city', 'capital' ] } ] } ] }, 'North America': { children: [ { name: 'Canada', children: [ { name: 'Toronto' }, { name: 'Ottawa' } ] }, { name: 'United States' } ] } } }
    
    console.log(search(myData, mySearchFn('capital')));
    // => 2
    console.log(myData);
    // => { root: { Europe: { children: [ { name: 'Germany' }, { name: 'England', children: [ { name: 'London', search_words: [ 'city', 'capital' ], children: [ { name: 'Westminster', search_words: [ 'borough' ] } ], css: { highlight: true } }, { name: 'Manchester', search_words: [ 'city' ] } ] }, { name: 'France', children: [ { name: 'Paris', search_words: [ 'city', 'capital' ], css: { highlight: true } } ] } ] }, 'North America': { children: [ { name: 'Canada', children: [ { name: 'Toronto' }, { name: 'Ottawa' } ] }, { name: 'United States' } ] } } }
    .as-console-wrapper {max-height: 100% !important; top: 0}
    &lt;script src="https://bundle.run/object-scan@13.8.0"&gt;&lt;/script&gt;

    免责声明:我是object-scan的作者

    使用库对您来说可能不值得,这是一种权衡。如果您对此答案有任何疑问/想法,请告诉我。

    【讨论】:

      猜你喜欢
      • 2015-11-30
      • 1970-01-01
      • 1970-01-01
      • 2017-06-27
      • 2018-11-16
      • 2018-01-29
      • 2021-04-03
      • 2020-11-21
      • 2022-01-20
      相关资源
      最近更新 更多