beo-mcdp 0.2.0__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.
- beo_mcdp-0.2.0/PKG-INFO +161 -0
- beo_mcdp-0.2.0/README.md +141 -0
- beo_mcdp-0.2.0/beo_mcdp.egg-info/PKG-INFO +161 -0
- beo_mcdp-0.2.0/beo_mcdp.egg-info/SOURCES.txt +37 -0
- beo_mcdp-0.2.0/beo_mcdp.egg-info/dependency_links.txt +1 -0
- beo_mcdp-0.2.0/beo_mcdp.egg-info/requires.txt +7 -0
- beo_mcdp-0.2.0/beo_mcdp.egg-info/top_level.txt +1 -0
- beo_mcdp-0.2.0/cdp_scraper/__init__.py +24 -0
- beo_mcdp-0.2.0/cdp_scraper/_version.py +1 -0
- beo_mcdp-0.2.0/cdp_scraper/browser/__init__.py +0 -0
- beo_mcdp-0.2.0/cdp_scraper/browser/actions.py +74 -0
- beo_mcdp-0.2.0/cdp_scraper/browser/capture.py +191 -0
- beo_mcdp-0.2.0/cdp_scraper/browser/diagnostics.py +57 -0
- beo_mcdp-0.2.0/cdp_scraper/browser/helpers.py +2 -0
- beo_mcdp-0.2.0/cdp_scraper/browser/session.py +142 -0
- beo_mcdp-0.2.0/cdp_scraper/browser/state.py +2 -0
- beo_mcdp-0.2.0/cdp_scraper/config.py +79 -0
- beo_mcdp-0.2.0/cdp_scraper/errors.py +10 -0
- beo_mcdp-0.2.0/cdp_scraper/logs.py +56 -0
- beo_mcdp-0.2.0/cdp_scraper/messaging/__init__.py +5 -0
- beo_mcdp-0.2.0/cdp_scraper/messaging/redpanda.py +105 -0
- beo_mcdp-0.2.0/cdp_scraper/parsers.py +2 -0
- beo_mcdp-0.2.0/cdp_scraper/parsing/__init__.py +6 -0
- beo_mcdp-0.2.0/cdp_scraper/parsing/mapper.py +202 -0
- beo_mcdp-0.2.0/cdp_scraper/parsing/paths.py +80 -0
- beo_mcdp-0.2.0/cdp_scraper/parsing/transforms.py +123 -0
- beo_mcdp-0.2.0/cdp_scraper/platforms/__init__.py +7 -0
- beo_mcdp-0.2.0/cdp_scraper/platforms/registry.py +28 -0
- beo_mcdp-0.2.0/cdp_scraper/platforms/runner.py +124 -0
- beo_mcdp-0.2.0/cdp_scraper/platforms/spec.py +129 -0
- beo_mcdp-0.2.0/cdp_scraper/utils/__init__.py +9 -0
- beo_mcdp-0.2.0/cdp_scraper/utils/database.py +12 -0
- beo_mcdp-0.2.0/cdp_scraper/utils/redpanda_client.py +8 -0
- beo_mcdp-0.2.0/cdp_scraper/utils/utils.py +136 -0
- beo_mcdp-0.2.0/setup.cfg +4 -0
- beo_mcdp-0.2.0/setup.py +22 -0
- beo_mcdp-0.2.0/tests/test_examples.py +57 -0
- beo_mcdp-0.2.0/tests/test_parsing.py +80 -0
- beo_mcdp-0.2.0/tests/test_runtime.py +168 -0
beo_mcdp-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: beo-mcdp
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Core CDP Browser Automation and Utils Provider
|
|
5
|
+
Author: Dwi Hardiyanto
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: playwright>=1.57.0
|
|
9
|
+
Provides-Extra: patchright
|
|
10
|
+
Requires-Dist: patchright; extra == "patchright"
|
|
11
|
+
Provides-Extra: kafka
|
|
12
|
+
Requires-Dist: confluent-kafka; extra == "kafka"
|
|
13
|
+
Dynamic: author
|
|
14
|
+
Dynamic: description
|
|
15
|
+
Dynamic: description-content-type
|
|
16
|
+
Dynamic: provides-extra
|
|
17
|
+
Dynamic: requires-dist
|
|
18
|
+
Dynamic: requires-python
|
|
19
|
+
Dynamic: summary
|
|
20
|
+
|
|
21
|
+
# CDP Scraper Provider
|
|
22
|
+
|
|
23
|
+
Library *core* untuk ekstraksi data lewat browser Chrome (CDP). Satu mesin yang sama dipakai untuk
|
|
24
|
+
banyak sosial media: platform baru cukup **dideskripsikan** (URL, pola API, mapping field), tanpa
|
|
25
|
+
menulis ulang sesi browser, scroll, capture, maupun parser.
|
|
26
|
+
|
|
27
|
+
## Instalasi
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install git+https://github.com/dwihard/social_scrapers.git@v0.2.0
|
|
31
|
+
# opsional: pip install "cdp_scraper[kafka]" / "cdp_scraper[patchright]"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Cara kerja
|
|
35
|
+
|
|
36
|
+
1. Browser membuka halaman (`Endpoint.url`).
|
|
37
|
+
2. Library mendengarkan response API yang dipanggil halaman (`Endpoint.api_match`).
|
|
38
|
+
3. JSON-nya diubah jadi list dict oleh `parser` (biasanya `FieldMapper`), di-dedupe lewat `key`, lalu halaman di-scroll sampai `limit`/data habis.
|
|
39
|
+
|
|
40
|
+
## Menambah platform baru
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from cdp_scraper import (Endpoint, F, FieldMapper, Platform, Scraper,
|
|
44
|
+
const, make_scroller, register_platform, end_when_falsy)
|
|
45
|
+
|
|
46
|
+
parser = FieldMapper(
|
|
47
|
+
items="data.posts", # path ke list item
|
|
48
|
+
fields={
|
|
49
|
+
"id": ("id", "str", ""), # (path, transform, default)
|
|
50
|
+
"url": F(template="https://site.com/{author.handle}/{id}"),
|
|
51
|
+
"likes": ("stats.likes", "int", 0),
|
|
52
|
+
"tags": "tags[].name", # semua elemen
|
|
53
|
+
"posted": F("created", transform="iso_utc"),
|
|
54
|
+
"platform": const("site"),
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
site = Platform(
|
|
59
|
+
name="site", # juga jadi label log: [site] ...
|
|
60
|
+
cdp={"cdp_url": "http://localhost:9223"}, # opsional: Chrome/slot terpisah per platform
|
|
61
|
+
overlay_selectors=('div.popup-close',), # popup yang ditutup otomatis
|
|
62
|
+
login_url_hints=("/login",), # deteksi browser belum login
|
|
63
|
+
endpoints=[Endpoint(
|
|
64
|
+
"search", url="https://site.com/search?q={keyword}", api_match="api/search",
|
|
65
|
+
parser=parser, params=("keyword",), label="keyword", limit=20, max_limit=50,
|
|
66
|
+
end=end_when_falsy("has_more"),
|
|
67
|
+
scroll=make_scroller(wheel=2500, click_pattern="load more"),
|
|
68
|
+
)],
|
|
69
|
+
)
|
|
70
|
+
register_platform(site)
|
|
71
|
+
|
|
72
|
+
result = await Scraper("site").run("search", {"keyword": "kopi"}, limit=30) # satu query
|
|
73
|
+
results = await Scraper("site").run_many("search", [{"keyword": "a"}, {"keyword": "b"}])
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`run()` melempar exception (`PageCrashed`, `BrowserBusy`, `ValueError` untuk parameter salah).
|
|
77
|
+
`run_many()` memakai satu tab untuk banyak query dan menjadikan error per-item `status="error"`.
|
|
78
|
+
Contoh lengkap TikTok dan X/Twitter ada di `examples/`.
|
|
79
|
+
|
|
80
|
+
## Mapping parsing (`FieldMapper`)
|
|
81
|
+
|
|
82
|
+
| Sintaks path | Arti |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `a.b.c` | nilai bersarang |
|
|
85
|
+
| `a[0].b`, `a[-1]` | indeks |
|
|
86
|
+
| `a[].b` | semua elemen (hasil list) |
|
|
87
|
+
| `a[type=1].b` | elemen yang cocok filter (hasil list) |
|
|
88
|
+
|
|
89
|
+
Bentuk spec field: `"path"` · `("path", "transform", default)` · `F(path | any_of=[...] | template=... | fn=...,
|
|
90
|
+
default=, transform=, required=)` · `const(x)` · `callable(record)`.
|
|
91
|
+
|
|
92
|
+
Opsi `FieldMapper`: `item_path` (sub-path record di tiap item), `where` / `record_where`
|
|
93
|
+
(`{path: nilai | EXISTS | callable}` atau callable), `unwrap` (membuka pembungkus), `items` boleh
|
|
94
|
+
list path atau callable.
|
|
95
|
+
|
|
96
|
+
Transform bawaan: `int float str bool url lower upper strip clean_text username hashtag iso_utc
|
|
97
|
+
date_id join first count unique drop_empty` dan `each:<transform>` untuk list. Argumen format tanggal:
|
|
98
|
+
`iso_utc:%a %b %d %H:%M:%S %z %Y`. Tambah sendiri: `register_transform("nama", fn)`.
|
|
99
|
+
|
|
100
|
+
Jika logika terlalu rumit untuk mapping, gunakan `F(fn=...)` atau parser Python biasa
|
|
101
|
+
(`parser=` menerima callable apa pun yang mengembalikan list dict).
|
|
102
|
+
|
|
103
|
+
## Hook & aksi halaman
|
|
104
|
+
|
|
105
|
+
`prepare` (setelah halaman terbuka) dan `scroll` (tiap putaran) adalah fungsi `async def (page)`.
|
|
106
|
+
Bawaan: `make_scroller(...)`, `make_wait_for(selector)`, `close_overlays(page, selectors)`.
|
|
107
|
+
`Endpoint.capture_options` meneruskan kwargs tambahan ke `capture` (`poll_interval`, `settle`, dst).
|
|
108
|
+
|
|
109
|
+
## Konfigurasi browser
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from cdp_scraper import CDPConfig, setup_cdp
|
|
113
|
+
setup_cdp(cdp_url="http://localhost:9222", max_sessions=2, lock_timeout=60) # default global
|
|
114
|
+
cfg = CDPConfig.from_env("TWITTER_") # TWITTER_CDP_URL, TWITTER_MAX_CONCURRENT_SESSIONS, ...
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Setiap request memakai **tab baru sendiri** (ditutup saat selesai) dan dibatasi `max_sessions`
|
|
118
|
+
bersamaan per konfigurasi. Konfigurasi yang sama berbagi slot; `cdp_url` berbeda = slot berbeda.
|
|
119
|
+
Screenshot debug disimpan di `CDPConfig.debug_dir` (default folder kerja); matikan dengan
|
|
120
|
+
`debug_screenshots=False`.
|
|
121
|
+
|
|
122
|
+
## Logging
|
|
123
|
+
|
|
124
|
+
Format seragam `[job] Status message: detail`, job = nama platform:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
[2026-11-10 08:19:38] [INFO] [site] Success message: Capture api/search selesai: 20 data
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Library hanya memakai logger `cdp_scraper` (tanpa handler). Aktifkan dengan `setup_logging()`
|
|
131
|
+
atau injeksi logger sendiri: `setup_cdp(custom_logger=...)`. `get_logger("kafka").error("...")`
|
|
132
|
+
untuk log aplikasi dengan format yang sama.
|
|
133
|
+
|
|
134
|
+
## Kafka / Redpanda
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from cdp_scraper import RedpandaProducer, publish_chunks
|
|
138
|
+
producer = RedpandaProducer() # KAFKA_BOOTSTRAP_SERVERS; koneksi dibuat saat dipakai
|
|
139
|
+
if producer.available:
|
|
140
|
+
stats = publish_chunks(producer, "topic-x", keywords, chunk_size=2,
|
|
141
|
+
build_payload=lambda chunk: {"keywords": chunk})
|
|
142
|
+
# {"berhasil": n, "gagal": n}
|
|
143
|
+
```
|
|
144
|
+
Kontrak sumber data (keyword/akun) ada di `cdp_scraper.utils.database` (`KeywordSource`,
|
|
145
|
+
`AccountSource`); implementasinya milik aplikasi.
|
|
146
|
+
|
|
147
|
+
## Pengujian
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
python -m unittest discover -s tests -t .
|
|
151
|
+
```
|
|
152
|
+
Tes memakai browser palsu, jadi tidak butuh Chrome/Kafka.
|
|
153
|
+
|
|
154
|
+
## Perubahan dari 0.1.x (breaking)
|
|
155
|
+
|
|
156
|
+
- Konfigurasi sekarang `CDPConfig` (immutable, per platform); `setup_cdp(...)` tetap ada. Variabel modul `config.CDP_URL` dst dihapus.
|
|
157
|
+
- `get_session_slots` dan `PW_ENGINE` dihapus; pakai `CDPSessionManager` / `manager.engine`.
|
|
158
|
+
- `BrowserBusy` (subclass `TimeoutError`) menggantikan `TimeoutError` polos.
|
|
159
|
+
- `helpers.close_login_modal(page, selectors)` kini wajib diberi selector (tidak ada lagi selector TikTok bawaan).
|
|
160
|
+
- `utils`: tidak lagi mengimpor Flask; `extract_challenges` (khusus TikTok) dihapus; `calculate_engagement_rate` menerima `interaction_keys` / `view_key` (default sama dengan perilaku lama).
|
|
161
|
+
- `utils.database` kini berupa Protocol; `utils.redpanda_client` tidak lagi membuat producer saat import (gunakan `cdp_scraper.messaging`).
|
beo_mcdp-0.2.0/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# CDP Scraper Provider
|
|
2
|
+
|
|
3
|
+
Library *core* untuk ekstraksi data lewat browser Chrome (CDP). Satu mesin yang sama dipakai untuk
|
|
4
|
+
banyak sosial media: platform baru cukup **dideskripsikan** (URL, pola API, mapping field), tanpa
|
|
5
|
+
menulis ulang sesi browser, scroll, capture, maupun parser.
|
|
6
|
+
|
|
7
|
+
## Instalasi
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install git+https://github.com/dwihard/social_scrapers.git@v0.2.0
|
|
11
|
+
# opsional: pip install "cdp_scraper[kafka]" / "cdp_scraper[patchright]"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Cara kerja
|
|
15
|
+
|
|
16
|
+
1. Browser membuka halaman (`Endpoint.url`).
|
|
17
|
+
2. Library mendengarkan response API yang dipanggil halaman (`Endpoint.api_match`).
|
|
18
|
+
3. JSON-nya diubah jadi list dict oleh `parser` (biasanya `FieldMapper`), di-dedupe lewat `key`, lalu halaman di-scroll sampai `limit`/data habis.
|
|
19
|
+
|
|
20
|
+
## Menambah platform baru
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from cdp_scraper import (Endpoint, F, FieldMapper, Platform, Scraper,
|
|
24
|
+
const, make_scroller, register_platform, end_when_falsy)
|
|
25
|
+
|
|
26
|
+
parser = FieldMapper(
|
|
27
|
+
items="data.posts", # path ke list item
|
|
28
|
+
fields={
|
|
29
|
+
"id": ("id", "str", ""), # (path, transform, default)
|
|
30
|
+
"url": F(template="https://site.com/{author.handle}/{id}"),
|
|
31
|
+
"likes": ("stats.likes", "int", 0),
|
|
32
|
+
"tags": "tags[].name", # semua elemen
|
|
33
|
+
"posted": F("created", transform="iso_utc"),
|
|
34
|
+
"platform": const("site"),
|
|
35
|
+
},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
site = Platform(
|
|
39
|
+
name="site", # juga jadi label log: [site] ...
|
|
40
|
+
cdp={"cdp_url": "http://localhost:9223"}, # opsional: Chrome/slot terpisah per platform
|
|
41
|
+
overlay_selectors=('div.popup-close',), # popup yang ditutup otomatis
|
|
42
|
+
login_url_hints=("/login",), # deteksi browser belum login
|
|
43
|
+
endpoints=[Endpoint(
|
|
44
|
+
"search", url="https://site.com/search?q={keyword}", api_match="api/search",
|
|
45
|
+
parser=parser, params=("keyword",), label="keyword", limit=20, max_limit=50,
|
|
46
|
+
end=end_when_falsy("has_more"),
|
|
47
|
+
scroll=make_scroller(wheel=2500, click_pattern="load more"),
|
|
48
|
+
)],
|
|
49
|
+
)
|
|
50
|
+
register_platform(site)
|
|
51
|
+
|
|
52
|
+
result = await Scraper("site").run("search", {"keyword": "kopi"}, limit=30) # satu query
|
|
53
|
+
results = await Scraper("site").run_many("search", [{"keyword": "a"}, {"keyword": "b"}])
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`run()` melempar exception (`PageCrashed`, `BrowserBusy`, `ValueError` untuk parameter salah).
|
|
57
|
+
`run_many()` memakai satu tab untuk banyak query dan menjadikan error per-item `status="error"`.
|
|
58
|
+
Contoh lengkap TikTok dan X/Twitter ada di `examples/`.
|
|
59
|
+
|
|
60
|
+
## Mapping parsing (`FieldMapper`)
|
|
61
|
+
|
|
62
|
+
| Sintaks path | Arti |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `a.b.c` | nilai bersarang |
|
|
65
|
+
| `a[0].b`, `a[-1]` | indeks |
|
|
66
|
+
| `a[].b` | semua elemen (hasil list) |
|
|
67
|
+
| `a[type=1].b` | elemen yang cocok filter (hasil list) |
|
|
68
|
+
|
|
69
|
+
Bentuk spec field: `"path"` · `("path", "transform", default)` · `F(path | any_of=[...] | template=... | fn=...,
|
|
70
|
+
default=, transform=, required=)` · `const(x)` · `callable(record)`.
|
|
71
|
+
|
|
72
|
+
Opsi `FieldMapper`: `item_path` (sub-path record di tiap item), `where` / `record_where`
|
|
73
|
+
(`{path: nilai | EXISTS | callable}` atau callable), `unwrap` (membuka pembungkus), `items` boleh
|
|
74
|
+
list path atau callable.
|
|
75
|
+
|
|
76
|
+
Transform bawaan: `int float str bool url lower upper strip clean_text username hashtag iso_utc
|
|
77
|
+
date_id join first count unique drop_empty` dan `each:<transform>` untuk list. Argumen format tanggal:
|
|
78
|
+
`iso_utc:%a %b %d %H:%M:%S %z %Y`. Tambah sendiri: `register_transform("nama", fn)`.
|
|
79
|
+
|
|
80
|
+
Jika logika terlalu rumit untuk mapping, gunakan `F(fn=...)` atau parser Python biasa
|
|
81
|
+
(`parser=` menerima callable apa pun yang mengembalikan list dict).
|
|
82
|
+
|
|
83
|
+
## Hook & aksi halaman
|
|
84
|
+
|
|
85
|
+
`prepare` (setelah halaman terbuka) dan `scroll` (tiap putaran) adalah fungsi `async def (page)`.
|
|
86
|
+
Bawaan: `make_scroller(...)`, `make_wait_for(selector)`, `close_overlays(page, selectors)`.
|
|
87
|
+
`Endpoint.capture_options` meneruskan kwargs tambahan ke `capture` (`poll_interval`, `settle`, dst).
|
|
88
|
+
|
|
89
|
+
## Konfigurasi browser
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from cdp_scraper import CDPConfig, setup_cdp
|
|
93
|
+
setup_cdp(cdp_url="http://localhost:9222", max_sessions=2, lock_timeout=60) # default global
|
|
94
|
+
cfg = CDPConfig.from_env("TWITTER_") # TWITTER_CDP_URL, TWITTER_MAX_CONCURRENT_SESSIONS, ...
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Setiap request memakai **tab baru sendiri** (ditutup saat selesai) dan dibatasi `max_sessions`
|
|
98
|
+
bersamaan per konfigurasi. Konfigurasi yang sama berbagi slot; `cdp_url` berbeda = slot berbeda.
|
|
99
|
+
Screenshot debug disimpan di `CDPConfig.debug_dir` (default folder kerja); matikan dengan
|
|
100
|
+
`debug_screenshots=False`.
|
|
101
|
+
|
|
102
|
+
## Logging
|
|
103
|
+
|
|
104
|
+
Format seragam `[job] Status message: detail`, job = nama platform:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
[2026-11-10 08:19:38] [INFO] [site] Success message: Capture api/search selesai: 20 data
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Library hanya memakai logger `cdp_scraper` (tanpa handler). Aktifkan dengan `setup_logging()`
|
|
111
|
+
atau injeksi logger sendiri: `setup_cdp(custom_logger=...)`. `get_logger("kafka").error("...")`
|
|
112
|
+
untuk log aplikasi dengan format yang sama.
|
|
113
|
+
|
|
114
|
+
## Kafka / Redpanda
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from cdp_scraper import RedpandaProducer, publish_chunks
|
|
118
|
+
producer = RedpandaProducer() # KAFKA_BOOTSTRAP_SERVERS; koneksi dibuat saat dipakai
|
|
119
|
+
if producer.available:
|
|
120
|
+
stats = publish_chunks(producer, "topic-x", keywords, chunk_size=2,
|
|
121
|
+
build_payload=lambda chunk: {"keywords": chunk})
|
|
122
|
+
# {"berhasil": n, "gagal": n}
|
|
123
|
+
```
|
|
124
|
+
Kontrak sumber data (keyword/akun) ada di `cdp_scraper.utils.database` (`KeywordSource`,
|
|
125
|
+
`AccountSource`); implementasinya milik aplikasi.
|
|
126
|
+
|
|
127
|
+
## Pengujian
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
python -m unittest discover -s tests -t .
|
|
131
|
+
```
|
|
132
|
+
Tes memakai browser palsu, jadi tidak butuh Chrome/Kafka.
|
|
133
|
+
|
|
134
|
+
## Perubahan dari 0.1.x (breaking)
|
|
135
|
+
|
|
136
|
+
- Konfigurasi sekarang `CDPConfig` (immutable, per platform); `setup_cdp(...)` tetap ada. Variabel modul `config.CDP_URL` dst dihapus.
|
|
137
|
+
- `get_session_slots` dan `PW_ENGINE` dihapus; pakai `CDPSessionManager` / `manager.engine`.
|
|
138
|
+
- `BrowserBusy` (subclass `TimeoutError`) menggantikan `TimeoutError` polos.
|
|
139
|
+
- `helpers.close_login_modal(page, selectors)` kini wajib diberi selector (tidak ada lagi selector TikTok bawaan).
|
|
140
|
+
- `utils`: tidak lagi mengimpor Flask; `extract_challenges` (khusus TikTok) dihapus; `calculate_engagement_rate` menerima `interaction_keys` / `view_key` (default sama dengan perilaku lama).
|
|
141
|
+
- `utils.database` kini berupa Protocol; `utils.redpanda_client` tidak lagi membuat producer saat import (gunakan `cdp_scraper.messaging`).
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: beo-mcdp
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Core CDP Browser Automation and Utils Provider
|
|
5
|
+
Author: Dwi Hardiyanto
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: playwright>=1.57.0
|
|
9
|
+
Provides-Extra: patchright
|
|
10
|
+
Requires-Dist: patchright; extra == "patchright"
|
|
11
|
+
Provides-Extra: kafka
|
|
12
|
+
Requires-Dist: confluent-kafka; extra == "kafka"
|
|
13
|
+
Dynamic: author
|
|
14
|
+
Dynamic: description
|
|
15
|
+
Dynamic: description-content-type
|
|
16
|
+
Dynamic: provides-extra
|
|
17
|
+
Dynamic: requires-dist
|
|
18
|
+
Dynamic: requires-python
|
|
19
|
+
Dynamic: summary
|
|
20
|
+
|
|
21
|
+
# CDP Scraper Provider
|
|
22
|
+
|
|
23
|
+
Library *core* untuk ekstraksi data lewat browser Chrome (CDP). Satu mesin yang sama dipakai untuk
|
|
24
|
+
banyak sosial media: platform baru cukup **dideskripsikan** (URL, pola API, mapping field), tanpa
|
|
25
|
+
menulis ulang sesi browser, scroll, capture, maupun parser.
|
|
26
|
+
|
|
27
|
+
## Instalasi
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install git+https://github.com/dwihard/social_scrapers.git@v0.2.0
|
|
31
|
+
# opsional: pip install "cdp_scraper[kafka]" / "cdp_scraper[patchright]"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Cara kerja
|
|
35
|
+
|
|
36
|
+
1. Browser membuka halaman (`Endpoint.url`).
|
|
37
|
+
2. Library mendengarkan response API yang dipanggil halaman (`Endpoint.api_match`).
|
|
38
|
+
3. JSON-nya diubah jadi list dict oleh `parser` (biasanya `FieldMapper`), di-dedupe lewat `key`, lalu halaman di-scroll sampai `limit`/data habis.
|
|
39
|
+
|
|
40
|
+
## Menambah platform baru
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from cdp_scraper import (Endpoint, F, FieldMapper, Platform, Scraper,
|
|
44
|
+
const, make_scroller, register_platform, end_when_falsy)
|
|
45
|
+
|
|
46
|
+
parser = FieldMapper(
|
|
47
|
+
items="data.posts", # path ke list item
|
|
48
|
+
fields={
|
|
49
|
+
"id": ("id", "str", ""), # (path, transform, default)
|
|
50
|
+
"url": F(template="https://site.com/{author.handle}/{id}"),
|
|
51
|
+
"likes": ("stats.likes", "int", 0),
|
|
52
|
+
"tags": "tags[].name", # semua elemen
|
|
53
|
+
"posted": F("created", transform="iso_utc"),
|
|
54
|
+
"platform": const("site"),
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
site = Platform(
|
|
59
|
+
name="site", # juga jadi label log: [site] ...
|
|
60
|
+
cdp={"cdp_url": "http://localhost:9223"}, # opsional: Chrome/slot terpisah per platform
|
|
61
|
+
overlay_selectors=('div.popup-close',), # popup yang ditutup otomatis
|
|
62
|
+
login_url_hints=("/login",), # deteksi browser belum login
|
|
63
|
+
endpoints=[Endpoint(
|
|
64
|
+
"search", url="https://site.com/search?q={keyword}", api_match="api/search",
|
|
65
|
+
parser=parser, params=("keyword",), label="keyword", limit=20, max_limit=50,
|
|
66
|
+
end=end_when_falsy("has_more"),
|
|
67
|
+
scroll=make_scroller(wheel=2500, click_pattern="load more"),
|
|
68
|
+
)],
|
|
69
|
+
)
|
|
70
|
+
register_platform(site)
|
|
71
|
+
|
|
72
|
+
result = await Scraper("site").run("search", {"keyword": "kopi"}, limit=30) # satu query
|
|
73
|
+
results = await Scraper("site").run_many("search", [{"keyword": "a"}, {"keyword": "b"}])
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`run()` melempar exception (`PageCrashed`, `BrowserBusy`, `ValueError` untuk parameter salah).
|
|
77
|
+
`run_many()` memakai satu tab untuk banyak query dan menjadikan error per-item `status="error"`.
|
|
78
|
+
Contoh lengkap TikTok dan X/Twitter ada di `examples/`.
|
|
79
|
+
|
|
80
|
+
## Mapping parsing (`FieldMapper`)
|
|
81
|
+
|
|
82
|
+
| Sintaks path | Arti |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `a.b.c` | nilai bersarang |
|
|
85
|
+
| `a[0].b`, `a[-1]` | indeks |
|
|
86
|
+
| `a[].b` | semua elemen (hasil list) |
|
|
87
|
+
| `a[type=1].b` | elemen yang cocok filter (hasil list) |
|
|
88
|
+
|
|
89
|
+
Bentuk spec field: `"path"` · `("path", "transform", default)` · `F(path | any_of=[...] | template=... | fn=...,
|
|
90
|
+
default=, transform=, required=)` · `const(x)` · `callable(record)`.
|
|
91
|
+
|
|
92
|
+
Opsi `FieldMapper`: `item_path` (sub-path record di tiap item), `where` / `record_where`
|
|
93
|
+
(`{path: nilai | EXISTS | callable}` atau callable), `unwrap` (membuka pembungkus), `items` boleh
|
|
94
|
+
list path atau callable.
|
|
95
|
+
|
|
96
|
+
Transform bawaan: `int float str bool url lower upper strip clean_text username hashtag iso_utc
|
|
97
|
+
date_id join first count unique drop_empty` dan `each:<transform>` untuk list. Argumen format tanggal:
|
|
98
|
+
`iso_utc:%a %b %d %H:%M:%S %z %Y`. Tambah sendiri: `register_transform("nama", fn)`.
|
|
99
|
+
|
|
100
|
+
Jika logika terlalu rumit untuk mapping, gunakan `F(fn=...)` atau parser Python biasa
|
|
101
|
+
(`parser=` menerima callable apa pun yang mengembalikan list dict).
|
|
102
|
+
|
|
103
|
+
## Hook & aksi halaman
|
|
104
|
+
|
|
105
|
+
`prepare` (setelah halaman terbuka) dan `scroll` (tiap putaran) adalah fungsi `async def (page)`.
|
|
106
|
+
Bawaan: `make_scroller(...)`, `make_wait_for(selector)`, `close_overlays(page, selectors)`.
|
|
107
|
+
`Endpoint.capture_options` meneruskan kwargs tambahan ke `capture` (`poll_interval`, `settle`, dst).
|
|
108
|
+
|
|
109
|
+
## Konfigurasi browser
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from cdp_scraper import CDPConfig, setup_cdp
|
|
113
|
+
setup_cdp(cdp_url="http://localhost:9222", max_sessions=2, lock_timeout=60) # default global
|
|
114
|
+
cfg = CDPConfig.from_env("TWITTER_") # TWITTER_CDP_URL, TWITTER_MAX_CONCURRENT_SESSIONS, ...
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Setiap request memakai **tab baru sendiri** (ditutup saat selesai) dan dibatasi `max_sessions`
|
|
118
|
+
bersamaan per konfigurasi. Konfigurasi yang sama berbagi slot; `cdp_url` berbeda = slot berbeda.
|
|
119
|
+
Screenshot debug disimpan di `CDPConfig.debug_dir` (default folder kerja); matikan dengan
|
|
120
|
+
`debug_screenshots=False`.
|
|
121
|
+
|
|
122
|
+
## Logging
|
|
123
|
+
|
|
124
|
+
Format seragam `[job] Status message: detail`, job = nama platform:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
[2026-11-10 08:19:38] [INFO] [site] Success message: Capture api/search selesai: 20 data
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Library hanya memakai logger `cdp_scraper` (tanpa handler). Aktifkan dengan `setup_logging()`
|
|
131
|
+
atau injeksi logger sendiri: `setup_cdp(custom_logger=...)`. `get_logger("kafka").error("...")`
|
|
132
|
+
untuk log aplikasi dengan format yang sama.
|
|
133
|
+
|
|
134
|
+
## Kafka / Redpanda
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from cdp_scraper import RedpandaProducer, publish_chunks
|
|
138
|
+
producer = RedpandaProducer() # KAFKA_BOOTSTRAP_SERVERS; koneksi dibuat saat dipakai
|
|
139
|
+
if producer.available:
|
|
140
|
+
stats = publish_chunks(producer, "topic-x", keywords, chunk_size=2,
|
|
141
|
+
build_payload=lambda chunk: {"keywords": chunk})
|
|
142
|
+
# {"berhasil": n, "gagal": n}
|
|
143
|
+
```
|
|
144
|
+
Kontrak sumber data (keyword/akun) ada di `cdp_scraper.utils.database` (`KeywordSource`,
|
|
145
|
+
`AccountSource`); implementasinya milik aplikasi.
|
|
146
|
+
|
|
147
|
+
## Pengujian
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
python -m unittest discover -s tests -t .
|
|
151
|
+
```
|
|
152
|
+
Tes memakai browser palsu, jadi tidak butuh Chrome/Kafka.
|
|
153
|
+
|
|
154
|
+
## Perubahan dari 0.1.x (breaking)
|
|
155
|
+
|
|
156
|
+
- Konfigurasi sekarang `CDPConfig` (immutable, per platform); `setup_cdp(...)` tetap ada. Variabel modul `config.CDP_URL` dst dihapus.
|
|
157
|
+
- `get_session_slots` dan `PW_ENGINE` dihapus; pakai `CDPSessionManager` / `manager.engine`.
|
|
158
|
+
- `BrowserBusy` (subclass `TimeoutError`) menggantikan `TimeoutError` polos.
|
|
159
|
+
- `helpers.close_login_modal(page, selectors)` kini wajib diberi selector (tidak ada lagi selector TikTok bawaan).
|
|
160
|
+
- `utils`: tidak lagi mengimpor Flask; `extract_challenges` (khusus TikTok) dihapus; `calculate_engagement_rate` menerima `interaction_keys` / `view_key` (default sama dengan perilaku lama).
|
|
161
|
+
- `utils.database` kini berupa Protocol; `utils.redpanda_client` tidak lagi membuat producer saat import (gunakan `cdp_scraper.messaging`).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.py
|
|
3
|
+
beo_mcdp.egg-info/PKG-INFO
|
|
4
|
+
beo_mcdp.egg-info/SOURCES.txt
|
|
5
|
+
beo_mcdp.egg-info/dependency_links.txt
|
|
6
|
+
beo_mcdp.egg-info/requires.txt
|
|
7
|
+
beo_mcdp.egg-info/top_level.txt
|
|
8
|
+
cdp_scraper/__init__.py
|
|
9
|
+
cdp_scraper/_version.py
|
|
10
|
+
cdp_scraper/config.py
|
|
11
|
+
cdp_scraper/errors.py
|
|
12
|
+
cdp_scraper/logs.py
|
|
13
|
+
cdp_scraper/parsers.py
|
|
14
|
+
cdp_scraper/browser/__init__.py
|
|
15
|
+
cdp_scraper/browser/actions.py
|
|
16
|
+
cdp_scraper/browser/capture.py
|
|
17
|
+
cdp_scraper/browser/diagnostics.py
|
|
18
|
+
cdp_scraper/browser/helpers.py
|
|
19
|
+
cdp_scraper/browser/session.py
|
|
20
|
+
cdp_scraper/browser/state.py
|
|
21
|
+
cdp_scraper/messaging/__init__.py
|
|
22
|
+
cdp_scraper/messaging/redpanda.py
|
|
23
|
+
cdp_scraper/parsing/__init__.py
|
|
24
|
+
cdp_scraper/parsing/mapper.py
|
|
25
|
+
cdp_scraper/parsing/paths.py
|
|
26
|
+
cdp_scraper/parsing/transforms.py
|
|
27
|
+
cdp_scraper/platforms/__init__.py
|
|
28
|
+
cdp_scraper/platforms/registry.py
|
|
29
|
+
cdp_scraper/platforms/runner.py
|
|
30
|
+
cdp_scraper/platforms/spec.py
|
|
31
|
+
cdp_scraper/utils/__init__.py
|
|
32
|
+
cdp_scraper/utils/database.py
|
|
33
|
+
cdp_scraper/utils/redpanda_client.py
|
|
34
|
+
cdp_scraper/utils/utils.py
|
|
35
|
+
tests/test_examples.py
|
|
36
|
+
tests/test_parsing.py
|
|
37
|
+
tests/test_runtime.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cdp_scraper
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from ._version import __version__
|
|
2
|
+
from .browser.actions import close_overlays, make_scroller, make_wait_for, scroll_page_down
|
|
3
|
+
from .browser.capture import CaptureResult, capture, capture_api
|
|
4
|
+
from .browser.session import (CDPSessionManager, default_manager, get_manager,
|
|
5
|
+
is_browser_busy, playwright_session)
|
|
6
|
+
from .config import CDPConfig, get_default_config, set_default_config, setup_cdp
|
|
7
|
+
from .errors import BrowserBusy, CDPError, PageCrashed
|
|
8
|
+
from .logs import JobLogger, get_logger, setup_logging
|
|
9
|
+
from .messaging import RedpandaProducer, chunk_list, publish_chunks
|
|
10
|
+
from .parsing import EXISTS, F, FieldMapper, const, get_path, register_transform
|
|
11
|
+
from .platforms import (Endpoint, Platform, ScrapeResult, Scraper, end_when_empty,
|
|
12
|
+
end_when_falsy, get_platform, list_platforms, register_platform)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"__version__", "CDPConfig", "setup_cdp", "get_default_config", "set_default_config",
|
|
16
|
+
"CDPError", "PageCrashed", "BrowserBusy", "JobLogger", "get_logger", "setup_logging",
|
|
17
|
+
"CDPSessionManager", "get_manager", "default_manager", "playwright_session", "is_browser_busy",
|
|
18
|
+
"capture", "capture_api", "CaptureResult",
|
|
19
|
+
"close_overlays", "make_scroller", "make_wait_for", "scroll_page_down",
|
|
20
|
+
"FieldMapper", "F", "const", "EXISTS", "get_path", "register_transform",
|
|
21
|
+
"Platform", "Endpoint", "Scraper", "ScrapeResult", "register_platform", "get_platform",
|
|
22
|
+
"list_platforms", "end_when_falsy", "end_when_empty",
|
|
23
|
+
"RedpandaProducer", "publish_chunks", "chunk_list",
|
|
24
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
File without changes
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Aksi halaman yang bisa dirakit per platform: tutup overlay, scroll, tunggu elemen."""
|
|
2
|
+
from ..logs import get_logger
|
|
3
|
+
|
|
4
|
+
_SCROLL_JS = """(o) => {
|
|
5
|
+
if (o.window === 'bottom') window.scrollTo(0, document.documentElement.scrollHeight);
|
|
6
|
+
else if (o.window === 'viewport') window.scrollBy(0, window.innerHeight * o.factor);
|
|
7
|
+
if (o.containers) {
|
|
8
|
+
for (const el of document.querySelectorAll('div')) {
|
|
9
|
+
const st = getComputedStyle(el);
|
|
10
|
+
if ((st.overflowY === 'auto' || st.overflowY === 'scroll')
|
|
11
|
+
&& el.scrollHeight > el.clientHeight + 50) {
|
|
12
|
+
el.scrollTop = el.scrollHeight;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (o.pattern) {
|
|
17
|
+
const re = new RegExp(o.pattern, 'i');
|
|
18
|
+
const btn = [...document.querySelectorAll('button, [role="button"]')]
|
|
19
|
+
.find(b => re.test((b.innerText || '').trim()));
|
|
20
|
+
if (btn) btn.click();
|
|
21
|
+
}
|
|
22
|
+
}"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def close_overlays(page, selectors):
|
|
26
|
+
"""Klik tombol penutup popup/modal (selector CSS diberikan platform)."""
|
|
27
|
+
if not selectors:
|
|
28
|
+
return
|
|
29
|
+
try:
|
|
30
|
+
await page.evaluate(
|
|
31
|
+
"(sels) => document.querySelectorAll(sels.join(',')).forEach(b => b.click())",
|
|
32
|
+
list(selectors))
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def scroll_page_down(page):
|
|
38
|
+
await page.keyboard.press("PageDown")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def make_scroller(*, wheel=2500, window_scroll="bottom", viewport_factor=2.0,
|
|
42
|
+
containers=False, click_pattern=None, mouse=(0.5, 0.5), key=None):
|
|
43
|
+
"""Bangun fungsi scroll generik.
|
|
44
|
+
|
|
45
|
+
wheel: delta mouse wheel (0 = nonaktif); window_scroll: 'bottom' | 'viewport' | None;
|
|
46
|
+
containers: scroll semua div overflow; click_pattern: regex teks tombol 'load more'/'retry';
|
|
47
|
+
mouse: posisi kursor (fraksi viewport); key: tombol keyboard tambahan (mis. 'End').
|
|
48
|
+
"""
|
|
49
|
+
options = {"window": window_scroll, "factor": viewport_factor,
|
|
50
|
+
"containers": containers, "pattern": click_pattern}
|
|
51
|
+
use_js = bool(window_scroll or containers or click_pattern)
|
|
52
|
+
|
|
53
|
+
async def _scroll(page):
|
|
54
|
+
if wheel:
|
|
55
|
+
vp = page.viewport_size or {"width": 1280, "height": 800}
|
|
56
|
+
await page.mouse.move(vp["width"] * mouse[0], vp["height"] * mouse[1])
|
|
57
|
+
await page.mouse.wheel(0, wheel)
|
|
58
|
+
if use_js:
|
|
59
|
+
await page.evaluate(_SCROLL_JS, options)
|
|
60
|
+
if key:
|
|
61
|
+
await page.keyboard.press(key)
|
|
62
|
+
|
|
63
|
+
return _scroll
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def make_wait_for(selector, timeout_ms=15000, message=None):
|
|
67
|
+
"""Fungsi prepare: tunggu elemen pertama muncul (hanya warning jika tidak muncul)."""
|
|
68
|
+
async def _wait(page, log=None):
|
|
69
|
+
log = log or get_logger()
|
|
70
|
+
try:
|
|
71
|
+
await page.wait_for_selector(selector, state="attached", timeout=timeout_ms)
|
|
72
|
+
except Exception:
|
|
73
|
+
log.warning(message or f"Elemen '{selector}' belum muncul setelah {timeout_ms // 1000} detik.")
|
|
74
|
+
return _wait
|