in

如何保证Web爬取的数据质量

如何保证Web爬取的数据质量

公共网络上的数据通常以不可预测的格式、类型和结构出现。为了确保网络抓取时的数据质量,我们不能简单地相信我们的代码可以毫无问题地理解任何抓取的网页。因此,可以使用各种验证工具来测试和验证爬虫解析逻辑。 在本教程中,我们将了解如何使用Cerberus等 Python 工具来验证和测试报废的数据,以及如何使用Pydantic等工具来强制执行数据类型甚至规范化数据值。 为了说明常见的数据验证挑战,我们将从Glasdoor.com上抓取公司概览数据。

数据质量挑战

当网络抓取时,我们正在使用我们无法控制的公共资源,因此我们不能相信所收集数据的格式和质量。换句话说,我们永远无法确定网站何时会更改诸如日期格式之类的内容 – 从2022-11-262022/11/26– 或使用不同的数字格式 – 从10 00010,000。 为了提高网络抓取中的数据质量,我们可以利用多种工具来测试、验证甚至转换抓取的数据。 例如,我们可以编写一个验证函数来检查所有date字段是否遵循<4-digit-number>-<2-digit-number>-<2-digit-number>模式,甚至可以在可能的情况下自动将其更正为标准值。

设置

在本文中,我们将使用一些 Python 包:

  • Cerberus用于基于模式的数据验证
  • Pydantic用于严格的基于类型的数据验证。

我们还将使用流行的网络抓取包编写一个简短的示例抓取器:

  • httpx作为 HTTP 客户端收集公共 HTML 页面。
  • parsel作为 HTML 解析库,用于从 HTML 页面中提取详细信息。

pip我们可以使用控制台命令安装所有这些包:

$ pip install cerberus pydantic httpx parsel

使用 Cerberus 进行软验证

首先让我们看一下Cerberus——一个流行的 Python 数据验证库——以及我们如何将它用于网络废弃数据。 要开始使用 Cerberus,我们所要做的就是定义一些规则并将它们应用于我们的数据。

定义模式

为了验证数据,Cerberus 需要一个包含所有验证规则和结构预期的数据模式。例如:

from cerberus import Validator

schema = {
    "name": {
        # name should be a string
        "type": "string",
        # between 2 and 20 characters
        "minlength": 3, 
        "maxlength": 20, 
        "required": True,
    },
}

v = Validator(schema)
# this will pass
v.validate({"name": "Homer"})
print(v.errors)

# This will not
v.validate({"name": "H"})
print(v.errors)
# {'name': ['min length is 3']}

Cerberus 带有许多预定义规则,例如minlength,还有许多其他规则可以在验证规则文档maxlength中找到。不过,这里有一些常用的:

  • allowed – 验证允许值列表。
  • contains- 验证该值包含其他一些值。
  • required – 确保值存在。例如,id在抓取中经常需要像这样的字段。
  • depends – 确保相关字段存在。例如,如果存在产品折扣价,则也应存在全价。
  • regex – 检查值以匹配指定的正则表达式模式。这对于电话号码或电子邮件等字段非常有用。

然而,要真正利用这一点,我们可以定义自己的规则!

创建验证规则

Web 抓取的数据可能非常复杂。例如,仅名称字段就可以有多种形状和形式。 使用 Cerberus,我们可以在 Python 中提供我们自己的验证函数,可以验证复杂问题的数据值。

from cerberus import Validator


def validate_name(field, value, error):
    if "." in value:
        error(field, f"contains a dot character: {value}")
    if value.lower() in ["classified", "redacted", "missing"]:
        error(field, f"redacted value: {value}")
    if "<" in value.lower() and ">" in value.lower():
        error(field, f"contains html nodes: {value}")


schema = {
    "name": {
        # name should be a string
        "type": "string",
        # between 2 and 20 characters
        "minlength": 2,
        "maxlength": 20,
        # extra validation
        "check_with": validate_name,
    },
}

v = Validator(schema)

v.validate({"name": "H."})
print(v.errors)
# {'name': ['contains a dot character: H.']}

v.validate({"name": "Classified"})
print(v.errors)
# {'name': ['redacted value: Classified']}

v.validate({"name": "<a>Homer</a>"})
print(v.errors)
# {'name': ['contains html nodes: <a>Homer</a>']}

在上面的示例中,我们为名称字段添加了自己的验证函数,它执行更高级的字符串验证,例如检查无效值和潜在的 html 解析错误。

现实生活中的例子

让我们整理一个简单的示例网络抓取工具,我们将使用 Cerberus 对其进行验证。我们将在Glassdoor.com上抓取公司概览数据:

glassdoor公司概况亮点
在此示例中,我们将专注于抓取基本的公司详细信息。

