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 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
+ ]
@@ -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()