尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

10分钟手撸Python爬虫:从零构建轻量级网络数据抓取工具

10分钟手撸Python爬虫:从零构建轻量级网络数据抓取工具 1. 项目概述从“手撸”到“OpenClaw”的工程思维最近在技术社区里看到不少朋友在讨论“手撸”一个东西从框架到工具大家似乎都热衷于这种从零构建的乐趣。今天我们不聊那些复杂的分布式系统来聊点更“接地气”的——手撸一个“小龙虾”当然我们这里说的不是真的去烹饪而是一个代号为“OpenClaw”的轻量级网络爬虫工具。为什么叫“小龙虾”因为它目标明确、结构简单但那双“钳子”核心抓取与解析模块足够锋利能帮你从纷繁复杂的网页中精准“夹”出你需要的数据而且整个过程力求干净、可控没有过多依赖。“10分钟”是个吸引眼球的说法它背后传递的是一种理念核心功能的验证与实现不应该被复杂的配置和庞大的依赖所拖累。OpenClaw项目就是基于这个想法诞生的。它不是一个旨在替代Scrapy或Playwright等工业级框架的庞然大物而是一个教学与原型验证工具。通过它你可以清晰地理解一个爬虫最核心的工作流程发送请求、解析响应、提取数据、处理异常。这对于初学者理解爬虫原理或者对于有经验的开发者快速验证某个网站的数据抓取可行性都非常有帮助。我们将使用Python作为实现语言因为它拥有极其丰富和成熟的网络库生态能让我们的“手撸”过程事半功倍。2. 核心需求与设计思路拆解在开始敲代码之前我们必须想清楚这个“小龙虾”OpenClaw需要具备哪些基本能力以及我们为何要如此设计。一个最基础的爬虫其生命周期可以抽象为几个关键环节。2.1 核心需求解析首先我们需要明确OpenClaw的MVP最小可行产品应该做什么网络请求能够向指定的URL地址发送HTTP/HTTPS请求并获取服务器返回的响应内容HTML、JSON等。这是所有爬虫的起点。内容解析能够从获取到的HTML字符串中精准地定位并提取出我们感兴趣的数据。比如商品的价格、文章的标题、评论的内容等。数据输出将提取到的结构化数据例如字典或列表以一种易于查看和后续处理的方式保存下来比如输出到控制台、写入到CSV文件或JSON文件。基础容错网络世界并不稳定目标网站也可能随时变化。我们的爬虫需要具备基本的错误处理能力例如处理请求超时、页面不存在404、解析规则失效等情况避免程序因一个意外而彻底崩溃。2.2 技术选型与设计思路基于以上需求我们进行轻量级的技术选型请求库requests。这是Python社区公认的“人类友好”的HTTP库。相比Python标准库中的urllib它的API设计更加直观简洁能让我们专注于业务逻辑而非底层连接细节。它自动处理连接池、会话、重定向等是我们“小龙虾”强壮而灵活的“螯足”。解析库lxmlcssselect或parsel。解析HTML有很多选择如BeautifulSoup。这里我们选择lxml因为它的解析速度非常快并且支持XPath和CSS选择器。parsel库Scrapy内部使用的解析库封装了lxml提供了更一致的CSS和XPath选择器接口我们直接使用parsel会让选择器书写更流畅。这构成了我们精准定位数据的“复眼”和“口器”。数据输出标准库csv或json。对于MVP来说Python自带的模块完全够用无需引入额外依赖。这相当于“小龙虾”的消化系统将营养数据储存起来。结构设计面向过程与函数的结合。为了在10分钟内勾勒出清晰脉络我们不会一开始就设计复杂的类层次结构。我们将采用函数式模块化的设计一个函数负责发送请求一个函数负责解析内容一个函数负责保存数据。主程序像指挥中枢一样调用它们。这种设计直观便于理解和扩展。注意选择requests和lxml/parsel的组合是在开发效率、学习曲线和性能之间取得的一个平衡。对于初学者这个组合易于上手对于有经验者它能快速实现想法。如果项目后续需要并发、分布式或更复杂的JS渲染我们可以在此基础上引入aiohttp异步、scrapy框架或selenium浏览器自动化进行扩展这是OpenClaw设计上留出的“进化接口”。3. 环境准备与核心库安装任何项目的第一步都是搭建工作环境。为了保证过程清晰可复现我们使用虚拟环境来隔离项目依赖。3.1 创建项目目录与虚拟环境打开你的终端命令行依次执行以下命令# 1. 创建一个专门的项目目录 mkdir openclaw_project cd openclaw_project # 2. 创建Python虚拟环境这里使用venvPython3.3内置 python -m venv venv # 3. 激活虚拟环境 # 在 Windows 上 venv\Scripts\activate # 在 macOS/Linux 上 source venv/bin/activate激活后你的命令行提示符前通常会显示(venv)表示你已经在这个隔离的环境中工作了。接下来所有包的安装都只影响这个环境。3.2 安装必需的核心库我们将安装之前选定的requests和parsel。parsel会帮我们自动处理lxml的依赖。pip install requests parsel为了验证安装是否成功可以启动Python交互界面简单测试python -c “import requests, parsel; print(‘库导入成功’)”如果没有任何错误输出说明环境准备就绪。3.3 选择你的代码编辑器你可以使用任何熟悉的编辑器如VSCode、PyCharm、Sublime Text等。在项目根目录下创建一个名为openclaw.py的文件我们所有的代码都将写在这个文件里保持极简。实操心得务必使用虚拟环境。这是Python开发的最佳实践之首。它能避免不同项目间依赖包的版本冲突。想象一下你系统全局的requests是旧版而新项目需要新版如果没有虚拟环境升级可能会破坏其他老项目。这个小坑看似不起眼但却是很多新手后期依赖混乱的根源。4. 核心模块实现构建“小龙虾”的躯体现在我们开始编写OpenClaw的核心功能函数。每个函数都像小龙虾的一个器官各司其职。4.1 “螯足”模块发送网络请求这个函数负责伸出“钳子”去抓取网页。我们需要考虑基本的异常处理和请求头设置以模拟更真实的浏览器行为减少被简单反爬机制拦截的概率。import requests from requests.exceptions import RequestException import time def fetch_url(url, headersNone, timeout10): 抓取指定URL的页面内容。 参数: url (str): 要抓取的网页地址。 headers (dict): 可选的HTTP请求头。默认为None函数会提供一个基础头。 timeout (int): 请求超时时间秒。 返回: str: 成功则返回网页HTML文本失败则返回None。 if headers is None: # 一个基础的请求头模仿常见浏览器的访问 headers { ‘User-Agent’: ‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36’, ‘Accept’: ‘text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8’, } try: # 发送GET请求 response requests.get(url, headersheaders, timeouttimeout) # 检查HTTP状态码200表示成功 response.raise_for_status() # 通常使用‘utf-8’编码但有些网站可能不同这里用apparent_encoding做一个推测 response.encoding response.apparent_encoding return response.text except RequestException as e: # 捕获所有requests可能抛出的异常连接错误、超时、HTTP错误等 print(f“抓取 {url} 时出错: {e}”) return None关键点解析User-Agent这是告诉服务器“我是谁”的标识。使用一个常见的浏览器UA可以避免被一些网站直接拒绝服务因为它们可能直接屏蔽了爬虫库的默认UA。response.raise_for_status()这是一个非常实用的方法。如果响应状态码不是200成功它会抛出一个HTTPError异常让我们能及时捕获到404、500等错误。response.apparent_encodingrequests会从HTTP头或网页内容中猜测编码apparent_encoding比encoding猜测得更准一些能更好地处理中文等非ASCII字符的乱码问题。异常处理网络请求充满了不确定性。用try...except包裹起来并在出错时返回None可以让主调函数决定下一步怎么做比如重试或跳过而不是让整个程序崩溃。4.2 “复眼”与“口器”模块解析与提取数据获取到HTML“原料”后我们需要用parsel这把“手术刀”来解剖它提取出我们想要的“肉质”数据。from parsel import Selector def parse_html(html_text, css_selector): 使用CSS选择器从HTML文本中提取数据。 参数: html_text (str): 网页的HTML文本。 css_selector (str): 用于定位元素的CSS选择器。 返回: list: 提取到的文本内容列表。如果html_text为空或解析失败返回空列表。 if not html_text: return [] try: # 将HTML文本转换为Selector对象 selector Selector(texthtml_text) # 使用CSS选择器获取所有匹配的元素并提取其文本strip()去除首尾空格 items selector.css(css_selector).getall() # 对每个提取项进行清洗 cleaned_items [item.strip() for item in items if item.strip()] return cleaned_items except Exception as e: # 捕获解析过程中可能出现的意外错误 print(f“解析HTML时出错: {e}”) return []关键点解析Selector对象这是parsel的核心它封装了lxml的解析能力并提供了统一的css和xpath方法。.css(selector).getall()css方法返回一个选择器列表.getall()将这个列表中的所有元素提取出来以Python列表的形式返回。如果想只提取第一个可以用.get()。数据清洗网页文本常常包含大量的空白字符空格、换行、制表符。.strip()是必不可少的一步它能让我们得到干净的数据。if item.strip()确保了不会将空字符串或纯空白字符加入结果列表。选择器的编写这是爬虫的核心技能。你需要使用浏览器的开发者工具F12来检查网页元素找到包含目标数据的标签及其CSS路径。例如要抓取一个新闻列表每个新闻标题可能在一个h2 class”title”标签里那么选择器可能就是h2.title::text::text表示提取元素的文本内容。4.3 “消化系统”模块保存数据到文件数据提取出来后我们需要将其持久化。这里我们实现一个通用的保存函数支持保存为JSON或CSV格式。import json import csv import os def save_data(data, filename, format‘json’): 将数据保存到文件。 参数: data (list/dict): 要保存的数据。 filename (str): 输出文件名无需后缀函数会自动添加。 format (str): 输出格式支持 ‘json‘ 或 ’csv‘。 if not data: print(“没有数据需要保存。”) return # 根据格式确定文件后缀和写入模式 if format ‘json’: filepath f”{filename}.json” with open(filepath, ‘w’, encoding‘utf-8’) as f: # ensure_asciiFalse 确保中文正常显示indent2 让JSON文件有缩进更易读 json.dump(data, f, ensure_asciiFalse, indent2) print(f”数据已保存至 {filepath}”) elif format ‘csv’: filepath f”{filename}.csv” # 判断data是列表字典还是列表的列表 if data and isinstance(data[0], dict): keys data[0].keys() with open(filepath, ‘w’, newline‘’, encoding‘utf-8-sig’) as f: # utf-8-sig 解决Excel打开中文乱码 dict_writer csv.DictWriter(f, fieldnameskeys) dict_writer.writeheader() dict_writer.writerows(data) else: # 如果是列表的列表或其他可迭代结构 with open(filepath, ‘w’, newline‘’, encoding‘utf-8-sig’) as f: writer csv.writer(f) writer.writerows(data) print(f”数据已保存至 {filepath}”) else: print(f”不支持的格式: {format}”)关键点解析格式灵活性提供了JSON和CSV两种常见格式。JSON适合嵌套结构易于程序读写CSV则兼容性极强可以直接用Excel打开查看。编码问题处理中文时文件编码是另一个常见坑点。utf-8是通用标准但在Windows的Excel中直接打开UTF-8编码的CSV可能会显示乱码。使用utf-8-sig带BOM的UTF-8可以完美解决这个问题。newline‘’在写入CSV时这个参数至关重要。它可以防止在Windows系统上产生额外的空行。5. 组装与实战让“小龙虾”动起来有了所有“器官”现在我们需要一个“大脑”主函数来协调它们工作完成一个完整的抓取任务。我们以一个简单的实战为例抓取某个静态博客网站假设为示例网站的文章标题列表。5.1 编写主协调逻辑在openclaw.py文件中我们添加一个main函数并在文件末尾调用它。def main(): OpenClaw 主函数协调抓取、解析、保存流程。 # 1. 定义目标URL和选择器 target_url ‘https://httpbin.org/html‘ # 这是一个用于测试的公共服务返回一个简单的HTML页面 # 在实际项目中你需要替换为真实的URL并使用开发者工具分析出正确的CSS选择器 # 例如抓取h1标题’h1::text‘ css_selector_for_title ‘h1::text‘ print(f“开始抓取: {target_url}”) # 2. 抓取页面 html_content fetch_url(target_url) if html_content is None: print(“页面抓取失败程序终止。”) return # 3. 解析数据 print(“正在解析页面内容...”) titles parse_html(html_content, css_selector_for_title) if titles: print(f“成功提取到 {len(titles)} 条数据:”) for idx, title in enumerate(titles, 1): print(f” {idx}. {title}“) else: print(“未提取到任何数据请检查选择器是否正确。”) # 可选将HTML内容保存到文件以便调试 # with open(‘debug_page.html’, ‘w’, encoding‘utf-8’) as f: # f.write(html_content) # print(“已将原始页面保存为 debug_page.html请检查。”) return # 4. 保存数据 # 将数据组织成字典列表方便保存为结构化格式 data_to_save [{“id”: i, “title”: title} for i, title in enumerate(titles, 1)] save_data(data_to_save, ‘extracted_titles’, format‘json’) save_data(data_to_save, ‘extracted_titles’, format‘csv’) print(“任务完成”) if __name__ ‘__main__’: main()5.2 运行你的第一个爬虫在终端中确保你在虚拟环境下并且位于openclaw.py文件所在的目录然后运行python openclaw.py你应该会看到类似以下的输出开始抓取: https://httpbin.org/html 正在解析页面内容... 成功提取到 1 条数据: 1. Htmlish 数据已保存至 extracted_titles.json 数据已保存至 extracted_titles.csv 任务完成同时你的项目文件夹里会生成extracted_titles.json和extracted_titles.csv两个文件里面保存了抓取到的数据。恭喜你你的“小龙虾”OpenClaw已经成功完成了第一次捕食实操心得从测试页面开始。在编写针对具体网站的爬虫时强烈建议先使用httpbin.org这类服务或一个简单的静态页面进行测试。这可以确保你的请求和解析逻辑本身是正确的排除了网络不稳定和目标网站反爬等外部因素的干扰。逻辑正确后再替换成真实URL并调整选择器这是高效的调试路径。6. 功能增强与常见问题应对一个基础的爬虫跑起来后我们会立刻遇到现实世界的挑战。下面我们来为OpenClaw添加一些必要的“生存技能”。6.1 添加请求延迟与重试机制连续快速请求同一个网站很容易触发对方的反爬策略导致IP被暂时封锁。添加延迟是基本的礼貌和自我保护。import time import random def fetch_url_with_delay(url, headersNone, timeout10, delay_range(1, 3)): 抓取页面并在请求后添加随机延迟。 参数: delay_range (tuple): 延迟时间的随机范围秒例如(1,3)表示延迟1到3秒。 # ... fetch_url 的内部代码 ... # 在请求发送前或后添加延迟 time.sleep(random.uniform(*delay_range)) # 在每次请求后随机休眠 # 注意更合理的做法是在主循环中控制请求频率而非在函数内。 # 但为了函数功能完整此处演示。实际项目中建议在主逻辑中控制。更好的做法是在主调循环中控制节奏def main_advanced(): urls [‘url1‘, ‘url2‘, ‘url3‘] # 假设有多个页面要抓取 all_data [] for url in urls: print(f“抓取: {url}”) html fetch_url(url) if html: data parse_html(html, ‘some_selector‘) all_data.extend(data) # 在每次成功的请求后添加一个随机延迟 time.sleep(random.uniform(1.0, 2.5)) # 延迟1到2.5秒 save_data(all_data, ‘batch_results’)重试机制则可以在网络波动或遇到短暂错误时自动恢复。我们可以改造fetch_url函数def fetch_url_with_retry(url, headersNone, timeout10, max_retries3): 带重试机制的页面抓取。 for attempt in range(max_retries): try: response requests.get(url, headersheaders, timeouttimeout) response.raise_for_status() response.encoding response.apparent_encoding return response.text except RequestException as e: print(f“第 {attempt 1} 次尝试抓取 {url} 失败: {e}”) if attempt max_retries - 1: # 如果不是最后一次尝试则等待后重试 wait_time 2 ** attempt # 指数退避等待 1, 2, 4... 秒 print(f“等待 {wait_time} 秒后重试...”) time.sleep(wait_time) else: print(f“抓取 {url} 失败已达最大重试次数。”) return None6.2 处理动态加载内容现代网站大量使用JavaScript动态加载数据直接拿到的初始HTML里可能没有内容。对于这种情况简单的requests无法解决。此时我们需要更强大的工具。方案一寻找隐藏的数据接口。这是最高效的方法。打开浏览器的开发者工具切换到“网络”(Network)选项卡过滤XHR/Fetch请求刷新页面观察哪些请求返回了页面上展示的真实数据通常是JSON格式。然后我们的爬虫可以直接模拟请求这个接口URL直接拿到结构化数据省去了解析HTML的麻烦。方案二使用无头浏览器。当数据确实由JS渲染且没有独立接口时我们需要一个能执行JavaScript的“浏览器”。selenium或playwright是这类工具的代表。它们可以控制真实的浏览器如Chrome去加载页面等待JS执行完毕再获取完整的HTML。但这会显著增加资源消耗和运行时间不适合大规模抓取。# 使用 selenium 的示例需额外安装 selenium 和浏览器驱动如 chromedriver from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def fetch_dynamic_content(url): options webdriver.ChromeOptions() options.add_argument(‘--headless‘) # 无头模式不显示浏览器窗口 driver webdriver.Chrome(optionsoptions) # 确保 chromedriver 在PATH中 try: driver.get(url) # 显式等待某个关键元素出现确保页面已加载完成 WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, “.content-loaded”)) ) html driver.page_source return html finally: driver.quit()注意动态内容处理是爬虫进阶的关键分水岭。优先选择分析接口这比动用浏览器引擎要快几个数量级。只有当页面极其复杂所有数据都深埋在JS逻辑中时才考虑使用无头浏览器方案。6.3 解析失败与数据清洗的深入处理有时选择器写对了但提取到的数据包含大量无关字符如HTML标签、特殊符号、多余的空格和换行。我们需要更强大的清洗函数。import re def clean_extracted_text(text_list): 对提取的文本列表进行深度清洗。 cleaned_list [] for text in text_list: if not text: continue # 1. 去除首尾空白 text text.strip() # 2. 将多个连续空白字符包括换行、制表符替换为单个空格 text re.sub(r‘\s‘, ‘ ‘, text) # 3. 移除常见的无用字符或字符串根据实际情况调整 useless_patterns [‘\r‘, ‘\n‘, ‘\t‘, ‘ ‘, ‘查看更多‘, ‘...‘] for pattern in useless_patterns: text text.replace(pattern, ‘’) # 4. 如果清洗后不为空则加入结果 if text: cleaned_list.append(text) return cleaned_list # 在 parse_html 函数中调用 def parse_html_enhanced(html_text, css_selector): # ... 获取原始 items ... raw_items selector.css(css_selector).getall() cleaned_items clean_extracted_text(raw_items) return cleaned_items7. 项目扩展与架构优化建议一个简单的脚本已经完成。但如果想把它变成一个更健壮、可维护的小项目我们可以从以下几个方面进行扩展和优化。7.1 配置与命令行接口将URL、选择器、请求头等配置项从代码中分离出来放到配置文件如config.yaml或config.json或通过命令行参数传入会让工具更加灵活。# 使用 argparse 库处理命令行参数 import argparse def setup_cli(): parser argparse.ArgumentParser(description‘OpenClaw - 轻量级网络爬虫’) parser.add_argument(‘-u‘, ‘--url‘, requiredTrue, help‘目标URL’) parser.add_argument(‘-s‘, ‘--selector‘, requiredTrue, help‘CSS选择器’) parser.add_argument(‘-o‘, ‘--output‘, default‘output‘, help‘输出文件名不含后缀’) parser.add_argument(‘-f‘, ‘--format‘, choices[‘json‘, ‘csv‘], default‘json‘, help‘输出格式’) return parser.parse_args() if __name__ ‘__main__’: args setup_cli() # 使用 args.url, args.selector 等代替硬编码的值 html fetch_url(args.url) data parse_html(html, args.selector) save_data(data, args.output, formatargs.format)运行方式将变为python openclaw.py -u “https://example.com“ -s “h1.title::text“ -o my_data7.2 面向对象重构当功能变多时面向对象的封装能带来更好的组织结构。我们可以将爬虫的核心功能封装成一个类。class OpenClaw: def __init__(self, default_headersNone, delay0): self.session requests.Session() # 使用会话可以保持cookies提升效率 if default_headers: self.session.headers.update(default_headers) self.delay delay def fetch(self, url, **kwargs): # 使用 self.session 进行请求 time.sleep(self.delay) # 每次请求前延迟 try: resp self.session.get(url, **kwargs) resp.raise_for_status() resp.encoding resp.apparent_encoding return resp.text except RequestException as e: print(f“Request failed for {url}: {e}”) return None def parse(self, html, selector): # ... 解析逻辑 ... pass def run(self, url, selector, output_file): html self.fetch(url) if html: data self.parse(html, selector) save_data(data, output_file) return data return None # 使用方式 claw OpenClaw(delay1) claw.run(‘https://example.com‘, ‘h1::text‘, ‘result’)7.3 日志记录与监控在生产环境中打印到控制台的信息是不够的。我们需要将运行状态、错误信息记录到日志文件中便于事后排查。import logging def setup_logging(): logging.basicConfig( levellogging.INFO, format‘%(asctime)s - %(name)s - %(levelname)s - %(message)s‘, handlers[ logging.FileHandler(‘openclaw.log‘, encoding‘utf-8‘), logging.StreamHandler() # 同时输出到控制台 ] ) return logging.getLogger(__name__) logger setup_logging() # 在代码中用 logger.info(‘开始抓取...‘) 代替 print7.4 应对更复杂的反爬策略对于更专业的反爬可能需要考虑IP代理池轮换使用不同的IP地址发送请求避免单一IP被封。用户代理池随机切换不同的User-Agent字符串。请求指纹模拟处理诸如TLS/JA3指纹、浏览器指纹等高级检测这通常需要更底层的库如curl_cffi或playwright。验证码识别对于简单的图形验证码可以使用OCR库如ddddocr、tesseract对于复杂验证码可能需要接入打码平台。这些高级话题超出了“10分钟手撸”的范畴但它们是构建一个健壮爬虫系统时必须面对的挑战。OpenClaw作为一个起点其清晰的结构可以方便地集成这些高级组件。从一行代码开始到构建一个结构清晰、功能可扩展的爬虫工具这个过程本身就是对网络数据抓取技术栈的一次深刻遍历。OpenClaw项目麻雀虽小五脏俱全它为你理解更复杂的爬虫框架打下了坚实的基础。记住爬虫的核心永远是“请求-解析-存储”万变不离其宗。在实际操作中耐心分析网络请求、精心编写选择器、妥善处理异常和延迟远比使用多么高深的框架更重要。最后请务必遵守目标网站的robots.txt协议尊重数据所有者的权益合理控制抓取频率做一个有道德的“捕虾人”。
返回列表