Skip to content

Python 爬虫基础面试题

Q1: 什么是网络爬虫?它的基本工作流程是怎样的?

A: 网络爬虫(Web Crawler/Spider)是一种自动浏览万维网(World Wide Web)的程序或脚本。它们按照一定的规则,自动地抓取网络信息。

基本工作流程:

  1. 发起请求 (Sending Request): 爬虫模拟浏览器,向目标服务器发送 HTTP/HTTPS 请求(Request),请求访问特定的 URL。
  2. 获取响应 (Getting Response): 服务器收到请求后,会返回一个响应(Response),通常是 HTML、JSON 或其他格式的文本。
  3. 解析内容 (Parsing Content): 爬虫接收到响应后,需要解析响应内容(通常是 HTML 源码),提取出需要的数据或新的 URL 链接。常用的解析库有 Beautiful Soup, lxml, Scrapy Selectors, 正则表达式等。
  4. 存储数据 (Storing Data): 将提取到的有用数据存储到本地文件(如 CSV, JSON, TXT)或数据库(如 MySQL, MongoDB, Redis)中。
  5. (可选)持续爬取: 如果解析出了新的 URL,并且这些 URL 符合爬取规则,爬虫会将它们放入待抓取队列,重复上述步骤,实现递归或广度/深度优先的爬取。

拓展知识点:

  • HTTP/HTTPS: 爬虫的基础是网络请求,理解 HTTP/HTTPS 协议非常重要,包括请求方法(GET, POST 等)、请求头(Headers - 如 User-Agent, Cookies, Referer)、响应状态码(200 OK, 404 Not Found, 403 Forbidden, 500 Internal Server Error 等)。
  • URL (Uniform Resource Locator): 统一资源定位符,是爬虫访问网络资源的地址。
  • HTML (HyperText Markup Language): 网页内容的骨架,爬虫主要解析的对象。了解 HTML 标签结构(如 <div>, <a>, <img>, <span>, <table> 等)和层级关系是解析的关键。
  • robots.txt: 网站根目录下的一个文本文件,规定了允许哪些爬虫访问哪些页面。遵守 robots.txt 是爬虫的基本道德规范。

Q2: Python 中常用的爬虫相关库有哪些?它们各自的特点是什么?

A:

  • Requests:
    • 特点: 专注于发送 HTTP/HTTPS 请求,简单易用,功能强大,是目前最流行的 HTTP 客户端库。可以处理请求头、Cookies、会话保持、文件上传、代理等。
    • 用途: 主要负责发起请求和获取响应,通常与解析库配合使用。
  • Beautiful Soup 4 (BS4):
    • 特点: 强大的 HTML/XML 解析库,能够很好地处理不规范的标记语言,API 设计人性化,上手简单。
    • 用途: 主要负责从 HTML/XML 响应中提取数据,需要搭配请求库(如 Requests)使用。它不负责发送请求。
  • lxml:
    • 特点: 基于 C 语言编写的库,解析速度非常快,功能强大,支持 XPath 和 CSS 选择器。
    • 用途: 也是一个 HTML/XML 解析库,通常被认为是 BS4 的替代或补充(BS4 可以指定使用 lxml 作为解析器以提高速度)。
  • Scrapy:
    • 特点: 一个功能强大、高度可定制的异步爬虫框架。提供了从请求调度、下载、解析到数据处理、存储的完整解决方案。内置并发处理、请求延迟、中间件、数据管道等高级功能。
    • 用途: 适用于构建大型、复杂的、需要高效率和高并发的爬虫项目。
  • Selenium:
    • 特点: 一个浏览器自动化测试工具,可以通过 WebDriver 驱动真实的浏览器(如 Chrome, Firefox)执行操作,模拟用户行为(点击、输入、滚动等)。能够执行 JavaScript,获取动态加载的内容。
    • 用途: 主要用于爬取那些需要 JavaScript 渲染才能显示的动态网页内容,或者需要模拟登录、复杂交互才能获取数据的网站。

拓展知识点:

  • 异步 (Asynchronous): Scrapy 基于 Twisted 异步网络库,可以在等待一个请求响应的同时发送其他请求,大大提高了爬取效率。这是它与 Requests(同步库)的主要区别之一。
  • 爬虫框架 vs 库: Requests, BS4, lxml 是库(提供特定功能),而 Scrapy 是框架(提供一套完整的结构和规则)。Selenium 比较特殊,它是一个自动化工具,被用于爬虫场景。

Selenium 专题

Q3: 什么是 Selenium?它主要用来解决爬虫中的什么问题?

A: Selenium 是一个用于 Web 应用程序自动化测试的工具集。它可以通过编程方式驱动浏览器,模拟用户在浏览器上的各种操作,如打开网页、点击按钮、输入文本、滚动页面等。

在爬虫领域,Selenium 主要用来解决以下问题:

  1. JavaScript 动态加载内容: 很多现代网站使用 AJAX 或其他 JavaScript 技术动态加载数据。传统的爬虫库(如 Requests)只能获取到初始的 HTML 源码,无法获取由 JavaScript 执行后生成的内容。Selenium 驱动真实浏览器,可以执行页面中的 JavaScript,等待内容加载完成后再进行抓取,从而获取完整的页面数据。
  2. 模拟用户交互: 某些网站需要用户进行登录、点击特定按钮、滚动页面到底部等操作后才能看到所需信息。Selenium 可以精确模拟这些用户行为。
  3. 处理复杂验证: 如需要拖动滑块验证等,虽然有专门的反验证码技术,但在某些情况下,Selenium 模拟操作可能是相对直接的方式。

拓展知识点:

  • WebDriver: Selenium 的核心组件。它是一个接口,定义了与浏览器进行交互的标准方法。不同的浏览器(Chrome, Firefox, Edge等)需要对应的 WebDriver 可执行文件(如 chromedriver.exe, geckodriver.exe)来充当 Selenium 脚本和浏览器之间的桥梁。
  • Selenium IDE: 一个浏览器插件(主要用于 Firefox 和 Chrome),可以录制和回放用户操作,生成测试脚本,适合快速原型设计和简单任务。
  • Selenium Grid: 用于在多台机器上并行运行测试(或爬虫任务),提高效率。

Q4: 使用 Selenium 如何定位网页元素?有哪些常用的定位方法?

A: Selenium 提供了多种定位策略来查找网页上的元素(DOM 元素)。常用的方法都通过 driver.find_element() (查找单个元素) 或 driver.find_elements() (查找所有匹配元素) 实现,配合 By 类来指定定位策略。

常用定位方法 (By 类的属性):

  1. By.ID: 通过元素的 id 属性定位。这是最快、最推荐的方法(如果元素有唯一的 ID)。
    python
    element = driver.find_element(By.ID, "element_id")
  2. By.NAME: 通过元素的 name 属性定位。常用于表单元素。
    python
    element = driver.find_element(By.NAME, "element_name")
  3. By.CLASS_NAME: 通过元素的 class 属性定位。注意,如果 class 包含多个值(用空格分隔),只能使用其中一个。
    python
    elements = driver.find_elements(By.CLASS_NAME, "some_class")
  4. By.TAG_NAME: 通过元素的 HTML 标签名定位(如 div, a, input)。通常会返回多个元素。
    python
    links = driver.find_elements(By.TAG_NAME, "a")
  5. By.LINK_TEXT: 通过链接(<a> 标签)的完整可见文本定位。
    python
    link = driver.find_element(By.LINK_TEXT, "Click Here")
  6. By.PARTIAL_LINK_TEXT: 通过链接(<a> 标签)的部分可见文本定位。
    python
    link = driver.find_element(By.PARTIAL_LINK_TEXT, "Click")
  7. By.CSS_SELECTOR: 通过 CSS 选择器语法定位。非常强大和灵活,推荐使用。
    python
    # 通过 ID: #element_id
    # 通过 Class: .some_class
    # 通过标签和属性: input[name='username']
    # 层级关系: div#container > p.content
    element = driver.find_element(By.CSS_SELECTOR, "div#content p.intro")
  8. By.XPATH: 通过 XPath 表达式定位。功能最强大,可以处理复杂的层级关系和属性条件,但语法相对复杂,性能可能略低于 CSS Selector。
    python
    # 绝对路径: /html/body/div[1]/p
    # 相对路径(常用): //div[@id='content']//p[@class='intro']
    # 文本内容匹配: //button[text()='Submit']
    element = driver.find_element(By.XPATH, "//input[@name='password']")

