ScrapelessとOllamaを使ったローカルRAGパイプラインの構築方法
Expert in Web Scraping Technologies
TL;DR:
- 全てのループはローカルで実行され、埋め込みプロバイダーやLLMプロバイダーのAPIキーは不要です。 Scrapelessはこのパイプラインの唯一の有料呼び出しで、ソースページを取得します。チャンク化、埋め込み、ベクターストレージ、検索、応答生成は全てこのマシン上で実行されます。
- 検索は仮定ではなく証明されています。 異なる2人についての2つの質問は、48のチャンクを持つ5人の著者から正しい人物の伝記チャンクを引き出します — 実際の類似度距離が以下に示されています。
- 生成ステップはライブのローカル呼び出しであり、テンプレートの回答ではありません。 494MパラメータのOllamaモデルは、取得したチャンクのみを読み取り、数分前にスクレイピングしたテキストに基づいて正しく応答します。
- チャンク化と埋め込みは意図的に小さく保たれています。 各伝記は、オーバーラップする60語のチャンク4〜14に分割され、1つのバッチ処理されたローカル呼び出しで
sentence-transformersを使って384次元のベクターに変換されます — GPUは必要ありません。 - これは他の2つのScrapelessパイプラインの下流部分です。 すでにクリーンなチャンクやOpenAI埋め込みインデックスをお持ちの場合は、以下のステージ3またはステージ5にジャンプしてください — 違いはインラインで示されています。
- 無料でスタートできます。 app.scrapeless.comで無料プランのScrapeless APIキーを作成してください。
パイプラインの概要
元のRAGペーパーで定義された検索強化生成では、外部コーパス上の検索ツールと、検索ツールが見つけたものを読む生成ツールを組み合わせています。そのアイデアのほとんどのウォークスルーは、その半分で止まります。このパイプラインは、実際のスクレイピングページに対して両方の半分をエンドツーエンドで構築します:
- 取得 — Scrapeless Universal Scraping APIを通じて、固定の著者ページのレンダリングHTMLを引き出します。
- 抽出とチャンク化 — 各ページを伝記テキストに解析し、出所を持つオーバーラップする単語ウィンドウに分割します。
- 埋め込み — 各チャンクをローカルで
sentence-transformersを使ってベクターに変換します。 - 保存 — ベクターとそのソースメタデータをローカルのChromaコレクションに永続化します。
- 検索 — 質問を埋め込み、Chromaに最も近いチャンクを尋ね、正しい人物が戻ってきたかを確認します。
- 生成 — 取得したチャンクをローカルのOllamaモデルに渡し、クラウドLLMの呼び出しなしで根拠のある回答を取得します。
他の2つのScrapelessの投稿はすでにこの一部をカバーしています。ウェブテキストの取り込みガイドは取得、抽出、チャンク化を行い、corpus.jsonlに保存するところであえて止まります:埋め込みは「下流で、すでに使用しているスタックで行います。」LLMテキストパイプラインガイドはさらに進みます:OpenAIを使ってChromaに対して取得、抽出、チャンク化、および埋め込みを行います。しかし、埋め込まれたレコードで止まり、ストアを照会したり答えを生成することはありません — そして、支払いが必要なOPENAI_API_KEYが必要です。このガイドは、どちらのカバーにもない部分です:埋め込みプロバイダーキーなしのローカル埋め込み、検索が正しいソースを見つけることを実証するライブの証明、そして実際に質問に答えるローカルモデルです。(RAG自体の概念については、Retrieval-Augmented Generationとはをご覧ください。)
前提条件
- Python 3.10以降。
- Scrapeless APIキー、
SCRAPELESS_API_KEYとしてエクスポート — 取得ステージのみが必要です。 - Ollamaがインストールされ、実行中で、小さなモデルが取得されていること:
ollama pull qwen2.5:0.5b。 pip install sentence-transformers chromadb beautifulsoup4 requests。
インストール
sentence-transformersはPyTorchを取り込むため、最初のインストールは数分かかり、約1GBのディスクを使用します。その後のモデルの読み込み、埋め込み、照会は、モデルがキャッシュされればオフラインで実行されます。
bash
pip install sentence-transformers chromadb beautifulsoup4 requests
ollama pull qwen2.5:0.5b
export SCRAPELESS_API_KEY="sk_your_scrapeless_key"
ステージ1 — ソースページを取得
このウォークスルーのコーパスは、quotes.toscrape.com の5つの著者伝記ページです。これはスクレイピングの練習用に作られた公開サイトです: アルバート・アインシュタイン、ジェーン・オースティン、マリリン・モンロー、J.K.ローリング、トーマス・A・エジソン。5人の異なる人々が、5つの本当に異なる出生地、誕生日、キャリアを持っていることが、ステージ5の取得チェックを意義深いものにしています — そのうちの1人に関する質問には1つの正しいソースが存在し、5つの妥当なものはありません。
Universal Scraping APIへのページごとの1つのPOSTは、レスポンスのdataフィールドにレンダリングされたHTMLを返します:
python
# fetch.py -- Scrapeless Universal Scraping APIを介してレンダリングされた著者伝記ページを取得する
import os
import pathlib
import requests
ENDPOINT = "https://api.scrapeless.com/api/v2/unlocker/request"
HEADERS = {
"Content-Type": "application/json",
"x-api-token": os.environ["SCRAPELESS_API_KEY"],
}
SOURCE_HOST = "https://quotes.toscrape.com"
AUTHORS = ["Albert-Einstein", "Jane-Austen", "Marilyn-Monroe", "J-K-Rowling", "Thomas-A-Edison"]
pathlib.Path("pages").mkdir(exist_ok=True)
for slug in AUTHORS:
url = f"{SOURCE_HOST}/author/{slug}"
resp = requests.post(
ENDPOINT,
headers=HEADERS,
json={"actor": "unlocker.webunlocker", "input": {"url": url, "js_render": False, "redirect": True}},
timeout=120,
)
resp.raise_for_status()
html = resp.json()["data"]
pathlib.Path("pages", f"{slug}.html").write_text(html, encoding="utf-8")
print(f"{slug}: {len(html):,} bytes")
これらのページはサーバーサイドでレンダリングされ、著者の伝記を構築するクライアントサイドのJavaScriptはありませんので、js_renderはFalseのままです — アンロッカーは依然としてリクエストを処理し、ページを返しますが、初期HTMLにすでにコンテンツがあるためレンダリングコストはかかりません。ライブ実行では、著者ごとに異なるバイト数が返されます:
text
アルバート・アインシュタイン: 5,329 bytes
ジェーン・オースティン: 3,413 bytes
マリリン・モンロー: 3,663 bytes
J.K.ローリング: 5,347 bytes
トーマス・A・エジソン: 2,648 bytes
無料プランでAPIキーを取得: app.scrapeless.com
ステージ2 — 抽出とチャンク分け
各ページには、固定クラスの3つのフィールドが存在します: author-title、author-born-date、author-born-location、author-description。これらをBeautifulSoupを使って抽出し、伝記の先頭に明示的な出生文を加え、事実が独立したチャンクを形成するようにし、次に60単語のオーバーラップウィンドウに15単語の継続で分割します:
python
# chunk_corpus.py -- pages/*.html -> corpus.jsonl (出所のあるオーバーラップ単語ウィンドウチャンク)
import json
import pathlib
import re
from bs4 import BeautifulSoup
CHUNK_WORDS = 60
OVERLAP_WORDS = 15
def extract_author(html: str) -> dict:
soup = BeautifulSoup(html, "html.parser")
name = soup.find("h3", class_="author-title").get_text(strip=True)
born_date = soup.find("span", class_="author-born-date").get_text(strip=True)
born_loc = soup.find("span", class_="author-born-location").get_text(strip=True)
desc = soup.find("div", class_="author-description").get_text(" ", strip=True)
return {"name": name, "born_date": born_date, "born_location": born_loc,
"description": re.sub(r"\s+", " ", desc)}
def chunk(words: list[str]):
step = CHUNK_WORDS - OVERLAP_WORDS
for start in range(0, max(len(words) - OVERLAP_WORDS, 1), step):
yield start, " ".join(words[start:start + CHUNK_WORDS])
total = 0
with open("corpus.jsonl", "w", encoding="utf-8") as out:
for page in sorted(pathlib.Path("pages").glob("*.html")):
author = extract_author(page.read_text(encoding="utf-8"))
lead = f"{author['name']} was born {author['born_date']} {author['born_location']}."
words = f"{lead} {author['description']}".split()
for start, body in chunk(words):
out.write(json.dumps({"id": f"{page.stem}-{start}", "source": page.stem,
"author": author["name"], "word_offset": start,
"text": body}) + "\n")
total += 1
print(f"{total} chunks -> corpus.jsonl")
5つの取得したページに対するライブ実行では、合計48チャンクが生成され、各伝記が実際にどれだけの情報を持っているかによって不均等に分かれています:
text
アルバート・アインシュタイン: 615 words -> 14 chunks
J.K.ローリング: 644 words -> 14 chunks
ジェーン・オースティン: 327 words -> 7 chunks
マリリン・モンロー: 376 words -> 9 chunks
トーマス・A・エジソン: 195 words -> 4 chunks
48 chunks -> corpus.jsonl
60単語の25%のオーバーラップは小規模コーパス設定であり、上記の取り込みガイドでの220/40ワードウィンドウよりも明示的に厳しくなっています — これらの伝記はそれぞれ数百単語であり、チャンクのために何千単語にもわたる記事ではありません。ウィンドウの設定は、固定のデフォルトではなく、ソースの長さに合わせて調整してください。
ステージ3 — ローカルに埋め込む
すべてのチャンクは、all-MiniLM-L6-v2を使って384次元のベクトルになります。これは、CPUで実行できる十分に小型で、MTEBベンチマークスイートで大規模モデルと比較検討されたコンパクトな文埋め込みモデルです。このモデルは一度(約90MB)ダウンロードされ、その後のすべてのエンコード呼び出しはオフラインで実行されます:
python
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("all-MiniLM-L6-v2")
vectors = model.encode(texts, show_progress_bar=False).tolist()
ステージ4 — ローカルベクトルデータベースに保存
ChromaのPersistentClientはコレクションをディスクに書き込むため、サーバーを管理することなくインデックスは実行間で持続します。各ベクトルはメタデータとしてその著者とソースページを保持し、これにより取得されたチャンクは匿名のテキストではなく、特定のページに遡れることが可能になります:
python
import chromadb
client = chromadb.PersistentClient(path=".chroma")
collection = client.create_collection("author_bios")
collection.add(
ids=[c["id"] for c in chunks],
documents=[c["text"] for c in chunks],
embeddings=vectors,
metadatas=[{"author": c["author"], "source": c["source"], "word_offset": c["word_offset"]}
for c in chunks],
)
48のチャンクに対してライブ実行を行うと、コレクションは埋め込まれたすべてのデータを保持していることが確認されます:
text
埋め込まれた48チャンク、次元384、コレクションカウント48
ステージ5 — 正しいチャンクを取得
これはほとんどのローカルRAGのウォークスルーがスキップするステップです:取得が正しいソースを返すことを証明すること、つまり単に何かを返すのではなく。チャンクと同じ方法で質問を埋め込み、ChromaにデフォルトのL2距離に基づいて最近傍を尋ねます。ここで、小さい数字は近い一致を意味します:
python
query_vector = model.encode([question]).tolist()
result = collection.query(query_embeddings=query_vector, n_results=2)
top_author = result["metadatas"][0][0]["author"]
top_distance = result["distances"][0][0]
異なる2人についての2つの質問が同じ48チャンクインデックスに対して実行され、各々が正しい人物と他の誰の伝記も出力しません:
text
Q: アルベルト・アインシュタインはどこで生まれましたか?
トップ一致作者: アルベルト・アインシュタイン | 距離: 0.6465
チャンク: アルベルト・アインシュタインは1879年3月14日にドイツのウルムで生まれました。1879年にアルベルト・アインシュタインはドイツのウルムで生まれました。彼はチューリッヒ大学で博士号を取得しました...
Q: J.K.ローリングはどこで生まれましたか?
トップ一致作者: J.K.ローリング | 距離: 0.4566
チャンク: J.K.ローリングは1965年7月31日にイギリスのサウス・グロスタシャー州のイェイトで生まれました。参照:ロバート・ガルブレイス 彼女はペンネームの下で書いています...
いかなる質問もジェーン・オースティン、マリリン・モンロー、トーマス・エジソンからチャンクを取得せず、埋め込み空間は5つの無関係な伝記をうまく分離し、一人についての事実の質問が他の人物の情報をうっかり引き上げることはありません。
ステージ6 — グラウンデッドアンサーを生成
取得されたチャンクは、ローカルOllamaモデルが受け取る唯一のコンテキストとなります。プロンプトは単一のJSONフィールド — ただの答え — を要求します。なぜなら、出所はすでにステージ5の取得メタデータに存在するからです。小型モデルに引用フィールドを再表記させることは、ベクトルストアがすでに無料で行った仕事を求めることになり、494Mパラメータのモデルに頼るには信頼できる余分なフィールドとは言えません:
python
import json
import requests
prompt = (
'次の文脈のみを使用して、一文で質問に答えてください。'
'応答はJSON形式でのみ{"answer": "..."}にしてください。\n\n'
f"文脈:\n{context}\n\n質問: {question}"
)
resp = requests.post(
"http://localhost:11434/api/generate",
json={"model": "qwen2.5:0.5b", "prompt": prompt, "stream": False,
"format": "json", "options": {"temperature": 0, "num_predict": 40}},
timeout=600,
)
answer = json.loads(resp.json()["response"])
取得したアインシュタインのチャンクのみを与えたライブコールが、他の4つの伝記を見ずに正しく回答します:
text
取得されたチャンクの著者(ベクトルストアメタデータから): アルベルト・アインシュタイン
質問: アルベルト・アインシュタインはどこで生まれ、何年でしたか?
生成された回答: アルベルト・アインシュタインは1879年3月14日にドイツのウルムで生まれました。
これが完全なループです:質問が入ると、ベクトルストアが48の候補を1つの関連チャンクに絞り、localhost上で実行されるモデルがそのチャンクを文に変えます — OpenAIキーも不要で、スクレイピングブラウザも使わず、初期の取得後にマシンを離れることはありません。
完全なパイプライン
上記のすべてのステージは1つのスクリプトに連結されます:5ページを取得し、それをチャンクに分け、結果を埋め込み、保存し、両方の取得チェックを実行した後、最終的な回答を生成します。ここには何も偽造されたものや別々のセッションから組み立てられたものはなく、6つのステージが一度、上から下まで実行されます:
python
# full_pipeline.py -- フェッチ -> チャンク -> 埋め込む -> 保存する -> 取得する -> 生成する、エンドツーエンド
import json
import logging
import os
import pathlib
import re
os.environ.setdefault("HF_HOME", "/usr/local/share/hf-cache")
os.environ.setdefault("HF_HUB_OFFLINE", "1")
import chromadb
import requests
from bs4 import BeautifulSoup
from sentence_transformers import SentenceTransformer
logging.getLogger("chromadb.telemetry.product.posthog").setLevel(logging.CRITICAL)
# ステージ 1: フェッチ
ENDPOINT = "https://api.scrapeless.com/api/v2/unlocker/request"
HEADERS = {"Content-Type": "application/json", "x-api-token": os.environ["SCRAPELESS_API_KEY"]}
SOURCE_HOST = "https://quotes.toscrape.com"
AUTHORS = ["Albert-Einstein", "Jane-Austen", "Marilyn-Monroe", "J-K-Rowling", "Thomas-A-Edison"]
pathlib.Path("pages").mkdir(exist_ok=True)
for slug in AUTHORS:
url = f"{SOURCE_HOST}/author/{slug}"
resp = requests.post(
ENDPOINT, headers=HEADERS,
json={"actor": "unlocker.webunlocker", "input": {"url": url, "js_render": False, "redirect": True}},
timeout=120,
)
resp.raise_for_status()
html = resp.json()["data"]
pathlib.Path("pages", f"{slug}.html").write_text(html, encoding="utf-8")
print(f"{slug}: {len(html):,} バイト")
# ステージ 2: 抽出とチャンク化
CHUNK_WORDS, OVERLAP_WORDS = 60, 15
def extract_author(html: str) -> dict:
soup = BeautifulSoup(html, "html.parser")
name = soup.find("h3", class_="author-title").get_text(strip=True)
born_date = soup.find("span", class_="author-born-date").get_text(strip=True)
born_loc = soup.find("span", class_="author-born-location").get_text(strip=True)
desc = soup.find("div", class_="author-description").get_text(" ", strip=True)
return {"name": name, "born_date": born_date, "born_location": born_loc,
"description": re.sub(r"\s+", " ", desc)}
def chunk_words(words: list[str]):
step = CHUNK_WORDS - OVERLAP_WORDS
for start in range(0, max(len(words) - OVERLAP_WORDS, 1), step):
yield start, " ".join(words[start:start + CHUNK_WORDS])
chunks = []
with open("corpus.jsonl", "w", encoding="utf-8") as out:
for page in sorted(pathlib.Path("pages").glob("*.html")):
author = extract_author(page.read_text(encoding="utf-8"))
lead = f"{author['name']}は{author['born_date']} {author['born_location']}で生まれました。"
words = f"{lead} {author['description']}".split()
n_chunks = 0
for start, body in chunk_words(words):
rec = {"id": f"{page.stem}-{start}", "source": page.stem, "author": author["name"],
"word_offset": start, "text": body}
out.write(json.dumps(rec) + "\n")
chunks.append(rec)
n_chunks += 1
print(f"{author['name']}: {len(words)} 語 -> {n_chunks} チャンク")
print(f"{len(chunks)} チャンク -> corpus.jsonl")
# ステージ 3 & 4: 埋め込みと保存
model = SentenceTransformer("all-MiniLM-L6-v2")
client = chromadb.PersistentClient(path=".chroma", settings=chromadb.Settings(anonymized_telemetry=False))
if "author_bios" in [c.name for c in client.list_collections()]:
client.delete_collection("author_bios")
collection = client.create_collection("author_bios")
texts = [c["text"] for c in chunks]
vectors = model.encode(texts, show_progress_bar=False).tolist()
collection.add(
ids=[c["id"] for c in chunks],
documents=texts,
embeddings=vectors,
metadatas=[{"author": c["author"], "source": c["source"], "word_offset": c["word_offset"]} for c in chunks],
)
print(f"{len(chunks)} チャンクを埋め込み、次元 {len(vectors[0])}、コレクションのカウント {collection.count()}")
# ステージ 5: 取得
for question in ["アルバート・アインシュタインはどこで生まれましたか?", "J.K.ローリングはどこで生まれましたか?"]:
query_vector = model.encode([question]).tolist()
result = collection.query(query_embeddings=query_vector, n_results=2)
top_author = result["metadatas"][0][0]["author"]
top_distance = result["distances"][0][0]
top_text = result["documents"][0][0]
print(f"Q: {question}")
print(f" トップの一致著者: {top_author} | 距離: {top_distance:.4f}")
print(f" チャンク: {top_text[:160]}...")
# ステージ 6: 生成
question = "アルバート・アインシュタインはどこで生まれ、何年に生まれましたか?"
query_vector = model.encode([question]).tolist()
result = collection.query(query_embeddings=query_vector, n_results=1)
context = result["documents"][0][0]
retrieved_author = result["metadatas"][0][0]["author"]
prompt = (
"以下のコンテキストを使用して、質問に対する答えを短い文章で返してください。 "
'JSON {"answer": "..."} でのみ応答してください。\n\n'
f"コンテキスト:\n{context}\n\n質問: {question}"
)
resp = requests.post(
"http://localhost:11434/api/generate",
json={"model": "qwen2.5:0.5b", "prompt": prompt, "stream": False,
"format": "json", "options": {"temperature": 0, "num_predict": 40}},
timeout=600,
)
resp.raise_for_status()
answer = json.loads(resp.json()["response"])
```ja
print("取得したチャンクの著者(ベクトルストアのメタデータから):", retrieved_author)
print("質問:", question)
print("生成された回答:", answer["answer"])
段階を追って示されるすべての数値を再現するための単一の上から下への実行:5つのバイト数、48のチャンク、48のベクトルの384次元コレクション、2つの正しい取得、および1つの根拠のある回答。
責任を持った情報源の調達
伝記ページは実在の人物を描写しており、スクレイピング用に作られた練習サイトにおいても同様です。このパターンを本番ソースに向けるときに取得コーパスを防御可能に保つための3つの実践:公開ページのみを収集し、ドメインを取得リストに追加する前に各サイトのサービス利用規約とロボット指令を確認する;すべてのチャンクにソースURLと著者名を添付し、このパイプラインのメタデータが行うようにして、生成された回答がどこから来たのかを追跡できるようにする;リクエストのボリュームをこのウォークスルーが行うように扱う — 5ページ、1度、合意していないサイトに対する持続的なクローリングではなく。
結論
5ページに対して1つの根拠のある文が出て、初期取得後のすべての段階はこのマシンで実行されます。取得の半分は生成の半分を信頼する価値がある部分です:48のチャンク、2つの質問、2人の正しい著者、それを証明する距離 — そしてその後にローカルモデルが回答します。ソースページ、埋め込みモデル、またはオラマモデルを入れ替えても、その形状は保持されます。コンテンツが変わってもパイプラインは変わりません。
RAGパイプラインの構築の準備はできましたか?
Scrapeless上でデータパイプラインを構築するコミュニティに参加してください: Discord · Telegram。
app.scrapeless.com にサインアップして無料トライアルクレジットを取得し、ステージ1を自分の取得コーパスが必要とするページに向けます。開発者ドキュメントではunlocker.webunlockerリクエストの形をカバーし、現在の料金プランは料金ページにあります。
FAQ
Q: このパイプラインの各部分はScrapeless以外のクラウドAPIキーが必要ですか?
いいえ。Scrapelessは唯一の有料呼び出しであり、ステージ1の取得のためにのみ必要です。埋め込みはsentence-transformersを介してローカルで実行され、ベクトルストアはディスク上のChromaファイルであり、生成はローカルのオラマモデルを介して実行されます — 他にはプロバイダキーを必要としません。
Q: これはScrapelessの他のRAG関連の記事とどう違うのですか?
カバレッジであり、重複ではありません。取り込みガイドは、取得、抽出、チャンク化を行い、埋め込みの前で意図的に停止します。LLMテキストパイプラインガイドはOpenAIで埋め込みを行いChromaに格納し、クエリまたは生成の前で停止します。このガイドは、ループを完結させるローカルでゼロ埋め込みキーの半分です:取得し、次に生成します。
Q: GPUは必要ですか?
いいえ。all-MiniLM-L6-v2とqwen2.5:0.5bはこのウォークスルーでCPUで実行されました。GPUは大きなローカルモデルの速度を上げますが、埋め込みステップや小さな生成モデルを構築およびテストするのに必要とはされません。
Q: チャンクサイズはどう選びますか?
それは単一のソースが実際に言っている内容に合わせます。このウォークスルーの伝記はそれぞれ数百語で構成されているので、60語で15語のオーバーラップがあれば、チャンクはほぼ1つのアイデアに収束します。数千語の記事には、より幅広いウィンドウが求められます — 上記の取り込みガイドでは、正確にその理由のために220語を使用し、40語のオーバーラップがあります。
Q: もし取得が間違ったチャンクを返したら?
通常、それはコーパスに近似重複コンテンツが含まれていて埋め込みモデルがそれを分離できないか、質問がソーステキストの表現から遠く離れていることを意味します。チャンクのオーバーラップを広げたり、より大きなモデルで埋め込みを行ったり、トップ候補の上に2回目の再ランキングパスを追加することで対処できます;このウォークスルーの5つの異なる伝記は意図的に区別しやすく、クリーンな取得結果を示せる可能性を持たせています。
Q: 生成により大きなローカルモデルを入れ替えることはできますか?
はい — リクエストの形は変わらず、model文字列だけが変わります。qwen2.5:0.5bはCPU上でループを示すのに十分に速いですが、3Bまたはそれ以上のオラマモデルは、メモリに余裕があれば混ざったコンテキストでより信頼性のある回答を提供します。
Q: 伝記ページをスクレイピングしてそこから回答を生成することは合法ですか?
公開されているページのスクレイピングは一般的には許可されていますが、サイトの利用規約や管轄権が重要です。公開されているページのみを収集し、対象サイトの利用規約やロボット指令を最初に確認し、収集量に制限を設け、あなたに適用されるプライバシー法に従ってコーパス内の個人データを扱ってください。
Scrapelessでは、適用される法律、規制、およびWebサイトのプライバシーポリシーを厳密に遵守しながら、公開されているデータのみにアクセスします。 このブログのコンテンツは、デモンストレーションのみを目的としており、違法または侵害の活動は含まれません。 このブログまたはサードパーティのリンクからの情報の使用に対するすべての責任を保証せず、放棄します。 スクレイピング活動に従事する前に、法律顧問に相談し、ターゲットウェブサイトの利用規約を確認するか、必要な許可を取得してください。



