将 Google 搜索 JSON 导出为 CSV 进行分析
Expert Network Defense Engineer
TL;DR:
- 谷歌搜索结果 CSV 需要查询上下文在其行旁。 保留请求和观察时间,以便在导出后每个结果仍然可解释。
- CSV 是 JSON 的一种投影。 保留原始捕获,区分缺失和空值,并记录转换单元格的含义。
- 仅头文件需要运行记录。 待处理、失败、未映射和存在空值的捕获可能由于不同原因都不会产生有机行。
电子表格可以保留每个标题,但仍然失去产生它的搜索的含义。如果没有查询、提交上下文和观察时间,一行变得难以比较或追踪。空单元格创造了另一种模糊性:该值是缺失、空值还是被转换拒绝?
Scrapeless Google Search API 提供上游结构化搜索数据。本指南从一个保存的捕获构建一个谷歌搜索结果 CSV,使用本地 Python 导出器。CSV 生成、电子表格处理和文件存储是示例程序的功能,而不是 API 直接返回这种导出格式的声明。
先决条件和捕获信封
导出器需要 Python 和一个具有 request、http_status 和 response 的保存 JSON 捕获。包括 run_id、requested_at 和 received_at,当收集器记录它们时。这些外部字段属于收集应用程序,而不是 API 的本地响应包装。
Google Search 请求工作流 区分 HTTP 200 任务数据与 HTTP 201 待处理工作。在扁平化任何内容之前,保留响应的 HTTP 结果。读取待处理响应中的缺失有机字段并用空列表替代将会抹去该区分。
本地转换不需要 API 密钥或第三方包。生成账户捕获是一个单独的认证步骤,本文未执行。当地检查使用合成捕获来测试数据类型、空状态、Unicode 和电子表格敏感文本。
JSON 值模型 保留数组、对象、空和不直接映射到平面单元格的字符串。导出后保留原始捕获,以便后续分析师可以恢复省略的模块和确切的原始值。
决定哪些列承载上下文和含义
在每个结果行上重复运行标识符、请求和接收时间、可用时的确切查询和序列化请求。完整请求保留了超出固定便利列的设置,使导出更容易审核。
Google Search 参数 包括国家和语言设置,但全 URL 模式可以在 url 内携带配置。因此,空的 q 列并不一定表示缺失提交的查询。检查 request_json;不要通过从空的便利字段猜测来重建有效的查询。
有机数组顺序和返回位置是单独的列。ordinal 记录项目的零基源顺序。position 仅在其为正整数时保留,布尔值被明确拒绝。导出器不会从数组顺序制造排名。
文本字段和位置各自都有一个伴随状态列。missing、null 和 value 解释了常见情况。invalid 标记一个不可用的位置;unexpected_type 标记一个非字符串文本字段值,该值被保留为序列化 JSON,而不是默默丢弃。
将 CSV 引号与电子表格解释分开
CSV 编写器处理分隔符和引号文本;它不会决定电子表格如何评估单元格。使用 Python 的 CSV 编写器 来处理逗号、引号和嵌入的换行符,而不是手动连接字符串。
程序使用 newline="" 和 utf-8-sig 开始输出。第一个设置允许 CSV 模块管理记录边界。UTF-8 签名可以帮助电子表格识别带口音或非拉丁文本,但目标应用程序的导入行为仍需检查。
一个以公式字符开头的值在电子表格中打开时可能会被解释为表达式。CSV 注入指导描述了为什么语法上正确的 CSV 并不足以使不可信文本无效。
该导出程序为选定的以公式开头的字符(包括全角变体)以及以制表符或换行符开头的字符添加了撇号前缀。它将策略应用于请求上下文以及结果文本。数字位置使用单独的验证规则。
开始使用 Scrapeless 抓取
通过 Scrapeless 强化您的网络抓取和自动化工作流程!
今天注册并获得 $5 的免费积分 — 无需信用卡。现在在 Scrapeless Dashboard 领取您的免费积分。
运行本地 JSON 到 CSV 导出器
将程序保存为 serp_csv.py,然后运行 python3 serp_csv.py capture.json organic.csv。它读取捕获并写入 organic.csv 及其伴随的 organic.csv.run.json。现有的输出文件将被替换;选择专用的导出目录或使用唯一名称来保留多个版本。
该程序拒绝覆盖输入捕获的输出路径。它不会更改原始 JSON,提交 API 请求或检索挂起任务。
python
import argparse
import csv
import json
from pathlib import Path
FIELDS = ["run_id", "requested_at", "received_at", "q", "request_json", "ordinal",
"position", "position_state", "title", "title_state", "link", "link_state",
"snippet", "snippet_state"]
def spreadsheet_text(value):
text = "" if value is None else str(value)
stripped = text.lstrip()
if (stripped.startswith(("=", "+", "-", "@", "=", "+", "-", "@"))
or text.startswith(("\t", "\r", "\n"))):
return "'" + text
return text
def field(row, name):
if name not in row:
return "", "missing"
value = row[name]
if value is None:
return "", "null"
if name == "position":
return (value, "value") if type(value) is int and value > 0 else ("", "invalid")
if isinstance(value, str):
return spreadsheet_text(value), "value"
return spreadsheet_text(json.dumps(value, ensure_ascii=False)), "unexpected_type"
def export(source, target):
source, target = Path(source), Path(target)
sidecar = target.with_suffix(target.suffix + ".run.json")
if source.resolve() in (target.resolve(), sidecar.resolve()):
raise ValueError("Output paths must differ from input")
record = json.loads(source.read_text(encoding="utf-8"))
request = record.get("request")
if not isinstance(request, dict) or not isinstance(request.get("input"), dict):
raise ValueError("Expected a capture record with request.input")
payload = record.get("response")
rows = payload.get("organic_results") if isinstance(payload, dict) else None
status = record.get("http_status")
if status == 201:
state, rows = "pending", []
elif status != 200:
state, rows = ("transport_error" if status is None else "http_error"), []
elif not isinstance(rows, list) or any(not isinstance(x, dict) for x in rows):
state, rows = "unmapped", []
else:
state = "observed" if rows else "empty"
context = {
"run_id": spreadsheet_text(record.get("run_id")),
"requested_at": spreadsheet_text(record.get("requested_at")),
"received_at": spreadsheet_text(record.get("received_at")),
"q": spreadsheet_text(request["input"].get("q")),
"request_json": spreadsheet_text(json.dumps(request, ensure_ascii=False, sort_keys=True)),
}
with target.open("w", encoding="utf-8-sig", newline="") as handle:
writer = csv.DictWriter(handle, fieldnames=FIELDS)
writer.writeheader()
for ordinal, item in enumerate(rows):
output = dict(context, ordinal=ordinal)
for name in ("position", "title", "link", "snippet"):
output[name], output[name + "_state"] = field(item, name)
writer.writerow(output)
sidecar.write_text(json.dumps({"source": str(source), "run_id": record.get("run_id"),
"state": state, "rows": len(rows), "request": request,
"requested_at": record.get("requested_at"), "received_at": record.get("received_at"),
"export_policy": "spreadsheet_text_prefix_v1; original values remain in source JSON"},
ensure_ascii=False, indent=2), encoding="utf-8")
print(f"Exported {len(rows)} organic rows; state={state}; metadata={sidecar}")
return state, len(rows)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("source")
parser.add_argument("target")
args = parser.parse_args()
export(args.source, args.target)
侧车记录集合状态、预计行数、源路径、请求、时间戳和导出策略。仅含标题的 CSV 只要该记录保持在一起就可以解释。在将导出交给其他人时,保持两个输出文件在一起。
检查空值和格式错误的有机数据
存在的空有机数组生成 state=empty。HTTP 201 生成 pending,缺失的 HTTP 状态变为 transport_error,而另一个非 200 状态在该应用程序的状态模型中变为 http_error。这些结果可以共享零行计数而不共享意义。
如果有机字段缺失、不是数组,或包含非对象项,整个投影将变为 unmapped。该运行不会导出任何有机行。这避免了在描述剩余行时悄然丢弃格式错误的项目,同时保留其为完整的投影。
可选字段的处理更加严格。缺失的片段留空单元格和明确的字段状态,而不是丢弃有机项目。意外的复杂值仍然以 JSON 文本的形式表示,因此异常值仍可检查。
这些是应用程序政策。在导出旁边记录它们,并在消费者需要不同模式时进行故意修订。字段状态列的实用性恰恰在于,其他分析师不必从少数可见单元格中推断转换。
验证电子表格交接
使用 CSV 解析器读取输出,并比较逻辑记录,而不是物理行。引用的片段可能包含换行符而不会创建新的搜索结果。确认侧车计数与解析的行匹配,并且每行都携带其请求上下文。
检查团队实际使用的应用程序中的 Unicode 和对公式敏感的值。撇号策略故意改变导出的表示,并可能在某些查看器中可见。这并不是所有导入设置和电子表格应用程序普遍的保证。
在测试中包含保存和重新打开行为。电子表格导入讨论说明了为什么转义处理值得应用程序特定的审查。将相关列作为文本导入,并在需要确切源字符串时使用原始 JSON。
成功的本地往返确认了该投影的一致性。它并未确立上游搜索样本是全面的、代表性的,或适用于没有进一步分析的排名结论。
结论
将请求上下文与结果一起导出,在空白会隐藏意义的地方保持字段状态,并保留原始 JSON。CSV 加上运行记录为下一个分析师提供了足够的信息,以区分测量的空切片与不可用的集合。
一个 SERP 内容研究 工作流程可以使用导出的观察结果构建可审查的阅读列表,然后撰写内容简报。
构建下一个搜索观察
使用 Scrapeless Google Search API 获取该工作流程中的搜索数据。在规划数据收集时查看 Scrapeless 定价,并将 Google 搜索参数 放在配置旁边。
在 Discord 或 Telegram 上与社区讨论您的实现。
FAQ
问:Google Search API 会直接创建这个 CSV 吗?
不。演示的导出器在本地转换保存的 JSON。文件生成和侧载架构属于 Python 程序。
问:当查询已经有一列时,为什么还要保留 request_json?
完整请求保留了可选设置和完整 URL 输入,而单靠查询列无法表示这些内容。
问:CSV 引号会阻止公式解析吗?
不会。分隔符处理和电子表格评估是分开的。应用文档化的文本政策并验证预期的导入工作流程。
问:空 CSV 是否意味着搜索没有返回结果?
不一定。检查侧载状态以区分存在的空数组和待处理、失败或未映射的集合。
问:可以恢复原始值吗?
可以,从保留的输入捕获中恢复。CSV 是一个投影,有意转换一些文本表示以便于电子表格使用。
在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。