拓展知识点:

  • 选择策略: 优先使用 ID > Name > CSS Selector > XPath > Link Text > Class Name / Tag Name。尽量选择稳定、唯一的定位符。
  • 调试定位器: 可以在浏览器开发者工具(按 F12)的 Elements (元素) 面板中使用 Ctrl+F,直接测试 CSS Selector 或 XPath 表达式是否能准确命中目标元素。
  • find_element vs find_elements: find_element 找到第一个匹配的元素,找不到则抛出 NoSuchElementException 异常。find_elements 返回一个包含所有匹配元素的列表,找不到则返回空列表 []

Q5: Selenium 中的等待(Waits)是什么?为什么需要它们?有哪几种等待方式?

A: Selenium 中的等待(Waits)机制用于处理页面加载延迟和动态元素出现的问题。现代网页大量使用 AJAX 和 JavaScript,元素可能不会在页面加载完成后立即出现,而是稍后才被渲染或添加到 DOM 中。如果 Selenium 脚本在元素尚未可用时就尝试操作它,会抛出 NoSuchElementException 或其他错误。

为什么需要等待:

  • 异步加载: 确保脚本在元素完全加载并可交互后再执行操作。
  • 网络延迟: 适应不同的网络速度和服务器响应时间。
  • 提高稳定性: 避免因时间差导致的脚本失败,使爬虫或测试更健壮。

主要等待方式:

  1. 强制等待 (Forced Wait / Sleep):

    • 使用 time.sleep(seconds)
    • 缺点: 不管元素是否提前加载完成,都会固定等待指定时间,效率低下,且无法保证等待时间足够长,或造成不必要的等待。通常不推荐在生产代码中使用,仅用于临时调试。
    python
    import time
    # ... driver setup ...
    time.sleep(5) # 强制等待 5 秒
    element = driver.find_element(By.ID, "some_id")
  2. 隐式等待 (Implicit Wait):

    • 设置一个全局的超时时间 (driver.implicitly_wait(seconds))。当使用 find_element(s) 查找元素时,如果元素没有立即找到,WebDriver 会在 DOM 中持续查找,直到找到元素或超过设定的超时时间为止。
    • 优点: 设置一次,全局生效,代码相对简洁。
    • 缺点:
      • 只针对 find_element(s) 生效,对元素是否可见、可点击等状态无效。
      • 如果元素确实不存在,仍然需要等待整个超时时间,影响效率。
      • 混合使用隐式等待和显式等待可能会导致不可预测的行为,官方不推荐混合使用。
    python
    from selenium import webdriver
    
    driver = webdriver.Chrome()
    driver.implicitly_wait(10) # 设置全局隐式等待最多 10 秒
    driver.get("http://example.com")
    element = driver.find_element(By.ID, "dynamic_element") # 如果找不到,会等最多10秒
  3. 显式等待 (Explicit Wait):

    • 最灵活、最推荐的方式。针对特定条件进行等待,直到条件满足或超时。
    • 使用 WebDriverWait 类结合 expected_conditions 模块(或自定义条件函数)。
    • 优点: 精确控制等待条件和超时时间,只在需要时等待,效率高,更稳定。可以等待元素出现、可见、可点击、文本包含特定内容等多种状态。
    • 缺点: 代码量相对多一点。
    python
    from selenium.webdriver.support.ui import WebDriverWait
    from selenium.webdriver.support import expected_conditions as EC
    from selenium.webdriver.common.by import By
    from selenium import webdriver
    
    driver = webdriver.Chrome()
    driver.get("http://example.com")
    
    try:
        # 等待最多 10 秒,直到 ID 为 'myDynamicElement' 的元素可见
        element = WebDriverWait(driver, 10).until(
            EC.visibility_of_element_located((By.ID, "myDynamicElement"))
        )
        element.click() # 元素可见后进行点击操作
    except TimeoutException:
        print("等待超时,元素未在指定时间内可见!")

拓展知识点:

  • expected_conditions 模块: 提供了大量预定义的等待条件,如:
    • presence_of_element_located: 元素存在于 DOM 中(不一定可见)。
    • visibility_of_element_located: 元素存在且可见(宽高大于0)。
    • element_to_be_clickable: 元素可见且启用(enabled),可以点击。
    • text_to_be_present_in_element: 元素中文本包含特定字符串。
    • alert_is_present: 弹出框出现。
  • Fluent Wait: 虽然在 Python 的 Selenium 绑定中不直接叫 Fluent Wait,但 WebDriverWait 的设计思想类似,可以设置轮询频率 (poll_frequency) 和忽略特定异常 (ignored_exceptions)。
  • 最佳实践: 优先使用显式等待,它是处理动态内容最可靠、最高效的方式。可以少量使用隐式等待作为补充,但要小心其副作用。避免使用 time.sleep()

Q6: Selenium 如何执行 JavaScript 代码?什么场景下需要这样做?

A: Selenium 提供了 execute_script()execute_async_script() 方法来在当前浏览器窗口或框架的上下文中执行 JavaScript 代码。

  • execute_script(script, *args): 同步执行 JavaScript 代码。Python 脚本会等待 JS 执行完毕并获取返回值(如果有)。
  • execute_async_script(script, *args): 异步执行 JavaScript 代码。JS 代码需要通过调用 arguments[arguments.length - 1]() 来通知 Selenium 执行完成,可以传递返回值给这个回调函数。

常用场景:

  1. 滚动页面: 有时需要滚动到页面底部加载更多内容,或者滚动到某个元素使其可见。
    python
    # 滚动到页面底部
    driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
    
    # 滚动到某个元素可见
    element = driver.find_element(By.ID, "some_element")
    driver.execute_script("arguments[0].scrollIntoView(true);", element)
  2. 修改元素属性或样式: 比如移除元素的 disabled 属性使其可点击,或者修改 display: none 使其可见(虽然通常不推荐这样做来绕过逻辑)。
    python
    element = driver.find_element(By.ID, "disabled_button")
    # 移除 disabled 属性
    driver.execute_script("arguments[0].removeAttribute('disabled');", element)
    # 修改样式(需谨慎)
    driver.execute_script("arguments[0].style.display = 'block';", hidden_element)
  3. 直接调用页面上的 JavaScript 函数: 如果页面有全局可访问的 JS 函数,可以直接调用。
    python
    driver.execute_script("myPageFunction('some_argument');")
  4. 获取一些 Selenium API 无法直接获取的信息: 例如,获取伪元素 (::before, ::after) 的内容,或者获取 localStorage/sessionStorage 的数据。
    python
    # 获取 ::before 伪元素 content
    script = "return window.getComputedStyle(arguments[0], '::before').getPropertyValue('content');"
    pseudo_content = driver.execute_script(script, element)
    
    # 获取 localStorage
    local_storage = driver.execute_script("return window.localStorage;")
  5. 触发事件: 有时标准的 .click() 不生效,可以尝试用 JS 触发点击事件。
    python
    driver.execute_script("arguments[0].click();", element)

