dimdimm commited on
Commit
079f46c
·
0 Parent(s):

inisialisasi projek

Browse files
Files changed (12) hide show
  1. .env.example +2 -0
  2. .gitignore +9 -0
  3. base.py +11 -0
  4. browser.py +18 -0
  5. cleaner.py +20 -0
  6. jobstreet.py +41 -0
  7. linkedin.py +42 -0
  8. normalize.py +9 -0
  9. readme.md +59 -0
  10. requirements.txt +5 -0
  11. run.py +56 -0
  12. supabase_client.py +16 -0
.env.example ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ SUPABASE_URL=your_supabase_url_here
2
+ SUPABASE_KEY=your_supabase_anon_key_here
.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.pyc
4
+
5
+ # Local configuration and secrets
6
+ .env
7
+
8
+ # VS Code settings
9
+ .vscode/
base.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ import time
3
+
4
+ def human_delay(min_sec=2, max_sec=6):
5
+ time.sleep(random.uniform(min_sec, max_sec))
6
+
7
+
8
+ def jitter_scroll(page):
9
+ page.evaluate("""
10
+ window.scrollTo(0, document.body.scrollHeight / 2);
11
+ """)
browser.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from playwright.sync_api import sync_playwright
2
+
3
+ def get_browser():
4
+ p = sync_playwright().start()
5
+
6
+ browser = p.chromium.launch(
7
+ headless=True,
8
+ args=[
9
+ "--disable-blink-features=AutomationControlled"
10
+ ]
11
+ )
12
+
13
+ context = browser.new_context(
14
+ user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36",
15
+ viewport={"width": 1280, "height": 800}
16
+ )
17
+
18
+ return p, browser, context
cleaner.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def clean_job(job):
2
+ company = job.get("company")
3
+ title = job.get("title")
4
+ url = job.get("url")
5
+
6
+ # safety check
7
+ if not company or not title or not url:
8
+ return None
9
+
10
+ company = company.strip()
11
+ title = title.strip()
12
+ url = url.strip()
13
+
14
+ if len(company) < 2 or len(title) < 5:
15
+ return None
16
+
17
+ if not url.startswith("http"):
18
+ return None
19
+
20
+ return job
jobstreet.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from bs4 import BeautifulSoup
2
+ from browser import get_browser
3
+ from base import human_delay, jitter_scroll
4
+
5
+
6
+ def scrape_jobstreet(keyword="developer"):
7
+ p, browser, context = get_browser()
8
+ page = context.new_page()
9
+
10
+ url = f"https://www.jobstreet.co.id/id/job-search/{keyword}-jobs/"
11
+
12
+ page.goto(url, wait_until="domcontentloaded")
13
+ human_delay()
14
+
15
+ jitter_scroll(page)
16
+ human_delay()
17
+
18
+ soup = BeautifulSoup(page.content(), "html.parser")
19
+
20
+ jobs = []
21
+
22
+ cards = soup.select("article")
23
+
24
+ for card in cards:
25
+ try:
26
+ title = card.select_one("a")
27
+ company = card.select_one("span")
28
+
29
+ if title:
30
+ jobs.append({
31
+ "title": title.text.strip(),
32
+ "company": company.text.strip() if company else "",
33
+ "url": title["href"]
34
+ })
35
+ except:
36
+ continue
37
+
38
+ browser.close()
39
+ p.stop()
40
+
41
+ return jobs
linkedin.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from bs4 import BeautifulSoup
2
+ from browser import get_browser
3
+ from base import human_delay, jitter_scroll
4
+
5
+
6
+ def scrape_linkedin(keyword="software engineer"):
7
+ p, browser, context = get_browser()
8
+ page = context.new_page()
9
+
10
+ url = f"https://www.linkedin.com/jobs/search/?keywords={keyword}"
11
+
12
+ page.goto(url, wait_until="domcontentloaded")
13
+ human_delay()
14
+
15
+ jitter_scroll(page)
16
+ human_delay()
17
+
18
+ soup = BeautifulSoup(page.content(), "html.parser")
19
+
20
+ jobs = []
21
+
22
+ cards = soup.select("li")
23
+
24
+ for card in cards:
25
+ try:
26
+ title = card.select_one("h3")
27
+ company = card.select_one("h4")
28
+ link = card.select_one("a")
29
+
30
+ if title and company and link:
31
+ jobs.append({
32
+ "title": title.text.strip(),
33
+ "company": company.text.strip(),
34
+ "url": "https://www.linkedin.com" + link["href"]
35
+ })
36
+ except:
37
+ continue
38
+
39
+ browser.close()
40
+ p.stop()
41
+
42
+ return jobs
normalize.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ def normalize(job, source):
2
+ return {
3
+ "nama_perusahaan": job.get("company"),
4
+ "posisi_jabatan": job.get("title"),
5
+ "deskripsi_kualifikasi": job.get("title"),
6
+ "link_pendaftaran": job.get("url"),
7
+ "sumber_data": source,
8
+ "is_active": True
9
+ }
readme.md ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Job Scraper
2
+
3
+ Proyek ini adalah scraper lowongan kerja yang mengumpulkan data dari LinkedIn dan JobStreet, membersihkannya, lalu menyimpannya ke database Supabase.
4
+
5
+ ## Fitur
6
+
7
+ - Scraping lowongan dari **LinkedIn** dan **JobStreet**
8
+ - Automasi browser menggunakan Playwright
9
+ - Pembersihan dan normalisasi data sebelum disimpan
10
+ - Penyimpanan data ke Supabase dengan mekanisme upsert
11
+ - Penjadwalan scraping setiap 24 jam
12
+
13
+ ## Setup
14
+
15
+ 1. Clone repository ini
16
+ 2. Buat virtual environment (disarankan Python 3.10+):
17
+
18
+ python -m venv venv
19
+ venv\Scripts\activate
20
+
21
+ 3. Install dependensi:
22
+
23
+ pip install -r requirements.txt
24
+
25
+ 4. Instal browser untuk Playwright:
26
+
27
+ playwright install chromium
28
+
29
+ 5. Buat file `.env` di direktori proyek:
30
+
31
+ SUPABASE_URL=your_supabase_url
32
+ SUPABASE_ANON_KEY=your_supabase_anon_key
33
+
34
+ 6. Jalankan scraper:
35
+
36
+ python run.py
37
+
38
+ ## Struktur Modul
39
+
40
+ - `run.py` - Entry point, menjadwalkan scraping dan menyimpan data
41
+ - `linkedin.py` - Scraping lowongan dari LinkedIn
42
+ - `jobstreet.py` - Scraping lowongan dari JobStreet
43
+ - `browser.py` - Konfigurasi Playwright (Chromium headless)
44
+ - `base.py` - Helper delay dan scroll untuk simulasi manusia
45
+ - `normalize.py` - Normalisasi format data sebelum disimpan
46
+ - `cleaner.py` - Validasi dan pembersihan data yang tidak valid
47
+ - `supabase_client.py` - Koneksi ke Supabase
48
+
49
+ ## Database
50
+
51
+ Pastikan tabel `lowongan_kerja` sudah ada di Supabase dengan kolom:
52
+ - `nama_perusahaan`
53
+ - `posisi_jabatan`
54
+ - `deskripsi_kualifikasi`
55
+ - `link_pendaftaran`
56
+ - `sumber_data`
57
+ - `is_active`
58
+
59
+ Kolom `link_pendaftaran` digunakan sebagai konflik untuk upsert.
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ beautifulsoup4==4.12.3
2
+ playwright==1.40.0
3
+ python-dotenv==1.0.1
4
+ schedule==1.2.2
5
+ supabase==2.3.4
run.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from linkedin import scrape_linkedin
2
+ from jobstreet import scrape_jobstreet
3
+ from normalize import normalize
4
+ from cleaner import clean_job
5
+ from supabase_client import supabase
6
+ import time
7
+ import schedule
8
+
9
+
10
+ def push(jobs, source):
11
+ batch = []
12
+ seen = set()
13
+
14
+ for job in jobs:
15
+
16
+ # CLEANING STEP
17
+ job = clean_job(job)
18
+ if not job:
19
+ continue
20
+
21
+ url = job["url"]
22
+
23
+ if url in seen:
24
+ continue
25
+ seen.add(url)
26
+
27
+ batch.append(normalize(job, source))
28
+
29
+ if batch:
30
+ supabase.table("lowongan_kerja") \
31
+ .upsert(batch, on_conflict="link_pendaftaran") \
32
+ .execute()
33
+
34
+ return len(batch)
35
+
36
+
37
+ def main():
38
+ try:
39
+ print("Scraping LinkedIn...")
40
+ linkedin_jobs = scrape_linkedin()
41
+ print("LinkedIn inserted:", push(linkedin_jobs, "LinkedIn"))
42
+
43
+ print("Scraping JobStreet...")
44
+ jobstreet_jobs = scrape_jobstreet()
45
+ print("JobStreet inserted:", push(jobstreet_jobs, "JobStreet"))
46
+ except Exception as e:
47
+ print(f"Error during scraping: {e}")
48
+
49
+
50
+ if __name__ == "__main__":
51
+ schedule.every(24).hours.do(main)
52
+
53
+ print("Job scraper running every 24 hours...")
54
+ while True:
55
+ schedule.run_pending()
56
+ time.sleep(60) # Check every minute
supabase_client.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dotenv import load_dotenv
2
+ import os
3
+ from supabase import create_client
4
+
5
+ load_dotenv()
6
+
7
+ def get_supabase():
8
+ url = os.getenv("SUPABASE_URL")
9
+ key = os.getenv("SUPABASE_ANON_KEY")
10
+
11
+ if not url or not key:
12
+ raise ValueError("Missing Supabase env")
13
+
14
+ return create_client(url, key)
15
+
16
+ supabase = get_supabase()