基础 HTML 抓取:迈出第一步

2025年2月23日

网页抓取一开始可能会让人觉得有些吓人,但和任何技能一样,最好的学习方式就是亲自动手实践。在前三章中,我们将探索如何从静态 HTML 页面中提取数据的基础方法。

第 1 章:HTML 抓取菜鸟 Chad

我们的旅程将从最简单的场景开始——从一个基础 HTML 页面中提取文本。本章会向你介绍以下核心概念:

  • 发起 HTTP 请求以获取网页
  • 将 HTML 内容加载到解析器中
  • 使用 CSS 选择器进行基础 DOM 选择

虽然这个例子看起来可能很简单(毕竟只是一个 “hello world”!),但它为后续所有内容打下了基础。

<!-- Example HTML structure -->
<div class="content">
  <p>Some text we want to extract</p>
</div>

下面先看一眼我们要处理的内容:

// Basic concepts (not the solution!)
import * as cheerio from 'cheerio';

// Loading HTML content
const $ = cheerio.load(htmlContent);

// Using CSS selectors
const text = $('p').text();  // Selects all <p> tags
const specific = $('.content p').text();  // More specific selection

第 2 章:结构化数据

当我们深入到一个模拟电商商品页面时,事情就变得更有意思了。在这里,我们会遇到多个结构相似的元素:

<!-- Example product structure (simplified) -->
<div class="product">
  <h2>Product Name</h2>
  <span class="price">$99.99</span>
  <div class="specs">
    <ul>
      <li>Size: M</li>
      <li>Color: Blue</li>
    </ul>
  </div>
</div>

在处理像这样结构化的数据时,你需要思考:

// Conceptual approach (not the solution!)
$('.product').each((index, element) => {
  // For each product, we might want to:
  // 1. Extract the basic info
  const name = $(element).find('h2').text();

  // 2. Parse nested data
  const specs = $(element).find('.specs li');

  // 3. Structure the output
  const data = {
    name,
    specs: specs.map(/* ... */),
  };
});

小提示:在写任何代码之前,先花点时间分析 HTML 结构。观察数据的组织方式是否有规律——是否存在一致的 class 名?父元素和子元素之间是怎样关联的?

第 3 章:AI 辅助抓取

现在真正有意思的部分来了!前面的挑战教给我们的是传统抓取技术,而第 3 章将引入一种现代方法:AI 辅助网页抓取。我们面对的是一个堪称噩梦的场景——不一致的 HTML 结构、混淆后的 class 名,以及多种框架模式杂糅在一起。

先来看看到底是什么让这个挑战如此特别:

<!-- Traditional product structure -->
<div data-testid="product-container-1" class="_3xj_item">
  <h2 data-qa="name">Red Sneakers</h2>
  <span data-price-current="5999">$59.99</span>
</div>

<!-- React-style component -->
<div class="ProductCard-root-1a2b3c">
  <div class="ProductCard-title-4d5e6f">Pink Walking Shoes</div>
  <div class="ProductCard-pricing-7g8h9i">$84.99</div>
</div>

<!-- Vue-style template -->
<div data-v-abcdef class="product">
  <h2 data-v-abcdef>Navy Boat Shoes</h2>
  <span data-v-abcdef>$79.99</span>
</div>

传统抓取方法在这里会举步维艰,因为:

  • class 名是随机化的,或者带有框架特定特征
  • 不同商品之间的数据结构各不相同
  • 不同框架使用不同的模式
  • 标记中的语义信息已经丢失

这正是 AI 大显身手的时候。与其编写脆弱的选择器,不如用自然语言描述我们想要的内容,让 AI 来完成模式匹配。本章的关键概念包括:

  • 面向网页抓取的 Prompt 工程
  • 使用 AI 理解语义含义
  • 处理不一致的数据结构
  • 应对框架特定的标记结构
  • 在不同模式之间保持数据一致性

AI 并不是魔法,但它非常擅长需要模式识别和自适应能力的任务。这让它在抓取现代 Web 应用时尤其有价值,因为这类应用往往无法保证标记模式始终一致。

关于使用 AI 的说明

请记住,AI 辅助并不意味着可以获得完全自动化的解决方案。最好的结果来自以下要素的结合:

  • 清晰的问题定义
  • 结构良好的提示词
  • 数据校验
  • 人工监督

你的挑战在于设计出合适的提示词,让 AI 同时理解你想提取内容的结构以及背后的意图。

准备好把传统网页抓取知识与现代 AI 能力结合起来了吗?让我们看看 AI 如何帮助我们拿下哪怕最混乱的 HTML!

提示

  1. 试试不同的 CSS 选择器:
// Different ways to select elements
$('.class')           // By class
$('#id')             // By ID
$('div > p')         // Direct children
$('div p')           // All descendants
$('[data-type="x"]') // By attribute
  1. 尝试修改输出格式
  2. 思考错误处理和边界情况
  3. 考虑你的方案在更大数据集上的扩展性

开始所需的全部代码都在项目仓库中。把它克隆下来,配置好环境,然后开始抓取吧!

git clone https://github.com/jonaylor89/housefly.git
cd housefly
npm install

在本地启动各章节服务器——每一章都运行在各自独立的端口上(3001–3011):

turbo dev

然后在 exercises/chapter-NN/starter/src/index.ts 中编辑你的抓取器,并进行验证:

npm run validate -- <chapter>

想看提示?运行 npm run hint -- <chapter> 获取逐步引导。每个挑战的源 HTML 都位于 apps/chapter{n}/ 目录中,参考解答则在 exercises/chapter-NN/solution/src/ 中。研究结构,规划方法,并记住——每一位专家都曾经是初学者。

记得以负责任的方式处理你的请求:

// Basic error handling example
async function fetchPage(url: string) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return await response.text();
  } catch (error) {
    console.error('Failed to fetch page:', error);
    throw error;
  }
}

祝你抓取愉快!