拓展知识点:

  • arguments 数组:execute_script 中,传递给方法的额外参数 (*args) 会在 JavaScript 代码中以 arguments 数组的形式接收(arguments[0], arguments[1], ...)。
  • 返回值: execute_script 可以返回简单的 JS 类型(数字、字符串、布尔值、数组、字典/对象)以及 DOM 元素(会被包装成 Selenium 的 WebElement 对象)。复杂的 JS 对象可能无法正确返回。
  • 安全性: 执行任意 JavaScript 可能存在安全风险,确保你了解执行的代码的作用。

Q7: Selenium 的优点和缺点是什么?

A:

优点:

  1. 能够处理 JavaScript 渲染: 这是 Selenium 最核心的优势,可以获取动态加载的内容,适用于现代 Web 应用。
  2. 模拟真实用户行为: 可以执行点击、输入、拖拽、下拉选择等复杂操作,处理需要交互的场景。
  3. 跨浏览器兼容: 支持 Chrome, Firefox, Edge, Safari 等主流浏览器。
  4. 多语言支持: 支持 Python, Java, C#, Ruby, JavaScript 等多种编程语言。
  5. 社区活跃,生态成熟: 有大量的文档、教程和第三方库支持。

缺点:

  1. 速度慢: 需要启动和控制真实的浏览器,相比直接发送 HTTP 请求(如 Requests)或 Scrapy,运行速度要慢得多。浏览器渲染、JS 执行都消耗时间。
  2. 资源消耗大: 运行浏览器实例会占用较多的 CPU 和内存资源,不适合大规模、高并发的爬取任务。
  3. 环境依赖: 需要安装浏览器和对应的 WebDriver,部署相对复杂。
  4. 稳定性问题: 容易受到浏览器版本更新、WebDriver 不兼容、网络波动、页面结构变化等因素影响,脚本可能需要频繁维护。
  5. 可能被检测: 一些网站会检测是否由 Selenium/WebDriver 驱动的浏览器在访问,并采取反爬措施。虽然有技术可以尝试绕过,但增加了复杂性。

拓展知识点:

  • Headless 模式: 大部分现代浏览器(如 Chrome, Firefox)支持无头模式运行 Selenium。这意味着浏览器在后台运行,没有图形用户界面,可以节省一些资源并适用于服务器环境。但它仍然是一个完整的浏览器实例,本质上的慢和资源消耗问题依然存在。
    python
    from selenium.webdriver.chrome.options import Options
    chrome_options = Options()
    chrome_options.add_argument("--headless")
    chrome_options.add_argument("--disable-gpu") # 推荐在 headless 时添加
    driver = webdriver.Chrome(options=chrome_options)
  • 替代方案: 对于只需要少量 JS 渲染的场景,可以考虑使用 requests-html 库,它内置了 Chromium,可以在需要时渲染页面,比完整 Selenium 轻量。对于大规模爬取,如果能找到 API 或分析出 JS 加载逻辑,还是优先使用 Requests 或 Scrapy。

Scrapy 专题

Q8: 什么是 Scrapy?它的主要组件有哪些?

A: Scrapy 是一个为了爬取网站数据、提取结构性数据而编写的应用框架 (Framework)。它基于 Python 编写,使用了 Twisted 异步网络库,使得爬取速度快、并发能力强。Scrapy 提供了一整套用于构建爬虫的组件和流程,让开发者可以专注于数据提取逻辑,而不用过多关心请求调度、并发、中间件等底层细节。

主要组件 (Architecture):

  1. Scrapy 引擎 (Engine):
    • 核心组件,负责控制所有组件之间的数据流,触发事务(事件)。协调整个爬虫的运行。
  2. 调度器 (Scheduler):
    • 接收引擎发来的请求 (Request 对象),并将其放入队列中,以便后续交给下载器执行。负责管理待爬取的 URL 队列,可以实现去重等逻辑。
  3. 下载器 (Downloader):
    • 负责获取页面数据。它从调度器接收请求,然后通过网络下载网页内容,并将获取到的响应 (Response 对象) 返回给引擎,最终交给 Spider 处理。
  4. 爬虫 (Spiders):
    • 开发者编写的主要部分。每个 Spider 负责爬取特定网站(或一组网站)并从中提取数据。
    • 主要工作:
      • 定义初始的爬取请求 (start_urls 或 start_requests 方法)。
      • 解析下载器返回的响应 (Response),提取出需要的数据(生成 Item 对象)。
      • 解析响应,生成新的请求 (Request 对象),交给引擎送往调度器,实现递归爬取。
  5. 项目管道 (Item Pipeline):
    • 负责处理由 Spider 提取生成的 Item 对象。当 Item 从 Spider 发出后,会按顺序通过多个 Pipeline 组件。
    • 常用功能:数据清洗、验证数据有效性、去重、将数据持久化存储(如存入数据库、写入文件)。
  6. 下载中间件 (Downloader Middlewares):
    • 位于引擎和下载器之间的一系列钩子(Hook)。可以在请求被发送到下载器之前(处理请求,如添加代理、修改 User-Agent)和响应被发送给引擎之前(处理响应,如处理异常、重试)进行自定义处理。
  7. 爬虫中间件 (Spider Middlewares):
    • 位于引擎和 Spider 之间的一系列钩子。可以在调用 Spider 的解析方法之前(处理输入 Response)和之后(处理输出的 Item 和 Request)进行自定义处理。

数据流 (Data Flow):

  1. 引擎从 Spider 获取初始请求。
  2. 引擎将请求发送给调度器。
  3. 引擎向调度器请求下一个要爬取的请求。
  4. 调度器返回下一个请求给引擎。
  5. 引擎将请求通过下载中间件发送给下载器。
  6. 下载器下载网页,生成响应,通过下载中间件发送回引擎。
  7. 引擎将响应通过爬虫中间件发送给 Spider 进行处理。
  8. Spider 处理响应,解析出 Item 和新的 Request,返回给引擎。
  9. 引擎将 Item 通过项目管道进行处理。
  10. 引擎将新的 Request 发送给调度器,重复步骤 3。

拓展知识点:

  • 异步处理: Scrapy 基于 Twisted,所有操作(网络请求、数据处理等)都是异步非阻塞的,这是它高效的关键。
  • 命令行工具: Scrapy 提供强大的命令行工具 (scrapy) 用于创建项目、生成模板代码、运行爬虫、调试等。
  • Settings.py: 项目的配置文件,用于配置各种组件的行为(如并发数、下载延迟、启用 Pipeline/Middleware、User-Agent 等)。

Q9: Scrapy 中的 Spider 是什么?如何定义一个 Spider?

A: Spider 是 Scrapy 中用于定义如何爬取特定网站(或一组网站)并从中提取数据的类。开发者需要继承 scrapy.Spider 类并实现其中的特定方法和属性。

定义一个基本的 Spider:

python
import scrapy

