Skip to content

Commit

Permalink
Merge branch 'release/0.3.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
Sunwood-ai-labs committed Jun 10, 2024
2 parents 3a3e93d + 72d07d2 commit d8faeca
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 30 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,4 @@ tmp2.md
.harmon_ai/README_template.md
output
urls.txt
memo.md
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,25 @@ pip install pegasus-surf

PEGASUSをコマンドラインから使用するには、以下のようなコマンドを実行します。

#### 検索スクレイピング
```shell
pegasus search --search-query "お好み焼き レシピ"
```

#### 再帰スクレイピング

```shell
# 単一のURLからスクレイピングを開始 🌐
pegasus --base-url https://example.com/start-page output_directory --exclude-selectors header footer nav --include-domain example.com --exclude-keywords login --output-extension txt
pegasus recursive --base-url https://www.otafuku.co.jp/recipe/cook/noodle/nood01.html --output_dir output/yakisoba --exclude-selectors header footer nav --include-domain example.com --exclude-keywords login --output-extension .txt

# 探索深度を指定して実行 🔍
pegasus --base-url https://docs.eraser.io/docs/what-is-eraser output/eraser_docs2 --exclude-selectors header footer nav aside .sidebar .header .footer .navigation .breadcrumbs --include-domain docs.eraser.io --exclude-keywords login --output-extension .txt --max-depth 2
pegasus recursive --base-url https://docs.eraser.io/docs/what-is-eraser --output_dir output/eraser_docs2 --exclude-selectors header footer nav aside .sidebar .header .footer .navigation .breadcrumbs --include-domain docs.eraser.io --exclude-keywords login --output-extension .txt --max-depth 2

# URLリストが記載されたテキストファイルからスクレイピングを開始 📜
pegasus --url-file urls.txt output/roomba --exclude-selectors header footer nav aside .sidebar .header .footer .navigation .breadcrumbs --exclude-keywords login --output-extension .txt --max-depth 1
pegasus recursive --url-file urls.txt --output_dir output/okonomi --exclude-selectors header footer nav aside .sidebar .header .footer .navigation .breadcrumbs --exclude-keywords login --output-extension .txt --max-depth 1

# LLMを使用したサイトの分類を行いながらスクレイピング 🧠
pegasus --url-file urls.txt output/roomba2 --exclude-selectors header footer nav aside .sidebar .header .footer .navigation .breadcrumbs --exclude-keywords login --output-extension .txt --max-depth 1 --system-message "あなたは、与えられたウェブサイトのコンテンツが特定のトピックに関連する有用な情報を含んでいるかどうかを判断するアシスタントです。トピックに関連する有益な情報が含まれている場合は「True」、そうでない場合は「False」と回答してください。" --classification-prompt "次のウェブサイトのコンテンツは、Roomba APIやiRobotに関する有益な情報を提供していますか? 提供している場合は「True」、そうでない場合は「False」と回答してください。"
pegasus recursive --url-file urls.txt --output_dir output/roomba2 --exclude-selectors header footer nav aside .sidebar .header .footer .navigation .breadcrumbs --exclude-keywords login --output-extension .txt --max-depth 1 --system-message "あなたは、与えられたウェブサイトのコンテンツが特定のトピックに関連する有用な情報を含んでいるかどうかを判断するアシスタントです。トピックに関連する有益な情報が含まれている場合は「True」、そうでない場合は「False」と回答してください。" --classification-prompt "次のウェブサイトのコンテンツは、Roomba APIやiRobotに関する有益な情報を提供していますか? 提供している場合は「True」、そうでない場合は「False」と回答してください。"
```

オプションの意味はこんな感じです!👀
Expand Down Expand Up @@ -158,6 +165,11 @@ pegasus.run("https://example.com/start-page")

