pytrends-modern 0.2.0__tar.gz → 0.2.2__tar.gz
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.
- {pytrends_modern-0.2.0/pytrends_modern.egg-info → pytrends_modern-0.2.2}/PKG-INFO +9 -4
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/README.md +8 -3
- pytrends_modern-0.2.2/examples/example_docker_usage.py +50 -0
- pytrends_modern-0.2.2/examples/test_async_integration.py +78 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pyproject.toml +1 -1
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/__init__.py +3 -1
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/browser_config_camoufox.py +5 -3
- pytrends_modern-0.2.2/pytrends_modern/request_async.py +342 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2/pytrends_modern.egg-info}/PKG-INFO +9 -4
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern.egg-info/SOURCES.txt +3 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/LICENSE +0 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/MANIFEST.in +0 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/examples/advanced_usage.py +0 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/examples/basic_usage.py +0 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/examples/example_browser_mode.py +0 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/browser_config.py +0 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/camoufox_setup.py +0 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/cli.py +0 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/config.py +0 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/exceptions.py +0 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/proxy_extension.py +0 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/py.typed +0 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/request.py +0 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/rss.py +0 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/scraper.py +0 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern/utils.py +0 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern.egg-info/dependency_links.txt +0 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern.egg-info/entry_points.txt +0 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern.egg-info/requires.txt +0 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern.egg-info/top_level.txt +0 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/setup.cfg +0 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/tests/conftest.py +0 -0
- {pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/tests/test_basic.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytrends-modern
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Modern Google Trends API - Combining the best of pytrends, with RSS feeds, Selenium scraping, DrissionPage browser automation, and enhanced features
|
|
5
5
|
Author: pytrends-modern contributors
|
|
6
6
|
License: MIT
|
|
@@ -175,11 +175,16 @@ RUN mkdir -p /root/.config && \
|
|
|
175
175
|
cd /root/.config && \
|
|
176
176
|
tar -xzf /tmp/profile.tar.gz
|
|
177
177
|
|
|
178
|
-
# 3. Use headless
|
|
179
|
-
config = BrowserConfig(headless=
|
|
178
|
+
# 3. Use headless="virtual" in container
|
|
179
|
+
config = BrowserConfig(headless="virtual") # Use Xvfb for Docker
|
|
180
180
|
```
|
|
181
181
|
|
|
182
|
-
|
|
182
|
+
**Headless Options:**
|
|
183
|
+
- `headless=False` - Show browser window (local development)
|
|
184
|
+
- `headless=True` - Standard headless (servers with display)
|
|
185
|
+
- `headless="virtual"` - Xvfb virtual display (Docker containers)
|
|
186
|
+
|
|
187
|
+
See `Dockerfile.example` and `examples/example_docker_usage.py` for complete Docker setup.
|
|
183
188
|
|
|
184
189
|
⚠️ **Security**: Profile contains Google session - keep secure, don't commit to git!
|
|
185
190
|
|
|
@@ -125,11 +125,16 @@ RUN mkdir -p /root/.config && \
|
|
|
125
125
|
cd /root/.config && \
|
|
126
126
|
tar -xzf /tmp/profile.tar.gz
|
|
127
127
|
|
|
128
|
-
# 3. Use headless
|
|
129
|
-
config = BrowserConfig(headless=
|
|
128
|
+
# 3. Use headless="virtual" in container
|
|
129
|
+
config = BrowserConfig(headless="virtual") # Use Xvfb for Docker
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
**Headless Options:**
|
|
133
|
+
- `headless=False` - Show browser window (local development)
|
|
134
|
+
- `headless=True` - Standard headless (servers with display)
|
|
135
|
+
- `headless="virtual"` - Xvfb virtual display (Docker containers)
|
|
136
|
+
|
|
137
|
+
See `Dockerfile.example` and `examples/example_docker_usage.py` for complete Docker setup.
|
|
133
138
|
|
|
134
139
|
⚠️ **Security**: Profile contains Google session - keep secure, don't commit to git!
|
|
135
140
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Example: Using pytrends-modern in Docker containers
|
|
4
|
+
|
|
5
|
+
When running in Docker, use headless="virtual" to enable Xvfb virtual display.
|
|
6
|
+
This prevents display errors in containerized environments.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from pytrends_modern import AsyncTrendReq, BrowserConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def main():
|
|
14
|
+
"""Example async usage in Docker container"""
|
|
15
|
+
|
|
16
|
+
# Configure for Docker environment
|
|
17
|
+
config = BrowserConfig(
|
|
18
|
+
headless="virtual", # Use Xvfb virtual display for Docker
|
|
19
|
+
humanize=True,
|
|
20
|
+
os='linux',
|
|
21
|
+
geoip=True
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# For local development, use:
|
|
25
|
+
# config = BrowserConfig(headless=False) # Show browser window
|
|
26
|
+
# config = BrowserConfig(headless=True) # Standard headless
|
|
27
|
+
|
|
28
|
+
print("🐳 Running pytrends-modern in Docker container...")
|
|
29
|
+
print(f"📁 Profile: {config.user_data_dir}")
|
|
30
|
+
|
|
31
|
+
async with AsyncTrendReq(browser_config=config) as pytrends:
|
|
32
|
+
pytrends.kw_list = ["Docker"]
|
|
33
|
+
|
|
34
|
+
# Get interest over time
|
|
35
|
+
print("\n📊 Fetching interest_over_time...")
|
|
36
|
+
df = await pytrends.interest_over_time()
|
|
37
|
+
print(f"✓ Got {len(df)} rows")
|
|
38
|
+
print(df.head())
|
|
39
|
+
|
|
40
|
+
# Get interest by region
|
|
41
|
+
print("\n🌍 Fetching interest_by_region...")
|
|
42
|
+
df_region = await pytrends.interest_by_region()
|
|
43
|
+
print(f"✓ Got {len(df_region)} rows")
|
|
44
|
+
print(df_region.head())
|
|
45
|
+
|
|
46
|
+
print("\n✅ Complete!")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Test AsyncTrendReq with Camoufox
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
from pytrends_modern import AsyncTrendReq, BrowserConfig
|
|
8
|
+
|
|
9
|
+
async def test_async_browser_mode():
|
|
10
|
+
"""Test async browser mode with all 4 APIs"""
|
|
11
|
+
print("=" * 70)
|
|
12
|
+
print("Testing AsyncTrendReq with Camoufox")
|
|
13
|
+
print("=" * 70)
|
|
14
|
+
|
|
15
|
+
# Configure browser
|
|
16
|
+
config = BrowserConfig(
|
|
17
|
+
headless=False,
|
|
18
|
+
humanize=True,
|
|
19
|
+
os='linux',
|
|
20
|
+
geoip=True
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
keyword = "Python"
|
|
24
|
+
print(f"\n🔍 Testing with keyword: {keyword}")
|
|
25
|
+
|
|
26
|
+
# Use async context manager
|
|
27
|
+
async with AsyncTrendReq(browser_config=config) as pytrends:
|
|
28
|
+
pytrends.kw_list = [keyword]
|
|
29
|
+
|
|
30
|
+
# Test interest_over_time
|
|
31
|
+
print("\n📊 Testing interest_over_time()...")
|
|
32
|
+
try:
|
|
33
|
+
df_iot = await pytrends.interest_over_time()
|
|
34
|
+
print(f"✓ interest_over_time: {len(df_iot)} rows")
|
|
35
|
+
print(df_iot.head())
|
|
36
|
+
except Exception as e:
|
|
37
|
+
print(f"✗ Error: {e}")
|
|
38
|
+
|
|
39
|
+
# Test interest_by_region
|
|
40
|
+
print("\n🌍 Testing interest_by_region()...")
|
|
41
|
+
try:
|
|
42
|
+
df_ibr = await pytrends.interest_by_region()
|
|
43
|
+
print(f"✓ interest_by_region: {len(df_ibr)} rows")
|
|
44
|
+
print(df_ibr.head())
|
|
45
|
+
except Exception as e:
|
|
46
|
+
print(f"✗ Error: {e}")
|
|
47
|
+
|
|
48
|
+
# Test related_topics
|
|
49
|
+
print("\n🔗 Testing related_topics()...")
|
|
50
|
+
try:
|
|
51
|
+
topics = await pytrends.related_topics()
|
|
52
|
+
if keyword in topics and topics[keyword].get('top') is not None:
|
|
53
|
+
print(f"✓ related_topics: {len(topics[keyword]['top'])} topics")
|
|
54
|
+
print(topics[keyword]['top'].head())
|
|
55
|
+
else:
|
|
56
|
+
print("✓ related_topics: No data (might be normal)")
|
|
57
|
+
except Exception as e:
|
|
58
|
+
print(f"✗ Error: {e}")
|
|
59
|
+
|
|
60
|
+
# Test related_queries
|
|
61
|
+
print("\n❓ Testing related_queries()...")
|
|
62
|
+
try:
|
|
63
|
+
queries = await pytrends.related_queries()
|
|
64
|
+
if keyword in queries and queries[keyword].get('top') is not None:
|
|
65
|
+
print(f"✓ related_queries: {len(queries[keyword]['top'])} queries")
|
|
66
|
+
print(queries[keyword]['top'].head())
|
|
67
|
+
else:
|
|
68
|
+
print("✓ related_queries: No data (might be normal)")
|
|
69
|
+
except Exception as e:
|
|
70
|
+
print(f"✗ Error: {e}")
|
|
71
|
+
|
|
72
|
+
print("\n" + "=" * 70)
|
|
73
|
+
print("✅ Async test complete!")
|
|
74
|
+
print("=" * 70)
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
# Run async test
|
|
78
|
+
asyncio.run(test_async_browser_mode())
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pytrends-modern"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Modern Google Trends API - Combining the best of pytrends, with RSS feeds, Selenium scraping, DrissionPage browser automation, and enhanced features"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
pytrends-modern: Modern Google Trends API
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
__version__ = "0.2.
|
|
5
|
+
__version__ = "0.2.2"
|
|
6
6
|
__author__ = "pytrends-modern contributors"
|
|
7
7
|
__license__ = "MIT"
|
|
8
8
|
|
|
9
9
|
from pytrends_modern.request import TrendReq
|
|
10
|
+
from pytrends_modern.request_async import AsyncTrendReq
|
|
10
11
|
from pytrends_modern.rss import TrendsRSS
|
|
11
12
|
from pytrends_modern.scraper import TrendsScraper
|
|
12
13
|
from pytrends_modern.browser_config_camoufox import BrowserConfig
|
|
@@ -21,6 +22,7 @@ from pytrends_modern.exceptions import (
|
|
|
21
22
|
|
|
22
23
|
__all__ = [
|
|
23
24
|
"TrendReq",
|
|
25
|
+
"AsyncTrendReq",
|
|
24
26
|
"TrendsRSS",
|
|
25
27
|
"TrendsScraper",
|
|
26
28
|
"BrowserConfig",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Browser configuration for Camoufox automation"""
|
|
2
2
|
|
|
3
|
-
from typing import Optional
|
|
3
|
+
from typing import Optional, Union
|
|
4
4
|
import os as os_module
|
|
5
5
|
|
|
6
6
|
|
|
@@ -18,7 +18,9 @@ class BrowserConfig:
|
|
|
18
18
|
|
|
19
19
|
Args:
|
|
20
20
|
headless: Run browser in headless mode (default: False)
|
|
21
|
-
|
|
21
|
+
- False: Show browser window (for local development)
|
|
22
|
+
- True: Standard headless mode (for servers with display)
|
|
23
|
+
- 'virtual': Use Xvfb virtual display (for Docker containers)
|
|
22
24
|
proxy_server: Proxy server URL (e.g., 'http://proxy.com:8080')
|
|
23
25
|
proxy_username: Proxy username (for authenticated proxies)
|
|
24
26
|
proxy_password: Proxy password (for authenticated proxies)
|
|
@@ -47,7 +49,7 @@ class BrowserConfig:
|
|
|
47
49
|
|
|
48
50
|
def __init__(
|
|
49
51
|
self,
|
|
50
|
-
headless: bool = False,
|
|
52
|
+
headless: Union[bool, str] = False,
|
|
51
53
|
proxy_server: Optional[str] = None,
|
|
52
54
|
proxy_username: Optional[str] = None,
|
|
53
55
|
proxy_password: Optional[str] = None,
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Async Google Trends API request module with Camoufox support
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Dict, Optional
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
from pytrends_modern import exceptions
|
|
12
|
+
from pytrends_modern.browser_config_camoufox import BrowserConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AsyncTrendReq:
|
|
16
|
+
"""
|
|
17
|
+
Async Google Trends API with Camoufox browser mode support
|
|
18
|
+
|
|
19
|
+
This class provides async methods for fetching Google Trends data
|
|
20
|
+
using Camoufox's async API for better performance in async applications.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
>>> import asyncio
|
|
24
|
+
>>> from pytrends_modern import AsyncTrendReq, BrowserConfig
|
|
25
|
+
>>>
|
|
26
|
+
>>> async def main():
|
|
27
|
+
... config = BrowserConfig(headless=True)
|
|
28
|
+
... async with AsyncTrendReq(browser_config=config) as pytrends:
|
|
29
|
+
... pytrends.kw_list = ['Python']
|
|
30
|
+
... df = await pytrends.interest_over_time()
|
|
31
|
+
... print(df.head())
|
|
32
|
+
>>>
|
|
33
|
+
>>> asyncio.run(main())
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, browser_config: BrowserConfig):
|
|
37
|
+
"""
|
|
38
|
+
Initialize async Google Trends request
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
browser_config: BrowserConfig instance for Camoufox
|
|
42
|
+
"""
|
|
43
|
+
self.browser_config = browser_config
|
|
44
|
+
self.browser = None
|
|
45
|
+
self.browser_context = None
|
|
46
|
+
self.browser_page = None
|
|
47
|
+
self.browser_responses_cache = {}
|
|
48
|
+
self.kw_list = []
|
|
49
|
+
|
|
50
|
+
async def __aenter__(self):
|
|
51
|
+
"""Async context manager entry"""
|
|
52
|
+
await self._init_camoufox()
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
56
|
+
"""Async context manager exit"""
|
|
57
|
+
await self._close_browser()
|
|
58
|
+
|
|
59
|
+
async def _init_camoufox(self) -> None:
|
|
60
|
+
"""Initialize Camoufox browser with persistent context (async)"""
|
|
61
|
+
try:
|
|
62
|
+
from camoufox.async_api import AsyncCamoufox
|
|
63
|
+
except ImportError:
|
|
64
|
+
raise ImportError(
|
|
65
|
+
"Camoufox is required for async browser mode. "
|
|
66
|
+
"Install with: pip install pytrends-modern[browser]"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Prepare browser options
|
|
70
|
+
import os
|
|
71
|
+
user_data_dir = os.path.expanduser(
|
|
72
|
+
self.browser_config.user_data_dir or "~/.config/camoufox-pytrends-profile"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Check if profile is configured (has Google login)
|
|
76
|
+
from pytrends_modern.camoufox_setup import is_profile_configured
|
|
77
|
+
if not is_profile_configured(user_data_dir):
|
|
78
|
+
raise exceptions.BrowserError(
|
|
79
|
+
f"Camoufox profile not configured at: {user_data_dir}\n"
|
|
80
|
+
"You must set up your Google account login first:\n\n"
|
|
81
|
+
" from pytrends_modern.camoufox_setup import setup_profile\n"
|
|
82
|
+
" setup_profile()\n\n"
|
|
83
|
+
"Or run from command line:\n"
|
|
84
|
+
" python -m pytrends_modern.camoufox_setup\n\n"
|
|
85
|
+
"This will open a browser for you to log in to Google."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Proxy configuration (if provided)
|
|
89
|
+
proxy_config = None
|
|
90
|
+
if self.browser_config.proxy_server:
|
|
91
|
+
proxy_config = {
|
|
92
|
+
"server": self.browser_config.proxy_server,
|
|
93
|
+
}
|
|
94
|
+
if self.browser_config.proxy_username:
|
|
95
|
+
proxy_config["username"] = self.browser_config.proxy_username
|
|
96
|
+
if self.browser_config.proxy_password:
|
|
97
|
+
proxy_config["password"] = self.browser_config.proxy_password
|
|
98
|
+
|
|
99
|
+
# Initialize AsyncCamoufox with persistent context
|
|
100
|
+
try:
|
|
101
|
+
# AsyncCamoufox() returns a context manager
|
|
102
|
+
camoufox_manager = AsyncCamoufox(
|
|
103
|
+
persistent_context=True,
|
|
104
|
+
user_data_dir=user_data_dir,
|
|
105
|
+
headless=self.browser_config.headless,
|
|
106
|
+
humanize=self.browser_config.humanize if hasattr(self.browser_config, 'humanize') else True,
|
|
107
|
+
os=self.browser_config.os if hasattr(self.browser_config, 'os') else 'linux',
|
|
108
|
+
geoip=self.browser_config.geoip if hasattr(self.browser_config, 'geoip') else True,
|
|
109
|
+
proxy=proxy_config
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Enter the context manager to get the browser context
|
|
113
|
+
self.browser = camoufox_manager
|
|
114
|
+
self.browser_context = await camoufox_manager.__aenter__()
|
|
115
|
+
|
|
116
|
+
# Use existing page if available (avoid opening 2 tabs)
|
|
117
|
+
if self.browser_context.pages:
|
|
118
|
+
self.browser_page = self.browser_context.pages[0]
|
|
119
|
+
else:
|
|
120
|
+
self.browser_page = await self.browser_context.new_page()
|
|
121
|
+
|
|
122
|
+
# Set up network interception
|
|
123
|
+
self.browser_page.on("response", self._handle_network_response)
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
raise exceptions.BrowserError(f"Failed to initialize AsyncCamoufox: {e}")
|
|
127
|
+
|
|
128
|
+
async def _close_browser(self) -> None:
|
|
129
|
+
"""Close browser if open (async)"""
|
|
130
|
+
if self.browser:
|
|
131
|
+
try:
|
|
132
|
+
# Exit the context manager
|
|
133
|
+
await self.browser.__aexit__(None, None, None)
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
self.browser = None
|
|
137
|
+
self.browser_context = None
|
|
138
|
+
self.browser_page = None
|
|
139
|
+
|
|
140
|
+
async def _handle_network_response(self, response) -> None:
|
|
141
|
+
"""
|
|
142
|
+
Handle network responses and cache Google Trends API data (async)
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
response: Playwright response object
|
|
146
|
+
"""
|
|
147
|
+
url = response.url
|
|
148
|
+
|
|
149
|
+
# Only process Google Trends API responses
|
|
150
|
+
if '/trends/api/widgetdata/' not in url:
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
# Get response body (async in AsyncPlaywright)
|
|
155
|
+
body = await response.body()
|
|
156
|
+
|
|
157
|
+
# Parse the response (remove Google's JSONP prefix - exactly 5 bytes)
|
|
158
|
+
if body.startswith(b")]}'\n"):
|
|
159
|
+
body = body[5:]
|
|
160
|
+
elif body.startswith(b")]}'"):
|
|
161
|
+
body = body[5:]
|
|
162
|
+
|
|
163
|
+
data = json.loads(body)
|
|
164
|
+
|
|
165
|
+
# Cache by URL pattern
|
|
166
|
+
if '/widgetdata/multiline' in url:
|
|
167
|
+
self.browser_responses_cache['interest_over_time'] = data
|
|
168
|
+
elif '/widgetdata/comparedgeo' in url:
|
|
169
|
+
self.browser_responses_cache['interest_by_region'] = data
|
|
170
|
+
elif '/widgetdata/relatedsearches' in url:
|
|
171
|
+
# keywordType is URL-encoded inside the req parameter
|
|
172
|
+
import urllib.parse
|
|
173
|
+
decoded_url = urllib.parse.unquote(url)
|
|
174
|
+
if 'keywordType":"ENTITY' in decoded_url:
|
|
175
|
+
self.browser_responses_cache['related_topics'] = data
|
|
176
|
+
elif 'keywordType":"QUERY' in decoded_url:
|
|
177
|
+
self.browser_responses_cache['related_queries'] = data
|
|
178
|
+
|
|
179
|
+
except Exception:
|
|
180
|
+
pass # Silently ignore parsing errors
|
|
181
|
+
|
|
182
|
+
async def _capture_all_api_responses(self, keyword: str) -> None:
|
|
183
|
+
"""
|
|
184
|
+
Navigate once and capture ALL API responses via network interception (async)
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
keyword: Search keyword to use
|
|
188
|
+
"""
|
|
189
|
+
if not self.browser_page:
|
|
190
|
+
raise exceptions.BrowserError("Browser not initialized")
|
|
191
|
+
|
|
192
|
+
# Clear cache
|
|
193
|
+
self.browser_responses_cache.clear()
|
|
194
|
+
|
|
195
|
+
# Build URL
|
|
196
|
+
import urllib.parse
|
|
197
|
+
encoded_keyword = urllib.parse.quote(keyword)
|
|
198
|
+
url = f"https://trends.google.com/trends/explore?date=today%201-m&q={encoded_keyword}&hl=en-GB"
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
# Navigate and wait for network idle
|
|
202
|
+
await self.browser_page.goto(url, wait_until='networkidle', timeout=60000)
|
|
203
|
+
|
|
204
|
+
# Give extra time for any delayed API calls
|
|
205
|
+
import asyncio
|
|
206
|
+
await asyncio.sleep(2)
|
|
207
|
+
|
|
208
|
+
except Exception as e:
|
|
209
|
+
raise exceptions.BrowserError(f"Failed to navigate to Google Trends: {e}")
|
|
210
|
+
|
|
211
|
+
def _parse_multiline_response(self, data: Dict) -> pd.DataFrame:
|
|
212
|
+
"""Parse multiline (interest over time) API response"""
|
|
213
|
+
# Import from sync version to reuse parsing logic
|
|
214
|
+
from pytrends_modern.request import TrendReq
|
|
215
|
+
temp = TrendReq(hl='en-US', tz=360)
|
|
216
|
+
return temp._parse_multiline_response(data)
|
|
217
|
+
|
|
218
|
+
def _parse_comparedgeo_response(self, data: Dict, inc_geo_code: bool = False) -> pd.DataFrame:
|
|
219
|
+
"""Parse comparedgeo (interest by region) API response"""
|
|
220
|
+
from pytrends_modern.request import TrendReq
|
|
221
|
+
temp = TrendReq(hl='en-US', tz=360)
|
|
222
|
+
return temp._parse_comparedgeo_response(data, inc_geo_code)
|
|
223
|
+
|
|
224
|
+
def _parse_relatedsearches_response(self, data: Dict) -> Dict[str, Optional[pd.DataFrame]]:
|
|
225
|
+
"""Parse relatedsearches (related topics/queries) API response"""
|
|
226
|
+
from pytrends_modern.request import TrendReq
|
|
227
|
+
temp = TrendReq(hl='en-US', tz=360)
|
|
228
|
+
return temp._parse_relatedsearches_response(data)
|
|
229
|
+
|
|
230
|
+
async def interest_over_time(self) -> pd.DataFrame:
|
|
231
|
+
"""
|
|
232
|
+
Get interest over time data (async)
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
DataFrame with date index and columns for each keyword
|
|
236
|
+
"""
|
|
237
|
+
if len(self.kw_list) != 1:
|
|
238
|
+
raise exceptions.InvalidParameterError(
|
|
239
|
+
"Async browser mode only supports 1 keyword. You provided: "
|
|
240
|
+
+ str(len(self.kw_list))
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
keyword = self.kw_list[0]
|
|
244
|
+
|
|
245
|
+
# Capture all responses if not already cached
|
|
246
|
+
if not self.browser_responses_cache:
|
|
247
|
+
await self._capture_all_api_responses(keyword)
|
|
248
|
+
|
|
249
|
+
# Get cached response
|
|
250
|
+
response_data = self.browser_responses_cache.get('interest_over_time')
|
|
251
|
+
|
|
252
|
+
if not response_data:
|
|
253
|
+
# Try one more navigation if cache is empty
|
|
254
|
+
await self._capture_all_api_responses(keyword)
|
|
255
|
+
response_data = self.browser_responses_cache.get('interest_over_time')
|
|
256
|
+
|
|
257
|
+
if not response_data:
|
|
258
|
+
raise exceptions.ResponseError("Failed to capture interest_over_time API response")
|
|
259
|
+
|
|
260
|
+
# Parse browser response to DataFrame
|
|
261
|
+
return self._parse_multiline_response(response_data)
|
|
262
|
+
|
|
263
|
+
async def interest_by_region(self, inc_geo_code: bool = False) -> pd.DataFrame:
|
|
264
|
+
"""
|
|
265
|
+
Get interest by region data (async)
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
inc_geo_code: Include geographic codes in results
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
DataFrame with region index
|
|
272
|
+
"""
|
|
273
|
+
if len(self.kw_list) != 1:
|
|
274
|
+
raise exceptions.InvalidParameterError(
|
|
275
|
+
"Async browser mode only supports 1 keyword"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
keyword = self.kw_list[0]
|
|
279
|
+
|
|
280
|
+
# Capture all responses if not already cached
|
|
281
|
+
if not self.browser_responses_cache:
|
|
282
|
+
await self._capture_all_api_responses(keyword)
|
|
283
|
+
|
|
284
|
+
# Get cached response
|
|
285
|
+
response_data = self.browser_responses_cache.get('interest_by_region')
|
|
286
|
+
|
|
287
|
+
if not response_data:
|
|
288
|
+
raise exceptions.ResponseError("Failed to capture interest_by_region API response")
|
|
289
|
+
|
|
290
|
+
return self._parse_comparedgeo_response(response_data, inc_geo_code)
|
|
291
|
+
|
|
292
|
+
async def related_topics(self) -> Dict[str, Dict[str, Optional[pd.DataFrame]]]:
|
|
293
|
+
"""
|
|
294
|
+
Get related topics (async)
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Dict with keyword as key and dict of 'top'/'rising' DataFrames as value
|
|
298
|
+
"""
|
|
299
|
+
if len(self.kw_list) != 1:
|
|
300
|
+
raise exceptions.InvalidParameterError(
|
|
301
|
+
"Async browser mode only supports 1 keyword"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
keyword = self.kw_list[0]
|
|
305
|
+
|
|
306
|
+
# Capture all responses if not already cached
|
|
307
|
+
if not self.browser_responses_cache:
|
|
308
|
+
await self._capture_all_api_responses(keyword)
|
|
309
|
+
|
|
310
|
+
# Get cached response
|
|
311
|
+
response_data = self.browser_responses_cache.get('related_topics')
|
|
312
|
+
|
|
313
|
+
if not response_data:
|
|
314
|
+
raise exceptions.ResponseError("Failed to capture related_topics API response")
|
|
315
|
+
|
|
316
|
+
return {keyword: self._parse_relatedsearches_response(response_data)}
|
|
317
|
+
|
|
318
|
+
async def related_queries(self) -> Dict[str, Dict[str, Optional[pd.DataFrame]]]:
|
|
319
|
+
"""
|
|
320
|
+
Get related queries (async)
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Dict with keyword as key and dict of 'top'/'rising' DataFrames as value
|
|
324
|
+
"""
|
|
325
|
+
if len(self.kw_list) != 1:
|
|
326
|
+
raise exceptions.InvalidParameterError(
|
|
327
|
+
"Async browser mode only supports 1 keyword"
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
keyword = self.kw_list[0]
|
|
331
|
+
|
|
332
|
+
# Capture all responses if not already cached
|
|
333
|
+
if not self.browser_responses_cache:
|
|
334
|
+
await self._capture_all_api_responses(keyword)
|
|
335
|
+
|
|
336
|
+
# Get cached response
|
|
337
|
+
response_data = self.browser_responses_cache.get('related_queries')
|
|
338
|
+
|
|
339
|
+
if not response_data:
|
|
340
|
+
raise exceptions.ResponseError("Failed to capture related_queries API response")
|
|
341
|
+
|
|
342
|
+
return {keyword: self._parse_relatedsearches_response(response_data)}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytrends-modern
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Modern Google Trends API - Combining the best of pytrends, with RSS feeds, Selenium scraping, DrissionPage browser automation, and enhanced features
|
|
5
5
|
Author: pytrends-modern contributors
|
|
6
6
|
License: MIT
|
|
@@ -175,11 +175,16 @@ RUN mkdir -p /root/.config && \
|
|
|
175
175
|
cd /root/.config && \
|
|
176
176
|
tar -xzf /tmp/profile.tar.gz
|
|
177
177
|
|
|
178
|
-
# 3. Use headless
|
|
179
|
-
config = BrowserConfig(headless=
|
|
178
|
+
# 3. Use headless="virtual" in container
|
|
179
|
+
config = BrowserConfig(headless="virtual") # Use Xvfb for Docker
|
|
180
180
|
```
|
|
181
181
|
|
|
182
|
-
|
|
182
|
+
**Headless Options:**
|
|
183
|
+
- `headless=False` - Show browser window (local development)
|
|
184
|
+
- `headless=True` - Standard headless (servers with display)
|
|
185
|
+
- `headless="virtual"` - Xvfb virtual display (Docker containers)
|
|
186
|
+
|
|
187
|
+
See `Dockerfile.example` and `examples/example_docker_usage.py` for complete Docker setup.
|
|
183
188
|
|
|
184
189
|
⚠️ **Security**: Profile contains Google session - keep secure, don't commit to git!
|
|
185
190
|
|
|
@@ -5,6 +5,8 @@ pyproject.toml
|
|
|
5
5
|
examples/advanced_usage.py
|
|
6
6
|
examples/basic_usage.py
|
|
7
7
|
examples/example_browser_mode.py
|
|
8
|
+
examples/example_docker_usage.py
|
|
9
|
+
examples/test_async_integration.py
|
|
8
10
|
pytrends_modern/__init__.py
|
|
9
11
|
pytrends_modern/browser_config.py
|
|
10
12
|
pytrends_modern/browser_config_camoufox.py
|
|
@@ -15,6 +17,7 @@ pytrends_modern/exceptions.py
|
|
|
15
17
|
pytrends_modern/proxy_extension.py
|
|
16
18
|
pytrends_modern/py.typed
|
|
17
19
|
pytrends_modern/request.py
|
|
20
|
+
pytrends_modern/request_async.py
|
|
18
21
|
pytrends_modern/rss.py
|
|
19
22
|
pytrends_modern/scraper.py
|
|
20
23
|
pytrends_modern/utils.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytrends_modern-0.2.0 → pytrends_modern-0.2.2}/pytrends_modern.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|