質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
86.02%
Node.js

Node.jsとはGoogleのV8 JavaScriptエンジンを使用しているサーバーサイドのイベント駆動型プログラムです。

再帰

情報工学における再帰とは、プログラムのあるメソッドの処理上で自身のメソッドが再び呼び出されている処理の事をいいます。

非同期処理

非同期処理とは一部のコードを別々のスレッドで実行させる手法です。アプリケーションのパフォーマンスを向上させる目的でこの手法を用います。

Q&A

解決済

node.jsでpromiseを使って再帰的にURL一覧を作成したい

sebisawa
sebisawa

総合スコア19

Node.js

Node.jsとはGoogleのV8 JavaScriptエンジンを使用しているサーバーサイドのイベント駆動型プログラムです。

再帰

情報工学における再帰とは、プログラムのあるメソッドの処理上で自身のメソッドが再び呼び出されている処理の事をいいます。

非同期処理

非同期処理とは一部のコードを別々のスレッドで実行させる手法です。アプリケーションのパフォーマンスを向上させる目的でこの手法を用います。

1回答

0グッド

0クリップ

1414閲覧

投稿2021/06/28 00:57

前提・実現したいこと

Promiseの勉強を兼ねて、node.jsでクローラーを作っています。基本的には、URLにアクセスしてサイト内リンクが見つかったら再起的にリンクを探すというものです。
色々と試しているのですが、「クローリングが完了する前に次のステップに進んでしまう」「取得したURLを無視して永久ループに陥ってしまう」など、こちらの期待通りに動いてくれません。

Promiseの挙動を完全に理解していないこともあり、アドバイスを頂戴できれば幸いです。

これをマスターしたら async/awaitにも挑戦したいのですが、まずはpromiseの理解を優先しています。

該当のソースコード

nodejs