class MySpider(scrapy.Spider):
    # 必需:Spider 的唯一标识名,用于在项目中运行它
    name = "my_spider"

    # 可选:允许爬取的域名范围,防止爬虫爬到其他网站
    allowed_domains = ["example.com"]

    # 可选:爬虫启动时要爬取的初始 URL 列表
    # Scrapy 会为列表中的每个 URL 自动创建一个 Request 对象
    # 并将响应传递给 parse 方法处理
    start_urls = [
        "http://www.example.com/page1",
        "http://www.example.com/page2",
    ]

    # 或者,你可以不使用 start_urls,而是实现 start_requests(self) 方法
    # 来生成初始请求,这提供了更大的灵活性(例如发送 POST 请求或添加 Headers)
    # def start_requests(self):
    #     urls = [
    #         "http://www.example.com/page1",
    #         "http://www.example.com/page2",
    #     ]
    #     for url in urls:
    #         yield scrapy.Request(url=url, callback=self.parse)

    # 必需:默认的回调函数,用于处理下载器返回的响应 (Response)
    # 当 start_urls 中的 URL 或 start_requests 生成的请求被下载后,
    # 其响应会作为参数传递给这个方法
    def parse(self, response):
        # response 参数是一个 TextResponse 对象,包含了页面内容和元数据

        # 1. 提取数据 (使用 Scrapy Selectors - 支持 CSS 和 XPath)
        # 示例:提取所有 h2 标题的文本
        titles = response.css('h2::text').getall()
        for title in titles:
            # 可以直接打印,或生成 Item 对象
            print(f"Found title: {title.strip()}")
            # 假设你定义了一个 Item 类叫 MyItem
            # item = MyItem()
            # item['title'] = title.strip()
            # yield item # 将 Item 交给 Item Pipeline 处理

        # 2. 提取链接并生成新的请求 (实现翻页或深入爬取)
        # 示例:查找下一页的链接
        next_page_url = response.css('a.next_page::attr(href)').get()
        if next_page_url:
            # 使用 response.urljoin 确保相对 URL 变成绝对 URL
            next_page_full_url = response.urljoin(next_page_url)
            print(f"Found next page: {next_page_full_url}")
            # 创建一个新的请求,指定回调函数为 self.parse (也可以是其他方法)
            yield scrapy.Request(url=next_page_full_url, callback=self.parse)

        # 也可以直接处理数据并存储,但推荐使用 Item Pipeline
        # filename = f'page-{response.url.split("/")[-1]}.html'
        # with open(filename, 'wb') as f:
        #     f.write(response.body)
        # self.log(f'Saved file {filename}')

核心要素:

  • name: 爬虫的唯一名称。
  • allowed_domains: 限制爬取范围(可选但推荐)。
  • start_urlsstart_requests(): 定义爬虫的入口点。
  • parse(self, response): 处理响应、提取数据、生成新请求的核心方法。

拓展知识点:

  • 回调函数 (Callback): scrapy.Request 中的 callback 参数指定了当该请求的响应下载完成后,应该由哪个 Spider 方法来处理。默认是 parse 方法,但你可以指定任何其他方法,实现不同的解析逻辑。
  • Selectors: Scrapy 内置了强大的选择器(基于 parsel 库),可以使用 response.css('...')response.xpath('...') 来提取数据。.get() 获取第一个匹配项,.getall() 获取所有匹配项列表。
  • Item: 用于封装提取到的结构化数据。通常在 items.py 文件中定义,继承自 scrapy.Item。使用 Item 可以方便地在 Pipeline 中进行统一处理和存储。
  • Request 对象: scrapy.Request 对象包含了 URL、回调函数、请求方法 (GET/POST)、请求头、请求体 (body)、元数据 (meta) 等信息。yield Request(...) 是将新请求交给 Scrapy 引擎的关键。
  • Meta 数据传递: 可以通过 Requestmeta 字典在请求和响应之间传递数据。例如,在翻页时将当前页码传递给下一个请求的回调函数。yield scrapy.Request(url=next_url, callback=self.parse, meta={'page_num': current_page + 1}),在 parse 方法中通过 response.meta['page_num'] 获取。

Q10: Scrapy 中的 Item Pipeline 是什么?它有什么作用?

A: Item Pipeline(项目管道)是 Scrapy 中用于处理由 Spider 提取生成的 Item 对象的一系列组件。当 Spider 使用 yield item 返回一个 Item 时,这个 Item 会被依次传递给在 settings.py 中配置启用的所有 Item Pipeline 组件进行处理。

主要作用:

  1. 数据清洗 (Data Cleaning): 清理 HTML 标签、格式化数据(如日期、数字)、去除空白字符等。
  2. 数据验证 (Data Validation): 检查提取到的数据是否符合预期格式、是否有缺失值等。如果数据无效,可以抛出 DropItem 异常来丢弃该 Item。
  3. 去重 (Deduplication): 判断当前 Item 是否已经爬取过(例如基于某个唯一 ID),如果重复则丢弃。
  4. 数据持久化 (Data Persistence): 将有效的 Item 数据存储到外部存储中,如:
    • 数据库 (MySQL, PostgreSQL, MongoDB, Redis等)
    • 文件 (JSON, CSV, XML等)
    • 云存储服务

如何定义一个 Item Pipeline:

在项目下的 pipelines.py 文件中定义一个或多个 Pipeline 类。每个 Pipeline 类需要实现 process_item(self, item, spider) 方法。

python
# pipelines.py
import pymongo
from scrapy.exceptions import DropItem
import logging

class PriceValidationPipeline:
    def process_item(self, item, spider):
        # 示例:验证价格字段是否存在且大于 0
        if 'price' in item and item['price'] > 0:
            return item # Item 有效,传递给下一个 Pipeline (如果还有)
        else:
            logging.warning(f"Dropping item due to invalid price: {item.get('title', 'N/A')}")
            raise DropItem("Missing or invalid price") # 丢弃 Item

class MongoPipeline:
    collection_name = 'products' # MongoDB 集合名称

    def __init__(self, mongo_uri, mongo_db):
        self.mongo_uri = mongo_uri
        self.mongo_db = mongo_db
        self.client = None
        self.db = None

    @classmethod
    def from_crawler(cls, crawler):
        # Scrapy 推荐的方式:从 settings.py 读取配置
        return cls(
            mongo_uri=crawler.settings.get('MONGO_URI'),
            mongo_db=crawler.settings.get('MONGO_DB', 'items') # 提供默认值
        )

    def open_spider(self, spider):
        # 当 Spider 启动时调用,用于初始化资源(如数据库连接)
        self.client = pymongo.MongoClient(self.mongo_uri)
        self.db = self.client[self.mongo_db]
        logging.info("MongoDB connection opened.")

    def close_spider(self, spider):
        # 当 Spider 关闭时调用,用于释放资源
        if self.client:
            self.client.close()
            logging.info("MongoDB connection closed.")

    def process_item(self, item, spider):
        # 核心处理方法:将 Item 存入 MongoDB
        try:
            # 注意:要将 Scrapy Item 转换为字典
            self.db[self.collection_name].insert_one(dict(item))
            logging.debug(f"Item stored in MongoDB: {item.get('title', 'N/A')}")
        except Exception as e:
            logging.error(f"Error storing item to MongoDB: {e}")
        return item # 必须返回 item,以便其他 Pipeline 处理(或者表明处理完成)

启用 Pipeline:

需要在 settings.py 文件中配置 ITEM_PIPELINES 字典。字典的键是 Pipeline 类的路径,值是一个整数(0-1000),表示 Pipeline 的执行顺序(数字越小越先执行)。

python
# settings.py
ITEM_PIPELINES = {
   'myproject.pipelines.PriceValidationPipeline': 100, # 先执行验证
   'myproject.pipelines.MongoPipeline': 300,          # 再执行存储
   # 'myproject.pipelines.DuplicatesPipeline': 200,   # 可以插入去重 Pipeline
}

# 还需要在 settings.py 中配置 MongoDB 连接信息 (如果使用 MongoPipeline)
MONGO_URI = 'mongodb://localhost:27017/'
MONGO_DB = 'scrapy_db'

拓展知识点:

  • process_item(self, item, spider): 必须实现的方法。需要返回 Item 对象(传递给下一个 Pipeline)或抛出 DropItem 异常(终止处理该 Item)。
  • open_spider(self, spider): 可选方法,在 Spider 启动时调用一次,适合初始化操作。
  • close_spider(self, spider): 可选方法,在 Spider 关闭时调用一次,适合清理操作。
  • from_crawler(cls, crawler): 类方法,用于创建 Pipeline 实例。可以通过 crawler.settings 获取配置信息,这是 Scrapy 推荐的依赖注入方式。
  • DropItem 异常:scrapy.exceptions 导入,用于告知 Scrapy 引擎丢弃当前 Item,不再进行后续处理。

