谷歌地图字段陷阱:latitude/longitude 与 title 别名
谷歌地图字段陷阱:latitude/longitude 与 title 别名maps/search 这个端点有个特点:入参用lat/lng,返回里经纬度叫latitude/longitude;结果里必填字段有一个title,但它只是name的别名;很多人习惯性找的review_count(评价数)字段,文档里根本没有。这篇把 maps/search 与 maps/detail 的字段口径完整捋一遍,每个坑都给出写代码时该怎么兜底。字段口径以 SerpBase 官方文档的 maps 端点字段表 为准;两个地图端点每次成功请求都是 2 credits,失败自动退款。入参:lat/lng 成对,zoom 有默认值POST /google/maps/search的入参:参数必填说明q是搜索词hl否默认engl否默认uspage否默认1,从 1 开始lat否地图中心纬度,必须与 lng 一起发lng否地图中心经度,必须与 lat 一起发zoom否地图缩放,取值 1-21;传坐标时默认 14maps/detail的入参更少:feature_id必填(格式0x...:0x...,从 maps/search 结果里拿),hl/gl可选。两个最常见的入参错误:只传 lat 不传 lng(或反过来)。文档明确写了Must be sent together——经纬度是一对,单个坐标是无效请求。传了坐标但没传 zoom,以为拿到的是精确范围。实际会按默认 zoom 14 处理,大约是区县尺度的视野。想圈更紧(比如单个商圈)要显式传 zoom 15/16;想扫整个城区用 zoom 13 配更大的采样步长。zoom 和你的采样步长是配套设计的,别只调一个。返回:三个必填字段,一个大可选列表places数组里每个 MapsPlace 对象,必填的只有三个:name— 商户名title—name的别名(alias for name),值和 name 相同feature_id— 商户标识,格式0x...:0x...,也是调 maps/detail 的钥匙其余全是可选字段,而且可选列表很长:position(仅 maps/search 有,rank 的别名)、place_id、data_id、cid、kgmid、google_maps_url、url、rating、types、category、address、address_components、plus_code、phone、phone_international、phone_uri、website、website_domain、latitude、longitude、image、thumbnail、photos、hours、open_status、attributes、related_places、short_description、description、snippet、timezone、region、country_code、language。文档对可选字段的注释基本都是When xxx is present(有该信息时才有)。翻译成工程语言就是:除了 name/title/feature_id,其它字段一个都不能假设存在。四个真正的陷阱陷阱一:入参lat/lng,返回latitude/longitude。这是最容易写错的地方——请求体里写lat,解析时按lat取返回,拿到一串空值还以为地图数据没坐标。入参和返回是两套命名,取返回一律用latitude/longitude。陷阱二:title是name的别名,不是门店全称。有些商户在 Google 地图上显示的标题带后缀(比如XX咖啡(中关村店)),你会本能地以为title是完整显示名、name是简称。不是——文档写得很清楚,title 就是 name 的别名,两者同值。选一个字段用(建议name),另一个当备份,别把它们当两个信息源做对比,那是自己吓自己。陷阱三:没有review_count。评分有(rating),评价数没有。很多竞对密度/口碑分析脚本习惯同时取 rating 和 review_count,在这里会直接 KeyError,或者更糟——有人会去related_places里翻,把别处的数字当评价数。如果分析需要评价量,文档化载荷给不了,要么换数据源,要么把分析口径改成只看 rating 分布。陷阱四:position仅 maps/search 有,detail 里没有。做搜索结果位次 → 详情补全的两步流水线时,detail 结果里找 position 会找不到,位次信息只在搜索那一步的响应里,需要在流水线里自己带着走。防御性解析模板importrequests API_SEARCHhttps://api.serpbase.dev/google/maps/searchAPI_DETAILhttps://api.serpbase.dev/google/maps/detailKEY你的 API KeyHEADERS{X-API-Key:KEY,Content-Type:application/json}defmaps_search(q:str,lat:floatNone,lng:floatNone,zoom:intNone):payload{q:q}# lat/lng 必须成对;zoom 只在传坐标时生效,不传则默认 14if(latisNone)!(lngisNone):raiseValueError(lat 和 lng 必须成对传入)iflatisnotNone:payload[lat],payload[lng]lat,lng payload[zoom]zoomifzoomisnotNoneelse14# 显式写死默认值,别依赖隐式行为resprequests.post(API_SEARCH,headersHEADERS,jsonpayload,timeout30)dataresp.json()ifdata.get(status)!0:raiseRuntimeError(f{data.get(status)}:{data.get(error)})returndata.get(places,[])defmaps_detail(feature_id:str):入参只能是 feature_id(0x...:0x... 格式);place_id/data_id 不能当入参。resprequests.post(API_DETAIL,headersHEADERS,json{feature_id:feature_id},timeout30)dataresp.json()ifdata.get(status)!0:raiseRuntimeError(f{data.get(status)}:{data.get(error)})returndata.get(place,data)# 详情对象的键名以实际返回为准,拿不到就整体兜底defnormalize(p:dict)-dict:把一条 place 拍平成稳定结构:必填直取,可选全兜底。return{name:p[name],# 必填,直取feature_id:p[feature_id],# 必填,直取title:p.get(title,),# name 的别名,同值rating:p.get(rating,),review_count:,# 没有这个字段,显式留空,别去别处找address:p.get(address,),phone:p.get(phone,),website:p.get(website,),latitude:p.get(latitude,),# 注意:返回是 latitude 不是 latlongitude:p.get(longitude,),hours:p.get(hours,),open_status:p.get(open_status,),}forplaceinmaps_search(咖啡店,lat39.9042,lng116.4074,zoom14):rownormalize(place)print(row[name],row[rating],row[latitude],row[longitude])几个写法上的说明:payload[zoom] zoom if zoom is not None else 14是把文档默认值显式化——你自己清楚此刻用的是 14,而不是赌服务端默认;review_count那行显式留空,是给 downstream 的一个明确信号这个数据不存在,比下游到处.get(review_count)然后默默当 0 处理要好;maps_detail的入参只能用feature_id,不能用place_id或data_id代替,这两个是结果里的标识字段,不是 detail 的入参。FAQmaps/detail 用 place_id 调用行不行?不行。detail 的入参只有feature_id(必填),文档没给 place_id/data_id 的入口。搜索结果里三个标识字段都在,但能当钥匙的只有 feature_id。格式对但查不到的 feature_id 会怎样?返回 1004(未找到)。所以详情补全脚本要把 1004 当正常分支处理——商户可能已关闭或被合并,跳过并记录,不要当故障重试。地图端点为什么比搜索贵?maps/search 和 maps/detail 都是 2 credits(搜索/新闻/视频是 1)。做网格化采集时先把格点数 × 查询数算出来再跑,超预算了先停,别等 1020(余额不足)报错。title 和 name 会不会哪天不一样?按文档定义 title 就是 name 的别名,当前不会。但代码里两个字段都存一份成本极低,万一文档更新,你手里有原始数据可以重新处理——这也是一种防御。把 normalize 函数抄进你的项目,比到处p[lat]然后对着一堆空值debug 要省事得多。