1/* 2 * Modules 3 */ 4 5const fetch = require('node-fetch'); 6const jsdom = require('jsdom'); 7const { JSDOM } = jsdom; 8 9/* 10 * Config 11 */ 12 13const base_url = "https://www.example.com/"; 14 15/* 16 * Functions 17 */ 18 19let urls = {}; 20 21function parserHtmlByUrlPromised(url) { 22 23 return new Promise((resolve, reject) => { 24 25 26 // 処理開始 27 console.log("Requesting HTML: " + url); 28 29 // 結果用オブジェクトの初期化 30 if (typeof urls[url] !== 'undefined') { 31 resolve("Already Created"); 32 } 33 urls[url] = {}; 34 35 // HTMLを取得 36 fetch(url, {}) 37 .then(res => { 38 if (res.ok) { // res.status >= 200 && res.status < 300 39 return res; 40 } else { 41 urls[url].success = false; 42 urls[url].error_msg = res.statusText; 43 reject(); 44 } 45 }) 46 .then(res => res.text()) 47 .then(html => { 48 // HTMLのパース 49 const dom = new JSDOM(html, { url: base_url }); 50 // TITLE 51 urls[url].title = dom.window.document.querySelector('title').textContent; 52 // URLの洗い出し 53 var abc = dom.window.document.querySelectorAll('a') 54 .forEach(a => { 55 // 不適切なURLは無視 56 if (typeof a.href !== 'undefined' && a.href.indexOf(base_url) === -1) return; 57 if (a.href.match(/.(css|jpg|png|gif|pdf)($|?.*)/)) return; 58 59 // 処理済みかどうかを確認 60 if (typeof urls[target_url] === 'undefined') { 61 62 // ここを無効にすると無限ループ(なぜ?) 63 urls[target_url] = {}; 64 65 // 再帰的に処理 66 parserHtmlByUrlPromised(target_url) 67 .then(() => { 68 return Promise.resolve(); 69 }); 70 } 71 }) 72 // HTMLのパース終了 73 resolve(url); 74 }).catch(e => { 75 urls[url].success = false; 76 urls[url].error_msg = e; 77 reject(); 78 }) 79 }); 80}; 81 82/* 83 * Main 84 */ 85parserHtmlByUrlPromised(base_url) 86 .then(() => { 87 console.log(urls); 88 }) 89 .catch(error => { 90 console.error(error); 91 });

試したこと

補足情報(FW/ツールのバージョンなど)

  • v16.3.0

以下のような質問にはグッドを送りましょう

  • 質問内容が明確
  • 自分も答えを知りたい
  • 質問者以外のユーザにも役立つ

グッドが多くついた質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

気になる質問をクリップする

クリップした質問は、後からいつでもマイページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

下記のような質問は推奨されていません。

  • 間違っている
  • 質問になっていない投稿
  • スパムや攻撃的な表現を用いた投稿

適切な質問に修正を依頼しましょう。

回答1

1

ベストアンサー

これをマスターしたら async/awaitにも挑戦したいのですが、まずはpromiseの理解を優先しています。

Promiseとループを組み合わせる」というのは相当に書きづらいです。async/awaitで書くことをおすすめします。

投稿2021/06/28 01:17

maisumakun

総合スコア141456

sebisawa👍を押しています

良いと思った回答にはグッドを送りましょう。
グッドが多くついた回答ほどページの上位に表示されるので、他の人が素晴らしい回答を見つけやすくなります。

下記のような回答は推奨されていません。

  • 間違っている回答
  • 質問の回答になっていない投稿
  • スパムや攻撃的な表現を用いた投稿

このような回答には修正を依頼しましょう。

回答へのコメント

sebisawa

2021/06/28 01:32

ありがとうございます。async/awaitでの実装も視野に入れます。可能でしたら、上記コードのどこにasync/awaitをセットすればよいか、ヒントだけでもいただけると助かります。
maisumakun

2021/06/28 01:34

await fetchで結果を取得すれば、その値を普通に利用可能です。
sebisawa

2021/06/29 01:45

async/awaitで書き直してみましたが、まだ解決に至っていません。 私の期待は、mainでcrawlWebPage()を呼び出し、crawlWebPage()で処理が完全に終わってから次のステップに進むというものです。 最初のfetchは待ってくれるようになったのですが、そのページにあるリンクに対して再帰的にcrawlWebPage()を実行させると、それらの処理は待ってくれないです。 ``` urls[url].links.forEach(async url => { await crawlWebPage(url); }) ``` 恐縮ですが、アドバイスをいただけると助かります。 ``` /* * Modules */ const fetch = require('node-fetch'); const jsdom = require('jsdom'); const { JSDOM } = jsdom; /* * Config */ const BASE_URL = "https://www.example.com/"; const IGNORE_QUERY_PARAMS = ['utm_campaign', "utm_source", "utm_keyword", "utm_content", "utm_medium"]; /* * Global Object(s) */ let urls = {}; /* * Functions */ // 非同期で HTMLのタイトルを取得 const getHtmlByUrl = async(url) => { if (typeof urls[url] !== 'undefined' && typeof urls[url].html !== 'undefined') return; try { const res = await fetch(url); if (!res.ok) throw NodeFetchError(res.statusText); return res.text(); } catch (e) { console.error('Fetch failed: ' + e); return; } }; // URLの有効性をチェック const isUrlEligible = (url) => { if (typeof url !== 'undefined' && url.indexOf(BASE_URL) !== 0) return false; if (url.match(/.(css|jpg|png|gif|pdf)($|?.*)/)) return false; return true; } // URLのフォーマット const formatUrl = (url) => { var formatted = url; IGNORE_QUERY_PARAMS.forEach(param => { var regexp = new RegExp(param + "=[^&]*"); formatted = formatted.replace(regexp, ""); }); formatted = formatted .replace(/#.*$/, '') .replace(/?&$/, '') .replace(//index.(html?|php|asp|cgi|jsp)??/, ''); formatted += formatted.match(/(.(php|html?|jsp|cgi)|/)$/) ? '' : '/'; return formatted; } // 重複を除外したURL一覧を生成 const getLinkUrlsFromDom = (dom) => { var url_list = {}; dom.window.document.querySelectorAll('a') .forEach(a => { if (!isUrlEligible(a.href)) { return; } url_list[formatUrl(a.href)] = 1 }); return Object.keys(url_list); } // Webページのクロール const crawlWebPage = async(url) => { // グローバルオブジェクトの初期化 if (typeof urls[url] !== 'undefined') return; urls[url] = {}; // HTMLの取得 const html = await getHtmlByUrl(url); if (!html) { urls[url].is_success = false; return; } // HTMLのパース const dom = new JSDOM(html, { url: BASE_URL }); // 取り出したい情報 urls[url].title = dom.window.document.querySelector('title').textContent; // HTMLに含まれるリンクの一覧を生成(重複は除外) urls[url].links = url_list = getLinkUrlsFromDom(dom); urls[url].links.forEach(async url => { await crawlWebPage(url); }) } // main (async() => { // URLからページの情報を取得してグローバルオブジェクトを更新 await crawlWebPage(BASE_URL); // ここで待ってほしい!!! // URL一覧にあるURLにもアクセス console.log(urls); })(); ```
maisumakun

2021/06/29 01:49

Array.forEachはasync-awaitと無関係に進行します。for-ofで回せば、awaitで待機して1つずつ進みます。
sebisawa

2021/06/29 02:05

早速のコメントありがとうございます! これから仕事なので、夜に試して報告します。

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
86.02%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問

同じタグがついた質問を見る

Node.js

Node.jsとはGoogleのV8 JavaScriptエンジンを使用しているサーバーサイドのイベント駆動型プログラムです。

再帰

情報工学における再帰とは、プログラムのあるメソッドの処理上で自身のメソッドが再び呼び出されている処理の事をいいます。

非同期処理

非同期処理とは一部のコードを別々のスレッドで実行させる手法です。アプリケーションのパフォーマンスを向上させる目的でこの手法を用います。