Skip to main content
Version: 3.6

进一步抓取

真实项目章节中,我们创建了一个关于示例商店产品信息的收集清单。让我们回顾一下。

  • URL: 网址
  • Manufacturer: 制造商
  • SKU: 商品SKU
  • Title: 商品标题
  • Current price: 商品当前售价
  • Stock available: 可用库存

data to scrape

抓取URL、制造商和SKU

有些信息就在我们面前,甚至无需触摸产品详细页面。我们已经拥有URL - request.url。仔细观察它,我们意识到还可以从URL中提取制造商(因为所有产品的URL都以/products/<manufacturer>开头)。然后我们只需分割字符串就行了!

info

你也可以使用 request.loadedUrl。记住区别:request.url 是你队列要访问的链接,而 request.loadedUrl 是返回结果,可能经过重定向后被处理的链接。

// request.url = https://warehouse-theme-metal.myshopify.com/products/sennheiser-mke-440-professional-stereo-shotgun-microphone-mke-440

const urlPart = request.url.split('/').slice(-1); // ['sennheiser-mke-440-professional-stereo-shotgun-microphone-mke-440']
const manufacturer = urlPart[0].split('-')[0]; // 'sennheiser'
tip

这是一个偏好问题,是否将这些信息单独存储在生成的数据集中完全取决于你。使用数据集的人也可以轻松地从URL中解析出“制造商”,所以是否有必要重复数据呢?我认为,除非增加的数据消耗太大而难以承受,否则最好尽可能使数据集丰富。假如,有人可能想按“制造商”进行筛选。

caution

你可能会注意到的一件事是,“制造商”的名称中可能有 -。如果是这种情况,最好的方法是从详情页面中提取它,但这并非强制要求。归根结底,你应该调整并选择始终最适合你项目方法和抓取网站的解决方案。

现在是时候向结果中添加更多数据了。让我们打开其中一个产品详细页面,例如Sony XBR-950G 页面,并使用我们的 DevTools-Fu 🥋 来找出如何获取产品的标题。

获取标题

product title

通过使用元素选择工具,你可以看到标题位于<h1>标签下面,正如标题应该出现的位置。<h1>标签被包含在一个class为product-meta<div>中。我们可以利用这一点来创建一个组合选择器.product-meta h1。它选择class为 product-meta 的子级下的 <h1> 元素。

tip

请记住,你可以在DevTools的Elements选项卡中按下CTRL+F(或Mac上的CMD+F)来打开搜索栏,在那里你可以快速使用选择器搜索元素。始终使用DevTools验证你的抓取过程和假设。这比一直更改爬虫代码要快得多。

为了获取标题,我们需要使用Playwright.product-meta h1选择器来找到它,该选择器选择我们正在寻找的<h1>元素,如果找到多个,则会抛出异常。这很好。通常最好是让爬虫崩溃而不是悄悄地返回错误数据。

const title = await page.locator('.product-meta h1').textContent();

获取SKU

通过DevTools,我们发现产品SKU位于一个带有类名product-meta__sku-number<span>标签内。由于页面上没有其他具有该类名的<span>,因此我们可以放心使用它。

product sku selector

const sku = await page.locator('span.product-meta__sku-number').textContent();

获取当前售价

DevTools告诉我们currentPrice可以在带有price类的<span>元素中找到。但它还显示,它作为原始文本嵌套在另一个带有visually-hidden类的 <span> 元素旁边。我们不需要那个,所以我们需要过滤掉它,并且我们可以使用 hasText 辅助函数来实现这一点。

product current price selector

const priceElement = page
.locator('span.price')
.filter({
hasText: '$',
})
.first();

const currentPriceString = await priceElement.textContent();
const rawPrice = currentPriceString.split('$')[1];
const price = Number(rawPrice.replaceAll(',', ''));

乍一看可能会显得有点复杂,但让我们来逐步解释一下我们做了什么。首先,我们通过筛选带有“$”符号的元素来找到price标签的正确部分(具体是实际价格)。当这样做时,我们将获得类似于“特价$1,398.00”的字符串。单独拿出来并不太有用,所以我们通过按照“$”符号进行分割来提取实际的数字部分。

一旦我们这样做,我们会收到一个代表价格的字符串,但是我们将把它转换成数字。 我们通过用空白替换所有逗号(以便解析为数字),然后使用Number()将其解析为数字。

获取可用库存

