Spaces:
Running
Running
syurein
commited on
Commit
·
31d2be8
1
Parent(s):
7f525ef
Fix-gitignore-png
Browse files- .gitignore +2 -0
- __pycache__/search.cpython-312.pyc +0 -0
- app.py +1 -1
- search.py +76 -30
.gitignore
CHANGED
@@ -10,6 +10,8 @@ llm_aidentify/
|
|
10 |
# Saved images
|
11 |
saved_images/
|
12 |
output_*.jpg
|
|
|
|
|
13 |
|
14 |
# Jupyter Notebook checkpoints
|
15 |
.ipynb_checkpoints/
|
|
|
10 |
# Saved images
|
11 |
saved_images/
|
12 |
output_*.jpg
|
13 |
+
*.png
|
14 |
+
duckduckgo_search_results.png
|
15 |
|
16 |
# Jupyter Notebook checkpoints
|
17 |
.ipynb_checkpoints/
|
__pycache__/search.cpython-312.pyc
CHANGED
Binary files a/__pycache__/search.cpython-312.pyc and b/__pycache__/search.cpython-312.pyc differ
|
|
app.py
CHANGED
@@ -298,7 +298,7 @@ async def llm_to_process_image_simple_auto(risk_level, image_path, point1, point
|
|
298 |
debug_image_path = os.path.join("./saved_images", debug_image_name)
|
299 |
|
300 |
# 個人情報流出に関する事例を検索し、クリーンなコンテンツを取得
|
301 |
-
scraper = WebScraper(headless=
|
302 |
personal_breach_docs = await scraper.get_processed_documents(
|
303 |
search_query="個人情報流出 事例 SNS",
|
304 |
num_search_results=10
|
|
|
298 |
debug_image_path = os.path.join("./saved_images", debug_image_name)
|
299 |
|
300 |
# 個人情報流出に関する事例を検索し、クリーンなコンテンツを取得
|
301 |
+
scraper = WebScraper(headless=True)
|
302 |
personal_breach_docs = await scraper.get_processed_documents(
|
303 |
search_query="個人情報流出 事例 SNS",
|
304 |
num_search_results=10
|
search.py
CHANGED
@@ -4,11 +4,26 @@ from bs4 import BeautifulSoup
|
|
4 |
from bs4.element import Comment
|
5 |
from urllib.parse import urlparse, parse_qs
|
6 |
from typing import List, Dict, Optional
|
|
|
7 |
|
8 |
class WebScraper:
|
9 |
"""
|
10 |
DuckDuckGoでの検索、URLからのコンテンツ取得、HTMLクリーンアップを行うクラス。
|
11 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
def __init__(self, headless: bool = True, default_timeout: int = 30000):
|
13 |
"""
|
14 |
WebScraperのインスタンスを初期化します。
|
@@ -29,7 +44,17 @@ class WebScraper:
|
|
29 |
if not self._browser or not self._browser.is_connected():
|
30 |
if self._playwright_instance is None:
|
31 |
self._playwright_instance = await async_playwright().start()
|
32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
return self._browser
|
34 |
|
35 |
async def _close_browser(self):
|
@@ -46,6 +71,22 @@ class WebScraper:
|
|
46 |
browser = await self._launch_browser() # ブラウザが起動または取得される
|
47 |
page = await browser.new_page()
|
48 |
page.set_default_timeout(self.default_timeout)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
49 |
return page
|
50 |
|
51 |
async def search_duckduckgo(self, query: str, num_results: int = 3) -> List[Dict[str, str]]:
|
@@ -57,38 +98,38 @@ class WebScraper:
|
|
57 |
|
58 |
try:
|
59 |
page = await self._get_new_page()
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
await page.
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
# DuckDuckGoの検索URLは一般的に `?q=` パラメータを使用します
|
69 |
-
await page.goto(f"https://duckduckgo.com/?q={query}")
|
70 |
-
|
71 |
# 検索結果のタイトルリンク要素を特定するセレクタ
|
72 |
-
# DuckDuckGoのHTML
|
73 |
-
# 現在の一般的なセレクタは 'a[data-testid="result-title-link"]'
|
74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
|
76 |
-
# 検索結果のタイトルリンク要素を取得 (await は不要、Locatorオブジェクトを返す)
|
77 |
search_links = page.locator('h2 > a')
|
78 |
|
79 |
-
# 取得する結果の数を制限
|
80 |
for i in range(min(num_results, await search_links.count())):
|
81 |
link_element = search_links.nth(i)
|
82 |
|
83 |
-
# タイトルはリンク要素のテキストコンテンツ
|
84 |
title = await link_element.text_content()
|
85 |
-
# URLはリンク要素のhref属性
|
86 |
url = await link_element.get_attribute("href")
|
87 |
|
88 |
-
# DuckDuckGoのリダイレクトURLのデコードとクリーンアップ
|
89 |
if url:
|
90 |
parsed_url = urlparse(url)
|
91 |
-
# DuckDuckGoのリダイレクトURL
|
92 |
if parsed_url.netloc == 'duckduckgo.com' and parsed_url.path == '/l/':
|
93 |
decoded_url = parse_qs(parsed_url.query).get('uddg', [''])[0]
|
94 |
url = decoded_url
|
@@ -96,6 +137,11 @@ class WebScraper:
|
|
96 |
# 結果を追加する前に、タイトルとURLが有効か軽くチェック
|
97 |
if title and url and title.strip() != "" and url.strip() != "":
|
98 |
results.append({"title": title.strip(), "url": url.strip()})
|
|
|
|
|
|
|
|
|
|
|
99 |
|
100 |
except Exception as e:
|
101 |
print(f"DuckDuckGo検索中にエラーが発生しました: {e}")
|
@@ -111,12 +157,12 @@ class WebScraper:
|
|
111 |
page: Optional[Page] = None
|
112 |
try:
|
113 |
page = await self._get_new_page()
|
114 |
-
print(f"
|
115 |
-
#
|
116 |
-
await page.goto(url
|
117 |
return await page.content()
|
118 |
except Exception as e:
|
119 |
-
print(f"
|
120 |
return None
|
121 |
finally:
|
122 |
if page:
|
@@ -157,7 +203,7 @@ class WebScraper:
|
|
157 |
|
158 |
Returns:
|
159 |
List[Dict[str, str]]: 処理されたドキュメントのリスト。
|
160 |
-
|
161 |
"""
|
162 |
processed_documents = []
|
163 |
|
@@ -181,10 +227,10 @@ class WebScraper:
|
|
181 |
"original_url": result['url'],
|
182 |
"cleaned_html_content": cleaned_content
|
183 |
})
|
184 |
-
print(f"
|
185 |
-
print(f"
|
186 |
else:
|
187 |
-
print("
|
188 |
else:
|
189 |
print("検索結果が見つからなかったため、処理をスキップします。")
|
190 |
finally:
|
@@ -195,7 +241,7 @@ class WebScraper:
|
|
195 |
|
196 |
# クラスの使用例
|
197 |
async def main():
|
198 |
-
scraper = WebScraper(headless=False) #
|
199 |
query = "個人情報流出 事例"
|
200 |
documents = await scraper.get_processed_documents(query, num_search_results=2)
|
201 |
|
|
|
4 |
from bs4.element import Comment
|
5 |
from urllib.parse import urlparse, parse_qs
|
6 |
from typing import List, Dict, Optional
|
7 |
+
import random # randomモジュールを追加
|
8 |
|
9 |
class WebScraper:
|
10 |
"""
|
11 |
DuckDuckGoでの検索、URLからのコンテンツ取得、HTMLクリーンアップを行うクラス。
|
12 |
"""
|
13 |
+
# User-Agentのリストをクラス変数として定義
|
14 |
+
USER_AGENTS = [
|
15 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
|
16 |
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
|
17 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36",
|
18 |
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36",
|
19 |
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
|
20 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/108.0",
|
21 |
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/108.0",
|
22 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", # 最新版に近いChrome
|
23 |
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Safari/605.1.15", # Safari
|
24 |
+
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 OPR/106.0.0.0", # Opera
|
25 |
+
]
|
26 |
+
|
27 |
def __init__(self, headless: bool = True, default_timeout: int = 30000):
|
28 |
"""
|
29 |
WebScraperのインスタンスを初期化します。
|
|
|
44 |
if not self._browser or not self._browser.is_connected():
|
45 |
if self._playwright_instance is None:
|
46 |
self._playwright_instance = await async_playwright().start()
|
47 |
+
# ヘッドレスモードでの検出を避けるための引数を追加
|
48 |
+
self._browser = await self._playwright_instance.chromium.launch(
|
49 |
+
headless=self.headless,
|
50 |
+
args=[
|
51 |
+
'--no-sandbox',
|
52 |
+
'--disable-setuid-sandbox',
|
53 |
+
'--disable-infobars',
|
54 |
+
'--window-size=1280,720', # 一般的なデスクトップサイズに設定
|
55 |
+
'--disable-blink-features=AutomationControlled' # ヘッドレス検出を回避
|
56 |
+
]
|
57 |
+
)
|
58 |
return self._browser
|
59 |
|
60 |
async def _close_browser(self):
|
|
|
71 |
browser = await self._launch_browser() # ブラウザが起動または取得される
|
72 |
page = await browser.new_page()
|
73 |
page.set_default_timeout(self.default_timeout)
|
74 |
+
|
75 |
+
# User-Agentをランダムに選択して設定
|
76 |
+
await page.set_extra_http_headers({
|
77 |
+
"User-Agent": random.choice(self.USER_AGENTS)
|
78 |
+
})
|
79 |
+
|
80 |
+
# より包括的なステルス対策をページに適用
|
81 |
+
await page.evaluate("""Object.defineProperty(navigator, 'webdriver', { get: () => false });""")
|
82 |
+
await page.evaluate("""Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });""")
|
83 |
+
await page.evaluate("""Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });""")
|
84 |
+
await page.evaluate("""window.chrome = { runtime: {}, loadTimes: function() {}, csi: function() {}, app: {} };""")
|
85 |
+
await page.evaluate("""Object.defineProperty(navigator.permissions, 'query', { enumerable: true, configurable: true, writable: true, value: async (parameters) => ({ state: 'prompt' }) });""")
|
86 |
+
|
87 |
+
# ページロード後の追加待機
|
88 |
+
await asyncio.sleep(random.uniform(2, 5))
|
89 |
+
|
90 |
return page
|
91 |
|
92 |
async def search_duckduckgo(self, query: str, num_results: int = 3) -> List[Dict[str, str]]:
|
|
|
98 |
|
99 |
try:
|
100 |
page = await self._get_new_page()
|
101 |
+
|
102 |
+
print(f"Bingで '{query}' を検索中...")
|
103 |
+
# networkidle でより安定したページロードを待機
|
104 |
+
await page.goto(f"https://www.bing.com/search?q=={query}&setlang=ja", wait_until='networkidle')
|
105 |
+
|
106 |
+
# デバッグのためにページのスクリーンショットを保存
|
107 |
+
await page.screenshot(path="./duckduckgo_search_results.png")
|
108 |
+
|
|
|
|
|
|
|
109 |
# 検索結果のタイトルリンク要素を特定するセレクタ
|
110 |
+
# DuckDuckGoのHTML構造は変更される可能性があるため、適宜調整が必要です。
|
111 |
+
# 現在の一般的なセレクタは 'a[data-testid="result-title-link"]' もしくは 'h2 > a' ですが、
|
112 |
+
# ページ構造が変わった場合は、開発者ツールで適切なセレクタを見つけてください。
|
113 |
+
|
114 |
+
# 要素が見つかるまで、より長く待機するか、別のセレクタを試す
|
115 |
+
try:
|
116 |
+
await page.wait_for_selector('h2 > a', timeout=20000) # タイムアウトを長くする
|
117 |
+
except Exception as e:
|
118 |
+
print(f"セレクタ 'h2 > a' の待機中にタイムアウトしました: {e}")
|
119 |
+
# ここで代替のセレクタを試すか、処理を終了する
|
120 |
+
return []
|
121 |
|
|
|
122 |
search_links = page.locator('h2 > a')
|
123 |
|
|
|
124 |
for i in range(min(num_results, await search_links.count())):
|
125 |
link_element = search_links.nth(i)
|
126 |
|
|
|
127 |
title = await link_element.text_content()
|
|
|
128 |
url = await link_element.get_attribute("href")
|
129 |
|
|
|
130 |
if url:
|
131 |
parsed_url = urlparse(url)
|
132 |
+
# DuckDuckGoのリダイレクトURLのデコードとクリーンアップ
|
133 |
if parsed_url.netloc == 'duckduckgo.com' and parsed_url.path == '/l/':
|
134 |
decoded_url = parse_qs(parsed_url.query).get('uddg', [''])[0]
|
135 |
url = decoded_url
|
|
|
137 |
# 結果を追加する前に、タイトルとURLが有効か軽くチェック
|
138 |
if title and url and title.strip() != "" and url.strip() != "":
|
139 |
results.append({"title": title.strip(), "url": url.strip()})
|
140 |
+
|
141 |
+
# 検索結果が一つも見つからなかった場合もスクリーンショットを保存
|
142 |
+
if not results:
|
143 |
+
print(f"検索結果が見つかりませんでした。ページのスクリーンショットを './duckduckgo_no_results.png' に保存します。")
|
144 |
+
await page.screenshot(path="./duckduckgo_no_results.png")
|
145 |
|
146 |
except Exception as e:
|
147 |
print(f"DuckDuckGo検索中にエラーが発生しました: {e}")
|
|
|
157 |
page: Optional[Page] = None
|
158 |
try:
|
159 |
page = await self._get_new_page()
|
160 |
+
print(f" URL: {url} のコンテンツを取得中...")
|
161 |
+
# networkidle でより安定したページロードを待機
|
162 |
+
await page.goto(url)
|
163 |
return await page.content()
|
164 |
except Exception as e:
|
165 |
+
print(f" URL: {url} のコンテンツ取得中にエラーが発生しました: {e}")
|
166 |
return None
|
167 |
finally:
|
168 |
if page:
|
|
|
203 |
|
204 |
Returns:
|
205 |
List[Dict[str, str]]: 処理されたドキュメントのリスト。
|
206 |
+
各ドキュメン��は 'title', 'original_url', 'cleaned_html_content' を含む。
|
207 |
"""
|
208 |
processed_documents = []
|
209 |
|
|
|
227 |
"original_url": result['url'],
|
228 |
"cleaned_html_content": cleaned_content
|
229 |
})
|
230 |
+
print(f" クリーンなコンテンツの長さ: {len(cleaned_content)} 文字")
|
231 |
+
print(f" クリーンなコンテンツ(一部):\n{cleaned_content[:500]}...")
|
232 |
else:
|
233 |
+
print(" クリーンなコンテンツを取得できませんでした。")
|
234 |
else:
|
235 |
print("検索結果が見つからなかったため、処理をスキップします。")
|
236 |
finally:
|
|
|
241 |
|
242 |
# クラスの使用例
|
243 |
async def main():
|
244 |
+
scraper = WebScraper(headless=False) # まずはheadless=Trueで試してください
|
245 |
query = "個人情報流出 事例"
|
246 |
documents = await scraper.get_processed_documents(query, num_search_results=2)
|
247 |
|