Skip to content

Commit ae2b1ea

Browse files
authored
Bdjobs Fixed (#280)
1 parent 53b3b41 commit ae2b1ea

6 files changed

Lines changed: 432 additions & 2 deletions

File tree

jobspy/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pandas as pd
77

88
from jobspy.bayt import BaytScraper
9+
from jobspy.bdjobs import BDJobs
910
from jobspy.glassdoor import Glassdoor
1011
from jobspy.google import Google
1112
from jobspy.indeed import Indeed
@@ -25,6 +26,8 @@
2526
from jobspy.ziprecruiter import ZipRecruiter
2627

2728

29+
# Update the SCRAPER_MAPPING dictionary in the scrape_jobs function
30+
2831
def scrape_jobs(
2932
site_name: str | list[str] | Site | list[Site] | None = None,
3033
search_term: str | None = None,
@@ -60,6 +63,7 @@ def scrape_jobs(
6063
Site.GOOGLE: Google,
6164
Site.BAYT: BaytScraper,
6265
Site.NAUKRI: Naukri,
66+
Site.BDJOBS: BDJobs, # Add BDJobs to the scraper mapping
6367
}
6468
set_logger_level(verbose)
6569
job_type = get_enum_from_value(job_type) if job_type else None
@@ -213,4 +217,10 @@ def worker(site):
213217
by=["site", "date_posted"], ascending=[True, False]
214218
).reset_index(drop=True)
215219
else:
216-
return pd.DataFrame()
220+
return pd.DataFrame()
221+
222+
223+
# Add BDJobs to __all__
224+
__all__ = [
225+
"BDJobs",
226+
]

jobspy/bdjobs/__init__.py

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
#__init__.py
2+
from __future__ import annotations
3+
4+
import random
5+
import time
6+
from datetime import datetime
7+
from typing import Optional, List, Dict, Any
8+
from urllib.parse import urljoin
9+
10+
from bs4 import BeautifulSoup
11+
from bs4.element import Tag
12+
13+
from jobspy.exception import BDJobsException
14+
from jobspy.bdjobs.constant import headers, search_params
15+
from jobspy.bdjobs.util import parse_location, parse_date, find_job_listings, is_job_remote
16+
from jobspy.model import (
17+
JobPost,
18+
Location,
19+
JobResponse,
20+
Country,
21+
Scraper,
22+
ScraperInput,
23+
Site,
24+
DescriptionFormat,
25+
)
26+
from jobspy.util import (
27+
extract_emails_from_text,
28+
create_session,
29+
create_logger,
30+
remove_attributes,
31+
markdown_converter,
32+
)
33+
34+
log = create_logger("BDJobs")
35+
36+
37+
class BDJobs(Scraper):
38+
base_url = "https://jobs.bdjobs.com"
39+
search_url = "https://jobs.bdjobs.com/jobsearch.asp"
40+
delay = 2
41+
band_delay = 3
42+
43+
def __init__(
44+
self, proxies: list[str] | str | None = None, ca_cert: str | None = None
45+
):
46+
"""
47+
Initializes BDJobsScraper with the BDJobs job search url
48+
"""
49+
super().__init__(Site.BDJOBS, proxies=proxies, ca_cert=ca_cert)
50+
self.session = create_session(
51+
proxies=self.proxies,
52+
ca_cert=ca_cert,
53+
is_tls=False,
54+
has_retry=True,
55+
delay=5,
56+
clear_cookies=True,
57+
)
58+
self.session.headers.update(headers)
59+
self.scraper_input = None
60+
self.country = "bangladesh"
61+
62+
def scrape(self, scraper_input: ScraperInput) -> JobResponse:
63+
"""
64+
Scrapes BDJobs for jobs with scraper_input criteria
65+
:param scraper_input:
66+
:return: job_response
67+
"""
68+
self.scraper_input = scraper_input
69+
job_list: list[JobPost] = []
70+
seen_ids = set()
71+
page = 1
72+
request_count = 0
73+
74+
# Set up search parameters
75+
params = search_params.copy()
76+
params["txtsearch"] = scraper_input.search_term
77+
78+
continue_search = lambda: len(job_list) < scraper_input.results_wanted
79+
80+
while continue_search():
81+
request_count += 1
82+
log.info(f"search page: {request_count}")
83+
84+
try:
85+
# Add page parameter if needed
86+
if page > 1:
87+
params["pg"] = page
88+
89+
response = self.session.get(
90+
self.search_url,
91+
params=params,
92+
timeout=getattr(scraper_input, 'request_timeout', 60)
93+
)
94+
95+
# DEBUG: Save the received HTML content
96+
try:
97+
with open("scraper_received_bdjobs.html", "w", encoding="utf-8") as f:
98+
f.write(response.text)
99+
log.info(f"Saved scraper response to scraper_received_bdjobs.html")
100+
except Exception as e_write:
101+
log.error(f"Error writing debug HTML file: {e_write}")
102+
103+
if response.status_code != 200:
104+
log.error(f"BDJobs response status code {response.status_code}")
105+
break
106+
107+
soup = BeautifulSoup(response.text, "html.parser")
108+
job_cards = find_job_listings(soup)
109+
110+
if not job_cards or len(job_cards) == 0:
111+
log.info("No more job listings found")
112+
break
113+
114+
log.info(f"Found {len(job_cards)} job cards on page {page}")
115+
116+
for job_card in job_cards:
117+
try:
118+
job_post = self._process_job(job_card)
119+
if job_post and job_post.id not in seen_ids:
120+
seen_ids.add(job_post.id)
121+
job_list.append(job_post)
122+
123+
if not continue_search():
124+
break
125+
except Exception as e:
126+
log.error(f"Error processing job card: {str(e)}")
127+
128+
page += 1
129+
# Add delay between requests
130+
time.sleep(random.uniform(self.delay, self.delay + self.band_delay))
131+
132+
except Exception as e:
133+
log.error(f"Error during scraping: {str(e)}")
134+
break
135+
136+
job_list = job_list[:scraper_input.results_wanted]
137+
return JobResponse(jobs=job_list)
138+
139+
def _process_job(self, job_card: Tag) -> Optional[JobPost]:
140+
"""
141+
Processes a job card element into a JobPost object
142+
:param job_card: Job card element
143+
:return: JobPost object
144+
"""
145+
try:
146+
# Extract job ID and URL
147+
job_link = job_card.find("a", href=lambda h: h and "jobdetail" in h.lower())
148+
if not job_link:
149+
return None
150+
151+
job_url = job_link.get("href")
152+
if not job_url.startswith("http"):
153+
job_url = urljoin(self.base_url, job_url)
154+
155+
# Extract job ID from URL
156+
job_id = job_url.split("jobid=")[-1].split("&")[0] if "jobid=" in job_url else f"bdjobs-{hash(job_url)}"
157+
158+
# Extract title
159+
title = job_link.get_text(strip=True)
160+
if not title:
161+
title_elem = job_card.find(["h2", "h3", "h4", "strong", "div"], class_=lambda c: c and "job-title-text" in c)
162+
title = title_elem.get_text(strip=True) if title_elem else "N/A"
163+
164+
# Extract company name - IMPROVED
165+
company_elem = job_card.find(["span", "div"], class_=lambda c: c and "comp-name-text" in (c or "").lower())
166+
if company_elem:
167+
company_name = company_elem.get_text(strip=True)
168+
else:
169+
# Try alternative selectors
170+
company_elem = job_card.find(["span", "div"], class_=lambda c: c and any(term in (c or "").lower() for term in ["company", "org", "comp-name"]))
171+
company_name = company_elem.get_text(strip=True) if company_elem else "N/A"
172+
173+
# Extract location
174+
location_elem = job_card.find(["span", "div"], class_=lambda c: c and "locon-text-d" in (c or "").lower())
175+
if not location_elem:
176+
location_elem = job_card.find(["span", "div"], class_=lambda c: c and any(term in (c or "").lower() for term in ["location", "area", "locon"]))
177+
location_text = location_elem.get_text(strip=True) if location_elem else "Dhaka, Bangladesh"
178+
179+
# Create Location object
180+
location = parse_location(location_text, self.country)
181+
182+
# Extract date posted
183+
date_elem = job_card.find(["span", "div"], class_=lambda c: c and any(term in (c or "").lower() for term in ["date", "deadline", "published"]))
184+
date_posted = None
185+
if date_elem:
186+
date_text = date_elem.get_text(strip=True)
187+
date_posted = parse_date(date_text)
188+
189+
# Check if job is remote
190+
is_remote = is_job_remote(title, location=location)
191+
192+
# Create job post object
193+
job_post = JobPost(
194+
id=job_id,
195+
title=title,
196+
company_name=company_name, # Use company_name instead of company
197+
location=location,
198+
date_posted=date_posted,
199+
job_url=job_url,
200+
is_remote=is_remote,
201+
site=self.site,
202+
)
203+
204+
# Always fetch description for BDJobs
205+
job_details = self._get_job_details(job_url)
206+
job_post.description = job_details.get("description", "")
207+
job_post.job_type = job_details.get("job_type", "")
208+
209+
return job_post
210+
except Exception as e:
211+
log.error(f"Error in _process_job: {str(e)}")
212+
return None
213+
214+
def _get_job_details(self, job_url: str) -> Dict[str, Any]:
215+
"""
216+
Gets detailed job information from the job page
217+
:param job_url: Job page URL
218+
:return: Dictionary with job details
219+
"""
220+
try:
221+
response = self.session.get(job_url, timeout=60)
222+
if response.status_code != 200:
223+
return {}
224+
225+
soup = BeautifulSoup(response.text, "html.parser")
226+
227+
# Find job description - IMPROVED based on correct.py
228+
description = ""
229+
230+
# Try to find the job content div first (as in correct.py)
231+
job_content_div = soup.find('div', class_='jobcontent')
232+
if job_content_div:
233+
# Look for responsibilities section
234+
responsibilities_heading = job_content_div.find('h4', id='job_resp') or job_content_div.find(['h4', 'h5'], string=lambda s: s and 'responsibilities' in s.lower())
235+
if responsibilities_heading:
236+
responsibilities_elements = []
237+
# Find all following elements until the next heading or hr
238+
for sibling in responsibilities_heading.find_next_siblings():
239+
if sibling.name in ['hr', 'h4', 'h5']:
240+
break
241+
if sibling.name == 'ul':
242+
responsibilities_elements.extend(li.get_text(separator=' ', strip=True) for li in sibling.find_all('li'))
243+
elif sibling.name == 'p':
244+
responsibilities_elements.append(sibling.get_text(separator=' ', strip=True))
245+
246+
description = "\n".join(responsibilities_elements) if responsibilities_elements else ""
247+
248+
# If no description found yet, try the original approach
249+
if not description:
250+
description_elem = soup.find(["div", "section"], class_=lambda c: c and any(term in (c or "").lower() for term in ["job-description", "details", "requirements"]))
251+
if description_elem:
252+
description_elem = remove_attributes(description_elem)
253+
description = description_elem.prettify(formatter="html")
254+
if hasattr(self.scraper_input, 'description_format') and self.scraper_input.description_format == DescriptionFormat.MARKDOWN:
255+
description = markdown_converter(description)
256+
257+
# Extract job type
258+
job_type_elem = soup.find(["span", "div"], string=lambda s: s and any(term in (s or "").lower() for term in ["job type", "employment type"]))
259+
job_type = None
260+
if job_type_elem:
261+
job_type_text = job_type_elem.find_next(["span", "div"]).get_text(strip=True)
262+
job_type = job_type_text if job_type_text else None
263+
264+
# Extract company industry
265+
industry_elem = soup.find(["span", "div"], string=lambda s: s and "industry" in (s or "").lower())
266+
company_industry = None
267+
if industry_elem:
268+
industry_text = industry_elem.find_next(["span", "div"]).get_text(strip=True)
269+
company_industry = industry_text if industry_text else None
270+
271+
return {
272+
"description": description,
273+
"job_type": job_type,
274+
"company_industry": company_industry
275+
}
276+
277+
except Exception as e:
278+
log.error(f"Error getting job details: {str(e)}")
279+
return {}

jobspy/bdjobs/constant.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#constant.py
2+
# Headers for BDJobs requests
3+
headers = {
4+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
5+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
6+
"Accept-Language": "en-US,en;q=0.5",
7+
"Connection": "keep-alive",
8+
"Referer": "https://jobs.bdjobs.com/",
9+
"Cache-Control": "max-age=0",
10+
}
11+
12+
# Search parameters that work best for BDJobs
13+
search_params = {
14+
"hidJobSearch": "jobsearch",
15+
}
16+
17+
# Selectors for job listings
18+
job_selectors = [
19+
"div.job-item", # Catches both normal and premium job cards, as well as other types
20+
"div.sout-jobs-wrapper", # Catches job listings in the main search results page
21+
"div.norm-jobs-wrapper", # Catches normal job listings
22+
"div.featured-wrap", # Catches featured job listings
23+
]
24+
25+
# Date formats used by BDJobs
26+
date_formats = [
27+
"%d %b %Y",
28+
"%d-%b-%Y",
29+
"%d %B %Y",
30+
"%B %d, %Y",
31+
"%d/%m/%Y",
32+
]

0 commit comments

Comments
 (0)