JavaScriptLangChain.jsNode.js

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などがあるなら、それを使う方がよいなと思います。取得したい投稿タイプやカテゴリに絞ることがより簡単にできるというメリットもあります。

ただし利用しているサービスがスクレイピングでしか情報を取得できない場合などには、あまり負荷のかけない範囲・頻度でこのような方法を検討するのも良いかもしれません。

参考

ブックマークや限定記事(予定)など

WP Kyotoサポーター募集中

WordPressやフロントエンドアプリのホスティング、Algolia・AWSなどのサービス利用料を支援する「WP Kyotoサポーター」を募集しています。
月額または年額の有料プランを契約すると、ブックマーク機能などのサポーター限定機能がご利用いただけます。

14日間のトライアルも用意しておりますので、「このサイトよく見るな」という方はぜひご検討ください。

広告ここから
広告ここまで

Related Category posts