A: Scrapy 提供了多种方式来处理需要登录才能访问的网站:

  1. 使用 FormRequest 模拟表单提交:

    • 这是最常见的方式。首先,你需要找到登录表单所在的 URL,以及提交表单所需的参数(通常是用户名、密码,可能还有隐藏字段如 CSRF token)。
    • 在 Spider 的 start_requests 方法(或任何需要登录的地方)中,构造一个 scrapy.FormRequest 对象。
    • 设置 formdata 参数为一个包含表单字段的字典。
    • 设置 callback 参数为你登录成功后希望访问的页面的处理函数。Scrapy 会自动处理登录过程中产生的 Cookie,并在后续对该域名的请求中携带它们。
    python
    import scrapy
    from scrapy.http import FormRequest
    
    class LoginSpider(scrapy.Spider):
        name = 'login_spider'
        login_url = 'http://example.com/login'
        start_urls = ['http://example.com/dashboard'] # 假设 dashboard 需要登录
    
        def start_requests(self):
            # 先发送登录请求
            yield FormRequest(
                url=self.login_url,
                formdata={'username': 'your_username', 'password': 'your_password'},
                callback=self.after_login # 指定登录成功后的回调函数
            )
    
        def after_login(self, response):
            # 检查是否登录成功 (根据页面内容或状态码判断)
            if "authentication failed" in response.text.lower():
                self.logger.error("Login failed!")
                return
            else:
                self.logger.info("Login successful!")
                # 登录成功后,再去爬取需要登录才能访问的页面
                # Scrapy 会自动携带登录时获取的 Cookie
                for url in self.start_urls:
                    yield scrapy.Request(url=url, callback=self.parse_dashboard)
    
        def parse_dashboard(self, response):
            # 解析需要登录才能访问的页面内容
            self.logger.info(f"Successfully accessed dashboard page: {response.url}")
            # ... 提取数据 ...
            yield {'data': response.css('div.user-data::text').get()}
  2. 手动管理 Cookie:

    • 如果你已经通过其他方式(如浏览器开发者工具)获取到了登录后的有效 Cookie,可以在 Request 中直接设置 cookies 参数。
    python
    yield scrapy.Request(
        url='http://example.com/protected_page',
        cookies={'session_id': 'abcdef123456', 'user_role': 'admin'},
        callback=self.parse_protected
    )
    • 也可以在 settings.py 中启用 COOKIES_ENABLED = True (默认开启) 和 COOKIES_DEBUG = True (可选,用于调试查看 Cookie 传递情况)。Scrapy 会自动管理每个域名下的 Cookie。
  3. 使用中间件处理登录:

    • 对于复杂的登录流程(如需要多次跳转、处理验证码、动态 Token 等),可以编写一个下载中间件 (Downloader Middleware) 来专门处理登录逻辑。中间件可以在每次请求发送前检查登录状态,如果未登录或 Session 过期,则执行登录操作,然后再继续原请求。

拓展知识点:

  • CSRF Token: 很多网站使用 CSRF Token 防止跨站请求伪造。登录时,通常需要先访问登录页面,从 HTML 中提取隐藏的 CSRF Token 值,然后在 FormRequestformdata 中一起提交。
  • 分析登录请求: 使用浏览器开发者工具(Network 面板)观察登录时实际发送的网络请求,查看请求方法(GET/POST)、目标 URL、表单数据、请求头(尤其是 Cookie, Referer, User-Agent)等,是模拟登录的关键步骤。
  • 会话保持: Scrapy 的 Cookie 管理机制(通常是自动的)确保了登录状态在后续请求中得以保持(只要是在同一域名下且 Cookie 未过期)。

Q12: Scrapy 的优点和缺点是什么?

A:

优点:

  1. 高性能、高并发: 基于 Twisted 异步框架,能够同时处理大量请求,爬取速度非常快。
  2. 强大的框架结构: 提供了一整套成熟的组件(调度器、下载器、Spider、Pipeline、中间件),结构清晰,易于管理和扩展大型爬虫项目。
  3. 可扩展性强: 通过中间件和 Pipeline 可以方便地定制请求处理、响应处理、数据处理等各个环节。
  4. 内置常用功能: 自带请求调度、去重(基于 URL)、下载延迟、自动限速(AutoThrottle)、代理、Cookie 管理、数据导出(Feed Exports)等实用功能。
  5. 强大的选择器: 内置 CSS 和 XPath 选择器,数据提取方便。
  6. 健壮性: 提供了重试机制、错误处理等,使爬虫更加稳定。
  7. 活跃社区和文档: 有着非常完善的官方文档和活跃的社区支持。

缺点:

  1. 学习曲线相对陡峭: 相比于简单的 Requests + BS4 组合,Scrapy 的框架概念(组件、数据流、异步)需要一定的学习时间。
  2. 不直接支持 JavaScript 渲染: Scrapy 本身不执行 JavaScript。对于需要 JS 渲染的动态页面,需要结合其他工具,如:
    • Splash: 一个基于 WebKit 的 JavaScript 渲染服务,可以与 Scrapy 集成(通过 scrapy-splash 插件)。
    • Selenium/Playwright: 可以编写下载中间件,在中间件中使用 Selenium 或 Playwright 来获取渲染后的页面内容,再交给 Scrapy 的 Spider 解析。这会牺牲部分性能优势。
  3. 配置相对复杂: 项目结构和配置文件(settings.py)比简单脚本要多。
  4. 不适合非常小的任务: 对于只需要爬取几个简单页面的任务,使用 Scrapy 可能显得有些“杀鸡用牛刀”,Requests + BS4 可能更快捷。
  5. Windows 下安装 Twisted 可能稍麻烦: 虽然现在已经改善很多,但过去在 Windows 上安装 Twisted 依赖有时会遇到问题。

拓展知识点:

  • 何时选择 Scrapy: 当你需要构建一个稳定、高效、可扩展的爬虫系统,用于爬取大量数据、需要并发处理、需要定制化数据处理流程(如存储到数据库)时,Scrapy 是非常好的选择。
  • Scrapy Cloud: Scrapy 官方提供的云平台,用于部署、运行和管理 Scrapy 爬虫,省去了自己搭建服务器和运维的麻烦。
  • Scrapy 生态: 有许多优秀的第三方 Scrapy 插件(如 scrapy-proxies, scrapy-useragents, scrapy-fake-useragent, scrapy-deltafetch 等)可以进一步扩展 Scrapy 的功能。

Selenium vs Scrapy 对比

Q13: Selenium 和 Scrapy 的主要区别是什么?应该在什么情况下选择使用它们?

A: Selenium 和 Scrapy 是解决 Web 数据抓取问题的两种不同思路和工具,主要区别在于它们的工作方式和适用场景。

主要区别:

特性SeleniumScrapy
核心功能浏览器自动化测试工具Web 爬虫框架
工作方式驱动真实浏览器,模拟用户操作,执行 JavaScript直接发送 HTTP 请求,解析响应(默认不执行 JS)
JS 处理强项: 能够渲染和执行 JavaScript弱项: 本身不执行 JS (需集成 Splash/Selenium)
速度慢: 浏览器加载、渲染、JS 执行耗时快: 基于异步 IO,直接请求,高并发
资源消耗高: 运行浏览器实例占用 CPU/内存低: 相对轻量,适合大规模爬取
架构库/工具,自由组合完整的框架,有固定结构和组件
并发性有限(可通过多进程/线程/Selenium Grid 实现)强项: 内置异步并发支持
易用性上手相对简单(对有编程基础者)学习曲线稍陡峭(需要理解框架概念)
稳定性较低(易受浏览器/WebDriver/页面变化影响)相对较高(HTTP 请求更稳定)
反爬处理易被检测 (WebDriver 特征),需额外处理较易通过修改 Headers, 代理等方式伪装

