feat: add Boss直聘 job spider test script
Tests / tests (map[TOXENV:py310], macos-latest, 3.10) (push) Has been cancelled
Tests / tests (map[TOXENV:py311], macos-latest, 3.11) (push) Has been cancelled
Tests / tests (map[TOXENV:py312], macos-latest, 3.12) (push) Has been cancelled
Tests / tests (map[TOXENV:py313], macos-latest, 3.13) (push) Has been cancelled
Tests / tests (map[TOXENV:py310], macos-latest, 3.10) (push) Has been cancelled
Tests / tests (map[TOXENV:py311], macos-latest, 3.11) (push) Has been cancelled
Tests / tests (map[TOXENV:py312], macos-latest, 3.12) (push) Has been cancelled
Tests / tests (map[TOXENV:py313], macos-latest, 3.13) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
"""Boss直聘岗位爬虫 - headful模式,支持手动登录后爬取
|
||||
|
||||
使用方法:
|
||||
1. 第一次运行时,浏览器会弹出Boss直聘登录页,手动扫码/登录
|
||||
2. 登录成功后按回车键继续爬取
|
||||
3. 登录状态会保存到 user_data_dir,下次运行可跳过登录
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
from scrapling.fetchers import StealthyFetcher
|
||||
from scrapling.core._types import Any, Dict
|
||||
|
||||
|
||||
# ===== 配置 =====
|
||||
SEARCH_KEYWORD = "Python"
|
||||
SEARCH_CITY = "100010000" # 全国
|
||||
USER_DATA_DIR = os.path.join(os.path.dirname(__file__), "boss_browser_data")
|
||||
OUTPUT_FILE = os.path.join(os.path.dirname(__file__), "boss_zhipin_jobs.json")
|
||||
MAX_PAGES = 5
|
||||
|
||||
|
||||
def crawl_boss_zhipin():
|
||||
"""使用StealthyFetcher爬取Boss直聘岗位信息"""
|
||||
|
||||
# 构建搜索URL
|
||||
url = f"https://www.zhipin.com/web/geek/job?query={SEARCH_KEYWORD}&city={SEARCH_CITY}"
|
||||
print(f"目标URL: {url}")
|
||||
print(f"关键词: {SEARCH_KEYWORD} | 城市: {SEARCH_CITY}")
|
||||
print(f"最大页数: {MAX_PAGES}")
|
||||
print("-" * 60)
|
||||
|
||||
all_jobs = []
|
||||
|
||||
for page_num in range(1, MAX_PAGES + 1):
|
||||
page_url = f"{url}&page={page_num}"
|
||||
print(f"\n正在爬取第 {page_num}/{MAX_PAGES} 页...")
|
||||
|
||||
try:
|
||||
response = StealthyFetcher.fetch(
|
||||
page_url,
|
||||
headless=False, # headful模式,方便登录
|
||||
network_idle=True,
|
||||
wait=3000,
|
||||
timeout=90000,
|
||||
block_ads=True,
|
||||
disable_resources=True,
|
||||
locale="zh-CN",
|
||||
timezone_id="Asia/Shanghai",
|
||||
user_data_dir=USER_DATA_DIR, # 保存浏览器会话
|
||||
real_chrome=True, # 使用本机Chrome
|
||||
)
|
||||
|
||||
print(f" 状态码: {response.status}")
|
||||
print(f" 最终URL: {response.url}")
|
||||
|
||||
# 检查是否被重定向到登录页
|
||||
if "/web/user/" in str(response.url) or "注册登录" in (response.css("title::text").get() or ""):
|
||||
print(f"\n⚠️ 页面被重定向到登录页!")
|
||||
print(f" 浏览器窗口已打开,请在浏览器中完成登录(扫码/账号登录)。")
|
||||
print(f" 登录成功后,请在此处按回车键继续...")
|
||||
input(" >>> 按回车继续 <<<")
|
||||
|
||||
# 重新抓取
|
||||
print(f"\n 重新爬取第 {page_num} 页...")
|
||||
response = StealthyFetcher.fetch(
|
||||
page_url,
|
||||
headless=False,
|
||||
network_idle=True,
|
||||
wait=5000,
|
||||
timeout=90000,
|
||||
block_ads=True,
|
||||
disable_resources=True,
|
||||
locale="zh-CN",
|
||||
timezone_id="Asia/Shanghai",
|
||||
user_data_dir=USER_DATA_DIR,
|
||||
real_chrome=True,
|
||||
)
|
||||
print(f" 状态码: {response.status}")
|
||||
print(f" 最终URL: {response.url}")
|
||||
|
||||
# 解析岗位卡片
|
||||
job_cards = response.css(".job-card-wrapper")
|
||||
if not job_cards:
|
||||
job_cards = response.css('[class*="job-card"]')
|
||||
if not job_cards:
|
||||
# 尝试更通用的选择器
|
||||
job_cards = response.css('[data-type="job"]')
|
||||
|
||||
print(f" 找到 {len(job_cards)} 个岗位")
|
||||
|
||||
if not job_cards:
|
||||
# 调试: 打印页面信息
|
||||
title = response.css("title::text").get() or ""
|
||||
print(f" 页面标题: {title}")
|
||||
# 保存当前页面HTML供调试
|
||||
debug_path = os.path.join(os.path.dirname(__file__), f"boss_page_{page_num}_debug.html")
|
||||
with open(debug_path, "w", encoding="utf-8") as f:
|
||||
f.write(response.body.decode("utf-8", errors="ignore"))
|
||||
print(f" 页面HTML已保存到: {debug_path}")
|
||||
|
||||
if page_num == 1:
|
||||
print("\n 第一页未找到岗位,可能需要登录。")
|
||||
print(" 请在浏览器中完成登录后按回车重试...")
|
||||
input(" >>> 按回车继续 <<<")
|
||||
continue
|
||||
else:
|
||||
break
|
||||
|
||||
page_jobs = 0
|
||||
for card in job_cards:
|
||||
try:
|
||||
job_name = card.css(".job-name::text").get() or card.css('[class*="job-name"]::text').get() or ""
|
||||
salary = card.css(".salary::text").get() or card.css('[class*="salary"]::text').get() or ""
|
||||
company_name = (
|
||||
card.css(".company-name::text").get()
|
||||
or card.css('[class*="company-name"] a::text').get()
|
||||
or card.css(".company-name a::text").get()
|
||||
or ""
|
||||
)
|
||||
area = card.css(".job-area::text").get() or card.css('[class*="job-area"]::text').get() or ""
|
||||
tag_list = (
|
||||
card.css(".tag-list li::text").getall()
|
||||
or card.css('[class*="tag-list"]::text').getall()
|
||||
or []
|
||||
)
|
||||
job_link = card.css(".job-card-left::attr(href)").get() or card.css("a::attr(href)").get() or ""
|
||||
|
||||
# 获取经验/学历要求
|
||||
info_items = card.css(".job-info .tag-list li::text").getall() or []
|
||||
if not info_items:
|
||||
info_items = card.css('[class*="info"] li::text').getall() or []
|
||||
|
||||
item = {
|
||||
"job_name": job_name.strip() if job_name else "",
|
||||
"salary": salary.strip() if salary else "",
|
||||
"company_name": company_name.strip() if company_name else "",
|
||||
"area": area.strip() if area else "",
|
||||
"tags": [t.strip() for t in tag_list if t.strip()],
|
||||
"requirements": [t.strip() for t in info_items if t.strip()],
|
||||
"job_url": response.urljoin(job_link) if job_link else "",
|
||||
"page": page_num,
|
||||
}
|
||||
|
||||
if item["job_name"]:
|
||||
all_jobs.append(item)
|
||||
page_jobs += 1
|
||||
print(f" [{page_jobs}] {item['job_name']} | {item['salary']} | {item['company_name']}")
|
||||
except Exception as e:
|
||||
print(f" 解析岗位出错: {e}")
|
||||
|
||||
print(f" 第 {page_num} 页成功解析 {page_jobs} 个岗位")
|
||||
|
||||
if page_jobs == 0 and page_num > 1:
|
||||
print(" 没有更多岗位,停止翻页")
|
||||
break
|
||||
|
||||
# 翻页延迟
|
||||
if page_num < MAX_PAGES:
|
||||
delay = 3
|
||||
print(f" 等待 {delay}s 后翻页...")
|
||||
time.sleep(delay)
|
||||
|
||||
except Exception as e:
|
||||
print(f" 爬取出错: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
break
|
||||
|
||||
# 输出结果
|
||||
print("\n" + "=" * 60)
|
||||
print(f"爬取完成!")
|
||||
print(f" 总岗位数: {len(all_jobs)}")
|
||||
print("=" * 60)
|
||||
|
||||
for i, item in enumerate(all_jobs[:30]):
|
||||
print(f"\n[{i+1}] {item['job_name']}")
|
||||
print(f" 薪资: {item['salary']}")
|
||||
print(f" 公司: {item['company_name']}")
|
||||
print(f" 地区: {item['area']}")
|
||||
print(f" 标签: {', '.join(item.get('tags', []))}")
|
||||
print(f" 要求: {', '.join(item.get('requirements', []))}")
|
||||
print(f" 链接: {item['job_url']}")
|
||||
|
||||
# 保存到JSON
|
||||
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(all_jobs, f, ensure_ascii=False, indent=2)
|
||||
print(f"\n结果已保存到: {OUTPUT_FILE}")
|
||||
|
||||
return all_jobs
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
jobs = crawl_boss_zhipin()
|
||||
Reference in New Issue
Block a user