LangChain.jsのRecursiveUrlLoaderを使って、EmbeddingのためにWebページをスクレイピングする
RAG(Red, Amber, Green)を作る際には、通常はSQLやREST/GraphQLのAPI、またはAmazon S3などのファイルを読み込む方法でデータを取得すると思われますが、場合によってはスクレイピングが必要になることもあります。RecursiveUrlLoaderを使用してデータをスクレイピングする方法についてまとめました。ライブラリの追加や実装方法なども詳しく説明されています。また、スクレイピングするページを制御したり、除外したりする方法も紹介されています。REST API等が利用可能な場合はそちらの方が簡単ですが、スクレイピングが必要な場合にはこの方法を検討する価値があるかもしれません。
目次
RAGを作る際に参照するデータの取得には、大抵の場合SQLやREST / GraphQLのAPI、もしくはAmazon S3などを介してファイルを読み込む形で行うかと思います。しかし場合によっては、直接スクレイピングを行う必要があるケースもありそうです。ということで、RecursiveurlLoader
を使ってデータをスクレイピングする方法を調べてまとめました。
ライブラリの追加
RecursiveUrlLoader
を使う場合、二つのライブラリを利用します。jsdom
を利用してページを読み、html-to-text
は読み込んだデータをテキスト情報に変換する際に利用します。
npm i html-to-text jsdom
TypeScriptを使う場合、@types
もいれておきましょう。
npm i --save-dev @types/html-to-text
Webページをスクレイピングする
スクレイピング自体はLangChain.jsのdocument_loaders
から行います。
import { RecursiveUrlLoader } from "langchain/document_loaders/web/recursive_url";
import { compile } from "html-to-text";
読みたいサイトのURLや、Documentへの変換処理を定義しましょう。
const url = "https://hidetaka.dev/";
const compiledConvert = compile({ wordwrap: 130 }); // returns (text: string) => string;
LangChainのLoaderを作成し、読み込みます。
const loader = new RecursiveUrlLoader(url, {
extractor: compiledConvert,
maxDepth: 1,
});
const docs = await loader.load();
実装だけをみると、かなりシンプルですね。
実行結果をみる
先ほどのコードを実行してみましょう。どのようなページを読んでいるかをみるため、metadata
をログ出力させてみます。
const url = "https://hidetaka.dev/";
const compiledConvert = compile({ wordwrap: 130 }); // returns (text: string) => string;
const loader = new RecursiveUrlLoader(url, {
extractor: compiledConvert,
maxDepth: 1,
});
const docs = await loader.load();
docs.forEach(doc => {
console.log(doc.metadata)
})
このようなデータが取れました。本文自体は、doc.pageContent
に入っていますので、気になる方はそちらをログ出力してみましょう。
{
source: 'https://hidetaka.dev/',
title: 'Hidetaka.dev | Hidetaka Okamoto portfolio website',
description: 'The portfolio website of Hidetaka Okamoto',
language: 'en'
}
{
source: 'https://hidetaka.dev/about',
title: 'About Hidetaka Okamoto',
description: 'The portfolio website of Hidetaka Okamoto',
language: 'en'
}
{
source: 'https://hidetaka.dev/articles',
title: 'Revent publications',
description: 'The portfolio website of Hidetaka Okamoto',
language: 'en'
}
{
source: 'https://hidetaka.dev/projects',
title: 'My projects',
description: 'The portfolio website of Hidetaka Okamoto',
language: 'en'
}
{
source: 'https://hidetaka.dev/oss',
title: 'OSS activities',
description: 'The portfolio website of Hidetaka Okamoto',
language: 'en'
}
{
source: 'https://hidetaka.dev/speaking',
title: 'Revent speaking',
description: 'The portfolio website of Hidetaka Okamoto',
language: 'en'
}
どのページをスクレイピングしてくるかは要チェック
スクレイピングなので、意図したページを巡回してくれているかの確認が必要です。
const url = "https://wp-kyoto.net";
const compiledConvert = compile({ wordwrap: 130 }); // returns (text: string) => string;
const loader = new RecursiveUrlLoader(url, {
extractor: compiledConvert,
maxDepth: 1,
});
const docs = await loader.load();
docs.forEach(doc => {
console.log(doc.metadata.source)
})
console.log(docs.length)
これを実行すると、URLの一覧が見れます。
https://wp-kyoto.net
https://wp-kyoto.net/
https://wp-kyoto.net/about/
https://wp-kyoto.net/bookmarks/
https://wp-kyoto.net/mypage/home/
https://wp-kyoto.net/mypage/
https://wp-kyoto.net/recent-visited/
https://wp-kyoto.net/licenses/
https://wp-kyoto.net/en/
https://wp-kyoto.net/category/ai-ml/
https://wp-kyoto.net/category/javascript/
https://wp-kyoto.net/category/langchain-js/
...
38
カテゴリー一覧などは、あまりEmbeddingするメリットがなさそうです。もしembeddingしたくないURLがある場合は、excludeDirs
で除外できます。
const loader = new RecursiveUrlLoader(url, {
extractor: compiledConvert,
maxDepth: 1,
excludeDirs: ["https://wp-kyoto.net/category/", "https://wp-kyoto.net/pages"],
});
取得したい記事があまり取れていない様子なら、maxDepth
を調整しましょう。ただしスクレイピングをより深く行う関係上、処理時間が長くなることや、対象のサイトに負荷がかかる可能性がある点に注意しましょう。
const loader = new RecursiveUrlLoader(url, {
extractor: compiledConvert,
maxDepth: 3,
excludeDirs: ["https://wp-kyoto.net/category/", "https://wp-kyoto.net/pages"],
});
ハッシュ付きURLを除外する
また、目次などがある場合、ハッシュ付きのURLがリストに出てくることがあります。
https://wp-kyoto.net/create-rag-app-using-cloudflare-workers-r2-and-langchain/
https://wp-kyoto.net/create-rag-app-using-cloudflare-workers-r2-and-langchain/#%E5%AE%9A%E6%9C%9F%E7%9A%84%E3%81%ABEmbedding%E3%82%92%E4%BD%9C%E6%88%90%E3%83%BB%E6%9B%B4%E6%96%B0%E3%81%99%E3%82%8B%E5%87%A6%E7%90%86%E3%82%92Workers%E3%81%A7%E6%9B%B8%E3%81%8F
https://wp-kyoto.net/create-rag-app-using-cloudflare-workers-r2-and-langchain/#%E6%A4%9C%E7%B4%A2%E5%81%B4%E3%81%AEAPI%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B
https://wp-kyoto.net/create-rag-app-using-cloudflare-workers-r2-and-langchain/#%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9%E6%AF%94%E8%BC%83
https://wp-kyoto.net/create-rag-app-using-cloudflare-workers-r2-and-langchain/#Hono%E3%81%A7RAG%E3%81%AEGUI%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%82%E7%94%A8%E6%84%8F%E3%81%99%E3%82%8B
https://wp-kyoto.net/create-rag-app-using-cloudflare-workers-r2-and-langchain/#API%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9%E3%82%92Stream%E3%81%A7%E8%BF%94%E3%81%99
https://wp-kyoto.net/create-rag-app-using-cloudflare-workers-r2-and-langchain/#Cloudflare_Workers%E3%81%AB%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E3%81%97%E3%81%A6%E5%8B%95%E4%BD%9C%E3%82%92%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B
https://wp-kyoto.net/create-rag-app-using-cloudflare-workers-r2-and-langchain/#%E3%82%84%E3%81%A3%E3%81%A6%E3%81%BF%E3%81%9F%E6%84%9F%E6%83%B3
これもEmbedding対象に含める必要はないので、次のような処理で除外しておきましょう。
const loader = new RecursiveUrlLoader(url, {
extractor: compiledConvert,
maxDepth: 3,
excludeDirs: ["https://wp-kyoto.net/category/", "https://wp-kyoto.net/pages"],
});
const docs = await loader.load();
const items = docs.filter(doc => {
if (!doc.metadata || !doc.metadata.source) return false
if (/\/#/.test(doc.metadata.source)) return false
return true
})
items.forEach(doc => {
console.log(doc.metadata.source)
})
console.log(docs.length)
console.log(items.length)
100ページほど減らすことができました。
https://wp-kyoto.net/switch-langchain-text-splitter-by-file-extentions/
https://wp-kyoto.net/save-vector-index-by-faiss-node-with-langchain/
https://wp-kyoto.net/privacy-policy/
https://wp-kyoto.net/specified-commercial-transactions-act/
https://wp-kyoto.net/term-of-service/
https://wp-kyoto.net/search/
262
161
やってみての感想
REST APIなどがあるなら、それを使う方がよいなと思います。取得したい投稿タイプやカテゴリに絞ることがより簡単にできるというメリットもあります。
ただし利用しているサービスがスクレイピングでしか情報を取得できない場合などには、あまり負荷のかけない範囲・頻度でこのような方法を検討するのも良いかもしれません。