mirror of
https://github.com/Bunsly/JobSpy.git
synced 2026-03-04 19:44:30 -08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02caf1b38d | ||
|
|
8e2ab277da | ||
|
|
ce3bd84ee5 | ||
|
|
1ccf2290fe | ||
|
|
ec2eefc58a |
@@ -80,6 +80,7 @@ Optional
|
|||||||
JobPost
|
JobPost
|
||||||
├── title (str)
|
├── title (str)
|
||||||
├── company (str)
|
├── company (str)
|
||||||
|
├── company_url (str)
|
||||||
├── job_url (str)
|
├── job_url (str)
|
||||||
├── location (object)
|
├── location (object)
|
||||||
│ ├── country (str)
|
│ ├── country (str)
|
||||||
@@ -158,16 +159,11 @@ persist, [submit an issue](https://github.com/Bunsly/JobSpy/issues).
|
|||||||
**Q: Received a response code 429?**
|
**Q: Received a response code 429?**
|
||||||
**A:** This indicates that you have been blocked by the job board site for sending too many requests. All of the job board sites are aggressive with blocking. We recommend:
|
**A:** This indicates that you have been blocked by the job board site for sending too many requests. All of the job board sites are aggressive with blocking. We recommend:
|
||||||
|
|
||||||
- Waiting a few seconds between requests.
|
- Waiting some time between scrapes (site-dependent).
|
||||||
- Trying a VPN or proxy to change your IP address.
|
- Trying a VPN or proxy to change your IP address.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Q: Experiencing a "Segmentation fault: 11" on macOS Catalina?**
|
|
||||||
**A:** This is due to `tls_client` dependency not supporting your architecture. Solutions and workarounds include:
|
|
||||||
|
|
||||||
- Upgrade to a newer version of MacOS
|
|
||||||
- Reach out to the maintainers of [tls_client](https://github.com/bogdanfinn/tls-client) for fixes
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "python-jobspy"
|
name = "python-jobspy"
|
||||||
version = "1.1.38"
|
version = "1.1.41"
|
||||||
description = "Job scraper for LinkedIn, Indeed, Glassdoor & ZipRecruiter"
|
description = "Job scraper for LinkedIn, Indeed, Glassdoor & ZipRecruiter"
|
||||||
authors = ["Zachary Hampton <zachary@bunsly.com>", "Cullen Watson <cullen@bunsly.com>"]
|
authors = ["Zachary Hampton <zachary@bunsly.com>", "Cullen Watson <cullen@bunsly.com>"]
|
||||||
homepage = "https://github.com/Bunsly/JobSpy"
|
homepage = "https://github.com/Bunsly/JobSpy"
|
||||||
|
|||||||
@@ -246,6 +246,8 @@ class GlassdoorScraper(Scraper):
|
|||||||
location_type = "CITY"
|
location_type = "CITY"
|
||||||
elif location_type == "S":
|
elif location_type == "S":
|
||||||
location_type = "STATE"
|
location_type = "STATE"
|
||||||
|
elif location_type == 'N':
|
||||||
|
location_type = "COUNTRY"
|
||||||
return int(items[0]["locationId"]), location_type
|
return int(items[0]["locationId"]), location_type
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -154,8 +154,9 @@ class IndeedScraper(Scraper):
|
|||||||
)
|
)
|
||||||
return job_post
|
return job_post
|
||||||
|
|
||||||
|
workers = 10 if scraper_input.full_description else 10 # possibly lessen 10 when fetching desc based on feedback
|
||||||
jobs = jobs["metaData"]["mosaicProviderJobCardsModel"]["results"]
|
jobs = jobs["metaData"]["mosaicProviderJobCardsModel"]["results"]
|
||||||
with ThreadPoolExecutor(max_workers=1) as executor:
|
with ThreadPoolExecutor(max_workers=workers) as executor:
|
||||||
job_results: list[Future] = [
|
job_results: list[Future] = [
|
||||||
executor.submit(process_job, job) for job in jobs
|
executor.submit(process_job, job) for job in jobs
|
||||||
]
|
]
|
||||||
@@ -206,7 +207,7 @@ class IndeedScraper(Scraper):
|
|||||||
parsed_url = urllib.parse.urlparse(job_page_url)
|
parsed_url = urllib.parse.urlparse(job_page_url)
|
||||||
params = urllib.parse.parse_qs(parsed_url.query)
|
params = urllib.parse.parse_qs(parsed_url.query)
|
||||||
jk_value = params.get("jk", [None])[0]
|
jk_value = params.get("jk", [None])[0]
|
||||||
formatted_url = f"{self.url}/viewjob?jk={jk_value}&spa=1"
|
formatted_url = f"{self.url}/m/viewjob?jk={jk_value}&spa=1"
|
||||||
session = create_session(self.proxy)
|
session = create_session(self.proxy)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -223,10 +224,18 @@ class IndeedScraper(Scraper):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = json.loads(response.text)
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
job_description = data["body"]["jobInfoWrapperModel"]["jobInfoModel"][
|
script_tags = soup.find_all('script')
|
||||||
"sanitizedJobDescription"
|
|
||||||
]
|
job_description = ''
|
||||||
|
for tag in script_tags:
|
||||||
|
if 'window._initialData' in tag.text:
|
||||||
|
json_str = tag.text
|
||||||
|
json_str = json_str.split('window._initialData=')[1]
|
||||||
|
json_str = json_str.rsplit(';', 1)[0]
|
||||||
|
data = json.loads(json_str)
|
||||||
|
job_description = data["jobInfoWrapperModel"]["jobInfoModel"]["sanitizedJobDescription"]
|
||||||
|
break
|
||||||
except (KeyError, TypeError, IndexError):
|
except (KeyError, TypeError, IndexError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ This module contains routines to scrape ZipRecruiter.
|
|||||||
"""
|
"""
|
||||||
import math
|
import math
|
||||||
import time
|
import time
|
||||||
import re
|
from datetime import datetime, timezone
|
||||||
from datetime import datetime, date
|
|
||||||
from typing import Optional, Tuple, Any
|
from typing import Optional, Tuple, Any
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
@@ -44,12 +43,12 @@ class ZipRecruiterScraper(Scraper):
|
|||||||
"""
|
"""
|
||||||
params = self.add_params(scraper_input)
|
params = self.add_params(scraper_input)
|
||||||
if continue_token:
|
if continue_token:
|
||||||
params["continue"] = continue_token
|
params["continue_from"] = continue_token
|
||||||
try:
|
try:
|
||||||
response = self.session.get(
|
response = self.session.get(
|
||||||
f"https://api.ziprecruiter.com/jobs-app/jobs",
|
f"https://api.ziprecruiter.com/jobs-app/jobs",
|
||||||
headers=self.headers(),
|
headers=self.headers(),
|
||||||
params=self.add_params(scraper_input),
|
params=params
|
||||||
)
|
)
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
raise ZipRecruiterException(
|
raise ZipRecruiterException(
|
||||||
@@ -68,7 +67,7 @@ class ZipRecruiterScraper(Scraper):
|
|||||||
with ThreadPoolExecutor(max_workers=self.jobs_per_page) as executor:
|
with ThreadPoolExecutor(max_workers=self.jobs_per_page) as executor:
|
||||||
job_results = [executor.submit(self.process_job, job) for job in jobs_list]
|
job_results = [executor.submit(self.process_job, job) for job in jobs_list]
|
||||||
|
|
||||||
job_list = [result.result() for result in job_results if result.result()]
|
job_list = list(filter(None, (result.result() for result in job_results)))
|
||||||
return job_list, next_continue_token
|
return job_list, next_continue_token
|
||||||
|
|
||||||
def scrape(self, scraper_input: ScraperInput) -> JobResponse:
|
def scrape(self, scraper_input: ScraperInput) -> JobResponse:
|
||||||
@@ -95,16 +94,15 @@ class ZipRecruiterScraper(Scraper):
|
|||||||
if not continue_token:
|
if not continue_token:
|
||||||
break
|
break
|
||||||
|
|
||||||
if len(job_list) > scraper_input.results_wanted:
|
return JobResponse(jobs=job_list[: scraper_input.results_wanted])
|
||||||
job_list = job_list[: scraper_input.results_wanted]
|
|
||||||
|
|
||||||
return JobResponse(jobs=job_list)
|
def process_job(self, job: dict) -> JobPost | None:
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def process_job(job: dict) -> JobPost:
|
|
||||||
"""Processes an individual job dict from the response"""
|
"""Processes an individual job dict from the response"""
|
||||||
title = job.get("name")
|
title = job.get("name")
|
||||||
job_url = job.get("job_url")
|
job_url = f"https://www.ziprecruiter.com/jobs//j?lvk={job['listing_key']}"
|
||||||
|
if job_url in self.seen_urls:
|
||||||
|
return
|
||||||
|
self.seen_urls.add(job_url)
|
||||||
|
|
||||||
job_description_html = job.get("job_description", "").strip()
|
job_description_html = job.get("job_description", "").strip()
|
||||||
description_soup = BeautifulSoup(job_description_html, "html.parser")
|
description_soup = BeautifulSoup(job_description_html, "html.parser")
|
||||||
@@ -120,17 +118,7 @@ class ZipRecruiterScraper(Scraper):
|
|||||||
job_type = ZipRecruiterScraper.get_job_type_enum(
|
job_type = ZipRecruiterScraper.get_job_type_enum(
|
||||||
job.get("employment_type", "").replace("_", "").lower()
|
job.get("employment_type", "").replace("_", "").lower()
|
||||||
)
|
)
|
||||||
|
date_posted = datetime.fromisoformat(job['posted_time'].rstrip("Z")).date()
|
||||||
save_job_url = job.get("SaveJobURL", "")
|
|
||||||
posted_time_match = re.search(
|
|
||||||
r"posted_time=(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)", save_job_url
|
|
||||||
)
|
|
||||||
if posted_time_match:
|
|
||||||
date_time_str = posted_time_match.group(1)
|
|
||||||
date_posted_obj = datetime.strptime(date_time_str, "%Y-%m-%dT%H:%M:%SZ")
|
|
||||||
date_posted = date_posted_obj.date()
|
|
||||||
else:
|
|
||||||
date_posted = date.today()
|
|
||||||
|
|
||||||
return JobPost(
|
return JobPost(
|
||||||
title=title,
|
title=title,
|
||||||
@@ -173,7 +161,6 @@ class ZipRecruiterScraper(Scraper):
|
|||||||
params = {
|
params = {
|
||||||
"search": scraper_input.search_term,
|
"search": scraper_input.search_term,
|
||||||
"location": scraper_input.location,
|
"location": scraper_input.location,
|
||||||
"form": "jobs-landing",
|
|
||||||
}
|
}
|
||||||
job_type_value = None
|
job_type_value = None
|
||||||
if scraper_input.job_type:
|
if scraper_input.job_type:
|
||||||
|
|||||||
Reference in New Issue
Block a user