@danhachuel/thunderbolt 0.6.50 → 0.6.52
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 +112 -39
- package/package.json +1 -1
|
@@ -238,15 +238,58 @@ 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
|
)
|
|
247
|
-
|
|
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 structured profile JSON requested by the public Instagram page."""
|
|
288
|
+
headers = headers or _instagram_headers()
|
|
289
|
+
cookies = cookies or _instagram_cookies()
|
|
290
|
+
captured_user: dict[str, Any] | None = None
|
|
248
291
|
try:
|
|
249
|
-
from playwright.sync_api import sync_playwright
|
|
292
|
+
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError, sync_playwright
|
|
250
293
|
with sync_playwright() as playwright:
|
|
251
294
|
browser = playwright.chromium.launch(headless=True)
|
|
252
295
|
try:
|
|
@@ -256,26 +299,37 @@ 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
|
-
|
|
302
|
+
|
|
303
|
+
def capture_profile_response(response: Any) -> None:
|
|
304
|
+
nonlocal captured_user
|
|
305
|
+
if '/api/v1/users/web_profile_info/' not in response.url:
|
|
306
|
+
return
|
|
260
307
|
try:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
308
|
+
payload = response.json()
|
|
309
|
+
user = ((payload.get('data') or {}).get('user') if isinstance(payload, Mapping) else None)
|
|
310
|
+
if isinstance(user, Mapping):
|
|
311
|
+
captured_user = _merge_profile(captured_user, user)
|
|
312
|
+
LOGGER.debug('Instagram Playwright API profile captured user=%s', username)
|
|
313
|
+
except (ValueError, TypeError, AttributeError) as exc:
|
|
314
|
+
LOGGER.debug('Não foi possível interpretar resposta web_profile_info: %s', exc)
|
|
315
|
+
|
|
316
|
+
page.on('response', capture_profile_response)
|
|
317
|
+
url = f'https://www.instagram.com/{username}/'
|
|
318
|
+
response = page.goto(url, wait_until='domcontentloaded', timeout=30000)
|
|
319
|
+
LOGGER.debug('Instagram Playwright profile URL=%s status=%s', url, response.status if response else None)
|
|
320
|
+
if response is None or response.status >= 400:
|
|
321
|
+
return None
|
|
322
|
+
try:
|
|
323
|
+
page.wait_for_load_state('networkidle', timeout=10000)
|
|
324
|
+
except (PlaywrightTimeoutError, TimeoutError):
|
|
325
|
+
LOGGER.debug('Instagram Playwright networkidle excedido; usando resposta API já capturada.')
|
|
326
|
+
LOGGER.debug('Instagram Playwright API profile user=%s quality=%s', bool(captured_user), _profile_quality(captured_user or {}))
|
|
273
327
|
finally:
|
|
274
328
|
browser.close()
|
|
275
329
|
except Exception as exc:
|
|
276
330
|
LOGGER.warning('Falha no Playwright do perfil Instagram: %s', exc, exc_info=True)
|
|
277
331
|
_streamlit_message('error', f'Playwright não conseguiu consultar o perfil Instagram: {exc}', 'profile_playwright_error')
|
|
278
|
-
return
|
|
332
|
+
return captured_user
|
|
279
333
|
|
|
280
334
|
|
|
281
335
|
def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
@@ -283,13 +337,14 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
283
337
|
cookies = _instagram_cookies()
|
|
284
338
|
headers = _instagram_headers()
|
|
285
339
|
headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36'
|
|
340
|
+
if platform.system() == 'Windows':
|
|
341
|
+
LOGGER.debug('Windows detectado: perfil Instagram exclusivamente via Playwright.')
|
|
342
|
+
return _fetch_profile_with_playwright(username, headers, cookies)
|
|
343
|
+
|
|
286
344
|
endpoints = (
|
|
287
345
|
f'https://www.instagram.com/api/v1/users/web_profile_info/?username={username}',
|
|
288
346
|
f'https://i.instagram.com/api/v1/users/web_profile_info/?username={username}',
|
|
289
347
|
)
|
|
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
348
|
|
|
294
349
|
request_headers = [
|
|
295
350
|
headers,
|
|
@@ -516,6 +571,8 @@ def fetch_public_instagram_profile(source: str) -> IntegrationResult:
|
|
|
516
571
|
api_user = _fetch_web_profile_user(reference['username'])
|
|
517
572
|
if api_user:
|
|
518
573
|
return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', _profile_data_from_api(api_user, reference))
|
|
574
|
+
if platform.system() == 'Windows':
|
|
575
|
+
return IntegrationResult(False, 'Não foi possível extrair os dados públicos do Instagram com o Playwright.', reference)
|
|
519
576
|
try:
|
|
520
577
|
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
578
|
except requests.RequestException as exc:
|
|
@@ -609,12 +666,13 @@ def _post_from_node(node: dict[str, Any]) -> dict[str, Any] | None:
|
|
|
609
666
|
}
|
|
610
667
|
|
|
611
668
|
|
|
612
|
-
def _fetch_posts_with_playwright(reference: Mapping[str, str], limit: int, cookies: Mapping[str, str]) -> list[dict[str, Any]]:
|
|
613
|
-
# CORREÇÃO WINDOWS: posts são extraídos exclusivamente
|
|
669
|
+
def _fetch_posts_with_playwright(reference: Mapping[str, str], limit: int, cookies: Mapping[str, str] | None = None) -> list[dict[str, Any]]:
|
|
670
|
+
# CORREÇÃO WINDOWS: posts são extraídos exclusivamente da resposta API capturada pelo Playwright.
|
|
614
671
|
posts: list[dict[str, Any]] = []
|
|
615
672
|
seen: set[str] = set()
|
|
673
|
+
cookies = cookies or _instagram_cookies()
|
|
616
674
|
try:
|
|
617
|
-
from playwright.sync_api import sync_playwright
|
|
675
|
+
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError, sync_playwright
|
|
618
676
|
with sync_playwright() as playwright:
|
|
619
677
|
browser = playwright.chromium.launch(headless=True)
|
|
620
678
|
try:
|
|
@@ -624,22 +682,37 @@ def _fetch_posts_with_playwright(reference: Mapping[str, str], limit: int, cooki
|
|
|
624
682
|
)
|
|
625
683
|
if cookies:
|
|
626
684
|
page.context.add_cookies([{'name': name, 'value': value, 'domain': '.instagram.com', 'path': '/'} for name, value in cookies.items()])
|
|
685
|
+
|
|
686
|
+
def capture_profile_posts(response: Any) -> None:
|
|
687
|
+
if '/api/v1/users/web_profile_info/' not in response.url:
|
|
688
|
+
return
|
|
689
|
+
try:
|
|
690
|
+
payload = response.json()
|
|
691
|
+
user = ((payload.get('data') or {}).get('user') if isinstance(payload, Mapping) else None)
|
|
692
|
+
media = user.get('edge_owner_to_timeline_media') if isinstance(user, Mapping) else None
|
|
693
|
+
edges = media.get('edges') if isinstance(media, Mapping) else []
|
|
694
|
+
for edge in edges or []:
|
|
695
|
+
node = edge.get('node') if isinstance(edge, Mapping) else None
|
|
696
|
+
if not isinstance(node, dict):
|
|
697
|
+
continue
|
|
698
|
+
post = _post_from_node(node)
|
|
699
|
+
if post and post['id'] not in seen:
|
|
700
|
+
seen.add(post['id'])
|
|
701
|
+
posts.append(post)
|
|
702
|
+
except (ValueError, TypeError, AttributeError) as exc:
|
|
703
|
+
LOGGER.debug('Não foi possível interpretar posts web_profile_info: %s', exc)
|
|
704
|
+
|
|
705
|
+
page.on('response', capture_profile_posts)
|
|
627
706
|
response = page.goto(reference['url'], wait_until='domcontentloaded', timeout=30000)
|
|
628
707
|
LOGGER.debug('Instagram Playwright posts URL=%s status=%s', reference['url'], response.status if response else None)
|
|
629
708
|
if response is None or response.status >= 400:
|
|
630
709
|
return []
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
continue
|
|
638
|
-
seen.add(post['id'])
|
|
639
|
-
posts.append(post)
|
|
640
|
-
LOGGER.debug('Instagram Playwright post extracted id=%s', post['id'])
|
|
641
|
-
if len(posts) >= max(1, int(limit)):
|
|
642
|
-
return posts
|
|
710
|
+
try:
|
|
711
|
+
page.wait_for_load_state('networkidle', timeout=10000)
|
|
712
|
+
except (PlaywrightTimeoutError, TimeoutError):
|
|
713
|
+
LOGGER.debug('Instagram Playwright networkidle excedido; usando posts API já capturados.')
|
|
714
|
+
LOGGER.debug('Instagram Playwright API posts extracted=%s limit=%s', len(posts), limit)
|
|
715
|
+
return posts[:max(1, int(limit))]
|
|
643
716
|
finally:
|
|
644
717
|
browser.close()
|
|
645
718
|
except Exception as exc:
|
package/package.json
CHANGED