Python 爬虫基础面试题
Q1: 什么是网络爬虫?它的基本工作流程是怎样的?
A: 网络爬虫(Web Crawler/Spider)是一种自动浏览万维网(World Wide Web)的程序或脚本。它们按照一定的规则,自动地抓取网络信息。
基本工作流程:
- 发起请求 (Sending Request): 爬虫模拟浏览器,向目标服务器发送 HTTP/HTTPS 请求(Request),请求访问特定的 URL。
- 获取响应 (Getting Response): 服务器收到请求后,会返回一个响应(Response),通常是 HTML、JSON 或其他格式的文本。
- 解析内容 (Parsing Content): 爬虫接收到响应后,需要解析响应内容(通常是 HTML 源码),提取出需要的数据或新的 URL 链接。常用的解析库有 Beautiful Soup, lxml, Scrapy Selectors, 正则表达式等。
- 存储数据 (Storing Data): 将提取到的有用数据存储到本地文件(如 CSV, JSON, TXT)或数据库(如 MySQL, MongoDB, Redis)中。
- (可选)持续爬取: 如果解析出了新的 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 主要用来解决以下问题:
- JavaScript 动态加载内容: 很多现代网站使用 AJAX 或其他 JavaScript 技术动态加载数据。传统的爬虫库(如 Requests)只能获取到初始的 HTML 源码,无法获取由 JavaScript 执行后生成的内容。Selenium 驱动真实浏览器,可以执行页面中的 JavaScript,等待内容加载完成后再进行抓取,从而获取完整的页面数据。
- 模拟用户交互: 某些网站需要用户进行登录、点击特定按钮、滚动页面到底部等操作后才能看到所需信息。Selenium 可以精确模拟这些用户行为。
- 处理复杂验证: 如需要拖动滑块验证等,虽然有专门的反验证码技术,但在某些情况下,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 类的属性):
- By.ID: 通过元素的
id属性定位。这是最快、最推荐的方法(如果元素有唯一的 ID)。pythonelement = driver.find_element(By.ID, "element_id") - By.NAME: 通过元素的
name属性定位。常用于表单元素。pythonelement = driver.find_element(By.NAME, "element_name") - By.CLASS_NAME: 通过元素的
class属性定位。注意,如果 class 包含多个值(用空格分隔),只能使用其中一个。pythonelements = driver.find_elements(By.CLASS_NAME, "some_class") - By.TAG_NAME: 通过元素的 HTML 标签名定位(如
div,a,input)。通常会返回多个元素。pythonlinks = driver.find_elements(By.TAG_NAME, "a") - By.LINK_TEXT: 通过链接(
<a>标签)的完整可见文本定位。pythonlink = driver.find_element(By.LINK_TEXT, "Click Here") - By.PARTIAL_LINK_TEXT: 通过链接(
<a>标签)的部分可见文本定位。pythonlink = driver.find_element(By.PARTIAL_LINK_TEXT, "Click") - 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") - 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_elementvsfind_elements:find_element找到第一个匹配的元素,找不到则抛出NoSuchElementException异常。find_elements返回一个包含所有匹配元素的列表,找不到则返回空列表[]。
Q5: Selenium 中的等待(Waits)是什么?为什么需要它们?有哪几种等待方式?
A: Selenium 中的等待(Waits)机制用于处理页面加载延迟和动态元素出现的问题。现代网页大量使用 AJAX 和 JavaScript,元素可能不会在页面加载完成后立即出现,而是稍后才被渲染或添加到 DOM 中。如果 Selenium 脚本在元素尚未可用时就尝试操作它,会抛出 NoSuchElementException 或其他错误。
为什么需要等待:
- 异步加载: 确保脚本在元素完全加载并可交互后再执行操作。
- 网络延迟: 适应不同的网络速度和服务器响应时间。
- 提高稳定性: 避免因时间差导致的脚本失败,使爬虫或测试更健壮。
主要等待方式:
强制等待 (Forced Wait / Sleep):
- 使用
time.sleep(seconds)。 - 缺点: 不管元素是否提前加载完成,都会固定等待指定时间,效率低下,且无法保证等待时间足够长,或造成不必要的等待。通常不推荐在生产代码中使用,仅用于临时调试。
pythonimport time # ... driver setup ... time.sleep(5) # 强制等待 5 秒 element = driver.find_element(By.ID, "some_id")- 使用
隐式等待 (Implicit Wait):
- 设置一个全局的超时时间 (
driver.implicitly_wait(seconds))。当使用find_element(s)查找元素时,如果元素没有立即找到,WebDriver 会在 DOM 中持续查找,直到找到元素或超过设定的超时时间为止。 - 优点: 设置一次,全局生效,代码相对简洁。
- 缺点:
- 只针对
find_element(s)生效,对元素是否可见、可点击等状态无效。 - 如果元素确实不存在,仍然需要等待整个超时时间,影响效率。
- 混合使用隐式等待和显式等待可能会导致不可预测的行为,官方不推荐混合使用。
- 只针对
pythonfrom 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秒- 设置一个全局的超时时间 (
显式等待 (Explicit Wait):
- 最灵活、最推荐的方式。针对特定条件进行等待,直到条件满足或超时。
- 使用
WebDriverWait类结合expected_conditions模块(或自定义条件函数)。 - 优点: 精确控制等待条件和超时时间,只在需要时等待,效率高,更稳定。可以等待元素出现、可见、可点击、文本包含特定内容等多种状态。
- 缺点: 代码量相对多一点。
pythonfrom 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 执行完成,可以传递返回值给这个回调函数。
常用场景:
- 滚动页面: 有时需要滚动到页面底部加载更多内容,或者滚动到某个元素使其可见。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) - 修改元素属性或样式: 比如移除元素的
disabled属性使其可点击,或者修改display: none使其可见(虽然通常不推荐这样做来绕过逻辑)。pythonelement = 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) - 直接调用页面上的 JavaScript 函数: 如果页面有全局可访问的 JS 函数,可以直接调用。python
driver.execute_script("myPageFunction('some_argument');") - 获取一些 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;") - 触发事件: 有时标准的
.click()不生效,可以尝试用 JS 触发点击事件。pythondriver.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:
优点:
- 能够处理 JavaScript 渲染: 这是 Selenium 最核心的优势,可以获取动态加载的内容,适用于现代 Web 应用。
- 模拟真实用户行为: 可以执行点击、输入、拖拽、下拉选择等复杂操作,处理需要交互的场景。
- 跨浏览器兼容: 支持 Chrome, Firefox, Edge, Safari 等主流浏览器。
- 多语言支持: 支持 Python, Java, C#, Ruby, JavaScript 等多种编程语言。
- 社区活跃,生态成熟: 有大量的文档、教程和第三方库支持。
缺点:
- 速度慢: 需要启动和控制真实的浏览器,相比直接发送 HTTP 请求(如 Requests)或 Scrapy,运行速度要慢得多。浏览器渲染、JS 执行都消耗时间。
- 资源消耗大: 运行浏览器实例会占用较多的 CPU 和内存资源,不适合大规模、高并发的爬取任务。
- 环境依赖: 需要安装浏览器和对应的 WebDriver,部署相对复杂。
- 稳定性问题: 容易受到浏览器版本更新、WebDriver 不兼容、网络波动、页面结构变化等因素影响,脚本可能需要频繁维护。
- 可能被检测: 一些网站会检测是否由 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):
- Scrapy 引擎 (Engine):
- 核心组件,负责控制所有组件之间的数据流,触发事务(事件)。协调整个爬虫的运行。
- 调度器 (Scheduler):
- 接收引擎发来的请求 (Request 对象),并将其放入队列中,以便后续交给下载器执行。负责管理待爬取的 URL 队列,可以实现去重等逻辑。
- 下载器 (Downloader):
- 负责获取页面数据。它从调度器接收请求,然后通过网络下载网页内容,并将获取到的响应 (Response 对象) 返回给引擎,最终交给 Spider 处理。
- 爬虫 (Spiders):
- 开发者编写的主要部分。每个 Spider 负责爬取特定网站(或一组网站)并从中提取数据。
- 主要工作:
- 定义初始的爬取请求 (start_urls 或 start_requests 方法)。
- 解析下载器返回的响应 (Response),提取出需要的数据(生成 Item 对象)。
- 解析响应,生成新的请求 (Request 对象),交给引擎送往调度器,实现递归爬取。
- 项目管道 (Item Pipeline):
- 负责处理由 Spider 提取生成的 Item 对象。当 Item 从 Spider 发出后,会按顺序通过多个 Pipeline 组件。
- 常用功能:数据清洗、验证数据有效性、去重、将数据持久化存储(如存入数据库、写入文件)。
- 下载中间件 (Downloader Middlewares):
- 位于引擎和下载器之间的一系列钩子(Hook)。可以在请求被发送到下载器之前(处理请求,如添加代理、修改 User-Agent)和响应被发送给引擎之前(处理响应,如处理异常、重试)进行自定义处理。
- 爬虫中间件 (Spider Middlewares):
- 位于引擎和 Spider 之间的一系列钩子。可以在调用 Spider 的解析方法之前(处理输入 Response)和之后(处理输出的 Item 和 Request)进行自定义处理。
数据流 (Data Flow):
- 引擎从 Spider 获取初始请求。
- 引擎将请求发送给调度器。
- 引擎向调度器请求下一个要爬取的请求。
- 调度器返回下一个请求给引擎。
- 引擎将请求通过下载中间件发送给下载器。
- 下载器下载网页,生成响应,通过下载中间件发送回引擎。
- 引擎将响应通过爬虫中间件发送给 Spider 进行处理。
- Spider 处理响应,解析出 Item 和新的 Request,返回给引擎。
- 引擎将 Item 通过项目管道进行处理。
- 引擎将新的 Request 发送给调度器,重复步骤 3。
拓展知识点:
- 异步处理: Scrapy 基于 Twisted,所有操作(网络请求、数据处理等)都是异步非阻塞的,这是它高效的关键。
- 命令行工具: Scrapy 提供强大的命令行工具 (
scrapy) 用于创建项目、生成模板代码、运行爬虫、调试等。 - Settings.py: 项目的配置文件,用于配置各种组件的行为(如并发数、下载延迟、启用 Pipeline/Middleware、User-Agent 等)。
Q9: Scrapy 中的 Spider 是什么?如何定义一个 Spider?
A: Spider 是 Scrapy 中用于定义如何爬取特定网站(或一组网站)并从中提取数据的类。开发者需要继承 scrapy.Spider 类并实现其中的特定方法和属性。
定义一个基本的 Spider:
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_urls或start_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 数据传递: 可以通过
Request的meta字典在请求和响应之间传递数据。例如,在翻页时将当前页码传递给下一个请求的回调函数。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 组件进行处理。
主要作用:
- 数据清洗 (Data Cleaning): 清理 HTML 标签、格式化数据(如日期、数字)、去除空白字符等。
- 数据验证 (Data Validation): 检查提取到的数据是否符合预期格式、是否有缺失值等。如果数据无效,可以抛出
DropItem异常来丢弃该 Item。 - 去重 (Deduplication): 判断当前 Item 是否已经爬取过(例如基于某个唯一 ID),如果重复则丢弃。
- 数据持久化 (Data Persistence): 将有效的 Item 数据存储到外部存储中,如:
- 数据库 (MySQL, PostgreSQL, MongoDB, Redis等)
- 文件 (JSON, CSV, XML等)
- 云存储服务
如何定义一个 Item Pipeline:
在项目下的 pipelines.py 文件中定义一个或多个 Pipeline 类。每个 Pipeline 类需要实现 process_item(self, item, spider) 方法。
# 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 的执行顺序(数字越小越先执行)。
# 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,不再进行后续处理。
Q11: Scrapy 如何处理登录和 Cookie?
A: Scrapy 提供了多种方式来处理需要登录才能访问的网站:
使用
FormRequest模拟表单提交:- 这是最常见的方式。首先,你需要找到登录表单所在的 URL,以及提交表单所需的参数(通常是用户名、密码,可能还有隐藏字段如 CSRF token)。
- 在 Spider 的
start_requests方法(或任何需要登录的地方)中,构造一个scrapy.FormRequest对象。 - 设置
formdata参数为一个包含表单字段的字典。 - 设置
callback参数为你登录成功后希望访问的页面的处理函数。Scrapy 会自动处理登录过程中产生的 Cookie,并在后续对该域名的请求中携带它们。
pythonimport 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()}手动管理 Cookie:
- 如果你已经通过其他方式(如浏览器开发者工具)获取到了登录后的有效 Cookie,可以在
Request中直接设置cookies参数。
pythonyield 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。
- 如果你已经通过其他方式(如浏览器开发者工具)获取到了登录后的有效 Cookie,可以在
使用中间件处理登录:
- 对于复杂的登录流程(如需要多次跳转、处理验证码、动态 Token 等),可以编写一个下载中间件 (Downloader Middleware) 来专门处理登录逻辑。中间件可以在每次请求发送前检查登录状态,如果未登录或 Session 过期,则执行登录操作,然后再继续原请求。
拓展知识点:
- CSRF Token: 很多网站使用 CSRF Token 防止跨站请求伪造。登录时,通常需要先访问登录页面,从 HTML 中提取隐藏的 CSRF Token 值,然后在
FormRequest的formdata中一起提交。 - 分析登录请求: 使用浏览器开发者工具(Network 面板)观察登录时实际发送的网络请求,查看请求方法(GET/POST)、目标 URL、表单数据、请求头(尤其是
Cookie,Referer,User-Agent)等,是模拟登录的关键步骤。 - 会话保持: Scrapy 的 Cookie 管理机制(通常是自动的)确保了登录状态在后续请求中得以保持(只要是在同一域名下且 Cookie 未过期)。
Q12: Scrapy 的优点和缺点是什么?
A:
优点:
- 高性能、高并发: 基于 Twisted 异步框架,能够同时处理大量请求,爬取速度非常快。
- 强大的框架结构: 提供了一整套成熟的组件(调度器、下载器、Spider、Pipeline、中间件),结构清晰,易于管理和扩展大型爬虫项目。
- 可扩展性强: 通过中间件和 Pipeline 可以方便地定制请求处理、响应处理、数据处理等各个环节。
- 内置常用功能: 自带请求调度、去重(基于 URL)、下载延迟、自动限速(AutoThrottle)、代理、Cookie 管理、数据导出(Feed Exports)等实用功能。
- 强大的选择器: 内置 CSS 和 XPath 选择器,数据提取方便。
- 健壮性: 提供了重试机制、错误处理等,使爬虫更加稳定。
- 活跃社区和文档: 有着非常完善的官方文档和活跃的社区支持。
缺点:
- 学习曲线相对陡峭: 相比于简单的 Requests + BS4 组合,Scrapy 的框架概念(组件、数据流、异步)需要一定的学习时间。
- 不直接支持 JavaScript 渲染: Scrapy 本身不执行 JavaScript。对于需要 JS 渲染的动态页面,需要结合其他工具,如:
- Splash: 一个基于 WebKit 的 JavaScript 渲染服务,可以与 Scrapy 集成(通过
scrapy-splash插件)。 - Selenium/Playwright: 可以编写下载中间件,在中间件中使用 Selenium 或 Playwright 来获取渲染后的页面内容,再交给 Scrapy 的 Spider 解析。这会牺牲部分性能优势。
- Splash: 一个基于 WebKit 的 JavaScript 渲染服务,可以与 Scrapy 集成(通过
- 配置相对复杂: 项目结构和配置文件(settings.py)比简单脚本要多。
- 不适合非常小的任务: 对于只需要爬取几个简单页面的任务,使用 Scrapy 可能显得有些“杀鸡用牛刀”,Requests + BS4 可能更快捷。
- 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 数据抓取问题的两种不同思路和工具,主要区别在于它们的工作方式和适用场景。
主要区别:
| 特性 | Selenium | Scrapy |
|---|---|---|
| 核心功能 | 浏览器自动化测试工具 | 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: 从网页上爬取到的原始数据往往是“脏”的,直接使用可能会导致分析错误或程序异常。数据清洗是数据预处理的关键步骤,目的是提高数据质量,使其适用于后续的分析、存储或展示。
为什么需要清洗:
- 格式不统一: 网页数据来源多样,相同类型的数据可能有不同格式(如日期:"2023-10-26", "Oct 26, 2023", "26/10/2023")。
- 包含无关内容: 提取时可能带有多余的 HTML 标签、CSS 样式、JavaScript 代码、广告文本、导航链接等。
- 存在冗余字符: 如多余的空格、换行符、制表符 (
\t)、HTML 实体(如 ,&)。 - 数据类型错误: 数字被提取为字符串(如 "1,234"),布尔值表示不一等。
- 缺失值: 某些字段在某些页面上可能不存在,导致提取结果为
None或空字符串。 - 结构混乱: 数据可能嵌套在复杂的标签中,或者一个字段包含了多个信息需要拆分。
常见的数据清洗任务:
- 去除 HTML 标签和实体: 从文本中移除
<div>,<span>, 等。 - 处理空白字符: 去除首尾空格、将多个连续空格替换为单个空格、移除换行符等。
- 格式标准化: 将日期统一为
YYYY-MM-DD格式,将货币去除符号并转为数字。 - 数据类型转换: 将字符串形式的数字转为
int或float。 - 处理缺失值: 根据策略决定是填充默认值、删除该条记录,还是使用特定方法估算。
- 内容提取与拆分: 从混合文本中提取特定信息(如从 "价格:¥99.9" 中提取 99.9),或将一个字段拆分为多个(如将 "北京 朝阳区" 拆分为 "城市" 和 "区县")。
- 去重: 移除完全重复的数据记录。
拓展知识点:
- 数据质量维度: 清洗的目标通常是提升数据的完整性 (Completeness)、一致性 (Consistency)、准确性 (Accuracy)、有效性 (Validity)、唯一性 (Uniqueness)。
- GIGO (Garbage In, Garbage Out): 这是数据处理中的一句名言,意指如果输入的数据质量差,那么输出的结果(分析、模型等)也必然不可靠。因此数据清洗至关重要。
Q15: Python 中有哪些常用的库或技术可以用来进行数据清洗?
A: Python 提供了丰富的库和内置功能来支持数据清洗:
- 内置字符串方法:
- 核心:
str.strip(),str.lstrip(),str.rstrip()(去首尾空白),str.replace()(替换子串),str.split()(分割字符串),' '.join()(合并列表为字符串)。 - 优点: Python 内置,无需安装,简单直接,性能较好,适用于基本的文本清理。
- 缺点: 功能相对有限,处理复杂模式匹配能力弱。
- 核心:
- 正则表达式 (
re模块):- 核心:
re.sub()(查找并替换),re.findall()(查找所有匹配项),re.search()(查找第一个匹配项),re.match()(从头匹配),re.split()(按模式分割)。 - 优点: 非常强大,能够处理复杂的文本模式匹配、提取和替换任务,灵活性高。
- 缺点: 语法学习曲线较陡峭,写得不好的正则表达式可能效率不高或难以理解。
- 核心:
- Beautiful Soup 4 (BS4):
- 核心: 虽然主要用于解析,但其
.get_text()方法可以方便地去除所有 HTML 标签,得到纯文本。也可以选择性地.extract()或.decompose()移除特定标签。 - 优点: 在解析阶段就能进行一定程度的清洗(去标签),与解析过程结合紧密。
- 缺点: 主要功能是解析,不擅长纯粹的字符串操作或结构化数据清洗。清洗能力有限。
- 核心: 虽然主要用于解析,但其
- Pandas:
- 核心: 提供
DataFrame和Series数据结构,以及大量的用于数据处理和清洗的方法,如.strip(),.replace(),.fillna()(填充缺失值),.dropna()(删除缺失值),.astype()(类型转换),.apply()(应用自定义函数),.str访问器 (对 Series 执行字符串方法),.to_datetime()(日期转换) 等。 - 优点: 处理结构化数据(表格型)极为强大和高效,支持向量化操作,提供了处理缺失值、数据类型转换、数据对齐等的完善方案。非常适合处理从 API 获取的 JSON 或爬取后整理成类似表格的数据。
- 缺点: 对于非结构化或纯文本的简单清洗任务,引入 Pandas 可能有些“重”。需要将数据加载到其特定数据结构中。
- 核心: 提供
- 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: 去除首尾空白及多余空格
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 标签
import re
html_text = "<p>这是<b>重要</b>的 <a href='#'>链接</a> 内容.</p>"
# 1. 使用正则表达式 (简单场景)
# <.*?> 是一个非贪婪匹配,匹配 < 开头 > 结尾的最短字符串
cleaned_text_re = re.sub(r'<.*?>', '', html_text)
print(f"Regex (basic tag removal): '{cleaned_text_re}'")
# 输出: 'Regex (basic tag removal): '这是重要的 链接 内容.''
# 注意:简单的正则无法完美处理所有嵌套或复杂 HTML,且 还在
# 2. 处理 HTML 实体 (如 )
cleaned_text_re = cleaned_text_re.replace(' ', ' ') # 替换实体为空格
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: 提取文本中的数字 (如价格)
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 的角色:
- 数据结构化: 将爬取到的数据(通常是字典列表、JSON、或需要组织的零散信息)加载到
DataFrame(二维表格)或Series(一维数组)中,提供了一个清晰、规范的操作对象。 - 批量处理: 支持向量化操作,可以对整列数据(Series)进行快速的清洗操作(如
df['column'].str.strip()会同时作用于列中所有字符串),比逐行循环处理效率高得多。 - 缺失值处理: 提供
isnull(),notnull(),fillna(),dropna()等方便的函数来检测、填充或删除缺失值(NaN)。 - 数据类型转换: 使用
.astype()方法轻松转换列的数据类型(如将字符串价格列转为float)。 - 文本清洗: 通过
.str访问器,可以在 Series 上调用大部分 Python 内置字符串方法(strip,replace,contains,split等)以及正则表达式方法。 - 数据标准化与转换: 使用
.apply()或.map()应用自定义函数进行复杂的格式转换或计算。例如,统一日期格式,或从地址中提取省市。 - 去重: 使用
.duplicated()和.drop_duplicates()方便地检测和移除重复行。
什么时候应该使用 Pandas:
- 处理的数据是结构化的或可以被结构化为表格形式: 例如,爬取商品列表(包含名称、价格、销量、链接等字段),或者从 API 获取的 JSON 数组。
- 数据量较大: Pandas 的向量化操作在高数据量下性能优势明显。
- 需要进行多种清洗和转换操作: Pandas 提供了丰富的功能集,可以链式调用多种操作,代码相对简洁。
- 需要处理缺失值: Pandas 有专门且高效的缺失值处理机制。
- 清洗后的数据需要进一步分析或可视化: Pandas 是后续进行数据分析(统计、聚合)和可视化(配合 Matplotlib, Seaborn)的基础。
使用案例:
假设爬取到如下字典列表数据:
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 清洗:
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 的工作方式:
- 定义 Item: 首先,你在
items.py中定义好你的数据结构 (Item)。 - 创建 Item Loader: 在 Spider 中,针对每个要提取的数据项(比如一个商品),你创建一个 Item Loader 实例,通常将
response或一个Selector对象传递给它。 - 添加值 (Add Value): 使用
.add_xpath(),.add_css(), 或.add_value()方法向 Item Loader 中添加待处理的值。这些方法不是直接赋值,而是将提取到的原始数据(通常是列表)暂存起来。 - 处理器 (Processors): Item Loader 的核心在于输入处理器 (Input Processors) 和输出处理器 (Output Processors)。
- 输入处理器: 在值被添加到 Item Loader 时(通过
add_xpath/css/value)被调用。通常用来对提取到的原始值进行初步处理,比如去除 HTML 标签、解码等。可以为每个字段定义不同的输入处理器。 - 输出处理器: 在调用
.load_item()方法时被调用,作用于该字段收集到的所有值(通常是一个列表)。负责将这些值处理成最终赋给 Item 字段的单一值。常见的输出处理包括:取列表第一个元素(TakeFirst)、合并列表元素(Join)、类型转换、最终格式化等。
- 输入处理器: 在值被添加到 Item Loader 时(通过
- 加载 Item: 调用
.load_item()方法,此时 Item Loader 会应用所有定义的处理器,并将最终处理好的值填充到 Item 对象的相应字段中,然后返回这个干净的 Item。
示例:
# 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 代码简洁和专注于定位元素时。它提供了一种更健壮、可维护的方式来管理数据预处理流程。