Polars网络爬虫:从实时页面到快速DataFrame
Scraping and Proxy Management Expert
TL;DR:
- Polars 是一个快速、基于 Arrow 的 DataFrame 库,与 Scrapeless 配合使用,将动态页面转化为一个类型化的可查询框架。
- Polars 没有 HTTP 客户端和 HTML 解析器,因此获取和提取步骤来自 Scrapeless 和解析器;一旦记录存在,Polars 就可以接管。
- 使用
pl.DataFrame(records)从字典列表构建框架,在提取过程中强制转换值时,模式将从第一行推断类型。 - 表达式 API 替代了 pandas 风格的链式索引:在
group_by(...).agg(...)中的pl.col("price_gbp").mean()读取和运行都非常简洁。 - 懒加载路径
df.lazy()...collect()使 Polars 在接触数据之前规划和优化整个查询,这使它在处理更大的数据集时领先。 - 从 Scrapeless 免费计划 开始,将提取步骤指向您自己的目录。
Polars 在数据工作中不断出现,因为它速度快且 API 友好,但几乎每个教程都使用已经存在的 Parquet 文件。抓取数据正好相反。数据是动态的,格式为 HTML,直到您将其类型化,它才没有类型。本指南将一个公共书籍目录从 URL 转化为可分组和聚合的 Polars DataFrame,并诚实地指出 Polars 不会为您做的事情:获取页面。
获取步骤来自 Scrapeless Universal Scraping API,该 API 为公共 URL 返回渲染的 HTML。解析器将 HTML 转换为记录。Polars 处理剩下的事情,并且速度很快。
Polars 为抓取工作流添加了什么
Polars 是一个构建在 Apache Arrow 列存内存格式上的 DataFrame 库,这使得它的类型化列和分组聚合速度很快。对于抓取的数据来说,收益是具体的:一旦您的记录在框架中,清理和汇总它们只需要几个表达式,而不是手动编写循环,并且列保持其类型。Polars 依赖于 Apache Arrow 列存格式 的内存布局,因此价格列是一个真实的数值列,而不是您在每个操作中重新解析的字符串列表。
安装
Polars 处理框架;解析器处理 HTML。请同时安装两者。
bash
pip install polars lxml
在 shell 中设置您的 Scrapeless API 密钥。运行时使用真实的密钥,并将占位符保持在源代码之外。
bash
export SCRAPELESS_API_KEY="sk_your_key_here"
获取、提取和构建框架
第一阶段获取页面,为每个产品提取一个记录,并构建 DataFrame。由于 Polars 不能获取或解析 HTML,这一阶段是 Scrapeless 和解析器执行其工作的地方;Polars 接收最终的记录。
python
import json
import os
import urllib.request
import polars as pl
from lxml import html as lxhtml
API_URL = "https://api.scrapeless.com/api/v2/unlocker/request"
RATINGS = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}
def fetch_html(url: str) -> str:
payload = json.dumps(
{"actor": "unlocker.webunlocker", "input": {"url": url, "js_render": False, "headless": False}}
).encode()
request = urllib.request.Request(
API_URL,
data=payload,
headers={"x-api-token": os.environ["SCRAPELESS_API_KEY"], "Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(request, timeout=90) as response:
return json.loads(response.read())["data"]
dom = lxhtml.fromstring(fetch_html("https://books.toscrape.com/"))
records = []
for card in dom.cssselect("article.product_pod"):
records.append(
{
"title": card.cssselect("h3 a")[0].get("title"),
"price_gbp": float(card.cssselect("p.price_color")[0].text_content().strip().lstrip("£")),
"rating": RATINGS[card.cssselect("p.star-rating")[0].get("class").split()[-1]],
"in_stock": "In stock" in card.cssselect("p.instock.availability")[0].text_content(),
}
)
df = pl.DataFrame(records)
print(f"records extracted: {df.height}")
print("schema:", dict(df.schema))
提取内容使用 CSS 选择器标准 通过 lxml 的 cssselect,并在读取时强制转换每个值:价格转换为 float,星级评分单词转换为整数,而可用性转换为布尔值。这个强制转换是一个重要的习惯。使用真实的 Python 类型构建记录,Polars 将从中推断出真实模式。
text
records extracted: 20
schema: {'title': String, 'price_gbp': Float64, 'rating': Int64, 'in_stock': Boolean}
第一页上的20个产品卡片变成了一个带有类型模式的框架:String、Float64、Int64和Boolean。没有列被保留为文本,稍后需要重新解析。
使用表达式转换
Polars用表达式API替代了链式索引,这是值得认真学习的部分。像pl.col("price_gbp").mean()这样的表达式描述了Polars高效运行的计算,表达式可以在filter、group_by和agg内部组合。
python
summary = (
df.lazy()
.filter(pl.col("in_stock"))
.group_by("rating")
.agg(pl.len().alias("books"), pl.col("price_gbp").mean().round(2).alias("avg_price"))
.sort("rating")
.collect()
)
print(summary)
df.lazy()将急切的框架转变为查询计划,链条描述工作,而.collect()则执行它。在这小一组数据上,差异是不可见的,但延迟路径使Polars能够分析整个查询,并在读取单个值之前跳过工作,这就是它能够扩展的原因。聚合将有库存的书籍按星级评分分组,并报告每组的数量和平均价格。
text
shape: (5, 3)
┌────────┬───────┬───────────┐
│ rating ┆ books ┆ avg_price │
│ --- ┆ --- ┆ --- │
│ i64 ┆ u32 ┆ f64 │
╞════════╪═══════╪═══════════╡
│ 1 ┆ 6 ┆ 40.02 │
│ 2 ┆ 3 ┆ 36.83 │
│ 3 ┆ 3 ┆ 42.32 │
│ 4 ┆ 4 ┆ 31.1 │
│ 5 ┆ 4 ┆ 39.75 │
└────────┴───────┴───────────┘
完整脚本
将各个阶段组合在一起,添加另一个表达式来查找最便宜的五星书籍,并将框架存储为Parquet格式。
python
import json
import os
import urllib.request
import polars as pl
from lxml import html as lxhtml
API_URL = "https://api.scrapeless.com/api/v2/unlocker/request"
RATINGS = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}
def fetch_html(url: str) -> str:
payload = json.dumps(
{"actor": "unlocker.webunlocker", "input": {"url": url, "js_render": False, "headless": False}}
).encode()
request = urllib.request.Request(
API_URL,
data=payload,
headers={"x-api-token": os.environ["SCRAPELESS_API_KEY"], "Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(request, timeout=90) as response:
return json.loads(response.read())["data"]
dom = lxhtml.fromstring(fetch_html("https://books.toscrape.com/"))
records = []
for card in dom.cssselect("article.product_pod"):
records.append(
{
"title": card.cssselect("h3 a")[0].get("title"),
"price_gbp": float(card.cssselect("p.price_color")[0].text_content().strip().lstrip("£")),
"rating": RATINGS[card.cssselect("p.star-rating")[0].get("class").split()[-1]],
"in_stock": "In stock" in card.cssselect("p.instock.availability")[0].text_content(),
}
)
df = pl.DataFrame(records)
print(f"提取的记录数: {df.height} | 列: {df.columns}")
summary = (
df.lazy()
.filter(pl.col("in_stock"))
.group_by("rating")
.agg(pl.len().alias("books"), pl.col("price_gbp").mean().round(2).alias("avg_price"))
.sort("rating")
.collect()
)
print(summary)
cheapest = df.filter(pl.col("rating") == 5).sort("price_gbp").select("title", "price_gbp").head(1)
print("最便宜的五星书籍:", cheapest.to_dicts())
df.write_parquet("books.parquet")
print(f"parquet字节数: {os.path.getsize('books.parquet')}")
运行时报告每个步骤,最后生成一个保留模式的Parquet文件,以供下一个读取者使用。
text
提取的记录数: 20 | 列: ['title', 'price_gbp', 'rating', 'in_stock']
shape: (5, 3)
┌────────┬───────┬───────────┐
│ rating ┆ books ┆ avg_price │
│ --- ┆ --- ┆ --- │
│ i64 ┆ u32 ┆ f64 │
╞════════╪═══════╪═══════════╡
│ 1 ┆ 6 ┆ 40.02 │
│ 2 ┆ 3 ┆ 36.83 │
│ 3 ┆ 3 ┆ 42.32 │
│ 4 ┆ 4 ┆ 31.1 │
│ 5 ┆ 4 ┆ 39.75 │
└────────┴───────┴───────────┘
最便宜的五星书籍: [{'title': 'Set Me Free', 'price_gbp': 17.46}]
parquet字节数: 2233
write_parquet将带类型的列存储在Apache Parquet文件格式中,因此下一个进程将price_gbp读取为浮点数,无需重新转换。Polars用户指南记录了完整的表达式和延迟API,当你超越这个示例时,可以查看详细信息。
Polars不会做的事情
Polars是一个数据框架引擎,这就是它的全部边界:它没有HTTP客户端,也没有HTML解析器。它不会请求一个URL,不会渲染JavaScript,也不会将<article>标签转换为行。这是出于设计考虑,这也是为什么这个工作流程在其前面使用了两个工具。Scrapeless负责获取页面,而一个解析器将标记读取为记录。如果你需要对解析步骤进行更深入的处理,可以查看关于使用BeautifulSoup进行网络爬虫的指南,它涵盖了Python中的HTML提取,并且相同的记录直接输入到pl.DataFrame中。
当目标用JavaScript渲染其内容时,第一次抓取会返回一个空的框架,提取循环找不到任何卡片。在请求中将js_render设置为True,以便Scrapeless在其脚本运行后返回页面,而当标记已经由服务器渲染时则将其保留为False,正如这个目录所示。
在你扩大抓取范围到第一页之外之前,请阅读目标网站的robots.txt文件和服务条款。机器人排除协议规定了一个网站请求自动化客户端避免的路径,遵循这一点可以保持工作流程的可持续性。保持抓取量在合理范围内,并确保数据是公开的,就像这个示例所示。
准备好在你自己的数据上尝试了吗?创建一个免费的Scrapeless账户,并在抓取阶段更改URL和选择器。
结论
Polars可以将抓取的记录转化为类型化、可查询的框架,并且只需很少的代码,前提是其他工具已先将页面获取到。Scrapeless获取HTML,一个解析器用真实类型构建记录,而Polars通过其表达式和惰性API对其进行分组、聚合和存储。从完整的脚本开始,替换URL和选择器为你自己的目标,当你的数据增长时,选择惰性路径。
开始使用Scrapeless免费计划来抓取你自己的页面,并在你计划定期作业时查看Scrapeless定价。
常见问题
问:Polars会自己抓取网页吗?
不会。Polars是一个没有HTTP客户端和HTML解析器的DataFrame库,因此它无法请求URL或读取标记。将它与像Scrapeless这样的抓取层和像lxml或BeautifulSoup这样的解析器结合使用;一旦记录存在,Polars就会接管。
问:为什么选择Polars而不是pandas来处理抓取的数据?
Polars构建在Apache Arrow列式格式之上,并提供惰性查询引擎,因此在大型框架上,分组聚合和过滤都是快速的,表达式API是一致的。一个相关的权衡涉及抓取:pandas拥有read_html用于表格,而Polars期望你自行根据记录构建框架,这更适合卡片样式的页面,而不是仅限表格的页面。
问:Polars中的急切与惰性有什么区别?
急切的DataFrame会立即运行每个操作,而df.lazy()构建一个查询计划,该计划仅在你调用.collect()时执行。惰性路径使得Polars能够优化整个查询并跳过不必要的工作,这就是它在大型数据集上更具可扩展性的原因,尽管在小数据集上结果是相同的。
问:为什么我抓取的数字在Polars中显示为字符串?
抓取的值以文本形式到达,如果你直接将它们传递给pl.DataFrame,列的类型将是String。在提取过程中强制转换每个值,正如示例中将价格转换为float并将评分单词转换为整数,这样架构就从第一行开始被类型化,后续无需重新解析列。
问:如何从抓取的记录列表中构建Polars DataFrame?
将每条记录作为具有一致键的字典进行收集,并将该列表传递给pl.DataFrame(records)。Polars根据值推断架构,因此字典中的类型值会产生一个类型化的框架,准备好用于表达式API。
在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。