[View on Eraser![](https://app.eraser.io/workspace/8cnaevNcF1kxMsfSfGl0/preview?elements=KStkmTRUuZ5AiJNPC8E40g&type=embed)](https://app.eraser.io/workspace/8cnaevNcF1kxMsfSfGl0?elements=KStkmTRUuZ5AiJNPC8E40g)

### SourceSage

```shell
sourcesage --mode DocuMind --docuMind-model "gemini/gemini-1.5-pro-latest" --docuMind-db ".SourceSageAssets\DOCUMIND\Repository_summary.md" --docuMind-release-report ".SourceSageAssets\RELEASE_REPORT\Report_v0.3.0.md" --docuMind-changelog ".SourceSageAssets\Changelog\CHANGELOG_release_5.0.0.md" --docuMind-output ".SourceSageAssets/DOCUMIND/RELEASE_NOTES_v0.3.0.md" --docuMind-prompt-output ".SourceSageAssets/DOCUMIND/_PROMPT_v0.3.0.md" --repo-name "SourceSage" --repo-version "v0.3.0"
```

## 🤝 貢献

Expand Down
21 changes: 21 additions & 0 deletions example/ex06_ddg_recipe_query.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from duckduckgo_search import DDGS
import json

# クエリ
with DDGS() as ddgs:
results = list(ddgs.text(
keywords='お好み焼き レシピ', # 検索ワード
region='jp-jp', # リージョン 日本は"jp-jp",指定なしの場合は"wt-wt"
safesearch='off', # セーフサーチOFF->"off",ON->"on",標準->"moderate"
timelimit=None, # 期間指定 指定なし->None,過去1日->"d",過去1週間->"w",
# 過去1か月->"m",過去1年->"y"
max_results=4 # 取得件数
))

# レスポンスの表示
for line in results:
print(json.dumps(
line,
indent=2,
ensure_ascii=False
))
86 changes: 74 additions & 12 deletions pegasus/Pegasus.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@
from litellm import completion
from tqdm import tqdm
import litellm
# litellm.set_verbose=True
from duckduckgo_search import DDGS
import json

logger = loguru.logger

class Pegasus:
def __init__(self, output_dir, exclude_selectors=None, include_domain=None, exclude_keywords=None, output_extension=".md",
dust_size=1000, max_depth=None, system_message=None, classification_prompt=None, max_retries=3,
model='gemini/gemini-1.5-pro-latest', rate_limit_sleep=60, other_error_sleep=10):
model='gemini/gemini-1.5-pro-latest', rate_limit_sleep=60, other_error_sleep=10, search_query=None, max_results=10, base_url=None, url_file="urls.txt"):
self.output_dir = output_dir
self.exclude_selectors = exclude_selectors
self.include_domain = include_domain
Expand All @@ -33,6 +34,10 @@ def __init__(self, output_dir, exclude_selectors=None, include_domain=None, excl
self.model = model
self.rate_limit_sleep = rate_limit_sleep
self.other_error_sleep = other_error_sleep
self.search_query = search_query
self.max_results = max_results
self.base_url = base_url
self.url_file = url_file
tprint(" Pegasus ", font="rnd-xlarge")
logger.info("初期化パラメータ:")
logger.info(f" output_dir: {output_dir}")
Expand All @@ -48,6 +53,34 @@ def __init__(self, output_dir, exclude_selectors=None, include_domain=None, excl
logger.info(f" model: {model}")
logger.info(f" rate_limit_sleep: {rate_limit_sleep}")
logger.info(f" other_error_sleep: {other_error_sleep}")
logger.info(f" search_query: {search_query}")
logger.info(f" max_results: {max_results}")
logger.info(f" base_url: {base_url}")
logger.info(f" url_file: {url_file}")


def search_scraping(self):
tprint(">> Search Scraping ")
if self.search_query is None:
return

with DDGS() as ddgs:
results = list(ddgs.text(
keywords=self.search_query,
region='jp-jp',
safesearch='off',
timelimit=None,
max_results=self.max_results
))

with open("urls.txt", "w", encoding="utf-8") as file:
for result in results:
url = result['href']
title = result['title']
body = result['body']
file.write(f"{url}, {title}, {body[:20]}\n")

logger.info(f"検索スクレイピング完了 .... {self.max_results}件取得")

def filter_site(self, markdown_content):
if(self.classification_prompt is None):
Expand All @@ -61,7 +94,7 @@ def filter_site(self, markdown_content):
{"role": "user", "content": f"{self.classification_prompt}\n\n{markdown_content}"}
]
response = completion(
model="gemini/gemini-1.5-pro-latest",
model=self.model,
messages=messages
)
content = response.get('choices', [{}])[0].get('message', {}).get('content')
Expand All @@ -77,17 +110,17 @@ def filter_site(self, markdown_content):
logger.warning(f"フィルタリングでエラーが発生しました。リトライします。({retry_count}/{self.max_retries}\nError: {e}")

if "429" in str(e):
sleep_time = self.rate_limit_sleep # レート制限エラー時のスリープ時間をself.rate_limit_sleepから取得
sleep_time = self.rate_limit_sleep
else:
sleep_time = self.other_error_sleep # その他のエラー時のスリープ時間をself.other_error_sleepから取得
sleep_time = self.other_error_sleep

for _ in tqdm(range(sleep_time), desc="Sleeping", unit="s"):
time.sleep(1)

logger.error(f"フィルタリングに失敗しました。リトライ回数の上限に達しました。({self.max_retries}回)")
return True

def download_and_convert(self, url, depth=0):
def download_and_convert(self, url, title, depth=0):
if url in self.visited_urls:
return
self.visited_urls.add(url)
Expand All @@ -106,6 +139,11 @@ def download_and_convert(self, url, depth=0):
markdown_content = markdownify.markdownify(str(soup))
markdown_content = re.sub(r'\n{5,}', '\n\n\n\n', markdown_content)

# 文字化けチェック
if not self.is_valid_text(markdown_content):
logger.warning(f"文字化けを検出したため除外: {url}")
return

if not self.filter_site(markdown_content):
parsed_url = urlparse(url)
domain = parsed_url.netloc
Expand All @@ -130,7 +168,7 @@ def download_and_convert(self, url, depth=0):
with open(output_file, 'w', encoding='utf-8') as file:
file.write(markdown_content)

logger.info(f"[{depth}]変換成功: {url} ---> {output_file} [{len(markdown_content)/1000}kb]")
logger.info(f"[Depth:{depth}]変換成功: {url} ---> {output_file} [{len(markdown_content)/1000}kb]")

if domain not in self.domain_summaries:
self.domain_summaries[domain] = []
Expand All @@ -148,22 +186,46 @@ def download_and_convert(self, url, depth=0):
if any(keyword in absolute_url for keyword in self.exclude_keywords):
continue
absolute_url = absolute_url.split('#')[0]
self.download_and_convert(absolute_url, depth + 1)
self.download_and_convert(absolute_url, title, depth + 1)

except requests.exceptions.RequestException as e:
logger.error(f"ダウンロードエラー: {url}: {e}")
except IOError as e:
logger.error(f"書き込みエラー: {output_file}: {e}")

def is_valid_text(self, text):
# ASCII範囲外の文字の割合を計算
non_ascii_chars = re.findall(r'[^\x00-\x7F]', text)
non_ascii_ratio = len(non_ascii_chars) / len(text)

# 割合が一定以上であれば文字化けとみなす
if non_ascii_ratio > 0.3:
return False
else:
return True

def create_domain_summaries(self):
for domain, summaries in self.domain_summaries.items():
summary_file = os.path.join(self.output_dir, f"{domain}_summary{self.output_extension}")
with open(summary_file, 'w', encoding='utf-8') as file:
file.write('\n\n'.join(summaries))
logger.info(f"サマリーファイル作成: {summary_file}")

def run(self, base_url):
logger.info(f"スクレイピング開始: base_url={base_url}")
self.download_and_convert(base_url)
def recursive_scraping(self):
tprint(">> Recursive Scraping ")
logger.info("再帰スクレイピング開始")
if self.base_url:
logger.info(f"base_url={self.base_url} から再帰スクレイピングを開始します")
self.download_and_convert(self.base_url, "")
else:
with open("urls.txt", "r", encoding="utf-8") as file:
for line in file:
parts = line.strip().split(",")
url = parts[0]
title = parts[1] if len(parts) > 1 else ""
logger.info(f"---------------------------------------")
logger.info(f"スクレイピング開始: url={url}")
if(title): logger.info(f"タイトル: {title})")
self.download_and_convert(url, title)
self.create_domain_summaries()
logger.info("スクレイピング完了")
logger.info("再帰スクレイピング完了")
33 changes: 19 additions & 14 deletions pegasus/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@

def main():
parser = argparse.ArgumentParser(description='Pegasus')
parser.add_argument('--base-url', help='スクレイピングを開始するベースURL')
parser.add_argument('--url-file', help='スクレイピングするURLが記載されたテキストファイル')
parser.add_argument('output_dir', help='Markdownファイルの出力ディレクトリ')
parser.add_argument('mode', choices=['search', 'recursive'], help='実行モード(検索スクレイピングまたは再帰スクレイピング)')
parser.add_argument('--output_dir', default='output', help='Markdownファイルの出力ディレクトリ')
parser.add_argument('--exclude-selectors', nargs='+', help='除外するCSSセレクター')
parser.add_argument('--include-domain', default='', help='URLマッチングに含めるドメイン')
parser.add_argument('--exclude-keywords', nargs='+', help='URLマッチングから除外するキーワード')
Expand All @@ -20,7 +19,11 @@ def main():
parser.add_argument('--model', default='gemini/gemini-1.5-pro-latest', help='LiteLLMのモデル名 (デフォルト: gemini/gemini-1.5-pro-latest)')
parser.add_argument('--rate-limit-sleep', type=int, default=60, help='レート制限エラー時のスリープ時間(秒) (デフォルト: 60)')
parser.add_argument('--other-error-sleep', type=int, default=10, help='その他のエラー時のスリープ時間(秒) (デフォルト: 10)')

parser.add_argument('--search-query', help='検索スクレイピングで使用するクエリ')
parser.add_argument('--max-results', type=int, default=3, help='検索スクレイピングの最大数')
parser.add_argument('--base-url', help='再帰スクレイピングを開始するベースURL')
parser.add_argument('--url-file', default="urls.txt", help='スクレイピングするURLが記載されたテキストファイル')

args = parser.parse_args()

pegasus = Pegasus(
Expand All @@ -33,18 +36,20 @@ def main():
max_depth=args.max_depth,
system_message=args.system_message,
classification_prompt=args.classification_prompt,
max_retries=args.max_retries
max_retries=args.max_retries,
model=args.model,
rate_limit_sleep=args.rate_limit_sleep,
other_error_sleep=args.other_error_sleep,
search_query=args.search_query,
max_results=args.max_results,
base_url=args.base_url,
url_file=args.url_file
)

if args.base_url:
pegasus.run(args.base_url)
elif args.url_file:
with open(args.url_file, 'r') as file:
urls = file.read().splitlines()
for url in urls:
pegasus.run(url)
else:
parser.error("--base-url または --url-file のいずれかを指定してください。")
if args.mode == 'search':
pegasus.search_scraping()
elif args.mode == 'recursive':
pegasus.recursive_scraping()

if __name__ == '__main__':
main()
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
'litellm',
'python-dotenv',
'google-generativeai',
'duckduckgo-search',
],
entry_points={
'console_scripts': [
Expand Down

0 comments on commit d8faeca

Please sign in to comment.