python-jobspy-damarowen 1.2.0__py3-none-any.whl
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.
- jobspy/__init__.py +229 -0
- jobspy/bayt/__init__.py +145 -0
- jobspy/bdjobs/__init__.py +353 -0
- jobspy/bdjobs/constant.py +32 -0
- jobspy/bdjobs/util.py +100 -0
- jobspy/exception.py +50 -0
- jobspy/glassdoor/__init__.py +322 -0
- jobspy/glassdoor/constant.py +184 -0
- jobspy/glassdoor/util.py +42 -0
- jobspy/google/__init__.py +202 -0
- jobspy/google/constant.py +52 -0
- jobspy/google/util.py +41 -0
- jobspy/indeed/__init__.py +260 -0
- jobspy/indeed/constant.py +109 -0
- jobspy/indeed/util.py +83 -0
- jobspy/jobstreet/__init__.py +254 -0
- jobspy/jobstreet/constant.py +16 -0
- jobspy/jobstreet/util.py +50 -0
- jobspy/linkedin/__init__.py +345 -0
- jobspy/linkedin/constant.py +8 -0
- jobspy/linkedin/util.py +96 -0
- jobspy/model.py +336 -0
- jobspy/naukri/__init__.py +304 -0
- jobspy/naukri/constant.py +11 -0
- jobspy/naukri/util.py +38 -0
- jobspy/util.py +363 -0
- jobspy/ziprecruiter/__init__.py +219 -0
- jobspy/ziprecruiter/constant.py +29 -0
- jobspy/ziprecruiter/util.py +31 -0
- python_jobspy_damarowen-1.2.0.dist-info/METADATA +288 -0
- python_jobspy_damarowen-1.2.0.dist-info/RECORD +33 -0
- python_jobspy_damarowen-1.2.0.dist-info/WHEEL +4 -0
- python_jobspy_damarowen-1.2.0.dist-info/licenses/LICENSE +21 -0
jobspy/__init__.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
4
|
+
from typing import Tuple
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from jobspy.bayt import BaytScraper
|
|
9
|
+
from jobspy.bdjobs import BDJobs
|
|
10
|
+
from jobspy.glassdoor import Glassdoor
|
|
11
|
+
from jobspy.google import Google
|
|
12
|
+
from jobspy.indeed import Indeed
|
|
13
|
+
from jobspy.linkedin import LinkedIn
|
|
14
|
+
from jobspy.naukri import Naukri
|
|
15
|
+
from jobspy.model import JobType, Location, JobResponse, Country
|
|
16
|
+
from jobspy.model import SalarySource, ScraperInput, Site
|
|
17
|
+
from jobspy.util import (
|
|
18
|
+
set_logger_level,
|
|
19
|
+
extract_salary,
|
|
20
|
+
create_logger,
|
|
21
|
+
get_enum_from_value,
|
|
22
|
+
map_str_to_site,
|
|
23
|
+
convert_to_annual,
|
|
24
|
+
desired_order,
|
|
25
|
+
)
|
|
26
|
+
from jobspy.ziprecruiter import ZipRecruiter
|
|
27
|
+
from jobspy.jobstreet import JobStreet
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Update the SCRAPER_MAPPING dictionary in the scrape_jobs function
|
|
31
|
+
|
|
32
|
+
def scrape_jobs(
|
|
33
|
+
site_name: str | list[str] | Site | list[Site] | None = None,
|
|
34
|
+
search_term: str | None = None,
|
|
35
|
+
google_search_term: str | None = None,
|
|
36
|
+
location: str | None = None,
|
|
37
|
+
distance: int | None = 50,
|
|
38
|
+
is_remote: bool = False,
|
|
39
|
+
job_type: str | None = None,
|
|
40
|
+
easy_apply: bool | None = None,
|
|
41
|
+
results_wanted: int = 15,
|
|
42
|
+
country_indeed: str = "usa",
|
|
43
|
+
proxies: list[str] | str | None = None,
|
|
44
|
+
ca_cert: str | None = None,
|
|
45
|
+
description_format: str = "markdown",
|
|
46
|
+
linkedin_fetch_description: bool | None = False,
|
|
47
|
+
linkedin_company_ids: list[int] | None = None,
|
|
48
|
+
offset: int | None = 0,
|
|
49
|
+
hours_old: int = None,
|
|
50
|
+
enforce_annual_salary: bool = False,
|
|
51
|
+
verbose: int = 0,
|
|
52
|
+
user_agent: str = None,
|
|
53
|
+
**kwargs,
|
|
54
|
+
) -> pd.DataFrame:
|
|
55
|
+
"""
|
|
56
|
+
Scrapes job data from job boards concurrently
|
|
57
|
+
:return: Pandas DataFrame containing job data
|
|
58
|
+
"""
|
|
59
|
+
SCRAPER_MAPPING = {
|
|
60
|
+
Site.LINKEDIN: LinkedIn,
|
|
61
|
+
Site.INDEED: Indeed,
|
|
62
|
+
Site.ZIP_RECRUITER: ZipRecruiter,
|
|
63
|
+
Site.GLASSDOOR: Glassdoor,
|
|
64
|
+
Site.GOOGLE: Google,
|
|
65
|
+
Site.BAYT: BaytScraper,
|
|
66
|
+
Site.NAUKRI: Naukri,
|
|
67
|
+
Site.BDJOBS: BDJobs,
|
|
68
|
+
Site.JOBSTREET: JobStreet,
|
|
69
|
+
}
|
|
70
|
+
set_logger_level(verbose)
|
|
71
|
+
job_type = get_enum_from_value(job_type) if job_type else None
|
|
72
|
+
|
|
73
|
+
def get_site_type():
|
|
74
|
+
site_types = list(Site)
|
|
75
|
+
if isinstance(site_name, str):
|
|
76
|
+
site_types = [map_str_to_site(site_name)]
|
|
77
|
+
elif isinstance(site_name, Site):
|
|
78
|
+
site_types = [site_name]
|
|
79
|
+
elif isinstance(site_name, list):
|
|
80
|
+
site_types = [
|
|
81
|
+
map_str_to_site(site) if isinstance(site, str) else site
|
|
82
|
+
for site in site_name
|
|
83
|
+
]
|
|
84
|
+
return site_types
|
|
85
|
+
|
|
86
|
+
country_enum = Country.from_string(country_indeed)
|
|
87
|
+
|
|
88
|
+
scraper_input = ScraperInput(
|
|
89
|
+
site_type=get_site_type(),
|
|
90
|
+
country=country_enum,
|
|
91
|
+
search_term=search_term,
|
|
92
|
+
google_search_term=google_search_term,
|
|
93
|
+
location=location,
|
|
94
|
+
distance=distance,
|
|
95
|
+
is_remote=is_remote,
|
|
96
|
+
job_type=job_type,
|
|
97
|
+
easy_apply=easy_apply,
|
|
98
|
+
description_format=description_format,
|
|
99
|
+
linkedin_fetch_description=linkedin_fetch_description,
|
|
100
|
+
results_wanted=results_wanted,
|
|
101
|
+
linkedin_company_ids=linkedin_company_ids,
|
|
102
|
+
offset=offset,
|
|
103
|
+
hours_old=hours_old,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def scrape_site(site: Site) -> Tuple[str, JobResponse]:
|
|
107
|
+
scraper_class = SCRAPER_MAPPING[site]
|
|
108
|
+
scraper = scraper_class(proxies=proxies, ca_cert=ca_cert, user_agent=user_agent)
|
|
109
|
+
scraped_data: JobResponse = scraper.scrape(scraper_input)
|
|
110
|
+
cap_name = site.value.capitalize()
|
|
111
|
+
site_name = "ZipRecruiter" if cap_name == "Zip_recruiter" else cap_name
|
|
112
|
+
site_name = "LinkedIn" if cap_name == "Linkedin" else cap_name
|
|
113
|
+
create_logger(site_name).info(f"finished scraping")
|
|
114
|
+
return site.value, scraped_data
|
|
115
|
+
|
|
116
|
+
site_to_jobs_dict = {}
|
|
117
|
+
|
|
118
|
+
def worker(site):
|
|
119
|
+
site_val, scraped_info = scrape_site(site)
|
|
120
|
+
return site_val, scraped_info
|
|
121
|
+
|
|
122
|
+
with ThreadPoolExecutor() as executor:
|
|
123
|
+
future_to_site = {
|
|
124
|
+
executor.submit(worker, site): site for site in scraper_input.site_type
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for future in as_completed(future_to_site):
|
|
128
|
+
site_value, scraped_data = future.result()
|
|
129
|
+
site_to_jobs_dict[site_value] = scraped_data
|
|
130
|
+
|
|
131
|
+
jobs_dfs: list[pd.DataFrame] = []
|
|
132
|
+
|
|
133
|
+
for site, job_response in site_to_jobs_dict.items():
|
|
134
|
+
for job in job_response.jobs:
|
|
135
|
+
job_data = job.dict()
|
|
136
|
+
job_url = job_data["job_url"]
|
|
137
|
+
job_data["site"] = site
|
|
138
|
+
job_data["company"] = job_data["company_name"]
|
|
139
|
+
job_data["job_type"] = (
|
|
140
|
+
", ".join(job_type.value[0] for job_type in job_data["job_type"])
|
|
141
|
+
if job_data["job_type"]
|
|
142
|
+
else None
|
|
143
|
+
)
|
|
144
|
+
job_data["emails"] = (
|
|
145
|
+
", ".join(job_data["emails"]) if job_data["emails"] else None
|
|
146
|
+
)
|
|
147
|
+
if job_data["location"]:
|
|
148
|
+
job_data["location"] = Location(
|
|
149
|
+
**job_data["location"]
|
|
150
|
+
).display_location()
|
|
151
|
+
|
|
152
|
+
# Handle compensation
|
|
153
|
+
compensation_obj = job_data.get("compensation")
|
|
154
|
+
if compensation_obj and isinstance(compensation_obj, dict):
|
|
155
|
+
job_data["interval"] = (
|
|
156
|
+
compensation_obj.get("interval").value
|
|
157
|
+
if compensation_obj.get("interval")
|
|
158
|
+
else None
|
|
159
|
+
)
|
|
160
|
+
job_data["min_amount"] = compensation_obj.get("min_amount")
|
|
161
|
+
job_data["max_amount"] = compensation_obj.get("max_amount")
|
|
162
|
+
job_data["currency"] = compensation_obj.get("currency", "USD")
|
|
163
|
+
job_data["salary_source"] = SalarySource.DIRECT_DATA.value
|
|
164
|
+
if enforce_annual_salary and (
|
|
165
|
+
job_data["interval"]
|
|
166
|
+
and job_data["interval"] != "yearly"
|
|
167
|
+
and job_data["min_amount"]
|
|
168
|
+
and job_data["max_amount"]
|
|
169
|
+
):
|
|
170
|
+
convert_to_annual(job_data)
|
|
171
|
+
else:
|
|
172
|
+
if country_enum == Country.USA:
|
|
173
|
+
(
|
|
174
|
+
job_data["interval"],
|
|
175
|
+
job_data["min_amount"],
|
|
176
|
+
job_data["max_amount"],
|
|
177
|
+
job_data["currency"],
|
|
178
|
+
) = extract_salary(
|
|
179
|
+
job_data["description"],
|
|
180
|
+
enforce_annual_salary=enforce_annual_salary,
|
|
181
|
+
)
|
|
182
|
+
job_data["salary_source"] = SalarySource.DESCRIPTION.value
|
|
183
|
+
|
|
184
|
+
job_data["salary_source"] = (
|
|
185
|
+
job_data["salary_source"]
|
|
186
|
+
if "min_amount" in job_data and job_data["min_amount"]
|
|
187
|
+
else None
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
#naukri-specific fields
|
|
191
|
+
job_data["skills"] = (
|
|
192
|
+
", ".join(job_data["skills"]) if job_data["skills"] else None
|
|
193
|
+
)
|
|
194
|
+
job_data["experience_range"] = job_data.get("experience_range")
|
|
195
|
+
job_data["company_rating"] = job_data.get("company_rating")
|
|
196
|
+
job_data["company_reviews_count"] = job_data.get("company_reviews_count")
|
|
197
|
+
job_data["vacancy_count"] = job_data.get("vacancy_count")
|
|
198
|
+
job_data["work_from_home_type"] = job_data.get("work_from_home_type")
|
|
199
|
+
|
|
200
|
+
job_df = pd.DataFrame([job_data])
|
|
201
|
+
jobs_dfs.append(job_df)
|
|
202
|
+
|
|
203
|
+
if jobs_dfs:
|
|
204
|
+
# Step 1: Filter out all-NA columns from each DataFrame before concatenation
|
|
205
|
+
filtered_dfs = [df.dropna(axis=1, how="all") for df in jobs_dfs]
|
|
206
|
+
|
|
207
|
+
# Step 2: Concatenate the filtered DataFrames
|
|
208
|
+
jobs_df = pd.concat(filtered_dfs, ignore_index=True)
|
|
209
|
+
|
|
210
|
+
# Step 3: Ensure all desired columns are present, adding missing ones as empty
|
|
211
|
+
for column in desired_order:
|
|
212
|
+
if column not in jobs_df.columns:
|
|
213
|
+
jobs_df[column] = None # Add missing columns as empty
|
|
214
|
+
|
|
215
|
+
# Reorder the DataFrame according to the desired order
|
|
216
|
+
jobs_df = jobs_df[desired_order]
|
|
217
|
+
|
|
218
|
+
# Step 4: Sort the DataFrame as required
|
|
219
|
+
return jobs_df.sort_values(
|
|
220
|
+
by=["site", "date_posted"], ascending=[True, False]
|
|
221
|
+
).reset_index(drop=True)
|
|
222
|
+
else:
|
|
223
|
+
return pd.DataFrame()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# Add BDJobs to __all__
|
|
227
|
+
__all__ = [
|
|
228
|
+
"BDJobs",
|
|
229
|
+
]
|
jobspy/bayt/__init__.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from bs4 import BeautifulSoup
|
|
7
|
+
|
|
8
|
+
from jobspy.model import (
|
|
9
|
+
Scraper,
|
|
10
|
+
ScraperInput,
|
|
11
|
+
Site,
|
|
12
|
+
JobPost,
|
|
13
|
+
JobResponse,
|
|
14
|
+
Location,
|
|
15
|
+
Country,
|
|
16
|
+
)
|
|
17
|
+
from jobspy.util import create_logger, create_session
|
|
18
|
+
|
|
19
|
+
log = create_logger("Bayt")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BaytScraper(Scraper):
|
|
23
|
+
base_url = "https://www.bayt.com"
|
|
24
|
+
delay = 2
|
|
25
|
+
band_delay = 3
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self, proxies: list[str] | str | None = None, ca_cert: str | None = None, user_agent: str | None = None
|
|
29
|
+
):
|
|
30
|
+
super().__init__(Site.BAYT, proxies=proxies, ca_cert=ca_cert)
|
|
31
|
+
self.scraper_input = None
|
|
32
|
+
self.session = None
|
|
33
|
+
self.country = "worldwide"
|
|
34
|
+
|
|
35
|
+
def scrape(self, scraper_input: ScraperInput) -> JobResponse:
|
|
36
|
+
self.scraper_input = scraper_input
|
|
37
|
+
self.session = create_session(
|
|
38
|
+
proxies=self.proxies, ca_cert=self.ca_cert, is_tls=False, has_retry=True
|
|
39
|
+
)
|
|
40
|
+
job_list: list[JobPost] = []
|
|
41
|
+
page = 1
|
|
42
|
+
results_wanted = (
|
|
43
|
+
scraper_input.results_wanted if scraper_input.results_wanted else 10
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
while len(job_list) < results_wanted:
|
|
47
|
+
log.info(f"Fetching Bayt jobs page {page}")
|
|
48
|
+
job_elements = self._fetch_jobs(self.scraper_input.search_term, page)
|
|
49
|
+
if not job_elements:
|
|
50
|
+
break
|
|
51
|
+
|
|
52
|
+
if job_elements:
|
|
53
|
+
log.debug(
|
|
54
|
+
"First job element snippet:\n" + job_elements[0].prettify()[:500]
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
initial_count = len(job_list)
|
|
58
|
+
for job in job_elements:
|
|
59
|
+
try:
|
|
60
|
+
job_post = self._extract_job_info(job)
|
|
61
|
+
if job_post:
|
|
62
|
+
job_list.append(job_post)
|
|
63
|
+
if len(job_list) >= results_wanted:
|
|
64
|
+
break
|
|
65
|
+
else:
|
|
66
|
+
log.debug(
|
|
67
|
+
"Extraction returned None. Job snippet:\n"
|
|
68
|
+
+ job.prettify()[:500]
|
|
69
|
+
)
|
|
70
|
+
except Exception as e:
|
|
71
|
+
log.error(f"Bayt: Error extracting job info: {str(e)}")
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
if len(job_list) == initial_count:
|
|
75
|
+
log.info(f"No new jobs found on page {page}. Ending pagination.")
|
|
76
|
+
break
|
|
77
|
+
|
|
78
|
+
page += 1
|
|
79
|
+
time.sleep(random.uniform(self.delay, self.delay + self.band_delay))
|
|
80
|
+
|
|
81
|
+
job_list = job_list[: scraper_input.results_wanted]
|
|
82
|
+
return JobResponse(jobs=job_list)
|
|
83
|
+
|
|
84
|
+
def _fetch_jobs(self, query: str, page: int) -> list | None:
|
|
85
|
+
"""
|
|
86
|
+
Grabs the job results for the given query and page number.
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
url = f"{self.base_url}/en/international/jobs/{query}-jobs/?page={page}"
|
|
90
|
+
response = self.session.get(url)
|
|
91
|
+
response.raise_for_status()
|
|
92
|
+
soup = BeautifulSoup(response.text, "html.parser")
|
|
93
|
+
job_listings = soup.find_all("li", attrs={"data-js-job": ""})
|
|
94
|
+
log.debug(f"Found {len(job_listings)} job listing elements")
|
|
95
|
+
return job_listings
|
|
96
|
+
except Exception as e:
|
|
97
|
+
log.error(f"Bayt: Error fetching jobs - {str(e)}")
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
def _extract_job_info(self, job: BeautifulSoup) -> JobPost | None:
|
|
101
|
+
"""
|
|
102
|
+
Extracts the job information from a single job listing.
|
|
103
|
+
"""
|
|
104
|
+
# Find the h2 element holding the title and link (no class filtering)
|
|
105
|
+
job_general_information = job.find("h2")
|
|
106
|
+
if not job_general_information:
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
job_title = job_general_information.get_text(strip=True)
|
|
110
|
+
job_url = self._extract_job_url(job_general_information)
|
|
111
|
+
if not job_url:
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
# Extract company name using the original approach:
|
|
115
|
+
company_tag = job.find("div", class_="t-nowrap p10l")
|
|
116
|
+
company_name = (
|
|
117
|
+
company_tag.find("span").get_text(strip=True)
|
|
118
|
+
if company_tag and company_tag.find("span")
|
|
119
|
+
else None
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Extract location using the original approach:
|
|
123
|
+
location_tag = job.find("div", class_="t-mute t-small")
|
|
124
|
+
location = location_tag.get_text(strip=True) if location_tag else None
|
|
125
|
+
|
|
126
|
+
job_id = f"bayt-{abs(hash(job_url))}"
|
|
127
|
+
location_obj = Location(
|
|
128
|
+
city=location,
|
|
129
|
+
country=Country.from_string(self.country),
|
|
130
|
+
)
|
|
131
|
+
return JobPost(
|
|
132
|
+
id=job_id,
|
|
133
|
+
title=job_title,
|
|
134
|
+
company_name=company_name,
|
|
135
|
+
location=location_obj,
|
|
136
|
+
job_url=job_url,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def _extract_job_url(self, job_general_information: BeautifulSoup) -> str | None:
|
|
140
|
+
"""
|
|
141
|
+
Pulls the job URL from the 'a' within the h2 element.
|
|
142
|
+
"""
|
|
143
|
+
a_tag = job_general_information.find("a")
|
|
144
|
+
if a_tag and a_tag.has_attr("href"):
|
|
145
|
+
return self.base_url + a_tag["href"].strip()
|