选择场景:

  • 选择 Selenium 当:

    • 目标网站内容是通过 JavaScript 动态加载的,不执行 JS 就无法获取完整数据。
    • 需要模拟复杂的用户交互(如登录、点击按钮展开内容、拖动滑块、处理复杂表单)才能获取数据。
    • 目标网站有较强的反爬措施针对非浏览器请求,模拟真实浏览器访问是唯一途径。
    • 任务量不大,对爬取速度和资源消耗要求不高。
    • 你需要进行 Web 应用的功能测试,顺便抓取数据。
  • 选择 Scrapy 当:

    • 目标网站内容主要是静态 HTML,或者可以通过**分析网络请求(API)**直接获取数据(JSON 等)。
    • 需要大规模、高效率地爬取大量数据。
    • 需要高并发能力。
    • 需要一个结构化、可扩展的爬虫框架来管理复杂的爬取逻辑和数据处理流程(如清洗、存储到数据库)。
    • 对爬取速度和资源利用率有较高要求。
    • 网站可以通过简单的 Headers 伪装、代理、Cookie 管理等方式应对反爬。

拓展知识点:

  • 混合使用: 在 Scrapy 中集成 Selenium (通常通过下载中间件) 是一种常见的策略。当 Scrapy 遇到需要 JS 渲染的页面时,调用 Selenium 中间件来获取渲染后的 HTML,然后再交给 Scrapy 的 Spider 解析。这样可以结合 Scrapy 的高效调度和 Selenium 的 JS 处理能力,但会牺牲部分性能。
    • 可以使用 scrapy-selenium 这样的第三方库来简化集成。
  • 分析优先: 在决定使用 Selenium 之前,强烈建议先使用浏览器开发者工具(Network 面板)仔细分析目标网站的数据加载方式。很多看似动态加载的页面,实际上是通过 AJAX 请求获取 JSON 数据,这种情况下直接用 Scrapy(或 Requests)模拟这些 API 请求会比用 Selenium 高效得多。

总结与建议

  • 基础为王: 无论使用哪个工具,扎实的 Python 基础、对 HTTP 协议的理解、HTML/CSS/JavaScript 基础知识、以及 CSS 选择器/XPath 的熟练运用都是必不可少的。
  • 工具选择: 根据目标网站的特点和爬取需求选择最合适的工具。不要盲目追求“高级”工具,简单任务用 Requests+BS4 可能更高效。
  • 遵守规则: 始终关注 robots.txt 文件,尊重网站的爬取策略。控制爬取速度,避免给目标服务器带来过大压力。
  • 处理反爬: 学习常见的反爬虫技术(User-Agent 检测、IP 限制、验证码、动态 Token、JS 混淆、蜜罐等)及其应对策略(User-Agent 轮换、代理 IP 池、打码平台接入、分析 JS 逻辑、小心隐藏链接等)。
  • 数据存储: 根据数据量和后续用途选择合适的存储方式(CSV, JSON, 数据库等)。
  • 法律与道德: 爬虫是把双刃剑,务必确保爬取行为合法合规,不侵犯他人隐私和数据版权。只爬取公开数据,并用于合理目的。

Q14: 为什么爬取到的数据通常需要清洗?常见的数据清洗任务有哪些?

A: 从网页上爬取到的原始数据往往是“脏”的,直接使用可能会导致分析错误或程序异常。数据清洗是数据预处理的关键步骤,目的是提高数据质量,使其适用于后续的分析、存储或展示。

为什么需要清洗:

  1. 格式不统一: 网页数据来源多样,相同类型的数据可能有不同格式(如日期:"2023-10-26", "Oct 26, 2023", "26/10/2023")。
  2. 包含无关内容: 提取时可能带有多余的 HTML 标签、CSS 样式、JavaScript 代码、广告文本、导航链接等。
  3. 存在冗余字符: 如多余的空格、换行符、制表符 (\t)、HTML 实体(如 &nbsp;, &amp;)。
  4. 数据类型错误: 数字被提取为字符串(如 "1,234"),布尔值表示不一等。
  5. 缺失值: 某些字段在某些页面上可能不存在,导致提取结果为 None 或空字符串。
  6. 结构混乱: 数据可能嵌套在复杂的标签中,或者一个字段包含了多个信息需要拆分。

常见的数据清洗任务:

  • 去除 HTML 标签和实体: 从文本中移除 <div>, <span>, &nbsp; 等。
  • 处理空白字符: 去除首尾空格、将多个连续空格替换为单个空格、移除换行符等。
  • 格式标准化: 将日期统一为 YYYY-MM-DD 格式,将货币去除符号并转为数字。
  • 数据类型转换: 将字符串形式的数字转为 intfloat
  • 处理缺失值: 根据策略决定是填充默认值、删除该条记录,还是使用特定方法估算。
  • 内容提取与拆分: 从混合文本中提取特定信息(如从 "价格:¥99.9" 中提取 99.9),或将一个字段拆分为多个(如将 "北京 朝阳区" 拆分为 "城市" 和 "区县")。
  • 去重: 移除完全重复的数据记录。

拓展知识点:

  • 数据质量维度: 清洗的目标通常是提升数据的完整性 (Completeness)、一致性 (Consistency)、准确性 (Accuracy)、有效性 (Validity)、唯一性 (Uniqueness)。
  • GIGO (Garbage In, Garbage Out): 这是数据处理中的一句名言,意指如果输入的数据质量差,那么输出的结果(分析、模型等)也必然不可靠。因此数据清洗至关重要。

Q15: Python 中有哪些常用的库或技术可以用来进行数据清洗?

A: Python 提供了丰富的库和内置功能来支持数据清洗:

  1. 内置字符串方法:
    • 核心: str.strip(), str.lstrip(), str.rstrip() (去首尾空白), str.replace() (替换子串), str.split() (分割字符串), ' '.join() (合并列表为字符串)。
    • 优点: Python 内置,无需安装,简单直接,性能较好,适用于基本的文本清理。
    • 缺点: 功能相对有限,处理复杂模式匹配能力弱。
  2. 正则表达式 (re 模块):
    • 核心: re.sub() (查找并替换), re.findall() (查找所有匹配项), re.search() (查找第一个匹配项), re.match() (从头匹配), re.split() (按模式分割)。
    • 优点: 非常强大,能够处理复杂的文本模式匹配、提取和替换任务,灵活性高。
    • 缺点: 语法学习曲线较陡峭,写得不好的正则表达式可能效率不高或难以理解。
  3. Beautiful Soup 4 (BS4):
    • 核心: 虽然主要用于解析,但其 .get_text() 方法可以方便地去除所有 HTML 标签,得到纯文本。也可以选择性地 .extract().decompose() 移除特定标签。
    • 优点: 在解析阶段就能进行一定程度的清洗(去标签),与解析过程结合紧密。
    • 缺点: 主要功能是解析,不擅长纯粹的字符串操作或结构化数据清洗。清洗能力有限。
  4. Pandas:
    • 核心: 提供 DataFrameSeries 数据结构,以及大量的用于数据处理和清洗的方法,如 .strip(), .replace(), .fillna() (填充缺失值), .dropna() (删除缺失值), .astype() (类型转换), .apply() (应用自定义函数), .str 访问器 (对 Series 执行字符串方法), .to_datetime() (日期转换) 等。
    • 优点: 处理结构化数据(表格型)极为强大和高效,支持向量化操作,提供了处理缺失值、数据类型转换、数据对齐等的完善方案。非常适合处理从 API 获取的 JSON 或爬取后整理成类似表格的数据。
    • 缺点: 对于非结构化或纯文本的简单清洗任务,引入 Pandas 可能有些“重”。需要将数据加载到其特定数据结构中。
  5. Scrapy Item Loaders (针对 Scrapy 框架):
    • 核心: Scrapy 提供的机制,通过定义输入处理器 (Input Processors) 和输出处理器 (Output Processors) 来在数据填充到 Item 之前进行预处理和格式化。
    • 优点: 将清洗逻辑与 Spider 的解析逻辑分离,使代码更清晰、可重用。与 Scrapy 框架集成度高。
    • 缺点: 仅适用于 Scrapy 项目。需要理解 Item Loader 的概念和用法。

