Build in Public · LF-05
Google Maps 评论 API:选对入口,再建立地点与评论记录
区分 Business Profile、Places API 与 EveryInfra 地点评论读取,核对地点身份、语言和时间筛选,保留评分与商家回复变化,并明确样本、存储和展示边界。
“我想获取 Google Maps 评论”可能指三件不同的事:管理自己的门店评价,在产品里展示某个地点的信息,或者研究一组地点的用户反馈。它们都涉及评论,却不使用同一套权限、接口与保存规则。选错入口以后,增加请求量或换一种解析方法都不能解决根本问题。
这篇文章先帮助你选择工作流,再以 EveryInfra 的 google_maps_reviews.reviews 说明如何确认地点、构造小请求、处理评分和商家回复,以及组织持续观察。重点是每条数据能关联回正确地点,并且你能解释样本缺口,而不是承诺导出任何地点的全部历史评论。
目录与实现核对日期为 2026-09-04。文中的付费请求没有在本轮执行;代码中的合成记录只用于演示字段变化检查。只有确认数据读取、处理、保存与展示的具体用途获准后,才应将模板用于业务。
先选入口:商家管理、地点展示与研究读取
管理你有权限运营的门店时,先看 Google Business Profile API。官方评论指南按账户和地点组织读取与商家回复操作,接入需要应用注册和 OAuth 凭据。需要回复用户评价时,应在相应管理权限下进行,而不是向任意地图链接发送一个通用写入请求。
官方指南中的 deleteReply 是删除商家回复,不是删除用户原评论。这个区别对运营工作流很重要:如果任务是撤回自己写错的回复,与处理一条认为不当的用户评价,应走各自对应的流程,不能仅看到一个 DELETE 示例就误用。
在产品里展示地点详情时,可以看 Google Places API 的 Place Details。它按地点 ID 查询,并用 FieldMask 选择所需字段;这条地点展示路径不能直接当成所有历史评论的批量导出接口。使用 Places 内容时,还要遵守相应缓存、存储和归属展示要求。
EveryInfra 提供的是另一份读取契约:platform 为 google_maps_reviews,action 为 reviews,以地点 URL 作为输入。下面只解释这条读取路径,不演示修改门店资料、回复或删除操作,也不把它称为 Google 官方 API。
选择第三方接口不会消除数据用途限制。Google Maps 附加条款列有复制、再分发和大量下载等限制;Places API 也有自身的内容规则。不能因为单次请求能返回数据,就认定可以长期囤积、批量转售或任意再展示。若计划用途不在许可范围内,应调整方案,而不是用技术替代许可判断。
需要官方商家管理路径的具体参数时,应直接查 reviews.list:它按账户和地点定位,分别提供评论列表、整体平均评分、评论总数与下一页标记。这个结构说明为什么“这页取得几条”和“地点总共有几条”要分开;不能把它的字段或分页规则移植成 EveryInfra 的保证。
第一步:确认地点身份,而不只确认店名
门店名称不是稳定的唯一标识。同一品牌可能有多家分店,同一个商圈也可能有相似店名;门店改名后,旧记录不一定应当变成新门店。先在业务目标清单中确认地点引用、地址与业务归属,再发起评论请求。
建议保留你用于请求的完整地点 URL,以及可靠取得的 place_id。只有经过确认的身份才用于合并;不要只凭经纬度接近、店名相同或字符串相似就拼接两家店的评论。无法确定的目标进入待核对清单,避免后面的分析把错店数据当作投诉趋势。
当前读取实现会使用 URL 中可识别的地点信息,但不是每一种地图地址都同样适合定位。只有店名和地图视角坐标的链接,不应被你自行当作已经确认的地点 ID。优先保留经过核对的完整来源链接,不拼接一个看起来像地图页的占位地址。
这里还有一条接口边界:实现内部能够识别某些地点信息,不表示公开请求允许你增加任意 placeId 或 place_id 参数。当前 reviews 的必填项是 url,客户端应按公开契约传参。
第二步:核对语言、排序与时间条件
curl -fsS --max-time 30 \
'https://api.everyinfra.com/api/v1/social/catalog?platform=google_maps_reviews' \
| jq -e '.capabilities[] | select(.action == "reviews") | {
platform, action, required_params, optional_params, param_meanings,
mode, returns_list, default_limit, max_limit, response_fields
}'此次目录要求 url,采用同步列表返回,默认条数为 20,上限为 100,可选项包括 lang、language、since 与 sort。准确平台标识是 google_maps_reviews;不要根据文章标题缩写成另一个 platform。
newest 与 relevance 对应不同的排序意图:前者用于关注较新内容,后者用于相关性样本。它们不是可以任意混用的两页数据。比较门店时应保持相同选择条件,并保存实际取得的评论 ID;更换排序以后,样本成员可能已经发生变化。
语言字段也不是作者国籍或居住地筛选。lang 的允许值需要看该接口枚举,例如当前有 en、zhcn 和 zhtw,不应仅凭泛化描述就随意填入另一个语言代码。lang 与 language 不要盲目同时填写;当前映射中 lang 会覆盖语言设置,初版选择一个明确支持的选项即可。
since 表达评论发布时间下界,目录建议使用 YYYY-MM-DD。它不是商家开业日期,不是评论的所有更新时间,更不是数据已经处理到哪里的可靠游标。需要时将它作为筛选条件,并单独验证边界日期与实际结果。
第三步:先检查一个获准地点的少量结果
下面假设你在服务端配置了 EVERYINFRA_API_KEY 和经过确认的 EVERYINFRA_MAPS_PLACE_URL。示例只提交一次,limit 必须放在 params 内;放到请求顶层不能作为已生效的条数控制。
: "${EVERYINFRA_API_KEY:?请先配置 API Key}"
: "${EVERYINFRA_MAPS_PLACE_URL:?请先配置获准的完整地点链接}"
jq -cn --arg url "${EVERYINFRA_MAPS_PLACE_URL}" '{
platform: "google_maps_reviews",
action: "reviews",
params: {url: $url, sort: "newest", limit: 3}
}' | curl --silent --show-error --include --max-time 180 \
'https://api.everyinfra.com/api/v1/social' \
-H "Authorization: Bearer ${EVERYINFRA_API_KEY}" \
-H 'Content-Type: application/json' --data-binary @-limit: 3 是便于检查的小样本设置,不保证一定返回三条,也不代表地点只有三条评价。180 秒为示例客户端等待预算,不是服务承诺。先检查 HTTP 状态与 error,再检查 results 是否为预期列表,以及记录能否对应目标地点。
空数组不能直接写成“这家店没有评价”。可能是本次没有取得记录,也可能是目标、筛选或可访问性需要核对。真实原因不明时应保留未知状态;若客户端超时,也不能自动重复 POST 并假设前一次没有执行或计费。
第四步:分开地点评分、单条评分与回复
当前记录字段包括 id、text、rating、rating_scale、posted_at、place_id、place_name、owner_response 和 owner_response_at 等。地点详情中的平均评分与一条评论的评分是不同对象;不能为了补齐空值,把门店平均分复制到每条评论。
curl -fsS --max-time 30 \
'https://api.everyinfra.com/api/v1/social/fields?platform=google_maps_reviews&action=reviews' \
| jq '{fields, undocumented}'评分与正文也要分开处理。一条只有评分、没有文字的记录,可能对评分样本有用,却不能直接送进文本主题分类。缺少 rating 时保留未知;跨平台比较时连同 rating_scale 一起判断,不能把不同量表的数字直接平均。
detailed_rating 如有返回,可承载某些分维度评分,但不能假设每个地点、每条评论都具备相同维度。is_local_guide 和历史评论数也不应直接变成“真实用户”认证或可信度分数。它们是平台提供的属性,不是对某条评价事实准确性的验证。
商家回复单独保存。没有 owner_response 只说明本次未取得这个值,不足以证明商家从未回复;后来取得一段回复,也不代表用户原评论是新发布的。将两者混算为新评论,会让监控产生无意义的新增提醒。
第五步:为身份、观察和变化分别建记录
- 地点身份:经过确认的 place_id、来源引用和业务归属。重复门店先核对身份,不按名称粗暴合并。
- 评论身份:地点 ID 与评论 ID 组合;没有可靠 ID 的记录单独处理,不用评论者昵称充当唯一键。
- 观察记录:请求 id、实际采集时间、排序与语言/日期条件、取得的字段及缺失情况。
- 变化记录:同一评论的内容、评分或商家回复出现了哪些可观察差别,不把未知字段解释成删除。
只有在许可允许的范围与期限内,才保存下面这种比较所需的字段;若不能保存正文,可调整为获准的最小状态记录,不应把“便于审计”当成无限保留理由。下面使用虚构地点、评论和文本,演示如何区分值变化、首次取得字段与本次未取得。
function compareReview(before, after) {
const identity = row => {
if (typeof row.place_id !== "string" || !row.place_id
|| typeof row.id !== "string" || !row.id) {
throw new Error("place and review ids required");
}
return JSON.stringify([row.place_id, row.id]);
};
if (identity(before) !== identity(after)) throw new Error("different review");
const t0 = Date.parse(before.observed_at);
const t1 = Date.parse(after.observed_at);
if (!Number.isFinite(t0) || !Number.isFinite(t1) || t1 <= t0) {
throw new Error("observations must have increasing times");
}
const changed = [], firstObserved = [], notObserved = [];
for (const field of ["text", "rating", "owner_response", "owner_response_at"]) {
if (!Object.hasOwn(after, field) || after[field] == null) {
notObserved.push(field);
} else if (!Object.hasOwn(before, field) || before[field] == null) {
firstObserved.push(field);
} else if (before[field] !== after[field]) {
changed.push(field);
}
}
return {changed, firstObserved, notObserved};
}
console.log(compareReview(
{place_id: "synthetic-place", id: "synthetic-review",
text: "合成评论", rating: 3, observed_at: "2026-09-01T00:00:00Z"},
{place_id: "synthetic-place", id: "synthetic-review",
text: "合成评论", rating: 3, owner_response: "合成回复",
observed_at: "2026-09-02T00:00:00Z"}
));这段例子会把 owner_response 记为 firstObserved,而不是新增评论;回复时间未取得,则留下 notObserved。即使 changed 中出现 text,也只说明两次取得的文本不同,仍可能涉及翻译或表示变化,不能在缺少依据时断言是用户编辑了原文。
时间顺序检查只能避免旧的观察记录覆盖较新的观察,并不能证明服务端数据的实时性。你记录的 observed_at 是自己看到响应的时刻,不是所有平台字段的更新时间。
跨时区保存观察记录时,可用 RFC 3339 的带偏移时间形式,原始来源值仍单独保留。格式统一只便于比较时刻,不会让观察时间变成评论更新时间,也不提高这次读取的覆盖范围。
第六步:增量观察要解释覆盖,而不是只推进日期
做持续监控时,可以在许可允许的前提下重复观察一组固定地点,并为日期筛选保留适当的重叠区间,再按评论身份排重。区间多长应根据实际出现的迟到记录与任务频率确定,没有一个可以从小样本直接给出的通用天数。
但重叠窗口不能解决所有遗漏:单次结果有数量限制,旧评论可能后来新增商家回复,排序也会改变返回样本。尤其不能假设 since 会捕获所有被更新的旧评论;它是发布时间筛选,不是完整变更流。
为每次运行分别记录计划检查地点数、有可用记录的地点数、失败或未知地点数,以及各地点实际返回条数。只有已知采集范围、排序和截断情况,才有资格谈覆盖;不能用某次返回条数除以一个来源不明的总量,制造看似精确的覆盖率。
门店比较同样需要克制。少量近期评论的平均分,不等于地图展示的地点总评分;评分下降也不能单靠一个变化的样本集合确认。想比较问题类型,可以先定义分类标准,再展示各门店的样本时间与缺失情况。
第七步:分析、内部处理与公开展示不是同一许可
语言选择可能影响你看到的内容表示,并不保证返回的每条评论都是该语言的原文。翻译文本与原文不能算作两个独立用户意见;分析结果应记录使用的语言条件,对跨语言变化保留人工核对。
如果使用 Places API 做公开展示,应按其现行政策处理内容归属、作者信息和来源入口等要求,而不是只把文字复制到自己的页面。若数据来自另一条路径,也要先确定适用的许可,不能自动套用或免除 Places 的要求。本文的内存示例不构成保存或再发布授权。
对于内部分析,优先只处理任务必要字段,限制访问者与保留期限;模型分类和紧急程度应标为派生判断。系统可以整理人审队列,但不要因为一条低评分评论就自动回复、公开联系作者或改变商家运营状态。
一个地点核对清楚,再扩成工作流
完成第一轮接入时,你应能解释:为什么有权处理这组数据、目标是哪家店、选了什么排序和语言、哪些字段有依据、回复变化如何识别,以及本轮缺失了什么。还应能把问题定位到请求 id,并核对实际计费,而不是只留一个匿名评论文件。
如果这些问题都有答案,再扩大地点清单或增加定时运行。一个可持续的评论工作流,不仅要读到数据,还要让地点身份、用途、样本范围与变化记录始终保持清楚。