You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Selenium抓取LinkedIn职位信息出现重复/空结果求助

LinkedIn职位抓取重复/空结果问题排查与修复

问题描述

我正在做一个练习项目,用Selenium抓取LinkedIn上的澳洲数据分析师职位信息。思路是先获取职位卡片元素和Job ID,逐个点击卡片加载详情,再提取职位名称、公司名等信息。但运行时出现重复或空结果:Job ID能正常更新,但职位详情会随机重复。相关代码如下:

import time
import pandas as pd
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException


login_page_link = 'https://www.linkedin.com/login'
search_page_link = 'https://www.linkedin.com/jobs/search/?geoId=101452733&keywords=data%20analyst&location=Australia&refresh=true'
job_list_item_class = 'jobs-search-results__list-item'
job_title_class = 'jobs-unified-top-card__job-title'
company_name_class = 'jobs-unified-top-card__company-name'


def get_browser_driver():
    browser = webdriver.Chrome()
    # maximise browser window
    browser.maximize_window()
    return browser

def login_to_linkedin(browser):
    browser.get(login_page_link)
    # enter login credentials
    browser.find_element(by=By.ID, value=username_id).send_keys("username@mail.com")
    browser.find_element(by=By.ID, value=password_id).send_keys("pwd")
    login_btn = browser.find_element(by=By.XPATH, value=login_btn_xpath)
    # attempt login
    login_btn.click()
    # wait till new page is loaded
    time.sleep(2)

def get_job_post_data(browser):
    # list to store job posts
    job_posts = []
    
    # get the search results list
    job_cards = browser.find_elements(by=By.CLASS_NAME, value=job_list_item_class)

    for job_card in job_cards:
        job_id = job_card.get_attribute('data-occludable-job-id')

        # dict to store each job post
        job_dict = {}
        # scroll job post into view
        browser.execute_script("arguments[0].scrollIntoView();", job_card)
        # click to load each job post
        job_card.click()
        time.sleep(5)

        # get elements from job post by css selector
        job_dict['Job ID'] = job_id
        job_dict['Job title'] = get_element_text_by_classname(browser, job_title_class)
        job_dict['Company name'] = get_element_text_by_classname(browser, company_name_class)
    
        job_posts.append(job_dict)
    return job_posts

def get_element_text_by_classname(browser, class_name):
    return browser.find_element(by=By.CLASS_NAME, value=class_name).text


browser = get_browser_driver()
login_to_linkedin(browser)
load_search_results_page(browser)
jobs_list = get_job_post_data(browser)
jobs_df = pd.DataFrame(all_jobs)

问题原因分析

  1. 失效元素引用(Stale Element Reference):LinkedIn职位列表是动态渲染的,页面滚动或点击卡片后,初始获取的job_cards列表元素会失效,后续操作可能触发旧元素,导致抓取到重复内容。
  2. 固定等待不可靠:time.sleep(5)的固定等待逻辑,要么因加载未完成提前提取数据,要么因页面更新导致元素状态变化,无法保证数据准确性。
  3. 全局定位歧义:用browser.find_element全局查找详情元素时,页面可能残留之前加载的卡片内容,导致提取到旧的职位信息。
  4. 变量名错误:代码最后用pd.DataFrame(all_jobs),但实际定义的变量是jobs_list,直接导致数据丢失报错。

修复方案

针对上述问题,调整后的完整代码如下:

import time
import pandas as pd
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


login_page_link = 'https://www.linkedin.com/login'
search_page_link = 'https://www.linkedin.com/jobs/search/?geoId=101452733&keywords=data%20analyst&location=Australia&refresh=true'
job_list_item_class = 'jobs-search-results__list-item'
job_title_class = 'jobs-unified-top-card__job-title'
company_name_class = 'jobs-unified-top-card__company-name'
# 补充缺失的登录元素定位变量
username_id = 'username'
password_id = 'password'
login_btn_xpath = '//button[@type="submit"]'

def get_browser_driver():
    browser = webdriver.Chrome()
    browser.maximize_window()
    return browser