我们处理可用库存。我们可以看到有一个带有product-form__inventory类的span,它包含文本In stock。我们可以再次使用hasText助手来过滤出正确的元素。

const inStockElement = await page
.locator('span.product-form__inventory')
.filter({
hasText: 'In stock',
})
.first();

const inStock = (await inStockElement.count()) > 0;

对于这一点,我们关心的只是元素是否存在,因此我们可以使用count()方法来检查是否有任何与我们的选择器匹配的元素。如果有,那么我们知道产品是有库存的。

好了!我们需要的所有数据都在这里。为了完整起见,让我们把所有属性加在一起,然后就可以开始了。

const urlPart = request.url.split('/').slice(-1); // ['sennheiser-mke-440-professional-stereo-shotgun-microphone-mke-440']
const manufacturer = urlPart.split('-')[0]; // 'sennheiser'

const title = await page.locator('.product-meta h1').textContent();
const sku = await page.locator('span.product-meta__sku-number').textContent();

const priceElement = page
.locator('span.price')
.filter({
hasText: '$',
})
.first();

const currentPriceString = await priceElement.textContent();
const rawPrice = currentPriceString.split('$')[1];
const price = Number(rawPrice.replaceAll(',', ''));

const inStockElement = await page
.locator('span.product-form__inventory')
.filter({
hasText: 'In stock',
})
.first();

const inStock = (await inStockElement.count()) > 0;

最后的尝试

我们已经准备好了一切,所以拿出我们新创建的抓取逻辑,将其放入原来的requestHandler()中,看魔法发生吧!

Run on
import { PlaywrightCrawler } from 'crawlee';

const crawler = new PlaywrightCrawler({
requestHandler: async ({ page, request, enqueueLinks }) => {
console.log(`Processing: ${request.url}`);
if (request.label === 'DETAIL') {
const urlPart = request.url.split('/').slice(-1); // ['sennheiser-mke-440-professional-stereo-shotgun-microphone-mke-440']
const manufacturer = urlPart[0].split('-')[0]; // 'sennheiser'

const title = await page.locator('.product-meta h1').textContent();
const sku = await page.locator('span.product-meta__sku-number').textContent();

const priceElement = page
.locator('span.price')
.filter({
hasText: '$',
})
.first();

const currentPriceString = await priceElement.textContent();
const rawPrice = currentPriceString.split('$')[1];
const price = Number(rawPrice.replaceAll(',', ''));

const inStockElement = page
.locator('span.product-form__inventory')
.filter({
hasText: 'In stock',
})
.first();

const inStock = (await inStockElement.count()) > 0;

const results = {
url: request.url,
manufacturer,
title,
sku,
currentPrice: price,
availableInStock: inStock,
};

console.log(results);
} else if (request.label === 'CATEGORY') {
// We are now on a category page. We can use this to paginate through and enqueue all products,
// as well as any subsequent pages we find

await page.waitForSelector('.product-item > a');
await enqueueLinks({
selector: '.product-item > a',
label: 'DETAIL', // <= note the different label
});

// Now we need to find the "Next" button and enqueue the next page of results (if it exists)
const nextButton = await page.$('a.pagination__next');
if (nextButton) {
await enqueueLinks({
selector: 'a.pagination__next',
label: 'CATEGORY', // <= note the same label
});
}
} else {
// This means we're on the start page, with no label.
// On this page, we just want to enqueue all the category pages.

await page.waitForSelector('.collection-block-item');
await enqueueLinks({
selector: '.collection-block-item',
label: 'CATEGORY',
});
}
},

// Let's limit our crawls to make our tests shorter and safer.
maxRequestsPerCrawl: 50,
});

await crawler.run(['https://warehouse-theme-metal.myshopify.com/collections']);

当你运行爬虫时,你将在控制台上看到已爬取的URL及其抓取的数据。输出结果会类似于这样:

{
"url": "https://warehouse-theme-metal.myshopify.com/products/sony-str-za810es-7-2-channel-hi-res-wi-fi-network-av-receiver",
"manufacturer": "sony",
"title": "Sony STR-ZA810ES 7.2-Ch Hi-Res Wi-Fi Network A/V Receiver",
"sku": "SON-692802-STR-DE",
"currentPrice": 698,
"availableInStock": true
}

下一节

在下一课中,我们将向你展示如何将你抓取的数据保存到磁盘上以供进一步处理。