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

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

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

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

再帰

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

非同期処理

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

Q&A

解決済

1回答

2280閲覧

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

sebisawa

総合スコア23

Node.js

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

再帰

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

非同期処理

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

0グッド

0クリップ

投稿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

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

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

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

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

投稿2021/06/28 01:17

maisumakun

総合スコア146018

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

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

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

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.35%

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

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

質問する

関連した質問