拓展知识点:

  • 组合使用: 实际项目中,通常会组合使用这些工具。例如,用 BS4 或 Scrapy Selectors 初步提取并去除标签,然后用字符串方法和正则表达式做精细文本处理,最后如果数据量大且结构化,可能会用 Pandas 进行整理、去重和最终的格式化。

Q16: 如何使用 Python 内置字符串方法和正则表达式 (re) 清洗常见的“脏”文本?给些例子。

A: 字符串方法和 re 模块是文本清洗的基础,非常常用。

场景1: 去除首尾空白及多余空格

python
import re

raw_text = "  \n   商品名称:  超值 T恤 \t  价格:99 元  \n "

# 1. 使用字符串方法
cleaned_text_str = raw_text.strip() # 去除首尾空白(包括换行符、制表符)
print(f"String Method (strip): '{cleaned_text_str}'")
# 输出: 'String Method (strip): '商品名称:  超值 T恤 \t  价格:99 元''

# 进一步处理中间多余空格 (较复杂,replace 不能很好处理多种空白混合)
# 通常结合 split 和 join
temp_list = cleaned_text_str.split()
cleaned_text_str_final = ' '.join(temp_list)
print(f"String Method (split/join): '{cleaned_text_str_final}'")
# 输出: 'String Method (split/join): '商品名称: 超值 T恤 价格:99 元''

# 2. 使用正则表达式 (更灵活处理中间空白)
# \s+ 匹配一个或多个任意空白字符
cleaned_text_re = re.sub(r'\s+', ' ', raw_text) # 将连续空白替换为单个空格
cleaned_text_re = cleaned_text_re.strip()       # 再去除首尾可能产生的空格
print(f"Regex Method: '{cleaned_text_re}'")
# 输出: 'Regex Method: '商品名称: 超值 T恤 价格:99 元''

场景2: 去除 HTML 标签

python
import re

html_text = "<p>这是<b>重要</b>的 <a href='#'>链接</a>&nbsp;内容.</p>"

# 1. 使用正则表达式 (简单场景)
# <.*?> 是一个非贪婪匹配,匹配 < 开头 > 结尾的最短字符串
cleaned_text_re = re.sub(r'<.*?>', '', html_text)
print(f"Regex (basic tag removal): '{cleaned_text_re}'")
# 输出: 'Regex (basic tag removal): '这是重要的 链接&nbsp;内容.''
# 注意:简单的正则无法完美处理所有嵌套或复杂 HTML,且 &nbsp; 还在

# 2. 处理 HTML 实体 (如 &nbsp;)
cleaned_text_re = cleaned_text_re.replace('&nbsp;', ' ') # 替换实体为空格
print(f"Regex + Replace Entity: '{cleaned_text_re}'")
# 输出: 'Regex + Replace Entity: '这是重要的 链接 内容.''

# 更好的方法通常是使用 HTML 解析库如 BS4
from bs4 import BeautifulSoup
soup = BeautifulSoup(html_text, 'html.parser')
cleaned_text_bs4 = soup.get_text(separator=' ', strip=True) # separator指定分隔符, strip去除首尾空白
print(f"BeautifulSoup get_text: '{cleaned_text_bs4}'")
# 输出: 'BeautifulSoup get_text: '这是 重要 的 链接 内容.''

场景3: 提取文本中的数字 (如价格)

python
import re

price_text_list = ["价格: ¥ 1,299.50", "仅售 88 元", "免费", "Price: $45.9"]

prices = []
for text in price_text_list:
    # 正则表达式:匹配可选的货币符号,后跟数字(可能带逗号和最多一个小数点)
    # (?:...) 是非捕获组
    # [\d,]+ 匹配一个或多个数字或逗号
    # (?:\.\d+)? 匹配可选的小数部分
    match = re.search(r'[\d,]+(?:\.\d+)?', text)
    if match:
        price_str = match.group(0)
        # 清理逗号,转换为浮点数
        price_float = float(price_str.replace(',', ''))
        prices.append(price_float)
    else:
        # 可以根据情况处理 "免费" 或无法匹配的情况
        if "免费" in text:
            prices.append(0.0)
        else:
            prices.append(None) # 或者标记为无法提取

print(f"Extracted Prices: {prices}")
# 输出: 'Extracted Prices: [1299.5, 88.0, 0.0, 45.9]'

优缺点总结:

  • 字符串方法:
    • 优点: 简单、直观、高效(用于基础操作)。
    • 缺点: 功能有限,不适合复杂模式。
  • 正则表达式:
    • 优点: 功能强大,灵活性高,能处理复杂模式。
    • 缺点: 语法复杂,可读性可能较差,性能可能不如优化过的字符串方法(但通常足够快)。

Q17: Pandas 库在数据清洗中扮演什么角色?什么时候应该使用 Pandas?

A: Pandas 是 Python 中用于数据分析和处理的基石库,它在数据清洗中扮演着处理结构化数据的核心角色。

Pandas 的角色:

  1. 数据结构化: 将爬取到的数据(通常是字典列表、JSON、或需要组织的零散信息)加载到 DataFrame(二维表格)或 Series(一维数组)中,提供了一个清晰、规范的操作对象。
  2. 批量处理: 支持向量化操作,可以对整列数据(Series)进行快速的清洗操作(如 df['column'].str.strip() 会同时作用于列中所有字符串),比逐行循环处理效率高得多。
  3. 缺失值处理: 提供 isnull(), notnull(), fillna(), dropna() 等方便的函数来检测、填充或删除缺失值(NaN)。
  4. 数据类型转换: 使用 .astype() 方法轻松转换列的数据类型(如将字符串价格列转为 float)。
  5. 文本清洗: 通过 .str 访问器,可以在 Series 上调用大部分 Python 内置字符串方法(strip, replace, contains, split 等)以及正则表达式方法。
  6. 数据标准化与转换: 使用 .apply().map() 应用自定义函数进行复杂的格式转换或计算。例如,统一日期格式,或从地址中提取省市。
  7. 去重: 使用 .duplicated().drop_duplicates() 方便地检测和移除重复行。

什么时候应该使用 Pandas:

  • 处理的数据是结构化的或可以被结构化为表格形式: 例如,爬取商品列表(包含名称、价格、销量、链接等字段),或者从 API 获取的 JSON 数组。
  • 数据量较大: Pandas 的向量化操作在高数据量下性能优势明显。
  • 需要进行多种清洗和转换操作: Pandas 提供了丰富的功能集,可以链式调用多种操作,代码相对简洁。
  • 需要处理缺失值: Pandas 有专门且高效的缺失值处理机制。
  • 清洗后的数据需要进一步分析或可视化: Pandas 是后续进行数据分析(统计、聚合)和可视化(配合 Matplotlib, Seaborn)的基础。

使用案例:

假设爬取到如下字典列表数据:

