网页抓取数据(如何最有效地分析Git存储库来跟踪数据源随时间的变化)
优采云 发布时间: 2022-01-23 13:13网页抓取数据(如何最有效地分析Git存储库来跟踪数据源随时间的变化)
大多数人都知道 Git 抓取,这是一种网络抓取编程技术,您可以定期将数据源的快照抓取到 Git 存储库,以跟踪数据源随时间的变化。
如何分析这些采集到的数据是一个公认的挑战。git-history 正是我为解决这个困境而设计的工具。
Git抓取技术回顾
将数据抓取到 Git 存储库的一大优势是抓取工具本身非常简单。
这是一个具体的例子:加州林业和消防局 (Cal Fire) 在 /incidents网站 维护了一张火灾地图,显示了该州最近发生的大火。
我找到了 网站 的基础数据:
curl /umbraco/Api/IncidentApi/GetIncidents
然后我构建了一个简单的爬虫,它每 20 分钟获取一次 网站 数据并将其提交给 Git。到目前为止,该工具已经运行了 14 个月,并采集了 1559 次提交。
Git 抓取最让我兴奋的是它可以创建真正独特的数据集。许多组织没有详细存档数据更改的内容和位置,因此通过抓取他们的 网站 数据并将其保存到 Git 存储库,您会发现自己比他们更了解他们的数据更改历史做。
然而,一个巨大的挑战是如何最有效地分析采集到的数据?面对成千上万的版本和大量的 JSON 和 CSV 文档,如果仅用肉眼观察差异,很难挖掘出数据背后的价值。
git历史
git-history 是我的新解决方案,它是一个命令行工具。它可以读取文件的整个历史版本,并随着时间的推移生成文件更改的 SQLite 数据库。然后可以使用 Datasette 分析来挖掘这些数据。
下面是通过使用 ca-fires-history 存储库运行 git-history 生成的示例数据库。我通过在存储库目录中运行以下命令创建了一个 SQLite 数据库:
git-history file ca-fires.db incidents.json \
--namespace incident \
--id UniqueId \
--convert 'json.loads(content)["Incidents"]'
在此示例中,我们正在获取文档 events.json 的历史版本。
我们使用 UniqueId 列来识别随时间变化的记录和新记录。
新创建的数据库表的默认名称是 item 和 item_version。我们通过 --namespace 事件将表名指定为事件和事件版本。
该工具中还嵌入了一段 Python 代码,可将存储在提交历史中的每个修订版转换为与该工具兼容的对象列表。
让数据库帮助我们回答有关过去 14 个月加州大火的一些问题。
事件表收录每次火灾的最新记录。有了这张表,我们可以得到一张所有火灾的地图:
此处使用 dataset-cluster-map 插件,它映射表中提供有效经度和纬度值的所有行。
真正有趣的是incident_version 表。该表记录了每次火灾的获取版本之间的数据更新。
有 250 起火灾的 2060 个记录版本。如果我们按 _item 分面,我们可以看到哪些火灾记录的版本最多。前十名分别是:
§ 迪克西火:268
§ 卡尔多火:153
§ 纪念碑火灾 65
§ 八月情结(包括 Doe Fire):64
§ 溪火:56
§ 法国火:53
§ 西尔维拉多火:52
§ 小鹿火:45
§ 蓝岭火:39
§ 麦克法兰火灾:34
版本数越高,火的持续时间越长。维基百科上什至有一个关于 Dixie Fire 的条目!
点击Dixie Fire,在弹出的页面中,可以看到所有抓取到的“版本”按版本号排列。
git-history 只在这个表中写入与上一个版本相比发生变化的值。因此,一目了然,您可以看到哪些信息随时间发生了变化:
经常变化的是 ConditionStatement 列,这一列是文本描述,另外两个有趣的列是 AcresBurned 和 PercentContained。
_commit 是 commits 表的外键,记录了工具的提交版本,所以当你再次运行工具时,工具可以定位到上次提交的版本。
加入提交表以查看每个版本的创建日期。您还可以使用 event_version_detail 视图执行连接操作。
通过这个视图,我们可以过滤所有_item值为174、AcresBurned值不为空的行,借助dataset-vega插件,比较_commit_at列(日期类型)和AcresBurned列(数值类型) 形成一个图表,可视化 Dixie Fire 火灾随时间的进展情况。
总结一下:我们首先使用 GitHub Actions 创建一个定时工作流,每 20 分钟获取一次 JSON API 端点的最新副本。现在,在 git-history、Datasette 和 datasette-vega 的帮助下,我们成功地绘制了过去 14 个月加州持续时间最长的野火之一的蔓延情况。
关于表结构设计
在 git-history 的设计过程中,最难的就是设计一个合适的表结构来存储以前的版本变更信息。
我的最终设计如下(为清楚起见进行了适当编辑):
CREATE TABLE [commits] (
[id] INTEGER PRIMARY KEY,
[hash] TEXT,
[commit_at] TEXT
);
CREATE TABLE [item] (
[_id] INTEGER PRIMARY KEY,
[_item_id] TEXT,
[IncidentID] TEXT,
[Location] TEXT,
[Type] TEXT,
[_commit] INTEGER
);
CREATE TABLE [item_version] (
[_id] INTEGER PRIMARY KEY,
[_item] INTEGER REFERENCES [item]([_id]),
[_version] INTEGER,
[_commit] INTEGER REFERENCES [commits]([id]),
[IncidentID] TEXT,
[Location] TEXT,
[Type] TEXT
);
CREATE TABLE [columns] (
[id] INTEGER PRIMARY KEY,
[namespace] INTEGER REFERENCES [namespaces]([id]),
[name] TEXT
);
CREATE TABLE [item_changed] (
[item_version] INTEGER REFERENCES [item_version]([_id]),
[column] INTEGER REFERENCES [columns]([id]),
PRIMARY KEY ([item_version], [column])
);
前面提到过item_version表记录了网站不同时间点的快照,但是为了节省数据库空间,提供简洁的版本浏览界面,这里只记录与之前版本相比发生变化的列。未更改的列用 null 写入。
但是这样的设计有一个隐患,就是fire里面某列的值更新为null怎么办?我们如何判断它是更新还是没有变化?
为了解决这个问题,我添加了一个多对多表 item_changed,它使用整数对来记录 item_version 表中哪些列有更新的内容。使用整数对的目的是尽可能少地占用空间。
item_version_detail 视图以 JSON 的形式呈现多对多表中的列。我过滤了一些数据,放到下图中,可以看到哪些列哪些火灾在哪些版本中更新了:
通过下面的 SQL 查询,我们可以知道加州火灾哪些数据更新最频繁:
select columns.name, count(*)
from incident_changed
join incident_version on incident_changed.item_version = incident_version._id
join columns on incident_changed.column = columns.id
where incident_version._version > 1
group by columns.name
order by count(*) desc
查询结果如下:
§ 更新:1785
§ 火灾被扑灭的比例:740
§ 状态说明:734
§ 火灾区域:616
§ 开始时间:327
§ 受灾人员:286
§ 消防泵:274
§ 消防员:256
§ 消防车:225
§ 无人机:211
§ 消防飞机:181
§ 建筑损坏:125
§ 直升机:122
直升机听起来很刺激!让我们过滤掉自第一个版本以来至少有一次更新的直升机火灾数量。您可以使用这样的嵌套 SQL 查询:
select * from incident
where _id in (
select _item from incident_version
where _id in (
select item_version from incident_changed where column = 15
)
and _version > 1
)
查询结果显示,19起火灾由直升机救援,我们在下图标注:
--convert 选项的高级用法
在过去的 8 个月中,Drew Breunig 一直在使用 Git 爬虫不断地从 网站 中抓取数据并将其保存到 dbreunig/511-events-history 存储库中。网站 记录旧金山湾区的交通事故。我将他的数据加载到 sf-bay-511 数据库中。
以数据库 sf-bay-511 为例,帮助我们理解 git-history 的用法和叠加的 --convert 选项。
git-history 要求抓取的数据为以下特定格式:由 JSON 对象组成的 JSON 列表,每个对象都有一个列,可以用作唯一标识列,以跟踪数据随时间的变化。
理想的 JSON 文件如下所示:
[
{
"IncidentID": "abc123",
"Location": "Corner of 4th and Vermont",
"Type": "fire"
},
{
"IncidentID": "cde448",
"Location": "555 West Example Drive",
"Type": "medical"
}
]
但抓取的数据通常不是这种理想的格式。
我找到了 网站 的 JSON 提要。有非常复杂的嵌套对象,数据量很大,其中一些对整体分析没有帮助,比如即使没有数据更新也会随着版本变化的更新时间戳,以及嵌套很深的对象“扩展”收录大量重复数据。
我编写了一段 Python 代码将每个 网站 快照转换为更简单的结构,并将此代码传递给脚本的 --convert 选项:
#!/bin/bash
git-history file sf-bay-511.db 511-events-history/events.json \
--repo 511-events-history \
--id id \
--convert '
data = json.loads(content)
if data.get("error"):
# {"code": 500, "error": "Error accessing remote data..."}
return
for event in data["Events"]:
event["id"] = event["extension"]["event-reference"]["event-identifier"]
# Remove noisy updated timestamp
del event["updated"]
# Drop extension block entirely
del event["extension"]
# "schedule" block is noisy but not interesting
del event["schedule"]
# Flatten nested subtypes
event["event_subtypes"] = event["event_subtypes"]["event_subtype"]
if not isinstance(event["event_subtypes"], list):
event["event_subtypes"] = [event["event_subtypes"]]
yield event
'
这个传递给 --convert 的单引号字符串被编译成 Python 函数,并在每个 Git 版本上按顺序运行。代码循环遍历嵌套的事件列表,修改每条记录,然后使用 yield 以可迭代的顺序输出。
一些历史记录显示服务器 500 错误,代码也能够识别和跳过这些记录。
使用 git-history 时,我发现自己大部分时间都在迭代转换脚本。将 Python 代码字符串传递给 git-history 等工具是一种有趣的模式,今年早些时候我尝试在 sqlite-utils 工具中覆盖转换。
试试看
如果您想尝试 git-history 工具,扩展文档 README 中提供了更多选项,示例中使用的脚本存储在 demos 文件夹中。
在 GitHub 上的 git-scraping 话题下,很多人创建了仓库,目前已经超过 200 个,海量的抓取数据等你去探索!
手稿来源:/2021/Dec/7/git-history/