昨日爬虫学习与改造记录

在今天的学习与实践中,我对一个使用 Selenium 进行招聘信息抓取的爬虫脚本进行了深入改造。通过代码优化与问题解决,我对如何设计、优化和调试爬虫有了更深入的理解。本文将详细记录今天的学习过程、遇到的问题以及改进后的代码。

1. 初步爬虫脚本构建

最初的爬虫脚本结构相对简单,主要实现了以下功能:

  • 页面加载与点击:通过 Selenium 模拟浏览器操作,访问目标招聘网站并点击分类页面。
  • 数据提取:逐一遍历分类页面,提取每个职位的详细信息,包括职位名称、公司、地点、薪资等,并保存到数据库。
  • 数据库操作:每获取到一个职位信息,都会连接数据库并插入数据。

初始代码(部分摘录)

browser = webdriver.Edge()
index_url = 'https://www.zhipin.com/?city=100010000&ka=city-sites-100010000'
browser.get(index_url)

show_ele = browser.find_element(by=By.XPATH, value='//*[@id="main"]/div/div[1]/div/div[1]/dl[1]/dd/b')
show_ele.click()

today = datetime.date.today().strftime('%Y-%m-%d')

for i in range(85):
current_a = browser.find_elements(by=By.XPATH, value='//*[@id="main"]/div/div[1]/div/div[1]/dl[1]/div/ul/li/div/a')[i]
current_category = current_a.find_element(by=By.XPATH, value='../../h4').text
sub_category = current_a.text
print("{}正在抓取{}--{}".format(today, current_category, sub_category))
current_a.click()
time.sleep(5)
browser.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(10)
browser.execute_script("window.scrollTo(0, document.body.scrollHeight);")
# 提取并保存职位数据...

这个代码的问题在于:

  1. 页面元素加载问题:在页面元素尚未完全加载时,Selenium 可能无法找到目标元素,从而导致失败。
  2. StaleElementReferenceException:当页面刷新或 DOM 结构发生变化时,Selenium 引用的元素会失效,导致操作失败。
  3. 频繁的数据库连接:每获取一个职位信息都会新建一次数据库连接,导致效率低下。
  4. 手动处理验证码:当网站检测到异常行为时,可能会触发验证码验证,这会中断爬虫运行。

2. 改进与优化

针对上述问题,我进行了多方面的改进,以下是主要的优化措施和相应的代码改变。

2.1 引入重试机制

为了应对页面元素加载失败或 DOM 变化带来的问题,我增加了一个 retry_on_failure 函数。该函数允许在遇到 StaleElementReferenceExceptionTimeoutException 时,自动重试指定次数,提高脚本的稳定性。

代码示例:

from selenium.common.exceptions import StaleElementReferenceException, TimeoutException

def retry_on_failure(max_retries, function, *args, **kwargs):
retries = 0
while retries < max_retries:
try:
return function(*args, **kwargs)
except (StaleElementReferenceException, TimeoutException) as e:
retries += 1
print(f"Error: {e}. Retrying {retries}/{max_retries}...")
time.sleep(1)
raise Exception(f"Failed after {max_retries} retries")

使用示例:

current_a = retry_on_failure(3, browser.find_elements, By.XPATH, '//*[@id="main"]/div/div[1]/div/div[1]/dl[1]/div/ul/li/div/a')[i]
2.2 优化页面跳转与元素定位

为了减少页面跳转带来的定位失效问题,我在点击元素前引入了显式等待,并结合了动作链(ActionChains)来确保元素能够正确点击。这有效减少了 StaleElementReferenceException 发生的概率。

同时,为了避免在复杂的页面场景中可能出现的点击失效问题,我将一些关键点击操作改为使用 JavaScript 强制执行。这种方式在某些情况下比 Selenium 的普通点击操作更为可靠。

代码示例:

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains

show_ele = WebDriverWait(browser, 10).until(
EC.element_to_be_clickable((By.XPATH, '//*[@id="main"]/div/div[1]/div/div[1]/dl[1]/dd/b'))
)
actions = ActionChains(browser)
actions.move_to_element(show_ele).perform()

# 使用 JavaScript 强制执行点击
browser.execute_script("arguments[0].click();", current_a)
2.3 增加 sleep 等待

为了确保页面元素完全加载,我在一些关键步骤前增加了 sleep 等待。这种方式虽然简单,但在处理需要等待页面完全渲染的场景时非常有效。

代码示例:

# 等待页面加载
time.sleep(5)

# 滚动页面以加载更多内容
browser.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(10)
browser.execute_script("window.scrollTo(0, document.body.scrollHeight);")
2.4 优化数据库连接

为了提高数据库操作的效率,我在程序启动时建立了一次数据库连接,并在整个脚本运行过程中复用该连接。这样做不仅减少了每次操作时的连接开销,还提升了程序的整体性能。

代码示例:

# 启动时建立数据库连接
db = DBUtils('数据库地址', '用户名', '密码', '数据库名')

# 在后续操作中复用该连接,而不是每次都新建连接
db.insert_data(
"insert into job_info(category, sub_category, job_title, province, job_location, job_company, job_industry, job_finance, job_scale, job_welfare, job_salary_range, job_experience, job_education, job_skills, create_time) values(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
args=(current_category, sub_category, job_title, province, job_location, job_company, job_industry, job_finance, job_scale, job_welfare, job_salary_range, job_experience, job_education, job_skills, today)
)
2.5 处理验证码与异常页面

在爬虫运行过程中,有时会遇到页面弹出验证码的情况。为了确保爬虫的持续运行,我增加了一个等待用户手动操作的功能,使得用户可以在需要时手动完成验证,然后继续自动化抓取。

代码示例:

def wait_for_user():
input("请手动完成操作后在控制台输入 'ok' 继续: ")

# 在需要时调用
wait_for_user()
2.6 日志记录与进度反馈

为了更好地监控爬虫的运行状态,我在脚本中增加了详细的日志输出,每个关键步骤都会记录日志,以便在发生问题时能够快速定位。此外,还增加了进度反馈,使得用户可以清楚地了解脚本的处理进度。

代码示例:

print(f"正在处理第 {i} 个分类(咱程序员喜欢从0开始数你懂得...)")
print(f"{today} 正在抓取 {current_category} -- {sub_category}")

3. 改造后的爬虫运行效果

经过优化后的爬虫,运行更加稳定高效,处理数据的速度有了明显提升,并且在应对复杂页面场景时表现更佳。以下是改造后脚本的一些显著优点:

  • 稳定性提升:通过重试机制和显式等待,有效减少了因元素定位失败导致的脚本中断。
  • 效率提升:通过优化数据库连接和减少页面跳转次数,大幅提高了数据抓取的效率。
  • 更好的用户交互:增加的日志输出和手动操作提示,使得脚本在自动化与人工干预之间达到了更好的平衡。