python
scraped_data = [
    {'title': '  Product A \n', 'price': ' ¥ 199.9 ', 'sales': '500+', 'date': '2023/10/26 '},
    {'title': 'Product B', 'price': ' 99 ', 'sales': None, 'date': 'Oct 27, 2023'},
    {'title': 'Product C ', 'price': '¥ 1,200', 'sales': ' 1k ', 'date': '2023-10-28'},
    {'title': 'Product A \n', 'price': ' ¥ 199.9 ', 'sales': '500+', 'date': '2023/10/26 '}, # 重复数据
]

使用 Pandas 清洗:

python
import pandas as pd
import re

# 1. 加载数据到 DataFrame
df = pd.DataFrame(scraped_data)
print("Original DataFrame:\n", df)

# 2. 去重
df.drop_duplicates(inplace=True)
print("\nAfter drop_duplicates:\n", df)

# 3. 清洗 'title' 列: 去除首尾空白
df['title'] = df['title'].str.strip()

# 4. 清洗 'price' 列: 去除货币符号、空格、逗号,转为 float
def clean_price(price_str):
    if isinstance(price_str, str):
        # 使用正则移除所有非数字和小数点的字符 (保留小数点)
        price_str = re.sub(r'[^\d.]', '', price_str)
        try:
            return float(price_str)
        except ValueError:
            return None # 转换失败则设为 None
    return None # 非字符串输入也设为 None

df['price'] = df['price'].apply(clean_price)

# 5. 清洗 'sales' 列: 处理 'k', '+', 空白,转为 int (假设 None 表示 0 销量)
def clean_sales(sales_str):
    if isinstance(sales_str, str):
        sales_str = sales_str.strip().replace('+', '')
        if 'k' in sales_str.lower():
            num = float(re.sub(r'[^\d.]', '', sales_str.lower().replace('k', '')))
            return int(num * 1000)
        else:
            try:
                return int(re.sub(r'[^\d]', '', sales_str))
            except ValueError:
                return 0 # 转换失败设为 0
    return 0 # None 或其他类型设为 0

df['sales'] = df['sales'].apply(clean_sales)

# 6. 清洗 'date' 列: 统一为 datetime 对象
df['date'] = pd.to_datetime(df['date'], errors='coerce') # errors='coerce' 会将无法解析的日期变为 NaT (Not a Time)

# 7. 处理缺失值 (例如,填充价格均值,日期填充特定值或删除)
# df['price'].fillna(df['price'].mean(), inplace=True)
# df.dropna(subset=['date'], inplace=True) # 删除日期无效的行

print("\nCleaned DataFrame:\n", df)
print("\nData Types:\n", df.dtypes)

优缺点总结:

  • 优点: 处理结构化数据非常强大、高效,功能全面,生态完善。
  • 缺点: 对于简单、非结构化文本清洗任务显得过重,有一定学习曲线,需要将数据加载到其数据结构。

Q18: Scrapy Item Loaders 是如何帮助进行数据清洗的?它和直接在 Spider 中清洗有什么不同?

A: Scrapy Item Loaders 提供了一种更结构化、可维护的方式来处理 Scrapy 爬虫中数据的提取和预处理(清洗)

Item Loaders 的工作方式:

  1. 定义 Item: 首先,你在 items.py 中定义好你的数据结构 (Item)。
  2. 创建 Item Loader: 在 Spider 中,针对每个要提取的数据项(比如一个商品),你创建一个 Item Loader 实例,通常将 response 或一个 Selector 对象传递给它。
  3. 添加值 (Add Value): 使用 .add_xpath(), .add_css(), 或 .add_value() 方法向 Item Loader 中添加待处理的值。这些方法不是直接赋值,而是将提取到的原始数据(通常是列表)暂存起来。
  4. 处理器 (Processors): Item Loader 的核心在于输入处理器 (Input Processors)输出处理器 (Output Processors)
    • 输入处理器: 在值被添加到 Item Loader 时(通过 add_xpath/css/value)被调用。通常用来对提取到的原始值进行初步处理,比如去除 HTML 标签、解码等。可以为每个字段定义不同的输入处理器。
    • 输出处理器: 在调用 .load_item() 方法时被调用,作用于该字段收集到的所有值(通常是一个列表)。负责将这些值处理成最终赋给 Item 字段的单一值。常见的输出处理包括:取列表第一个元素(TakeFirst)、合并列表元素(Join)、类型转换、最终格式化等。
  5. 加载 Item: 调用 .load_item() 方法,此时 Item Loader 会应用所有定义的处理器,并将最终处理好的值填充到 Item 对象的相应字段中,然后返回这个干净的 Item。

示例:

python
# items.py
import scrapy
from itemloaders.processors import TakeFirst, MapCompose, Join, Identity
from w3lib.html import remove_tags

def clean_text(text):
    # 自定义清洗函数:去除首尾空白
    return text.strip()

def parse_price(text):
    # 提取价格数字
    match = re.search(r'[\d,]+(?:\.\d+)?', text)
    if match:
        return float(match.group(0).replace(',', ''))
    return None

class ProductItem(scrapy.Item):
    name = scrapy.Field(
        input_processor=MapCompose(remove_tags, clean_text), # 先去标签,再清理空白
        output_processor=TakeFirst()                         # 取处理后列表的第一个元素
    )
    price = scrapy.Field(
        input_processor=MapCompose(remove_tags, parse_price), # 去标签,然后解析价格
        output_processor=TakeFirst()
    )
    description = scrapy.Field(
        input_processor=MapCompose(remove_tags, clean_text),
        output_processor=Join(separator='\n') # 将多段描述用换行符合并
    )
    # 如果某个字段不需要特殊处理,可以使用默认的 Identity 处理器
    url = scrapy.Field(output_processor=TakeFirst())


# spiders/my_spider.py
import scrapy
from scrapy.loader import ItemLoader
from myproject.items import ProductItem # 假设项目名叫 myproject

class MySpider(scrapy.Spider):
    name = 'product_spider'
    start_urls = ['http://example.com/products']

    def parse(self, response):
        for product_selector in response.css('div.product'):
            # 创建 ItemLoader 实例
            loader = ItemLoader(item=ProductItem(), selector=product_selector)

            # 添加值,此时会应用 input_processor
            loader.add_css('name', 'h2.product-name')
            loader.add_xpath('price', './/span[@class="price"]/text()')
            loader.add_css('description', 'div.desc p') # 可能匹配多个 p 标签
            loader.add_value('url', response.url) # 直接添加值

            # 加载 Item,此时会应用 output_processor
            item = loader.load_item()
            yield item

与直接在 Spider 中清洗的不同:

特点Item Loader 清洗直接在 Spider 中清洗 (parse 方法内)
代码组织清洗逻辑在 Item 定义或自定义处理器中清洗逻辑散布在 parse 方法的提取代码中
可重用性处理器可在多个字段或 Spider 中重用清洗代码通常与特定提取逻辑耦合,不易重用
可维护性逻辑分离,更清晰,易于修改和测试代码可能变得冗长复杂,不易维护
关注点分离Spider 关注提取定位,Loader 关注处理提取和处理逻辑混合在一起
复杂度需要理解 Loader 概念和处理器机制对于简单清洗,可能更直接
Scrapy 集成与 Scrapy 框架深度集成自由编写 Python 代码,不依赖特定 Scrapy 组件

总结:

  • 直接在 Spider 中清洗: 适合简单、一次性的清洗任务,或者当你不需要 Scrapy 的结构化特性时。代码可能更直接,但容易变得混乱。
  • 使用 Item Loaders: 强烈推荐用于 Scrapy 项目,尤其是当清洗逻辑较复杂、需要重用、或者希望保持 Spider 代码简洁和专注于定位元素时。它提供了一种更健壮、可维护的方式来管理数据预处理流程。