Foursquare 持有数百万个地点的公开位置数据:餐厅、咖啡馆、公园、酒吧和博物馆,每一个都带有一个名称、一个类别、一个地址和一个公开评分。对于本地商业研究、市场分析,或者构建一个推荐功能,那些公开的场所数据是真正有用的。问题在于 Foursquare 用 JavaScript 渲染它的页面,所以一个普通的 HTTP 请求返回的是一个近乎空白的外壳,而不是你在浏览器里能看到的场所列表。
本指南将向你展示如何用 Python 提取公开的 Foursquare 场所数据,借助 Crawling API,它在一次调用中渲染页面并把请求通过一个受信任的 IP 来路由。这里的一切都限定在公开的场所和地点数据上:名称、类别、地址和公开评分。它不涉及任何登录之后的内容,也不碰关于单个用户或其签到的个人数据。对于生产用途,官方的 Foursquare Places API 才是正确的工具,结尾附近的合法性部分会解释为什么。
你将构建什么
一个小巧的 Python 爬虫,它接收一个公开的 Foursquare 搜索 URL 或一个单独的场所 URL,通过 Crawling API 抓取完全渲染好的页面,并解析少数几个公开的场所字段:
- 场所名称 列表上显示的商家或地点名称。
- 类别 场所的类型,比如 Thai、Bakery 或 Bar。
- 地址 场所的公开街道地址。
- 评分 场所显示的聚合公开评分。
- 链接 通往该场所公开详情页的永久链接。
这个脚本处理一个搜索页里的多条结果,走访每一条列表,并把采集到的记录导出为 JSON 和 CSV,这样数据就准备好用于本地商业研究了。注意有意缺席的东西:没有单个用户资料,没有签到历史,没有任何与具名个人相关联的个人数据。那些在这里是有意排除在范围之外的。
为什么对 Foursquare 发起普通请求会失败
用一个裸的 HTTP 客户端去请求一个 Foursquare 搜索页,你得到的是一个技术上成功、实际上无用的响应。场所内容是动态加载的:真正的列表只有在页面的脚本在浏览器中运行并从内部端点获取数据之后才会出现。一个原始请求在那一切发生之前就捕获了页面,所以没有任何东西可解析。
在渲染之上,Foursquare 还盯着自动化流量。数据中心 IP 段和重复的请求模式会在有意思的内容加载之前就被挑战或限速。所以一个能用的爬虫在同一次请求里需要两样东西:一个渲染页面的真实浏览器,以及一个被平台读作普通访客的 IP 地址。你可以用一个无头浏览器和一池住宅代理来构建那套方案,但保持那套栈健康才是工作的大头。Crawling API 把这两者折叠进了一次调用。你把一个 URL 发给它,它在一个受信任的住宅 IP 背后渲染页面,并返回你能解析的完成的 HTML。要了解更深的背景,请看我们关于如何爬取 JavaScript 网站的指南。
Crawlbase 提供两种令牌类型。普通令牌抓取静态 HTML;JavaScript (JS) 令牌先在一个真实浏览器中渲染页面。Foursquare 是客户端渲染的,所以这里你需要 JS 令牌。普通令牌返回的和一个普通抓取得到的是同一个外壳,里面没有任何有用的东西可供解析。
前置条件
先要准备好几样东西。它们都不会花很久。
基础的 Python。你应当能够熟练地运行一个脚本并用 pip 安装包。如果你是解析 HTML 的新手,我们关于如何在 Python 中使用 BeautifulSoup 的入门讲解涵盖了提取那一侧。
Python 3.8 或更高版本。用 python --version 确认。如果你还没有,请从 python.org 安装它。
一个 Crawlbase 账户和 JS 令牌。注册,打开你的仪表盘,并复制你的 JavaScript (JS) 令牌。Crawlbase 最多给你 20,000 次免费请求来起步,而且你只为成功的请求付费。请把令牌当作密码对待:它会对你的请求进行身份认证,所以别把它放进版本控制。
搭建项目
创建一个隔离的虚拟环境,然后安装这个爬虫需要的两个库。
python --version python -m venv foursquare_env source foursquare_env/bin/activate pip install crawlbase beautifulsoup4
在 Windows 上,用 foursquare_env\Scripts\activate 来激活,而不是那行 source。两个依赖项承担了工作:crawlbase 是用于 Crawling API 的官方客户端,而 beautifulsoup4 解析返回的 HTML,让你能按选择器抽出各个字段。
第 1 步:抓取渲染好的搜索页
先从拿到完成的页面开始。导入 CrawlingAPI,用你的 JS 令牌初始化它,并请求一个公开的搜索 URL。因为 Foursquare 异步加载列表,请传入 ajax_wait 和 page_wait 选项,这样 API 会一直等到内容渲染完成。在解析之前检查状态,让失败保持响亮而不是悄无声息。
from crawlbase import CrawlingAPI crawling_api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) def make_crawlbase_request(url): options = { "ajax_wait": "true", "page_wait": "5000", } response = crawling_api.get(url, options) if response["headers"]["cb_status"] == "200": return response["body"].decode("utf-8") print(f"Failed to fetch the page. Crawlbase status: {response['headers']['cb_status']}") return None if __name__ == "__main__": url = "https://foursquare.com/explore?near=New%20York&q=Food" html = make_crawlbase_request(url) print(html[:500] if html else "No HTML returned")
这两个等待选项对一个客户端渲染的目标很重要。ajax_wait 告诉 API 等待异步内容完成加载,而 page_wait 在加载之后再保持固定的毫秒数,这样迟渲染的列表会在页面被捕获之前出现。五秒是一个合理的起点;如果列表返回为空就把它调高。状态检查从响应头里读取 cb_status(legacy pc_status),这是爬取本身的 Crawlbase 状态。运行脚本,你应该会看到真实的场所标记,这在你写下一个选择器之前就确认了渲染能工作。
Foursquare 在一次调用里需要一个在受信任 IP 背后渲染好的页面。Crawling API 接收一个 JS 令牌,在一个真实浏览器里运行页面,让那些 ajax_wait 的列表真正加载出来,在服务器端轮换住宅 IP,并把完成的 HTML 交给你,这样你就省去了自己运行一支无头浏览器队伍和一池代理。先在免费套餐上把它对准一个公开的搜索页。
第 2 步:检查标记并解析列表
在写选择器之前,在你的浏览器里打开一个 Foursquare 搜索结果页,右键点击一条列表,并选择 Inspect。你要找的是包住每个场所、并保存着你想要字段的那些元素。在搜索结果页上,每个地点都坐落在一个列表项里,而公开字段映射到这些选择器:
-
场所名称 位于一个
div.venueName里面的一个<a>标签中。 -
地址 位于一个
div.venueAddress中。 -
类别 位于一个
span.categoryName中。 -
链接 是同一个
div.venueName a锚点的href。
手里有了渲染好的 HTML,就把它加载进 BeautifulSoup 并走访每一条列表。每一行结果匹配 ul.recommendationList > li.singleRecommendation。用一个存在性检查守护每个字段,能让解析器在一个场所缺少其中之一时不至于崩溃。
from bs4 import BeautifulSoup def scrape_foursquare_listings(html): soup = BeautifulSoup(html, "html.parser") venues = [] listings = soup.select("ul.recommendationList > li.singleRecommendation") for listing in listings: name_el = listing.select_one("div.venueName a") address_el = listing.select_one("div.venueAddress") category_el = listing.select_one("span.categoryName") rating_el = listing.select_one("span.venueScore") href = name_el["href"] if name_el and name_el.has_attr("href") else "" venues.append({ "name": name_el.text.strip() if name_el else "", "category": category_el.text.strip() if category_el else "", "address": address_el.text.strip() if address_el else "", "rating": rating_el.text.strip() if rating_el else "", "link": f"https://foursquare.com{href}" if href else "", }) return venues
这个函数返回一个字典列表,每个场所一个,带着五个公开字段。链接是通过把相对的 href 拼接到 https://foursquare.com 源上构建的,这样每条记录都携带一个可用的永久链接。评分从 span.venueScore 读取;如果 Foursquare 在你检查的页面上重命名了那个类,就换成任何包住可见分数的类。把评分当作一个公开的聚合值,而不是关于任何单个评论者的信号。
Foursquare 会毫无预告地更改它的标记和类名。当一个字段返回为空时,在你浏览器的开发者工具里重新检查实时页面并更新选择器。定期维护对任何生产爬虫来说都是正常的,而不是有什么坏掉了的信号。上面那种带守护的提取意味着一个被重命名的类产出的是一个空字符串而不是一次崩溃。
第 3 步:处理多页结果
Foursquare 搜索结果使用基于按钮的分页:一个 "See more results" 按钮就地加载下一批场所,而不是导航到一个新 URL。Crawling API 能用 css_click_selector 选项替你点击那个按钮,这样你收到的渲染好的 HTML 已经包含了展开后的列表。把选择器对准负责加载更多的那个按钮。
def make_request_with_pagination(url): options = { "ajax_wait": "true", "page_wait": "5000", "css_click_selector": "li.moreResults > button", } response = crawling_api.get(url, options) if response["headers"]["cb_status"] == "200": return response["body"].decode("utf-8") print(f"Failed to fetch the page. Crawlbase status: {response['headers']['cb_status']}") return None
css_click_selector 值瞄准 li.moreResults 里面的按钮。如果按钮类在你检查的页面上有所不同,就更新选择器去匹配它。把量保持适中:公开数据研究不需要在一次运行里加载整座城市的列表。采样你需要的,然后停下。
第 4 步:组装完整的爬虫并导出
现在把抓取、解析和导出连进一个可运行的脚本。这个脚本带着分页抓取一个搜索页,解析每一条列表,并把记录写成 JSON 和 CSV 两种。JSON 让结构完整以便进一步处理;CSV 直接落进电子表格,方便快速做本地商业研究。
import json import csv from crawlbase import CrawlingAPI from bs4 import BeautifulSoup crawling_api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) def make_request_with_pagination(url): options = { "ajax_wait": "true", "page_wait": "5000", "css_click_selector": "li.moreResults > button", } response = crawling_api.get(url, options) if response["headers"]["cb_status"] == "200": return response["body"].decode("utf-8") print(f"Failed to fetch the page. Crawlbase status: {response['headers']['cb_status']}") return None def scrape_foursquare_listings(html): soup = BeautifulSoup(html, "html.parser") venues = [] listings = soup.select("ul.recommendationList > li.singleRecommendation") for listing in listings: name_el = listing.select_one("div.venueName a") address_el = listing.select_one("div.venueAddress") category_el = listing.select_one("span.categoryName") rating_el = listing.select_one("span.venueScore") href = name_el["href"] if name_el and name_el.has_attr("href") else "" venues.append({ "name": name_el.text.strip() if name_el else "", "category": category_el.text.strip() if category_el else "", "address": address_el.text.strip() if address_el else "", "rating": rating_el.text.strip() if rating_el else "", "link": f"https://foursquare.com{href}" if href else "", }) return venues def save_to_json(data, filename="foursquare_data.json"): with open(filename, "w", encoding="utf-8") as f: json.dump(data, f, indent=4, ensure_ascii=False) print(f"Saved {len(data)} venues to {filename}") def save_to_csv(data, filename="foursquare_data.csv"): if not data: return fields = ["name", "category", "address", "rating", "link"] with open(filename, "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=fields) writer.writeheader() writer.writerows(data) print(f"Saved {len(data)} venues to {filename}") if __name__ == "__main__": url = "https://foursquare.com/explore?near=New%20York&q=Food" html = make_request_with_pagination(url) if html: venues = scrape_foursquare_listings(html) save_to_json(venues) save_to_csv(venues)
这就是一个文件里的整条管道:带分页抓取,解析列表,并导出到两种格式。CSV 写入器把列顺序钉死在五个公开字段上,所以输出在不同运行之间保持稳定。把 url 换成任何一个公开的 Foursquare 搜索,就能把爬虫重新瞄准一座不同的城市或一个不同的类别。
输出长什么样
运行完整的脚本,你会得到一份干净的公开场所记录列表。这里是 foursquare_data.json 所保存内容的一个精简 JSON 样例。
[ { "name": "Thai Diner", "category": "Thai", "address": "186 Mott St (at Kenmare), New York", "rating": "9.5", "link": "https://foursquare.com/v/thai-diner/5e46e2ec5791a10008c55728" }, { "name": "Mah-Ze-Dahr Bakery", "category": "Bakery", "address": "28 Greenwich Ave (Charles Street), New York", "rating": "9.1", "link": "https://foursquare.com/v/mahzedahr-bakery/568c0ce238fafac5f5ffe631" } ]
CSV 版本把同样的字段以每个场所一行加一行表头来承载,它在任何电子表格工具里都能直接打开。从这里你可以按类别筛选、按街区分组,或者把地址与另一个数据集做连接以用于本地商业研究。如果价格信号是你分析的一部分,我们关于用网络爬取做价格情报的指南讲解了聚合的公开数据如何喂给那类工作。
扩展到场所详情页
搜索爬虫给你每个场所一个列表项和一个链接。要丰富每条记录,就把 link 字段重新喂过同一个抓取函数,并解析场所自己的页面,Foursquare 在那里暴露了更多结构化的公开详情。在一个场所页上,公开字段映射到这些选择器:名称坐落在一个 h1.venueName 里,地址在一个 div.venueAddress 里,评分在一个 span[itemprop="ratingValue"] 里,公开评论数在一个 div.numRatings 里。复用第 2 步里那种带守护的提取模式,给详情页抓取之间的请求安排节奏,并把这次运行限定在你实际需要的场所上,而不是爬取一个搜索返回的一切。
Foursquare 是一个有用的入口,但场所和本地商业数据存在于许多表面上。对于相邻的技术,请看我们关于如何从 Google Maps 抓取数据和爬取本地商家列表的指南,两者都把同样的渲染加受信任 IP 的方法应用到其他地图和目录来源上。
保持不被封禁
即便渲染由 Crawling API 处理了,Foursquare 仍盯着形状像爬虫的流量。几个习惯能让一次运行保持健康,它们适用于任何设防的目标。
- 给你的请求安排节奏。在一个紧密循环里猛敲页面是被限流最快的方式。在抓取之间加入真实的延迟,并抵制住激进并行化的冲动。
- 依靠轮换。一池住宅 IP 把请求分散到许多真实用户的地址上,这样没有任何单个 IP 会触发速率限制。Crawling API 替你处理这件事;如果你自建一套栈,这就是要做对的部分。
- 读状态码。一次开始返回挑战或错误的运行是在告诉你,当前的速率或 IP 层级已经不够了。退一步,而不是推得更猛。
- 让量保持低、目标保持多样。公开数据研究不需要爬取一整座城市。采样你需要的,然后停下。
要了解更宽泛的攻略,请看我们关于如何爬取网站而不被封禁的指南。
爬取 Foursquare 合法吗?
这是你在写生产代码之前要读的部分。爬取公开的场所数据处在一个灰色地带,它在很大程度上取决于你怎么做以及你采集什么。Foursquare 的服务条款限制自动化访问,所以把它们和网站的 robots.txt 一起读,并把两者都当作你采集什么、以及采集得多快的边界。上面的代码让技术部分能跑起来;它改变不了条款所允许的内容。
坚守公开的、非个人的场所数据。场所名称、类别、公开地址和聚合评分描述的是地点,而不是人,那正是这类研究的安全车道。你不该碰的:任何登录之后的内容、单个用户资料、签到历史,或任何关于可识别用户的个人数据。一个场所的聚合评分是一个关于地点的公开数字;那些留下评论或签到的个人不归你去收割。一旦涉及个人数据,像 GDPR 和 CCPA 这样的隐私法就适用了,这意味着你需要一个合法依据来处理它,而且你必须尊重删除请求。摆脱那份负担的最简单方式,是一开始就不采集个人数据,而这正是本指南所做的。
对于任何真实的、持续的或商业的用途,正确的工具是官方的 Foursquare Places API。它是被认可的途径,以一份清晰的使用许可给你结构化的场所和类别数据,并让你留在 Foursquare 的条款之内。本文是一篇范围狭窄地限定在公开场所数据上的技术演练,而不是对大规模采集或任何对个人用户数据处理的背书。如果你的项目需要的不止公开场所字段的一个样本,那么 Places API 或一份正式的数据协议才是正确的路径,而不是一个更聪明的爬虫。
核心要点
- Foursquare 是客户端渲染的。一个普通请求返回一个空壳,所以你必须在解析之前渲染页面,而 Crawling API 的 JS 令牌处理这件事。
-
照搬真实的选择器。列表位于
li.singleRecommendation里,名称、类别、地址和链接分别在venueName、categoryName和venueAddress里。 -
在服务器端处理分页。
css_click_selector选项点击 "See more results" 按钮,这样渲染好的 HTML 已经保存了展开后的列表。 - 为研究而导出。把场所记录写成用于结构的 JSON 和用于电子表格的 CSV,然后为本地商业分析对它们做筛选或连接。
- 只取公开场所,优先用官方 API。采集地点数据,绝不采集个人用户数据或签到,对于任何真实或商业的用途使用 Foursquare Places API。
常见问题
为什么一个普通请求从 Foursquare 返回不到数据?
因为 Foursquare 用 JavaScript 在客户端加载它的场所列表。初始的 HTML 是一个外壳,它只有在页面的脚本在浏览器中运行之后才会填上,所以一个原始的 HTTP 请求返回一个近乎空白的正文。要拿到真实的场所数据,你必须先渲染页面,而这正是 Crawling API 的 JS 令牌替你处理的。
对于 Foursquare 我需要普通令牌还是 JS 令牌?
JS 令牌。普通令牌抓取静态 HTML,在 Foursquare 上那和一个普通请求返回的是同一个空壳。JS 令牌在把 HTML 交回之前先在一个真实浏览器中渲染页面,所以当 BeautifulSoup 解析它们时场所字段都在场。
哪些 Foursquare 数据可以安全地爬取?
公开的、非个人的场所数据:场所名称、类别、公开地址、聚合评分,以及通往场所页的公开链接。任何登录之后的内容、单个用户资料和签到历史都不可触碰。那些是个人数据,采集它们与 Foursquare 的条款相抵触,而且在许多地方还与隐私法相抵触。
爬取 Foursquare 时我该如何处理分页?
Foursquare 搜索使用一个 "See more results" 按钮,而不是分开的 URL。把 css_click_selector 选项传给 Crawling API 并对准那个按钮(例如 li.moreResults > button),API 就会在渲染期间点击它,这样你收到的 HTML 已经包含了展开后的场所列表。
我该用官方的 Foursquare Places API 还是爬取这个网站?
对于任何真实的、持续的或商业的用途,使用官方的 Foursquare Places API。它是被认可的途径,以一份清晰的许可给出结构化的场所和类别数据,并让你留在 Foursquare 的条款之内。用这里的方法爬取一小撮公开场所字段的样本,适合那种没有可用 API 访问的轻量研究,只要你尊重条款、robots.txt 和速率限制。
爬取 Foursquare 时我该如何避免被封禁?
让你的每 IP 请求速率保持低,在请求之间加入真实的延迟,让你的目标多样化而不是爬取一整座城市,并通过轮换的住宅 IP 来路由,这样没有任何单个地址会触发速率限制。Crawling API 替你管理轮换和一池受信任的 IP。盯着状态码,一旦开始看到挑战就退一步。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
