@danhachuel/thunderbolt 0.6.50 → 0.6.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/integrations/instagram_public.py +67 -24
- package/package.json +1 -1
|
@@ -238,12 +238,55 @@ def _extract_profile_user(payload: Any) -> Mapping[str, Any] | None:
|
|
|
238
238
|
return next((candidate for candidate in candidates if isinstance(candidate, Mapping)), None)
|
|
239
239
|
|
|
240
240
|
|
|
241
|
-
def
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
241
|
+
def _shared_data_documents(document: str) -> list[Any]:
|
|
242
|
+
"""Extract JSON assigned to window._sharedData from the rendered profile HTML."""
|
|
243
|
+
decoder = json.JSONDecoder()
|
|
244
|
+
documents: list[Any] = []
|
|
245
|
+
patterns = (
|
|
246
|
+
r'window\s*\.\s*_sharedData\s*=\s*',
|
|
247
|
+
r'window\s*\[\s*["\']_sharedData["\']\s*\]\s*=\s*',
|
|
246
248
|
)
|
|
249
|
+
for pattern in patterns:
|
|
250
|
+
for match in re.finditer(pattern, document, flags=re.IGNORECASE):
|
|
251
|
+
start = document.find('{', match.end())
|
|
252
|
+
if start < 0:
|
|
253
|
+
continue
|
|
254
|
+
try:
|
|
255
|
+
parsed, _ = decoder.raw_decode(document[start:])
|
|
256
|
+
except json.JSONDecodeError:
|
|
257
|
+
continue
|
|
258
|
+
documents.append(parsed)
|
|
259
|
+
return documents
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _profile_users_from_document(document: str) -> list[Mapping[str, Any]]:
|
|
263
|
+
"""Return users found in legacy _sharedData and current embedded profile payloads."""
|
|
264
|
+
users: list[Mapping[str, Any]] = []
|
|
265
|
+
documents = _shared_data_documents(document) + _embedded_json_documents(document)
|
|
266
|
+
for payload in documents:
|
|
267
|
+
direct = _extract_profile_user(payload)
|
|
268
|
+
if isinstance(direct, Mapping):
|
|
269
|
+
users.append(direct)
|
|
270
|
+
for node in _walk_json(payload):
|
|
271
|
+
if not isinstance(node, Mapping):
|
|
272
|
+
continue
|
|
273
|
+
candidate = node.get('user')
|
|
274
|
+
if isinstance(candidate, Mapping) and any(key in candidate for key in ('biography', 'edge_follow', 'edge_followed_by', 'username')):
|
|
275
|
+
users.append(candidate)
|
|
276
|
+
unique: list[Mapping[str, Any]] = []
|
|
277
|
+
seen: set[int] = set()
|
|
278
|
+
for user in users:
|
|
279
|
+
marker = id(user)
|
|
280
|
+
if marker not in seen:
|
|
281
|
+
seen.add(marker)
|
|
282
|
+
unique.append(user)
|
|
283
|
+
return unique
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _fetch_profile_with_playwright(username: str, headers: Mapping[str, str] | None = None, cookies: Mapping[str, str] | None = None) -> dict[str, Any] | None:
|
|
287
|
+
"""Fetch the public profile page and read its embedded structured profile data."""
|
|
288
|
+
headers = headers or _instagram_headers()
|
|
289
|
+
cookies = cookies or _instagram_cookies()
|
|
247
290
|
best_user: dict[str, Any] | None = None
|
|
248
291
|
try:
|
|
249
292
|
from playwright.sync_api import sync_playwright
|
|
@@ -256,20 +299,15 @@ def _fetch_profile_with_playwright(username: str, headers: Mapping[str, str], co
|
|
|
256
299
|
)
|
|
257
300
|
if cookies:
|
|
258
301
|
page.context.add_cookies([{'name': name, 'value': value, 'domain': '.instagram.com', 'path': '/'} for name, value in cookies.items()])
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
best_user = _merge_profile(best_user, user)
|
|
269
|
-
if _profile_quality(best_user) >= 4:
|
|
270
|
-
return best_user
|
|
271
|
-
except (ValueError, TimeoutError) as exc:
|
|
272
|
-
LOGGER.warning('Falha ao interpretar resposta Playwright do perfil URL=%s: %s', endpoint, exc)
|
|
302
|
+
url = f'https://www.instagram.com/{username}/'
|
|
303
|
+
response = page.goto(url, wait_until='domcontentloaded', timeout=30000)
|
|
304
|
+
LOGGER.debug('Instagram Playwright profile URL=%s status=%s', url, response.status if response else None)
|
|
305
|
+
if response is None or response.status >= 400:
|
|
306
|
+
return None
|
|
307
|
+
document = page.content()
|
|
308
|
+
for user in _profile_users_from_document(document):
|
|
309
|
+
best_user = _merge_profile(best_user, user)
|
|
310
|
+
LOGGER.debug('Instagram Playwright _sharedData profile user=%s quality=%s', bool(best_user), _profile_quality(best_user or {}))
|
|
273
311
|
finally:
|
|
274
312
|
browser.close()
|
|
275
313
|
except Exception as exc:
|
|
@@ -283,13 +321,14 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
283
321
|
cookies = _instagram_cookies()
|
|
284
322
|
headers = _instagram_headers()
|
|
285
323
|
headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36'
|
|
324
|
+
if platform.system() == 'Windows':
|
|
325
|
+
LOGGER.debug('Windows detectado: perfil Instagram exclusivamente via Playwright.')
|
|
326
|
+
return _fetch_profile_with_playwright(username, headers, cookies)
|
|
327
|
+
|
|
286
328
|
endpoints = (
|
|
287
329
|
f'https://www.instagram.com/api/v1/users/web_profile_info/?username={username}',
|
|
288
330
|
f'https://i.instagram.com/api/v1/users/web_profile_info/?username={username}',
|
|
289
331
|
)
|
|
290
|
-
if platform.system() == 'Windows':
|
|
291
|
-
LOGGER.debug('Windows detectado: perfil Instagram exclusivamente via Playwright.')
|
|
292
|
-
return _fetch_profile_with_playwright(username, headers, cookies)
|
|
293
332
|
|
|
294
333
|
request_headers = [
|
|
295
334
|
headers,
|
|
@@ -516,6 +555,8 @@ def fetch_public_instagram_profile(source: str) -> IntegrationResult:
|
|
|
516
555
|
api_user = _fetch_web_profile_user(reference['username'])
|
|
517
556
|
if api_user:
|
|
518
557
|
return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', _profile_data_from_api(api_user, reference))
|
|
558
|
+
if platform.system() == 'Windows':
|
|
559
|
+
return IntegrationResult(False, 'Não foi possível extrair os dados públicos do Instagram com o Playwright.', reference)
|
|
519
560
|
try:
|
|
520
561
|
response = requests.get(reference['url'], headers={'User-Agent': 'Mozilla/5.0', 'Accept-Language': 'pt-BR,pt;q=0.9,en;q=0.8'}, timeout=12)
|
|
521
562
|
except requests.RequestException as exc:
|
|
@@ -609,10 +650,11 @@ def _post_from_node(node: dict[str, Any]) -> dict[str, Any] | None:
|
|
|
609
650
|
}
|
|
610
651
|
|
|
611
652
|
|
|
612
|
-
def _fetch_posts_with_playwright(reference: Mapping[str, str], limit: int, cookies: Mapping[str, str]) -> list[dict[str, Any]]:
|
|
653
|
+
def _fetch_posts_with_playwright(reference: Mapping[str, str], limit: int, cookies: Mapping[str, str] | None = None) -> list[dict[str, Any]]:
|
|
613
654
|
# CORREÇÃO WINDOWS: posts são extraídos exclusivamente do JSON/HTML obtido pelo Playwright.
|
|
614
655
|
posts: list[dict[str, Any]] = []
|
|
615
656
|
seen: set[str] = set()
|
|
657
|
+
cookies = cookies or _instagram_cookies()
|
|
616
658
|
try:
|
|
617
659
|
from playwright.sync_api import sync_playwright
|
|
618
660
|
with sync_playwright() as playwright:
|
|
@@ -630,7 +672,8 @@ def _fetch_posts_with_playwright(reference: Mapping[str, str], limit: int, cooki
|
|
|
630
672
|
return []
|
|
631
673
|
browser_document = page.content()
|
|
632
674
|
LOGGER.debug('Instagram Playwright posts document_bytes=%s', len(browser_document))
|
|
633
|
-
|
|
675
|
+
documents = _shared_data_documents(browser_document) + _embedded_json_documents(browser_document)
|
|
676
|
+
for document in documents:
|
|
634
677
|
for node in _walk_json(document):
|
|
635
678
|
post = _post_from_node(node)
|
|
636
679
|
if not post or post['id'] in seen:
|
package/package.json
CHANGED