例如,让我们抓取Ebay 的glassdoor 配置文件:

import httpx
from parsel import Selector

response = httpx.get("https://www.glassdoor.com/Overview/Working-at-eBay-EI_IE7853.11,15.htm")
selector = Selector(response.text)
overview_rows = selector.css('[data-test="employerOverviewModule"]>ul>li>div')
data = {}
for row in overview_rows:
    label = row.xpath("@data-test").get().split('-', 1)[-1]
    value = row.xpath("text()").get()
    data[label] = value
print(data)

这个简短的抓取工具将抓取基本的公司概览详细信息:

{
  "headquarters": "San Jose, CA",
  "size": "10000+ Employees",
  "founded": "1995",
  "type": "Company - Public (EBAY)",
  "revenue": "$10+ billion (USD)"
}

我们已经可以看到此页面上的原始字段有多么模糊。更大的公司将有数revenue十亿美元,不同地区将有不同的货币等。数据值是人类可读的,但没有标准化以供机器解释。 让我们将此数据解析为更通用的内容并使用 Cerberus 对其进行验证:

from cerberus import Validator


def parse_and_validate(data):
    schema = {}
    parsed = {}

    # company size is integer between 1 employee and several thousand
    parsed["size"] = int(data["size"].split()[0].strip("+"))
    schema["size"] = {"type": "integer", "min": 1, "max": 20_000}

    # founded date is a realistic year number
    parsed["founded"] = int(data["founded"])
    schema["founded"] = {"type": "integer", "min": 1900, "max": 2022}

    # headquarter details consist of city and state/province/country
    hq_details = data["headquarters"].split(", ")
    parsed["hq_city"] = hq_details[0] if hq_details else None
    parsed["hq_state"] = hq_details[1] if len(hq_details) > 1 else None
    schema["hq_city"] = {"type": "string", "minlength": 2, "maxlength": 20}
    schema["hq_state"] = {"type": "string", "minlength": 2, "maxlength": 2}

    # let's presume we only want to ensure we're scraping US and GB companies:
    parsed["revenue_currency"] = data["revenue"].split("(")[-1].strip("()")
    schema["revenue_currency"] = {"type": "string", "allowed": ["USD", "GBP"]}

    validator = Validator(schema)
    if not validator.validate(parsed):
        print("failed to validate parsed data:")
        for key, error in validator.errors.items():
            print(f"{key}={parsed[key]} got error: {error}")
    return parsed


ebay_data = {
    "headquarters": "San Jose, CA",
    "size": "10000+ Employees",
    "founded": "1995",
    "type": "Company - Public (EBAY)",
    "revenue": "$10+ billion (USD)",
}
print(parse_and_validate(ebay_data))
# will print:
{
  "size": 10000,
  "founded": 1995,
  "hq_city": "San Jose",
  "hq_state": "CA",
  "revenue_currency": "USD"
}

上面,我们编写了解析器,将原始公司概览数据转换为更标准和具体的内容。正如我们所看到的,它与我们的 Ebay 配置文件示例配合得很好,尽管我们编写了验证以确保我们的解析器提供一致的数据质量。样本量为 1 时,我们不能说我们的验证器做得很好。 让我们通过使用另一个公司页面 – Tesco扩展我们的测试样本来确认我们的验证模式

tesco_data = {
  "headquarters": "Welwyn Garden City, United Kingdom",
  "size": "10000+ Employees",
  "founded": "1919",
  "type": "Company - Private",
  "revenue": "Unknown / Non-Applicable"
}
print(parse_and_validate(tesco_data))
# will print:
# failed to validate parsed data:
# hq_state=United Kingdom got error: ['max length is 2']
# revenue_currency=Unknown / Non-Applicable got error: ['unallowed value Unknown / Non-Applicable']
{
  "size": 10000,
  "founded": 1919,
  "hq_city": "Welwyn Garden City",
  "hq_state": "United Kingdom",
  "revenue_currency": "Unknown / Non-Applicable"
}

正如我们所看到的,我们的验证器表明我们的解析器在处理 Tesco 的页面时没有像处理 Ebay 的那样好。 使用 Cerberus,我们可以快速定义所需输出的外观,这有助于开发和维护复杂的数据解析操作,从而确保一致的数据质量。


接下来,让我们看一下不同的验证技术——严格数据类型。

使用 Pydantic 进行类型化验证

我们的 Cerberus 验证让我们了解数据解析的不一致性,但如果我们要构建可靠的数据 API,我们可能需要更严格的东西。 Pydantic允许使用 Python 的类型提示系统定义严格的类型和验证规则。与 cerberus 不同,Pydantic 更加严格,并且在遇到无效数据值时会引发错误。 让我们从 Pydantic 的角度来看一下我们的 Glassdoor 示例。Pydantic 中的验证是通过显式BaseModel对象和 python 类型提示完成的。 例如,我们的 Glassdoor 公司概览验证器看起来像这样:

from typing import Optional
from pydantic import BaseModel, validator

# to validate data we must create a Pydantic Model:
class Company(BaseModel):
    # define allowed field names and types:
    size: int
    founded: int
    revenue_currency: str
    hq_city: str
    # some fields can be optional (i.e. have value of None)
    hq_state: Optional[str]

    # then we can define any extra validation functions:
    @validator("size")
    def must_be_reasonable_size(cls, v):
        if not (0 < v < 20_000):
            raise ValueError(f"unreasonable company size: {v}")
        return v

    @validator("founded")
    def must_be_reasonable_year(cls, v):
        if not (1900 < v < 2022):
            raise ValueError(f"unreasonable found date: {v}")
        return v

    @validator("hq_state")
    def looks_like_state(cls, v):
        if len(v) != 2:
            raise ValueError(f'state should be 2 character long, got "{v}"')
        return v


def parse_and_validate(data):
    parsed = {}

    parsed["size"] = data["size"].split()[0].strip("+")
    parsed["founded"] = data["founded"]
    hq_details = data["headquarters"].split(", ")
    parsed["hq_city"] = hq_details[0] if hq_details else None
    parsed["hq_state"] = hq_details[1] if len(hq_details) > 1 else None
    parsed["revenue_currency"] = data["revenue"].split("(")[-1].strip("()")
    return Company(**parsed)


ebay_data = {
    "headquarters": "San Jose, CA",
    "size": "10000+ Employees",
    "founded": "1995",
    "type": "Company - Public (EBAY)",
    "revenue": "$10+ billion (USD)",
}
print(parse_and_validate(ebay_data))

tesco_data = {
    "headquarters": "Welwyn Garden City, United Kingdom",
    "size": "10000+ Employees",
    "founded": "1919",
    "type": "Company - Private",
    "revenue": "Unknown / Non-Applicable",
}
print(parse_and_validate(tesco_data))

在这里,我们将解析器和验证器转换为使用 pydantics 模型来验证解析的数据。我们的 Ebay 数据将被成功验证,而 Tesco 数据将引发验证错误:

pydantic.error_wrappers.ValidationError: 1 validation error for Company
hq_state
  state should be 2 character long, got "United Kingdom" (type=value_error)

Pydandic 非常严格,只要无法验证或解释数据,就会引发异常。这是确保网络抓取数据质量的好方法,尽管它比我们的 Cerberus 示例具有更高的设置开销。

转换和解释

Pydantic 的另一个优势是能够从字符串转换和转换数据类型。例如,在我们的hq_state字段中,我们可以确保所有传入值都是小写的:

class Company(BaseModel):
    @validator("hq_state")
    def looks_like_state(cls, v):
        if len(v) != 2:
            raise ValueError(f'state should be 2 characters long, got "{v}"')
        return v.lower()

上面,我们的验证器将检查hq_state字段是否为 2 个字符长并将其转换为标准化我们抓取的数据集的小写字母。 Pydantic 还自动从字符串值中转换常见数据类型,使我们的解析过程更加精简且更容易理解:

from datetime import date
from pydantic import BaseModel

class Company(BaseModel):
    founded: date

print(Company(founded="1994-11-24"))
# will print:
# founded=datetime.date(1994, 11, 24)

如您所见,Pydantic 自动将字符串日期解释为 pythondate对象。

Cerberus 还是 Pydantic?

我们已经探索了这两种流行的数据验证包,尽管它们非常相似,但仍存在一些关键差异。 Pydantic 主要与 Python 的类型提示生态系统集成,而 Cerberus 使用更通用的模式方法。因此,虽然 Pydantic 可能有点复杂,但它可以提供主要的好处,例如 IDE 中的代码完成和自动数据转换。另一方面,Cerberus 更容易理解且不那么严格,因此非常适合小型网络抓取项目。

抓取的数据验证摘要

在本文中,我们了解了两种确保网络抓取数据质量的流行方法:

  • Cerberus – 基于模式的验证器,易于设置和配置。
  • Pydantic – 基于类型的验证器,不仅可以验证数据,还可以轻松地将其规范化为标准的 Python 数据类型,如日期和时间对象。

无论您选择哪种方法,当涉及到网络抓取中的数据质量时,两者都是非常强大的工具。

Written by 河小马

河小马是一位杰出的数字营销行业领袖,广告中国论坛的重要成员,其专业技能涵盖了PPC广告、域名停放、网站开发、联盟营销以及跨境电商咨询等多个领域。作为一位资深程序开发者,他不仅具备强大的技术能力,而且在出海网络营销方面拥有超过13年的经验。