crawling-datas / linkedin.py
dimdimm's picture
feat: upgrade scraper - multi keyword, --once mode, GitHub Actions workflow
e84ab4f
Raw History Blame Contribute Delete
4.72 kB
import logging
from bs4 import BeautifulSoup
from urllib.parse import urljoin
from base import goto_with_retry, human_delay, jitter_scroll, short_delay
from browser import get_browser
from extract import extract_contact, extract_job_type, extract_salary
from skills_keyword import extract_skills
log = logging.getLogger(__name__)
def scrape_linkedin(keyword="software engineer", max_jobs=25):
p, browser, context = get_browser()
jobs = []
try:
page = context.new_page()
base_url = "https://www.linkedin.com"
url = f"{base_url}/jobs/search/?keywords={keyword}&location=Indonesia"
if not goto_with_retry(page, url):
log.warning("[LinkedIn] Gagal memuat halaman pencarian: %s", url)
return jobs
human_delay()
jitter_scroll(page)
human_delay()
try:
page.wait_for_selector("div.base-card", timeout=30000)
except Exception:
log.warning("[LinkedIn] Kartu lowongan tidak muncul untuk keyword: %s", keyword)
soup = BeautifulSoup(page.content(), "html.parser")
cards = soup.select("div.base-card")
for card in cards:
if len(jobs) >= max_jobs:
break
try:
title_el = card.select_one(
"h3.base-search-card__title, "
"span.screen-reader-text, "
"h3"
)
company_el = card.select_one(
"h4.base-search-card__subtitle, "
"a.hidden-nested-link, "
"h4"
)
location_el = card.select_one(
"span.job-search-card__location, "
"span[class*='location']"
)
link_el = card.select_one("a.base-card__full-link, a[href*='/jobs/view/']")
if not (title_el and company_el and link_el):
continue
href = link_el.get("href", "")
if href.startswith("http"):
job_url = href.split("?")[0]
else:
job_url = urljoin(base_url, href.split("?")[0])
location_text = location_el.text.strip() if location_el else ""
description = ""
salary = None
job_type = "Full-time"
contact_info = None
try:
detail_page = context.new_page()
detail_page.goto(job_url, wait_until="domcontentloaded", timeout=15000)
short_delay()
detail_soup = BeautifulSoup(detail_page.content(), "html.parser")
desc_el = detail_soup.select_one(
"div.description__text, "
"div[class*='description'], "
"section[class*='description']"
)
if desc_el:
description = desc_el.text.strip()
page_text = detail_soup.get_text(" ", strip=True)
salary = extract_salary(description or page_text)
job_type = extract_job_type(page_text)
contact_info = extract_contact(description or page_text)
if not location_text:
loc_el = detail_soup.select_one(
"div[class*='location'], "
"span[class*='location']"
)
if loc_el:
location_text = loc_el.text.strip()
detail_page.close()
except Exception as e:
log.debug("[LinkedIn] Gagal ambil detail %s: %s", job_url, e)
if not location_text:
location_text = "Indonesia"
jobs.append({
"title": title_el.text.strip(),
"company": company_el.text.strip(),
"location": location_text,
"type": job_type,
"salary": salary,
"url": job_url,
"description": description,
"skills": extract_skills(description),
"contact_info": contact_info,
})
except Exception as e:
log.debug("[LinkedIn] Gagal proses kartu: %s", e)
continue
finally:
try:
browser.close()
except Exception:
pass
p.stop()
log.info("[LinkedIn] Keyword '%s' menghasilkan %d lowongan", keyword, len(jobs))
return jobs