CapMonster Cloud実装ガイド2025 - AI自動CAPTCHA解決システム
CapMonster CloudのAI駆動型CAPTCHA解決サービスを詳細解説。自動化ワークフローにおけるCAPTCHA処理から倫理的な活用まで、技術と責任を両立する実装方法を紹介します。
WebサイトをLLM学習やRAGシステムに最適なMarkdown形式に変換するFirecrawlの使い方を徹底解説。APIの活用方法からバッチ処理、実装例まで詳しく紹介します。
LLM や RAG システムの普及により、Web コンテンツを構造化されたデータとして取り込む需要が急増しています。Firecrawl は、Web サイトを LLM が理解しやすい Markdown 形式に変換する強力なツールです。
Firecrawl は、Web ページをクリーンな Markdown 形式に変換する API サービスです。JavaScript実行、動的コンテンツの取得、不要な要素の除去など、LLM 向けのデータ準備に必要な機能を包括的に提供します。
機能 | 説明 | 利点 |
---|---|---|
JavaScript実行 | SPAや動的サイトに対応 | 現代的なWebサイトに対応 |
クリーンな出力 | 広告やナビゲーションを除去 | LLMに最適化されたデータ |
構造化データ | メタデータを保持 | コンテキスト情報の保存 |
バッチ処理 | 大量URLの並列処理 | スケーラブルな処理 |
API統合 | RESTful API提供 | 既存システムへの組み込み |
マルチフォーマット | Markdown、JSON、HTML対応 | 用途に応じた出力 |
私たちの目標は、Web コンテンツを LLM が最も効果的に理解できる形式に変換することです。
firecrawl.devでアカウント作成
ダッシュボードからAPIキー取得
各言語のSDKをセットアップ
基本的なAPI呼び出しを確認
# npmを使用
npm install @mendable/firecrawl-js
# yarnを使用
yarn add @mendable/firecrawl-js
# 環境変数の設定
export FIRECRAWL_API_KEY="your-api-key-here"
// TypeScriptでの初期化
import FirecrawlApp from '@mendable/firecrawl-js';
const app = new FirecrawlApp({
apiKey: process.env.FIRECRAWL_API_KEY,
});
# pipを使用
pip install firecrawl-py
# poetryを使用
poetry add firecrawl-py
# 環境変数の設定
export FIRECRAWL_API_KEY="your-api-key-here"
# Pythonでの初期化
from firecrawl import FirecrawlApp
app = FirecrawlApp(api_key=os.environ.get('FIRECRAWL_API_KEY'))
# Go modulesを使用
go get github.com/mendableai/firecrawl-go
# 環境変数の設定
export FIRECRAWL_API_KEY="your-api-key-here"
// Goでの初期化
package main
import (
"github.com/mendableai/firecrawl-go"
"os"
)
func main() {
apiKey := os.Getenv("FIRECRAWL_API_KEY")
app := firecrawl.New(apiKey)
}
# 基本的なcURL呼び出し
curl -X POST https://api.firecrawl.dev/v0/scrape \
-H "Authorization: Bearer $FIRECRAWL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"pageOptions": {
"onlyMainContent": true
}
}'
// シンプルなページ取得
const result = await app.scrapeUrl('https://example.com');
console.log(result.markdown);
// # Example Domain
// This domain is for use in illustrative examples...
// 詳細なオプションを指定
const result = await app.scrapeUrl('https://example.com', {
pageOptions: {
onlyMainContent: true,
includeHtml: false,
waitFor: 2000,
screenshot: true,
fullPageScreenshot: false,
},
extractorOptions: {
mode: 'llm-extraction',
extractionPrompt: 'Extract the main article content',
extractionSchema: {
type: 'object',
properties: {
title: { type: 'string' },
author: { type: 'string' },
publishDate: { type: 'string' },
content: { type: 'string' },
tags: {
type: 'array',
items: { type: 'string' }
},
},
},
},
});
// シンプルなページ取得
const result = await app.scrapeUrl('https://example.com');
console.log(result.markdown);
// # Example Domain
// This domain is for use in illustrative examples...
// 詳細なオプションを指定
const result = await app.scrapeUrl('https://example.com', {
pageOptions: {
onlyMainContent: true,
includeHtml: false,
waitFor: 2000,
screenshot: true,
fullPageScreenshot: false,
},
extractorOptions: {
mode: 'llm-extraction',
extractionPrompt: 'Extract the main article content',
extractionSchema: {
type: 'object',
properties: {
title: { type: 'string' },
author: { type: 'string' },
publishDate: { type: 'string' },
content: { type: 'string' },
tags: {
type: 'array',
items: { type: 'string' }
},
},
},
},
});
import FirecrawlApp from '@mendable/firecrawl-js';
import { writeFile } from 'fs/promises';
import { join } from 'path';
interface CrawlOptions {
url: string;
maxDepth?: number;
limit?: number;
allowedDomains?: string[];
excludePatterns?: string[];
}
class SiteCrawler {
private app: FirecrawlApp;
constructor(apiKey: string) {
this.app = new FirecrawlApp({ apiKey });
}
async crawlSite(options: CrawlOptions) {
console.log(`Starting crawl of ${options.url}...`);
const crawlResult = await this.app.crawlUrl(options.url, {
crawlerOptions: {
maxDepth: options.maxDepth || 3,
limit: options.limit || 100,
allowedDomains: options.allowedDomains,
excludes: options.excludePatterns || [
'*/admin/*',
'*/login/*',
'*.pdf',
'*.zip',
],
generateImgAltText: true,
},
pageOptions: {
onlyMainContent: true,
includeHtml: false,
},
});
return crawlResult;
}
async saveResults(results: any[], outputDir: string) {
for (const [index, page] of results.entries()) {
const filename = `page-${index + 1}.md`;
const filepath = join(outputDir, filename);
const content = `---
url: ${page.url}
title: ${page.metadata?.title || 'Untitled'}
description: ${page.metadata?.description || ''}
crawled_at: ${new Date().toISOString()}
---
${page.markdown}
`;
await writeFile(filepath, content, 'utf-8');
console.log(`Saved: ${filename}`);
}
}
}
// 使用例
async function main() {
const crawler = new SiteCrawler(process.env.FIRECRAWL_API_KEY!);
const results = await crawler.crawlSite({
url: 'https://docs.example.com',
maxDepth: 2,
limit: 50,
allowedDomains: ['docs.example.com'],
excludePatterns: ['*/api-reference/*'],
});
await crawler.saveResults(results.data, './output');
console.log(`Crawled ${results.data.length} pages`);
}
main().catch(console.error);
チャートを読み込み中...
// LLMトレーニング用データ収集
interface TrainingDataCollector {
sources: string[];
outputFormat: 'jsonl' | 'parquet' | 'csv';
filters: DataFilter[];
}
class LLMDataCollector {
private firecrawl: FirecrawlApp;
private processedUrls: Set<string> = new Set();
async collectFromSources(sources: string[]) {
const allData: any[] = [];
for (const source of sources) {
console.log(`Processing source: ${source}`);
try {
const data = await this.firecrawl.crawlUrl(source, {
crawlerOptions: {
maxDepth: 3,
limit: 1000,
},
pageOptions: {
onlyMainContent: true,
includeRawHtml: false,
includeMarkdown: true,
removeTags: ['script', 'style', 'nav', 'footer'],
},
});
const processedData = data.data.map(page => ({
url: page.url,
domain: new URL(page.url).hostname,
title: page.metadata?.title,
content: this.cleanContent(page.markdown),
metadata: {
...page.metadata,
collectedAt: new Date().toISOString(),
source: source,
},
}));
allData.push(...processedData);
} catch (error) {
console.error(`Error processing ${source}:`, error);
}
}
return allData;
}
private cleanContent(markdown: string): string {
// 不要な空白行を削除
let cleaned = markdown.replace(/\n{3,}/g, '\n\n');
// 画像のalt textを保持しつつ、URLを簡略化
cleaned = cleaned.replace(
/!\[([^\]]*)\]\([^)]+\)/g,
'[Image: $1]'
);
// コードブロックを保持
// リンクを簡略化
cleaned = cleaned.replace(
/\[([^\]]+)\]\([^)]+\)/g,
'$1'
);
return cleaned.trim();
}
}
// データクリーニングとフィルタリング
interface CleaningRule {
name: string;
apply: (content: string) => string;
validate: (content: string) => boolean;
}
class DataCleaner {
private rules: CleaningRule[] = [
{
name: 'remove_email_addresses',
apply: (content) => content.replace(/[\w.-]+@[\w.-]+\.\w+/g, '[EMAIL]'),
validate: (content) => !content.match(/[\w.-]+@[\w.-]+\.\w+/),
},
{
name: 'remove_phone_numbers',
apply: (content) => content.replace(/\+?\d{1,4}?[-.\s]?\(?\d{1,3}?\)?[-.\s]?\d{1,4}[-.\s]?\d{1,4}[-.\s]?\d{1,9}/g, '[PHONE]'),
validate: (content) => true,
},
{
name: 'normalize_whitespace',
apply: (content) => content.replace(/\s+/g, ' ').trim(),
validate: (content) => !content.match(/\s{2,}/),
},
{
name: 'remove_special_chars',
apply: (content) => content.replace(/[^\w\s\-.,!?'"]/g, ''),
validate: (content) => true,
},
];
async cleanDataset(data: any[]) {
const cleaned = [];
for (const item of data) {
let content = item.content;
// 各ルールを適用
for (const rule of this.rules) {
content = rule.apply(content);
}
// 最小文字数チェック
if (content.length < 100) {
console.log(`Skipping short content: ${item.url}`);
continue;
}
// 言語検出(オプション)
const language = await this.detectLanguage(content);
if (language !== 'en' && language !== 'ja') {
console.log(`Skipping non-target language: ${language}`);
continue;
}
cleaned.push({
...item,
content,
metadata: {
...item.metadata,
language,
cleanedAt: new Date().toISOString(),
},
});
}
return cleaned;
}
private async detectLanguage(text: string): Promise<string> {
// 簡易的な言語検出(実際はライブラリを使用)
const japanesePattern = /[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/;
return japanesePattern.test(text) ? 'ja' : 'en';
}
}
// 構造化とフォーマット変換
interface StructuredData {
id: string;
source: string;
content: string;
metadata: Record<string, any>;
embeddings?: number[];
}
class DataStructurer {
async structureForLLM(data: any[]): Promise<StructuredData[]> {
const structured = [];
for (const item of data) {
// チャンク分割
const chunks = this.splitIntoChunks(item.content, {
maxTokens: 2048,
overlap: 200,
});
for (const [index, chunk] of chunks.entries()) {
structured.push({
id: `${item.url}-chunk-${index}`,
source: item.url,
content: chunk,
metadata: {
...item.metadata,
chunkIndex: index,
totalChunks: chunks.length,
domain: new URL(item.url).hostname,
},
});
}
}
return structured;
}
private splitIntoChunks(
text: string,
options: { maxTokens: number; overlap: number }
): string[] {
const chunks: string[] = [];
const sentences = text.match(/[^.!?]+[.!?]+/g) || [text];
let currentChunk = '';
let currentTokens = 0;
for (const sentence of sentences) {
const sentenceTokens = this.estimateTokens(sentence);
if (currentTokens + sentenceTokens > options.maxTokens) {
if (currentChunk) {
chunks.push(currentChunk.trim());
// オーバーラップ処理
const overlapSentences = currentChunk
.split(/[.!?]+/)
.slice(-2)
.join('. ');
currentChunk = overlapSentences + ' ' + sentence;
currentTokens = this.estimateTokens(currentChunk);
}
} else {
currentChunk += ' ' + sentence;
currentTokens += sentenceTokens;
}
}
if (currentChunk.trim()) {
chunks.push(currentChunk.trim());
}
return chunks;
}
private estimateTokens(text: string): number {
// 簡易的なトークン推定(実際はtiktokenなどを使用)
return Math.ceil(text.split(/\s+/).length * 1.3);
}
async exportToJSONL(data: StructuredData[], filepath: string) {
const jsonl = data
.map(item => JSON.stringify({
text: item.content,
metadata: item.metadata,
}))
.join('\n');
await writeFile(filepath, jsonl, 'utf-8');
}
}
// データ品質バリデーション
interface ValidationResult {
valid: boolean;
errors: string[];
warnings: string[];
stats: {
totalItems: number;
validItems: number;
avgContentLength: number;
uniqueDomains: number;
};
}
class DataValidator {
async validateDataset(data: StructuredData[]): Promise<ValidationResult> {
const errors: string[] = [];
const warnings: string[] = [];
const validItems: string[] = [];
// 基本的な統計情報
const stats = {
totalItems: data.length,
validItems: 0,
avgContentLength: 0,
uniqueDomains: new Set(data.map(d => d.metadata.domain)).size,
};
// 各アイテムのバリデーション
for (const [index, item of data.entries()) {
const itemErrors: string[] = [];
// 必須フィールドチェック
if (!item.id) {
itemErrors.push(`Item ${index}: Missing ID`);
}
if (!item.content || item.content.trim().length === 0) {
itemErrors.push(`Item ${index}: Empty content`);
}
// コンテンツ品質チェック
if (item.content.length < 50) {
warnings.push(`Item ${index}: Very short content (${item.content.length} chars)`);
}
// 重複チェック
const contentHash = this.hashContent(item.content);
if (this.seenHashes.has(contentHash)) {
warnings.push(`Item ${index}: Duplicate content detected`);
}
this.seenHashes.add(contentHash);
// 文字化けチェック
if (this.hasGarbledText(item.content)) {
itemErrors.push(`Item ${index}: Contains garbled text`);
}
if (itemErrors.length === 0) {
stats.validItems++;
validItems.push(item.id);
} else {
errors.push(...itemErrors);
}
}
// 平均コンテンツ長の計算
stats.avgContentLength = Math.round(
data.reduce((sum, item) => sum + item.content.length, 0) / data.length
);
return {
valid: errors.length === 0,
errors,
warnings,
stats,
};
}
private seenHashes = new Set<string>();
private hashContent(content: string): string {
// 簡易的なハッシュ(実際はcrypto.createHashを使用)
return content.slice(0, 100).toLowerCase().replace(/\s+/g, '');
}
private hasGarbledText(text: string): boolean {
// 文字化けの検出
const garbledPatterns = [
/[\x00-\x1F\x7F-\x9F]/, // 制御文字
/�{3,}/, // 連続する置換文字
/[^\x00-\x7F\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF\uFF00-\uFFEF]/, // 想定外の文字
];
return garbledPatterns.some(pattern => pattern.test(text));
}
}
import { Pinecone } from '@pinecone-database/pinecone';
import { OpenAI } from 'openai';
import FirecrawlApp from '@mendable/firecrawl-js';
class RAGPipeline {
private firecrawl: FirecrawlApp;
private pinecone: Pinecone;
private openai: OpenAI;
private index: any;
constructor(config: {
firecrawlApiKey: string;
pineconeApiKey: string;
openaiApiKey: string;
indexName: string;
}) {
this.firecrawl = new FirecrawlApp({ apiKey: config.firecrawlApiKey });
this.pinecone = new Pinecone({ apiKey: config.pineconeApiKey });
this.openai = new OpenAI({ apiKey: config.openaiApiKey });
this.index = this.pinecone.index(config.indexName);
}
async ingestWebsite(url: string, namespace: string = 'default') {
console.log(`Ingesting website: ${url}`);
// Webサイトをクロール
const crawlResult = await this.firecrawl.crawlUrl(url, {
crawlerOptions: {
maxDepth: 3,
limit: 100,
},
pageOptions: {
onlyMainContent: true,
},
});
// 各ページを処理
const vectors = [];
for (const page of crawlResult.data) {
// テキストをチャンクに分割
const chunks = this.splitIntoChunks(page.markdown);
// 各チャンクの埋め込みを生成
for (const [index, chunk] of chunks.entries()) {
const embedding = await this.generateEmbedding(chunk);
vectors.push({
id: `${page.url}-chunk-${index}`,
values: embedding,
metadata: {
url: page.url,
title: page.metadata?.title || 'Untitled',
chunk: chunk,
chunkIndex: index,
totalChunks: chunks.length,
crawledAt: new Date().toISOString(),
},
});
}
}
// Pineconeに保存
await this.index.namespace(namespace).upsert(vectors);
console.log(`Ingested ${vectors.length} chunks from ${crawlResult.data.length} pages`);
}
async query(question: string, namespace: string = 'default', topK: number = 5) {
// 質問の埋め込みを生成
const queryEmbedding = await this.generateEmbedding(question);
// 類似検索
const results = await this.index.namespace(namespace).query({
vector: queryEmbedding,
topK,
includeMetadata: true,
});
// コンテキストを構築
const context = results.matches
.map(match => match.metadata?.chunk)
.filter(Boolean)
.join('
');
// LLMで回答生成
const response = await this.openai.chat.completions.create({
model: 'gpt-4',
messages: [
{
role: 'system',
content: 'You are a helpful assistant. Answer based on the provided context.',
},
{
role: 'user',
content: `Context:
${context}
Question: ${question}`,
},
],
temperature: 0.7,
});
return {
answer: response.choices[0].message.content,
sources: results.matches.map(m => ({
url: m.metadata?.url,
title: m.metadata?.title,
score: m.score,
})),
};
}
private async generateEmbedding(text: string): Promise<number[]> {
const response = await this.openai.embeddings.create({
model: 'text-embedding-3-small',
input: text,
});
return response.data[0].embedding;
}
private splitIntoChunks(text: string, maxLength: number = 1000): string[] {
const chunks = [];
const paragraphs = text.split('
');
let currentChunk = '';
for (const paragraph of paragraphs) {
if (currentChunk.length + paragraph.length > maxLength) {
if (currentChunk) {
chunks.push(currentChunk.trim());
}
currentChunk = paragraph;
} else {
currentChunk += '
' + paragraph;
}
}
if (currentChunk) {
chunks.push(currentChunk.trim());
}
return chunks;
}
}
最適化手法 | 効果 | 実装難易度 | コスト削減 |
---|---|---|---|
バッチ処理 | 処理速度向上 | 低 | 20-30% |
キャッシュ活用 | API呼び出し削減 | 中 | 40-50% |
並列処理 | 時間短縮 | 中 | 0% |
差分更新 | データ量削減 | 高 | 60-70% |
圧縮転送 | 帯域幅削減 | 低 | 10-15% |
Firecrawl は、Web コンテンツを LLM や RAG システムに最適な形式に変換する強力なツールです。適切に活用することで、高品質なトレーニングデータの準備や、効果的な知識ベースの構築が可能になります。
特に、RAG システムの構築や LLM のファインチューニングを行う際には、Firecrawl は不可欠なツールとなるでしょう。