网页抓取解密(Git-history正是网页抓取工具编程技术技术回顾(组图))
优采云 发布时间: 2022-01-04 03:12网页抓取解密(Git-history正是网页抓取工具编程技术技术回顾(组图))
大多数人都知道 Git 抓取,这是一种用于网页抓取工具的编程技术。您可以定期将数据源的快照抓取到 Git 存储库中,以跟踪数据源随时间的变化。
如何分析采集到的数据是一个公认的问题。 git-history 是我设计用来解决这个问题的工具。
Git 抓取技术回顾
将数据抓取到 Git 存储库的一大优势在于抓取工具本身非常简单。
这是一个具体的例子:加州林业和消防局 (Cal Fire) 在 /incidents网站 上维护了一张火灾地图,该地图显示了该州最近发生的*敏*感*词*火灾。
我找到了网站的底层数据:
卷曲
然后我设置了一个简单的爬虫工具,每 20 分钟抓取一个 网站 数据的副本并提交给 Git。截至目前,该工具已运行14个月,已采集1559个提交版本。
Git 抓取最让我兴奋的是它可以创建真正独特的数据集。许多组织并没有详细归档数据更改的内容和位置,因此通过抓取他们的网站 数据并将其保存到 Git 存储库中,您会发现您比他们更了解他们的数据更改历史。
然而,一个巨大的挑战是如何最有效地分析采集到的数据?面对上千个版本和海量的JSON和CSV文档,如果只靠肉眼观察差异,很难挖掘出数据背后的价值。
git 历史
git-history 是我的新解决方案,它是一个命令行工具。它可以读取文件的所有历史版本并生成 SQLite 数据库来记录文件随时间的变化。然后你就可以使用Datasette来分析和挖掘这些数据了。
以下是通过使用 ca-fires-history 存储库运行 git-history 生成的数据库示例。我通过在存储库目录中运行以下命令创建了一个 SQLite 数据库:
git-history 文件 ca-fires.db events.json \
--命名空间事件 \
--id UniqueId \
--convert'json.loads(content)["事件"]'
在这个例子中,我们获取了文件 events.json 的历史版本。
我们使用 UniqueId 列来标识随时间变化的记录和新记录。
新数据库表的默认名称是item和item_version,我们通过--namespace event指定表名是incident和incident_version。
工具中还嵌入了一段 Python 代码,可以将提交历史中存储的每个版本转换为与工具兼容的对象列表。
让数据库帮助我们回答一些关于过去 14 个月加州火灾的问题。
事件表收录每次火灾的最新记录。通过这张表,我们可以得到一张所有火灾的地图:
这里使用了datasette-cluster-map插件,它在地图上标记了表中所有给出有效经纬度值的行。
真正有趣的是 event_version 表。此表记录了每次火灾的先前捕获版本之间的数据更新。
250 场火灾有 2,060 个记录版本。如果根据_item进行分面,我们可以看到哪些火灾记录的版本最多。排名前十的分别是:
迪克西火:268
卡尔多之火:153
纪念碑火 65
August Complex(包括 Doe Fire):64
溪火:56
法式火焰:53
西尔弗拉多火灾:52
小鹿火:45
蓝岭火:39
麦克法兰火:34
版本越多,火持续的时间就越长。维基百科上甚至还有 Dixie Fire 的条目!
点击Dixie Fire,在弹出的页面可以看到所有抓到的“版本”按版本号排序。
git-history 只在此表中写入与之前版本相比发生变化的值。因此,您可以一目了然地看到哪些信息随时间发生了变化:
最频繁的变化是 ConditionStatement 列。此栏是文字说明。另外两个有趣的列是 AcresBurned 和 PercentContained。
_commit 是提交表的外键。该表记录了该工具提交的版本,因此当您再次运行该工具时,该工具可以定位到上次提交的版本。
连接commits表查看每个版本的创建日期。您也可以使用incident_version_detail 视图来执行连接操作。
通过这个视图,我们可以过滤所有_item值为174、AcresBurned值不为空的行。借助datasette-vega插件,将_commit_at列(日期类型)和AcresBurned列(数字类型)进行对比,形成一个图表,直观地展示了Dixie Fire火灾随时间的蔓延情况。
总结:让我们首先使用 GitHub Actions 创建一个定时工作流,并每 20 分钟获取一次 JSON API 端点的最新副本。现在,在 git-history、Datasette 和 datasette-vega 的帮助下,我们已经成功地用图表展示了过去 14 个月加州最长的森林火灾的蔓延情况。
关于表结构设计
在git-history的设计过程中,最难的就是设计一个合适的表结构来存储之前版本变化的信息。
我的最终设计如下(为了清晰起见进行了适当编辑):
创建表 [提交] (
[id] 整数主键,
[hash] 文本,
[commit_at] 文本
);
创建表 [项目] (
[_id] 整数主键,
[_item_id] 文本,
[事件 ID] 文本,
[位置] 文本,
[类型] 文本,
[_commit] 整数
);
创建表 [item_version] (
[_id] 整数主键,
[_item] 整数引用 [item]([_id]),
[_version] 整数,
[_commit] 整数引用 [commits]([id]),
[事件 ID] 文本,
[位置] 文本,
[类型] 文本
);
创建表 [列] (
[id] 整数主键,
[namespace] 整数引用 [namespaces]([id]),
[名称] 文本
);
创建表 [item_changed] (
[item_version] 整数引用 [item_version]([_id]),
[column] 整数引用 [columns]([id]),
主键([item_version],[column])
);
前面提到,item_version表记录了网站在不同时间点的快照,但为了节省数据库空间,提供简洁的版本浏览界面,仅将与之前版本相比发生变化的列记录在这里。所有没有改变的列都写为空。
但是,这种设计有一个隐患,就是如果某列的值在某次火灾中被更新为null,我们该怎么办?我们如何判断它是否已更新或未更改?
为了解决这个问题,我添加了一个多对多表item_changed,它使用整数对来记录item_version表中哪些列更新了内容。使用整数对的目的是尽可能少占用空间。
item_version_detail 视图将多对多表中的列显示为 JSON。我过滤了一些数据,放在下图中,看看哪些列更新了哪些版本:
通过下面的SQL查询,我们可以知道加州火灾中哪些数据更新最频繁:
select columns.name, count(*)
来自 event_changed
在 event_changed.item_version = event_version._id 上加入 event_version
在 event_changed.column = columns.id 上加入列
where event_version._version> 1
按列名分组
按计数(*)降序
查询结果如下:
更新:1785
火灾被扑灭的百分比:740
状态描述:734
火灾区域:616
开始时间:327
受灾人数:286
消防泵:274
消防员:256
消防车:225
无人机:211
消防飞机:181
建筑损坏:125
直升机:122
直升机听起来很刺激!让我们过滤掉第一个版本后直升机数量至少更新一次的火灾。您可以使用以下嵌套 SQL 查询:
从事件中选择*
where _id 在 (
从事件版本中选择 _item
where _id 在 (
select item_version from event_changed where column = 15
)
和_version> 1
)
查询结果显示有19次直升机出动火灾,我们在下图标注:
--convert 选项的高级用法
在过去的 8 个月中,Drew Breunig 使用 Git 爬虫不断从 网站 中获取数据并将其保存到 dbreunig/511-events-history 存储库中。这个网站记录旧金山湾区的交通事故。我将他的数据加载到 sf-bay-511 数据库中。
以sf-bay-511数据库为例,有助于我们理解git-history和--convert选项overlay的用法。
Git-history 要求捕获的数据采用以下特定格式:由 JSON 对象组成的 JSON 列表,每个对象都有一列可用作唯一标识列,以跟踪数据随时间的变化。
理想的 JSON 文件如下所示:
[
{
"事件ID": "abc123",
“位置”:“第四和佛蒙特州的拐角处”,
“类型”:“火”
},
{
"事件ID": "cde448",
"Location": "555 West Example Drive",
“类型”:“医疗”
}
]
但捕获的数据通常不是这种理想格式。
我找到了 网站 的 JSON 提要。有非常复杂的嵌套对象,数据也很复杂,有些对整体分析没有帮助,比如更新的时间戳即使没有数据更新也会随着版本的变化而变化,深度嵌套的对象“扩展”收录大量重复数据。 .
我写了一段Python代码将每个网站快照转换成更简单的结构,然后将这段代码传递给脚本的--convert选项:
#!/bin/bash
git-history 文件 sf-bay-511.db 511-events-history/events.json \
--repo 511-events-history \
--id id \
--转换'
data = json.loads(content)
if data.get("error"):
# {"code": 500, "error": "访问远程数据时出错..."}
返回
对于数据中的事件["Events"]:
event["id"] = event["extension"]["event-reference"]["event-identifier"]
#去除嘈杂的更新时间戳
删除事件[“更新”]
# 完全删除扩展块
删除事件["扩展名"]
# "schedule" 块很吵但不有趣
del event["schedule"]
# 展平嵌套的子类型
event["event_subtypes"] = event["event_subtypes"]["event_subtype"]
如果不是 isinstance(event["event_subtypes"], list):
event["event_subtypes"] = [event["event_subtypes"]]
产量事件
'
传递给 --convert 的单引号字符串被编译成 Python 函数,并在每个 Git 版本上依次运行。代码在 Events 嵌套列表中循环运行,修改每条记录,然后使用 yield 以可迭代序列输出。
一些历史记录显示服务器 500 错误,代码也可以识别和跳过这些记录。
在使用 git-history 时,我发现我大部分时间都花在了迭代转换脚本上。将 Python 代码字符串传递给 git-history 等工具是一个非常有趣的模型。今年早些时候,我也尝试在 sqlite-utils 工具中叠加转换。
自己试试
如果您想尝试 git-history 工具,扩展文档 README 中提供了更多选项。示例中使用的脚本都保存在 demos 文件夹中。
GitHub 上的 git-scraping 话题下,已经有很多人创建了仓库,目前有 200 多个,还有丰富的爬取数据等你探索!