【问题标题】:What's this type of recursion called and how do I implement it in JavaScript (Node.js)?这种类型的递归称为什么以及如何在 JavaScript (Node.js) 中实现它?
【发布时间】:2019-08-09 17:12:30
【问题描述】:

我知道如何使用cheerio从使用request(或https)获得的responseText字符串中提取所有href属性(锚标记)的值,然后创建一个平面对象(唯一) 网址。

我不明白如何使用递归(无需手动编写每个循环)创建嵌套对象(嵌套对象)。

这个nested 对象具有一定的深度(方便地使用称为depth 的参数指定)。

例如,假设这是我的代码:

function get(url, callback) {
  // get "responseText" using "requests"
  // extract the anchor tags href from the "responseText"
  callback({"https://url1.com": {}, "https://url2.com": {});
}

get("https://www.example.com", (urls) => {
  console.log(urls);
});

当你运行代码时,输​​出应该是:

{ "https://url1.com": {}, "https://url2.com": {} }

我不明白的是如何递归地转到"https://url1.com" 然后得到这个输出:

{ "https://url1.com": { "https://sub-url-1.com": {} }, "https://url2.com": { "https://sub-url-2.com": {} } }

如果深度为 5 怎么办?我将如何递归地遍历每个子 URL 5 级深度然后获取它的子 URL?

这种类型的递归叫什么?如何在 JavaScript 中实现它?

【问题讨论】:

  • 我在您的代码中没有看到任何递归尝试?
  • @Bergi 这可能会让我看起来很愚蠢,我真的不知道从哪里开始,我什至不知道这种类型的递归叫什么:/

标签: javascript node.js recursion


【解决方案1】:

crawl 开始,它需要一个起始 url(字符串)和一个起始深度(int)并返回一个承诺的结果。我们的结果是我们预期输出的类型(或“形状”)。在这种情况下,它是一个以 url 字符串作为键的对象,值是空对象或另一个嵌套结果 -

// type url = string
// type result = (url, result) object | empty

// crawl : (string * int) -> result promise
const crawl = (initUrl = '/', initDepth = 0) =>
{ const loop = (urls, depth) =>
    parallel
      ( urls
      , u =>
          depth === 0
            ? [ u, {} ]
            : loop (get (u), depth - 1) 
                .then (r => [ u, r ])
      )
      .then (Object.fromEntries)
  return loop ([ initUrl ], initDepth)
}

垂直样式并不常见,但有助于眼睛识别与制表位垂直规则对齐的代码元素。开放空白允许 cmets,但随着对样式的熟悉,它们变得不那么必要了 -

// type url = string
// type result = (url, result) object | empty

// crawl : (string * int) -> result promise
const crawl = (initUrl = '/', initDepth = 0) =>
{ const loop = (urls, depth) =>
    parallel             // parallel requests
      ( urls             // each url
      , u =>             // as u
          depth === 0                   // exit condition
            ? [ u, {} ]                 // base: [ key, value ]
            : loop (get (u), depth - 1) // inductive: smaller problem
                .then (r => [ u, r ])   //   [ key, value ]
      )
      .then (Object.fromEntries)        // convert [ key, value ]
                                        //      to { key: value }

  return loop ([ initUrl ], initDepth)  // init loop
}

这利用了一个通用实用程序parallel,它对于处理承诺的数组很有用 -

// parallel : (('a array) promise * 'a -> 'b) -> ('b array) promise 
const parallel = async (p, f) =>
  Promise.all ((await p) .map (x => f (x)))

或者如果你不想依赖async-await -

// parallel : (('a array) promise * 'a -> 'b) -> ('b array) promise 
const parallel = (p, f) =>
  Promise.all
    ( Promise
        .resolve (p)
        .then (r => r .map (x => f (x)))
    )

给定一个模拟的sitemap 和对应的get 函数-

// sitemap : (string, string array) object
const sitemap =
  { "/": [ "/a", "/b", "/c" ]
  , "/a": [ "/a/1", "/a/11", "/a/111" ]
  , "/a/1": [ "/a/1/2", "a/1/22" ]
  , "/a/1/2": [ "/a/1/2/3" ]
  , "/a/1/2/3": [ "/a/1/2/3/4" ]
  , "/a/11": [ "/a/11/2", "a/11/22" ]
  , "/a/11/22": [ "/a/11/22/33"]
  , "/b": [ "/b/1" ]
  , "/b/1": [ "/b/1/2" ]
  }

// get : string -> (string array) promise      
const get = async (url = '') =>
  Promise
    .resolve (sitemap[url] || [] )
    .then (delay)

// delay : ('a * int) -> 'a promise
const delay = (x, ms = 250) =>
  new Promise (r => setTimeout (r, ms, x))

我们可以看到crawl 在不同深度的响应 -

crawl ('/') .then (console.log, console.error)
// { '/': {} }

crawl ('/', 1) .then (console.log, console.error)
// { '/': { '/a': {}, '/b': {}, '/c': {} } }

crawl ('/b', 1) .then (console.log, console.error)
// { '/b': { '/b/1': {} } }

crawl ('/b', 2) .then (console.log, console.error)
// {
//   "/b": {
//     "/b/1": {
//       "/b/1/2": {}
//     }
//   }
// }

这里我们爬取根"/",深度为Infinity -

crawl ("/", Infinity) .then (console.log, console.error)
// {
//   "/": {
//     "/a": {
//       "/a/1": {
//         "/a/1/2": {
//           "/a/1/2/3": {
//             "/a/1/2/3/4": {}
//           }
//         },
//         "a/1/22": {}
//       },
//       "/a/11": {
//         "/a/11/2": {},
//         "a/11/22": {}
//       },
//       "/a/111": {}
//     },
//     "/b": {
//       "/b/1": {
//         "/b/1/2": {}
//       }
//     },
//     "/c": {}
//   }
// }

只需将 get 替换为一个真正的函数,该函数接受一个输入 url 并返回一个 href 数组 - crawl 将同样工作。

展开下面的sn-p,在自己的浏览器中验证结果-

const parallel = async (p, f) =>
  Promise.all ((await p) .map (x => f (x)))

const crawl = (initUrl = '/', initDepth = 0) =>
{ const loop = (urls, depth) =>
    parallel
      ( urls
      , u =>
          depth === 0
            ? [ u, {} ]
            : loop (get (u), depth - 1) 
                .then (r => [ u, r ])
      )
      .then (Object.fromEntries)
  return loop ([ initUrl ], initDepth)
}

// mock
const sitemap =
  { "/": [ "/a", "/b", "/c" ]
  , "/a": [ "/a/1", "/a/11", "/a/111" ]
  , "/a/1": [ "/a/1/2", "a/1/22" ]
  , "/a/1/2": [ "/a/1/2/3" ]
  , "/a/1/2/3": [ "/a/1/2/3/4" ]
  , "/a/11": [ "/a/11/2", "a/11/22" ]
  , "/a/11/22": [ "/a/11/22/33"]
  , "/b": [ "/b/1" ]
  , "/b/1": [ "/b/1/2" ]
  }
  
const get = async (url = '') =>
  Promise
    .resolve (sitemap[url] || [] )
    .then (delay)

const delay = (x, ms = 250) =>
  new Promise (r => setTimeout (r, ms, x))

// demos
crawl ('/') .then (console.log, console.error)
// { '/': {} }

crawl ('/', 1) .then (console.log, console.error)
// { '/': { '/a': {}, '/b': {}, '/c': {} } }

crawl ('/b', 1) .then (console.log, console.error)
// { '/b': { '/b/1': {} } }

crawl ('/b', 2) .then (console.log, console.error)
// {
//   "/b": {
//     "/b/1": {
//       "/b/1/2": {}
//     }
//   }
// }

crawl ("/", Infinity) .then (console.log, console.error)
// {
//   "/": {
//     "/a": {
//       "/a/1": {
//         "/a/1/2": {
//           "/a/1/2/3": {
//             "/a/1/2/3/4": {}
//           }
//         },
//         "a/1/22": {}
//       },
//       "/a/11": {
//         "/a/11/2": {},
//         "a/11/22": {}
//       },
//       "/a/111": {}
//     },
//     "/b": {
//       "/b/1": {
//         "/b/1/2": {}
//       }
//     },
//     "/c": {}
//   }
// }

【讨论】:

  • 如果重复链接在对象内创建循环引用,那就太好了。我可能会写一个答案...
  • @PatrickRoberts 这是一个很好的建议。 Map 可以用作一种缓存。如果一个 url 已经被缓存,我们可以从缓存中获取对应的对象。如果您愿意,请随意复制此处的任何代码作为起点。
  • 我对 FP 并不像您看起来那么舒服,因此扩展您的答案并不自然。不过我也有同样的想法,使用 Map 作为缓存,所以我肯定会包含它。
  • 抱歉迟到了,感谢您的回答,感谢您的努力。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-10-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多