Pipeline untuk mencari URL adengan memanfaatkan teknik google dorking, menyimpan hasil ke JSON, dan mengambil isi konten halaman dengan Playwright.
Entry point utama sekarang adalah:
python scripts/get_url.pyScript ini menjalankan pencarian Google, menyimpan URL yang lolos filter, lalu secara acak dapat mengambil isi konten dari URL pending di pipeline yang sama.
url-discovery/
├── .env.example
├── config.py
├── research_config.example.json
├── requirements.txt
├── scripts/
│ ├── get_url.py
│ ├── get_content.py
│ ├── build_dataset.py
│ └── setup_browser_profile.py
├── services/
│ ├── browser_guard.py
│ ├── content_service.py
│ ├── dataset_service.py
│ ├── json_service.py
│ ├── query_service.py
│ ├── search_service.py
│ └── url_service.py
└── data/
├── blacklist.json
├── contents.json
├── datasets/
│ ├── dataset_<timestamp>.json
│ └── index.json
├── search_state.json
└── urls.json
pip install -r requirements.txt
python -m playwright install chromiumJalankan setup profil sebelum scraping:
python scripts/setup_browser_profile.pyChromium akan terbuka dengan profil persistent di data/browser_profile. Login akun Google, selesaikan consent/captcha jika muncul, lalu tekan Enter di terminal. Profil ini dipakai ulang oleh get_url.py dan get_content.py.
python scripts/get_url.pyAlur saat ini:
- Membuat kombinasi query pendek dari
LOCATIONS,TOPIC_GROUPS,ISSUE_GROUPS,PRIORITY_DORKS,SOCIAL_DOMAINS, danKNOWN_ENTITIES. - Query yang sudah
donedidata/search_state.jsondilewati, kecualiRESEARCH_COMPLETED_QUERIES=True. - Jika
RANDOMIZE_QUERY_ORDER=True, urutan query diacak. - Chromium dibuka dengan persistent profile.
- Tab Chromium pertama langsung dipakai untuk query pertama, tanpa membuka tab blank tambahan.
- Hasil Google dinormalisasi, difilter, dideduplikasi, lalu disimpan ke
data/urls.json. - URL yang lolos filter juga dapat dikirim ke background extractor HTTP strict berbasis
httpx.AsyncClient+ BeautifulSoup. - Dari hasil yang sedang tampil di halaman Google, pipeline bisa memilih URL yang lolos filter secara acak untuk ekstraksi konten Playwright.
- Konten Playwright dibuka dengan klik anchor hasil Google ke tab baru, bukan dengan membuka canonical URL langsung dari file JSON.
- Saat mengambil konten, tab konten dibiarkan standby seperti sedang membaca, melakukan scroll kecil acak, lalu ditutup.
- Setelah tab konten ditutup, fokus kembali ke tab Google dork.
- Jika masih ada halaman hasil berikutnya, pagination/query berikutnya dijalankan dari tab Google tersebut.
- Jika captcha terdeteksi, proses berhenti dan alasan captcha disimpan di state/output terkait.
Jika data/datasets/index.json sudah ada dan SKIP_BUILT_DATASET_ITEMS=true, get_url.py melewati query yang sudah dibuild serta URL canonical yang sudah pernah masuk dataset.
Konfigurasi tidak lagi ditulis langsung sebagai konstanta besar di config.py.
.envberisi konstanta runtime: tanggal, delay, limit, timeout, path data, dan toggle pipeline.research_config.jsonberisi data riset: lokasi, topik, isu, domain prioritas, entitas, blacklist domain, tracking params, dan keyword filter.config.pyhanya membaca.envdan JSON, lalu mengekspos nama konstanta yang dipakai service lain.
File aktif .env dan research_config.json masuk .gitignore. Template yang bisa dilacak repo adalah .env.example dan research_config.example.json.
Konfigurasi runtime utama di .env:
RESEARCH_CONFIG_FILE="research_config.json"
DATA_DIR="data"
DATASETS_DIR="data/datasets"
DATASET_INDEX_FILE="data/datasets/index.json"
SKIP_BUILT_DATASET_ITEMS=true
START_DATE="2024-01-01"
END_DATE="2026-06-26"
MAX_PAGES_PER_QUERY=1
MAX_QUERIES_PER_RUN=0
RANDOMIZE_QUERY_ORDER=true
SHORT_QUERY_MODE=true
MAX_GOOGLE_QUERY_LENGTH=180
SEARCH_QUERY_DELAY_MIN_SECONDS=18
SEARCH_QUERY_DELAY_MAX_SECONDS=60
ROTATE_USER_AGENTS=false
RELOAD_FAILED_QUERIES=false
HTTP_CONTENT_ENABLED=true
HTTP_CONTENT_MAX_WORKERS=2
HTTP_CONTENT_TIMEOUT_SECONDS=20
HTTP_CONTENT_MAX_BYTES=750000
HTTP_CONTENT_MIN_TEXT_LENGTH=300
HTTP_CONTENT_REQUIRE_TITLE=true
HTTP_CONTENT_REQUIRE_QUERY_OVERLAP=true
HTTP_CONTENT_MIN_QUERY_TERM_OVERLAP=2
HTTP_CONTENT_REQUIRE_RESULT_TITLE_OVERLAP=true
HTTP_CONTENT_MIN_RESULT_TITLE_OVERLAP=1Data riset utama di research_config.json:
{
"search_label": "SHS/PLTS Kalimantan Barat",
"primary_location": "Kalimantan Barat",
"stable_topic_terms": ["PLTS", "panel surya", "tenaga surya"],
"locations": ["Kalimantan Barat", "Kalbar", "Bengkayang"],
"topic_groups": {
"household_solar": ["Solar Home System", "panel surya rumah"],
"village_solar": ["PLTS desa", "PLTS komunal"]
},
"issue_groups": {
"experience": ["warga", "pengalaman", "keluhan warga"],
"benefit": ["manfaat", "penghematan"],
"problem": ["rusak", "tidak berfungsi"]
},
"priority_dorks": ["site:kalbar.antaranews.com", "site:kompas.com"],
"social_domains": ["youtube.com/watch", "facebook.com"],
"known_entities": ["Pulau Lemukutan", "Desa Temajuk"],
"excluded_domains": ["shopee.co.id", "tokopedia.com"],
"tracking_params": ["utm_source", "utm_medium", "fbclid"],
"url_required_keywords": ["panel surya", "plts", "energi terbarukan"]
}Jika research_config.json belum ada, config.py otomatis memakai research_config.example.json.
SHORT_QUERY_MODE=True membuat query Google dipecah menjadi banyak kombinasi pendek. Query tidak lagi memakai OR (...) panjang untuk semua sinonim sekaligus. MAX_GOOGLE_QUERY_LENGTH menjadi batas aman agar query yang terlalu panjang tidak dikirim.
Contoh bentuk query pendek:
"panel surya rumah" "Kalimantan Barat" after:2024-01-01 before:2026-06-27
site:kalbar.antaranews.com "PLTS desa" "Kalimantan Barat" after:2024-01-01 before:2026-06-27
"PLTS" "keluhan" "Bengkayang" after:2024-01-01 before:2026-06-27
MAX_QUERIES_PER_RUN=5 berarti satu run hanya mengambil maksimal 5 query pending. Jika ingin menjalankan semua query pending dalam satu run, set nilainya ke 0.
Pengambilan konten acak di get_url.py dikontrol oleh:
PIPELINE_CONTENT_ENABLED = True
PIPELINE_CONTENT_CHANCE = 0.95
PIPELINE_CONTENT_CONTINUE_CHANCE = 0.80
PIPELINE_MAX_CONTENT_BURST = 2
PIPELINE_CONTENT_DELAY_MIN_SECONDS = 8
PIPELINE_CONTENT_DELAY_MAX_SECONDS = 20Nilai PIPELINE_CONTENT_CHANCE dan PIPELINE_CONTENT_CONTINUE_CHANCE memakai skala 0.0 sampai < 1.0, sesuai random.random(). Contoh: 0.95 berarti sekitar 95%.
Kandidat konten hanya berasal dari hasil yang sedang terlihat di halaman Google dork dan:
- tidak masuk blacklist;
content_statusbelumsuccess;content_statusbelumpdf_skipped.
Karena konten diambil lewat klik hasil Google, URL lama yang ada di data/urls.json tetapi tidak tampil pada halaman Google saat ini tidak dipilih oleh pipeline utama. Untuk memproses backlog dari file, gunakan python scripts/get_content.py.
Selain ekstraksi Playwright acak, get_url.py dapat menjalankan ekstraksi latar belakang untuk setiap URL yang sudah lolos UrlService.merge_result(). Mode ini memakai httpx.AsyncClient untuk mengambil HTML dan BeautifulSoup untuk parsing. Worker berjalan di background, tetapi hasilnya tetap digabung dan disimpan dari main thread sehingga penulisan data/urls.json, data/contents.json, dan data/search_state.json tetap tersentral.
Mode ini bersifat oportunistik dan konservatif. Hasil HTTP hanya masuk ke data/contents.json jika semua guard strict lolos:
- HTTP status 2xx;
content-typeHTML;- redirect tetap ke host yang sama setelah normalisasi
www; - title wajib ada dan cukup panjang;
- halaman tidak terlihat seperti captcha, block page, login challenge, atau JavaScript/cookie wall;
- teks cukup panjang;
- isi memuat salah satu
URL_REQUIRED_KEYWORDS; - isi memiliki overlap minimum dengan query Google;
- isi memiliki overlap minimum dengan title hasil Google.
Jika guard menolak halaman, content_status tidak diubah menjadi failed. URL tetap bisa diproses oleh Playwright, baik dari pipeline utama maupun python scripts/get_content.py.
Hasil Playwright tetap diprioritaskan. Jika contents.json sudah memiliki status="success", hasil HTTP tidak menimpa record tersebut. Jika Playwright kemudian berhasil untuk URL yang sama, record Playwright akan menggantikan record HTTP.
Konfigurasi mode HTTP strict:
HTTP_CONTENT_ENABLED=true
HTTP_CONTENT_MAX_WORKERS=2
HTTP_CONTENT_TIMEOUT_SECONDS=20
HTTP_CONTENT_MAX_BYTES=750000
HTTP_CONTENT_MIN_TEXT_LENGTH=300
HTTP_CONTENT_REQUIRE_TITLE=true
HTTP_CONTENT_MIN_TITLE_LENGTH=8
HTTP_CONTENT_REQUIRE_QUERY_OVERLAP=true
HTTP_CONTENT_MIN_QUERY_TERM_OVERLAP=2
HTTP_CONTENT_REQUIRE_RESULT_TITLE_OVERLAP=true
HTTP_CONTENT_MIN_RESULT_TITLE_OVERLAP=1
HTTP_CONTENT_USER_AGENT=""Jika dependency httpx atau beautifulsoup4 belum tersedia, get_url.py akan menonaktifkan mode HTTP background dan tetap menjalankan pipeline Playwright normal.
URL yang ditemukan tidak langsung disimpan semuanya. UrlService hanya menerima hasil yang:
- bisa dinormalisasi menjadi URL HTTP/HTTPS;
- bukan domain di
EXCLUDED_DOMAINS; - mengandung salah satu
URL_REQUIRED_KEYWORDS; - belum menjadi duplikat canonical URL.
Pengecekan URL_REQUIRED_KEYWORDS tidak case sensitive dan dilakukan terhadap title, snippet, URL asli, serta canonical URL.
services/browser_guard.py mendeteksi captcha dari:
- URL marker seperti
/sorry/,captcha, ataurecaptcha; - elemen captcha/recaptcha/hcaptcha;
- title halaman;
- teks seperti unusual traffic atau verifikasi robot.
Stop otomatis hanya berlaku jika captcha muncul pada halaman Google setelah query dijalankan. Captcha, challenge, atau halaman verifikasi di situs konten/non-Google tidak menghentikan pipeline.
Jika captcha Google Search muncul:
- proses pencarian langsung berhenti;
- query dianggap gagal;
- alasan disimpan sebagai
captcha_reason; - URL halaman captcha disimpan sebagai
captcha_url; - title halaman disimpan sebagai
captcha_title; - jalankan ulang setup profil:
python scripts/setup_browser_profile.pyBerisi URL unik yang lolos filter:
urlcanonical_urldomaintitlesnippetpublished_date_hintis_blacklistedfirst_found_atlast_found_atfound_bycontent_statuscontent_checked_atcontent_lengthcontent_errorcaptcha_reasoncaptcha_urlcaptcha_titlehttp_content_checked_athttp_content_methodhttp_content_statushttp_content_reject_reason
Field konten/captcha hanya muncul ketika sudah relevan.
Menyimpan status per query:
pendingrunningdonefailed
State juga menyimpan jumlah halaman selesai, jumlah hasil, jumlah URL unik baru, hasil yang terfilter, error, dan alasan captcha jika ada.
Bagian meta.checkpoint menyimpan posisi terakhir:
current_query_idcurrent_querycurrent_pagelast_query_idlast_querylast_statuslast_pagecurrent_content_urllast_content_urllast_content_status
Jika proses berhenti di tengah query, run berikutnya melanjutkan dari pages_done + 1 untuk query berstatus running.
Berisi hasil ekstraksi konten:
canonical_urlsource_urlextracted_atstatushttp_statuscontent_typefinal_urltitledescriptionpublished_attextextraction_methodguarderrorcaptcha_reasoncaptcha_urlcaptcha_title
Status konten dapat berupa pending, running, success, too_short, pdf_skipped, atau failed.
Dibuat oleh python scripts/get_content.py untuk checkpoint ekstraksi konten terpisah. File ini menyimpan:
total_candidatescompleted_urlsfailed_urlscheckpoint.current_urlcheckpoint.current_indexcheckpoint.last_urlcheckpoint.last_status
get_content.py mengambil URL dengan content_status="pending" atau content_status="running". Status running diproses ulang agar proses yang mati paksa bisa dilanjutkan. URL yang sudah success, too_short, pdf_skipped, atau failed tidak diproses ulang otomatis. Untuk mengulang URL tertentu, ubah statusnya kembali ke pending.
Dibuat otomatis saat terjadi kegagalan di get_url.py atau get_content.py. File ini append-only dan berisi event analitik untuk investigasi:
failed_atprocess(get_urlatauget_content)stage(search_error,search_captcha,pipeline_content_error,content_error, dan sejenisnya)error_typeerrorcontext.query_run_indexcontext.querycontext.pagecontext.urlcontext.burst_indexcontext.content_extract_count_beforecontext.content_delay_secondscheckpointtotals.unique_urlstotals.contentstotals.query_status_countstotals.content_status_counts
Berisi URL yang tidak perlu diproses sebagai konten baru. Jika muncul lagi dari hasil pencarian, URL tetap dapat dicatat tetapi diberi is_blacklisted: true.
Dibuat oleh:
python scripts/build_dataset.pyFile ini adalah output siap konsumsi. Bagian records menggabungkan metadata URL dari data/urls.json, provenance found_by, dan hasil konten dari data/contents.json pada field content. Bagian queries menyimpan query dari data/search_state.json yang statusnya sudah selesai diproses dan bukan failed.
Index dataset historis yang dipakai run berikutnya untuk mencegah data lama dipakai ulang:
canonical_urlsquery_idsqueriesdatasets
Jika SKIP_BUILT_DATASET_ITEMS=true, get_url.py melewati query/URL dari index ini dan get_content.py melewati URL yang sudah masuk dataset.
Pipeline utama mengambil konten lewat klik hasil yang sedang tampil di Google. Jika ingin menjalankan ekstraksi konten dari URL yang sudah ada di file:
python scripts/get_content.pyScript ini membaca data/urls.json, memproses URL yang belum sukses atau belum pdf_skipped, lalu menyimpan hasil ke data/contents.json. Jika dataset index sudah ada, URL yang sudah masuk dataset dilewati.
Setelah URL dan konten terkumpul, build dataset konsumsi akhir:
python scripts/build_dataset.pyScript ini akan:
- membaca
data/urls.json,data/contents.json, dandata/search_state.json; - membuat
data/datasets/dataset_<timestamp>.json; - memperbarui
data/datasets/index.json; - mereset
data/search_state.json,data/urls.json, dandata/contents.json.
Jika ingin membuat dataset tanpa reset runtime:
python scripts/build_dataset.py --no-resetfailure_analysis.json dan content_state.json tidak direset oleh script ini.
Secara default query berstatus done tidak diulang.
Untuk mengulang query yang sudah selesai:
RESEARCH_COMPLETED_QUERIES=trueUntuk mengulang query yang gagal:
RETRY_FAILED_QUERIES=trueUntuk memprioritaskan query yang gagal terlebih dahulu sebelum lanjut ke alur normal:
RELOAD_FAILED_QUERIES=trueSaat aktif, query failed dijalankan dulu. Setelah query gagal habis, script lanjut ke query normal berdasarkan state terakhir. Kembalikan RELOAD_FAILED_QUERIES=false setelah selesai jika tidak ingin failed selalu diprioritaskan.
Ubah data riset di research_config.json:
locationstopic_groupsissue_groupspriority_dorkssocial_domainsknown_entitiesurl_required_keywordsexcluded_domains
Ubah konstanta runtime di .env:
SHORT_QUERY_MODEMAX_GOOGLE_QUERY_LENGTHPIPELINE_CONTENT_*SEARCH_QUERY_DELAY_*