def login_to_linkedin(browser):
    browser.get(login_page_link)
    # 显式等待登录元素加载完成
    WebDriverWait(browser, 10).until(
        EC.presence_of_element_located((By.ID, username_id))
    ).send_keys("username@mail.com")
    browser.find_element(by=By.ID, value=password_id).send_keys("pwd")
    login_btn = WebDriverWait(browser, 10).until(
        EC.element_to_be_clickable((By.XPATH, login_btn_xpath))
    )
    login_btn.click()
    # 等待登录跳转完成
    WebDriverWait(browser, 15).until(
        EC.url_contains("feed")
    )

def load_search_results_page(browser):
    browser.get(search_page_link)
    # 等待搜索结果加载
    WebDriverWait(browser, 15).until(
        EC.presence_of_element_located((By.CLASS_NAME, job_list_item_class))
    )
    # 滚动页面加载所有可见职位卡片
    last_height = browser.execute_script("return document.body.scrollHeight")
    while True:
        browser.execute_script("window.scrollTo(0, document.body.scrollHeight);")
        time.sleep(3)
        new_height = browser.execute_script("return document.body.scrollHeight")
        if new_height == last_height:
            break
        last_height = new_height

def get_element_text_by_classname(browser, class_name):
    try:
        element = WebDriverWait(browser, 10).until(
            EC.visibility_of_element_located((By.CLASS_NAME, class_name))
        )
        return element.text
    except NoSuchElementException:
        return ""

def get_job_post_data(browser):
    job_posts = []
    # 获取总职位数,控制循环次数
    total_jobs = len(browser.find_elements(by=By.CLASS_NAME, value=job_list_item_class))
    
    for i in range(total_jobs):
        try:
            # 每次循环重新获取职位卡片,避免失效元素问题
            job_cards = WebDriverWait(browser, 10).until(
                EC.presence_of_all_elements_located((By.CLASS_NAME, job_list_item_class))
            )
            job_card = job_cards[i]
            job_id = job_card.get_attribute('data-occludable-job-id')
            
            # 滚动到目标卡片并点击
            browser.execute_script("arguments[0].scrollIntoView({block: 'center'});", job_card)
            WebDriverWait(browser, 10).until(
                EC.element_to_be_clickable(job_card)
            ).click()
            
            # 验证当前加载的详情Job ID是否匹配目标卡片
            current_job_id = WebDriverWait(browser, 10).until(
                EC.presence_of_element_located((By.CSS_SELECTOR, f"div[data-job-id='{job_id}']"))
            ).get_attribute('data-job-id')
            
            if current_job_id != job_id:
                continue  # 不匹配则跳过,避免重复数据
            
            # 提取数据
            job_dict = {
                'Job ID': job_id,
                'Job title': get_element_text_by_classname(browser, job_title_class),
                'Company name': get_element_text_by_classname(browser, company_name_class)
            }
            job_posts.append(job_dict)
            
        except StaleElementReferenceException:
            # 遇到失效元素,重新执行当前循环
            continue
        except Exception as e:
            print(f"处理职位时出错: {e}")
            continue
    return job_posts

# 执行流程
browser = get_browser_driver()
try:
    login_to_linkedin(browser)
    load_search_results_page(browser)
    jobs_list = get_job_post_data(browser)
    jobs_df = pd.DataFrame(jobs_list)
    print(jobs_df.head())
    # 可选:保存到CSV文件
    jobs_df.to_csv('linkedin_jobs.csv', index=False)
finally:
    browser.quit()

关键修改说明

  • 显式等待替代固定sleep:所有元素操作前用WebDriverWait确保元素可交互/可见,提升稳定性。
  • 循环内重新获取卡片:每次循环重新定位job_cards,避免页面更新导致的失效元素错误。
  • Job ID验证机制:点击卡片后验证详情区域的Job ID,确保提取的数据与目标职位匹配。
  • 滚动加载所有卡片:进入详情页前先滚动页面加载所有可见职位,避免漏抓。
  • 异常捕获处理:增加对StaleElementReferenceException的捕获,遇到失效元素时重新尝试当前循环。
  • 变量错误修复:补充缺失的登录元素定位变量,修正all_jobs为jobs_list,并添加浏览器退出的finally块。

内容的提问来源于stack exchange,提问作者Fleur

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 05:25:16