Quora 是网络上较大的公开问答平台之一,其中托管的公开问题和回答是内容与话题研究的有用信号。真实问题的措辞、吸引的回答数量,以及哪些回答获得最多赞同票,综合呈现出人们真正关心的问题以及哪些表达方式引发共鸣。这使得公开的 Quora 帖子成为 SEO 规划、内容选题和受众研究的实用素材。

本指南展示如何用 JavaScript 和 Node.js 配合 cheerio 来抓取 Quora。您将构建一个小型、可运行的爬虫,通过开启渲染的 Crawling API 获取公开的 Quora 问题页面,解析问题文本、回答正文、回答数量和赞同数,然后将结果导出为 JSON 和 CSV。整个演练的范围仅限于公开问答内容。我们将作者姓名视为个人数据并进行聚合而非构建画像,末尾的法律部分并非套话,请在将此脚本用于任何实际规模之前仔细阅读。

您将构建什么

一个 Node.js 脚本,接收公开的 Quora 问题 URL,通过 Crawling API 获取已渲染的 HTML,并为该问题及其可见回答提取结构化记录。我们提取以下字段,与原版 Quora 爬虫输出保持一致:

  • 问题文本 实际问题内容,例如"How do I start playing video games?"。
  • 问题链接 该问题页面的规范 URL。
  • 回答数量 问题显示的回答总数,以及本次抓取页面中实际存在的回答数。
  • 回答文本 每条可见回答的正文,用于话题聚合分析而非再发布。
  • 赞同票数 每条回答显示的赞同总数,是您的主要热度信号。
  • 回答位置 回答出现的顺序,便于按排名加权。

作者姓名出现在标记中,旧版爬虫也曾抓取过。我们刻意不从中构建作者的个人画像。隐私部分解释了如何以聚合方式处理姓名,以及这样做的原因。

为什么普通请求在 Quora 上会失败

如果您用裸 HTTP 客户端请求 Quora 问题 URL,得到的是一个空壳而非完整帖子。Quora 在浏览器中用 JavaScript 渲染问题、回答和回答数量,因此初始 HTML 在页面脚本运行之前几乎是空的。此外,Quora 还会标记自动化流量:来自数据中心的 IP 以及不像真实浏览器的请求模式,会在到达回答之前被重定向到登录页或内容墙。

因此,一个可运行的 Quora 爬虫需要在单次请求中同时具备两点:能真正渲染帖子的浏览器,以及平台识别为真实访客的 IP。您可以自行组合无头浏览器和轮换住宅代理池,但拼凑并维护这些组件本身就是主要工作量。Crawling API 将两者合为一次调用:您发送 URL 并附带 JavaScript token,它在可信 IP 背后渲染页面,并返回已完成的 HTML 供您用 cheerio 解析。

为什么需要 JS token

Crawlbase 提供两种 token 类型。普通 token 获取静态 HTML;JavaScript(JS)token 会先在真实浏览器中渲染页面。Quora 在客户端加载问题正文和所有回答,因此 JS token 是获取完整页面的必要条件。普通 token 往往只返回没有任何回答可解析的空框架。

前提条件

在编写任何代码之前,您需要准备好以下几样东西。每项都不需要太长时间。

基本 JavaScript 和 Node.js 知识。您应该能够编写和运行 Node 脚本,用 npm 安装包,并使用 Promise 和 async 函数。如果选择器和 DOM 对您来说比较陌生,任何 JavaScript 入门资料都能覆盖本教程所需的基础知识。关于完整工作流程,请参阅如何用 Node.js 构建网络爬虫的配套指南。

Node.js 16 或更高版本。node --version 确认您的版本。如果没有,请从 Node.js 官网或通过 nvm 等版本管理器安装。

Crawlbase 账户和 JS token。注册后,打开控制台,从账户文档页面复制您的 JavaScript(JS)token。Crawlbase 提供 1,000 次免费请求入门,且只对成功请求计费。像对待密码一样对待 token:它用于验证您的请求,不要将其提交到版本控制系统。

设置项目

创建项目文件夹,初始化它,然后安装爬虫所需的两个库。

bash
node --version

mkdir quora-scraper && cd quora-scraper
npm init -y

npm install crawlbase cheerio

两个依赖完成工作:crawlbase 是 Crawling API 的官方 Node 客户端,cheerio 以 jQuery 风格的 API 解析返回的 HTML,让您可以通过 CSS 选择器提取各个字段。如果选择器对您来说比较陌生,关于如何爬取 JavaScript 网站的入门指南是渲染密集型目标的好配套读物。

第一步:获取已渲染的问题页面

首先获取已完成的页面。导入 CrawlingAPI 类,用您的 JS token 初始化它,并请求问题 URL。在解析之前检查状态码,可以让失败更明显而不是静默发生。

javascript
const { CrawlingAPI } = require('crawlbase');

const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_TOKEN' });

async function crawl(pageUrl) {
  const options = { ajax_wait: 'true', page_wait: 6000 };
  const response = await api.get(pageUrl, options);
  if (response.statusCode === 200) {
    return response.body;
  }
  console.error(`Request failed: ${response.statusCode}`);
  return null;
}

const quoraUrl = 'https://www.quora.com/How-do-I-start-playing-video-games';
crawl(quoraUrl).then((html) => {
  console.log(html ? html.slice(0, 500) : 'No HTML returned');
});

两个等待选项对这类客户端渲染目标至关重要。ajax_wait 告诉 API 等待异步内容加载完成,page_wait 在加载后保持固定的毫秒数,让延迟渲染的回答在页面被捕获之前出现。6 秒是合理的起点;如果回答列表返回不完整,可以适当增加。用 node scraper.js 运行脚本,您应该看到真实的问题标记,而不是简化的外壳。这确认了渲染在您编写任何选择器之前已正常工作。

Crawlbase Quora Scraper

Quora 需要在可信 IP 背后渲染完整帖子,一次调用完成,这正是您刚才看到 crawl 函数所做的事情。Crawling API 接收 JS token,在真实浏览器中运行页面,在服务器端轮换住宅 IP,并返回已完成的 HTML,让您无需自行运行无头浏览器集群和代理池。先在免费套餐中指向一个公开问题页面试试。

第二步:用 cheerio 解析问题和回答

拿到已渲染的 HTML 后,将其加载到 cheerio 并读取各字段。问题文本和链接位于页面顶部;每条回答则是下方重复出现的块状结构。Quora 将回答布局在 div.q-box 容器中,因此您选择回答块,再从中读取正文和赞同数。防御性地读取每个字段,确保一个缺失值不会让整个运行崩溃。

javascript
const cheerio = require('cheerio');

function parseQuestion(html, pageUrl) {
  const $ = cheerio.load(html);

  const questionText = $('div.puppeteer_test_question_title')
    .first()
    .text()
    .trim() || $('title').text().trim();

  const answers = [];
  $('div.q-box.qu-borderAll').each((i, el) => {
    const block = $(el);
    const answerText = block.find('.q-text').first().text().trim();
    if (!answerText) return;

    const upvoteRaw = block
      .find('.q-click-wrapper')
      .first()
      .text()
      .trim();

    answers.push({
      answerText,
      answerUpvoteCount: upvoteRaw || null,
      answerPosition: i + 1,
    });
  });

  return {
    question: {
      text: questionText,
      link: pageUrl,
      answerCountScraped: answers.length,
      answers,
    },
  };
}

这里的字段名与原版 Quora 爬虫输出保持一致:question.textquestion.linkanswerCountScrapedanswersanswerTextanswerUpvoteCountanswerPosition。我们从 .q-text 读取回答正文,从投票控件读取赞同总数,并按位置对每条回答建立索引,便于后续按热门程度加权。我们刻意没有将作者姓名或个人主页链接纳入记录;下一节解释了这一选择的原因。

选择器会漂移

Quora 的类名(q-boxq-textq-click-wrapper 以及 puppeteer_test_ 标记)是混淆过的,且会不经通知地更改。请将上述选择器视为起始模板,而非合同。当某个字段返回空值时,在浏览器开发者工具中重新检查实时页面并更新选择器。定期维护选择器是任何生产环境爬虫的正常工作,并非出了什么问题。

如果您想完全跳过选择器维护,Crawling API 也提供了现成的 quora-question 数据爬虫。在选项对象中传入 { scraper: 'quora-question' },API 将在 response.json.body 中返回解析后的 JSON,完全无需编写 cheerio。但手动编写 cheerio 解析器依然值得学习,因为它让您完全掌控保留哪些字段,这正是下方隐私指南所关注的核心。

第三步:组装完整脚本

现在将获取和解析连接成一个可运行的脚本。获取已渲染的 HTML,传给解析器,打印结构化记录。

javascript
const { CrawlingAPI } = require('crawlbase');
const cheerio = require('cheerio');

const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_TOKEN' });

async function crawl(pageUrl) {
  const options = { ajax_wait: 'true', page_wait: 6000 };
  const response = await api.get(pageUrl, options);
  if (response.statusCode === 200) return response.body;
  console.error(`Request failed: ${response.statusCode}`);
  return null;
}

function parseQuestion(html, pageUrl) {
  const $ = cheerio.load(html);
  const questionText = $('div.puppeteer_test_question_title')
    .first().text().trim() || $('title').text().trim();

  const answers = [];
  $('div.q-box.qu-borderAll').each((i, el) => {
    const block = $(el);
    const answerText = block.find('.q-text').first().text().trim();
    if (!answerText) return;
    const upvoteRaw = block.find('.q-click-wrapper').first().text().trim();
    answers.push({
      answerText,
      answerUpvoteCount: upvoteRaw || null,
      answerPosition: i + 1,
    });
  });

  return {
    question: {
      text: questionText,
      link: pageUrl,
      answerCountScraped: answers.length,
      answers,
    },
  };
}

async function main() {
  const quoraUrl = 'https://www.quora.com/How-do-I-start-playing-video-games';
  const html = await crawl(quoraUrl);
  if (!html) return;
  const data = parseQuestion(html, quoraUrl);
  console.log(JSON.stringify(data, null, 2));
}

main();

输出示例

node scraper.js 运行完整脚本,您将得到该问题及其可见回答的结构化记录,可直接写入 JSON 或 CSV。

json
{
  "question": {
    "text": "How do I start playing video games?",
    "link": "https://www.quora.com/How-do-I-start-playing-video-games",
    "answerCountScraped": 3,
    "answers": [
      {
        "answerText": "Playing video games is simple, the game will give you some rules, and you play by them.",
        "answerUpvoteCount": "7",
        "answerPosition": 1
      },
      {
        "answerText": "Start with a genre you already enjoy, then pick a beginner-friendly title and learn the controls slowly.",
        "answerUpvoteCount": "3.7K",
        "answerPosition": 2
      }
    ]
  }
}

注意记录中不含任何作者姓名或个人主页链接。赞同数保留为字符串,因为 Quora 会对大数值进行缩写(如"3.7K"),保留原始标签可以避免有损转换。对于话题研究,问题文本加上按赞同数排序的回答通常已经足够。

导出为 JSON 和 CSV

对于内容研究,您通常希望将数据保存到磁盘,而不只是打印在控制台。Node 内置的 fs 模块一行代码即可写入 JSON,一个小辅助函数则可将回答展平为 CSV 行,方便您在电子表格中打开并按赞同数排序。每个 CSV 行代表一条回答,问题文本作为上下文重复出现在每行。

javascript
const fs = require('fs');

function saveJson(data, file) {
  fs.writeFileSync(file, JSON.stringify(data, null, 2));
}

function csvCell(value) {
  const text = (value == null ? '' : String(value)).replace(/"/g, '""');
  return `"${text}"`;
}

function saveCsv(data, file) {
  const header = ['question', 'answerText', 'answerUpvoteCount', 'answerPosition'];
  const rows = data.question.answers.map((a) =>
    [data.question.text, a.answerText, a.answerUpvoteCount, a.answerPosition]
      .map(csvCell)
      .join(','),
  );
  fs.writeFileSync(file, [header.join(','), ...rows].join('\n'));
}

// In main(), after building `data`:
saveJson(data, 'quora_scraped.json');
saveCsv(data, 'quora_scraped.csv');

CSV 的列是问题、回答文本、赞同数和位置,正是内容或话题分析所需的字段。作者身份信息在两种导出格式中均刻意缺失。

扩展到多个问题

抓取一个问题只是演示;话题研究通常意味着处理一个主题下的多个问题列表。收集您关心的问题 URL(来自 Quora 搜索、站点地图或您自己的列表),然后循环遍历,通过 Crawling API 获取每一个,用相同的函数解析,并合并记录。由于每个问题页面共享相同的结构,您已经编写好的解析器无需修改即可应用于所有页面。

javascript
async function scrapeMany(urls) {
  const all = [];
  for (const url of urls) {
    const html = await crawl(url);
    if (html) all.push(parseQuestion(html, url).question);
  }
  return all;
}

const questions = [
  'https://www.quora.com/How-do-I-start-playing-video-games',
  'https://www.quora.com/What-is-Quora',
];

scrapeMany(questions).then((rows) => {
  console.log(`Collected ${rows.length} questions`);
});

控制循环节奏,保持请求量适中。Quora 会监测爬虫形态的流量,因此分散请求并通过轮换住宅 IP 路由(Crawling API 为您处理)可以保持运行健康。关于更广泛的应对策略,请参阅如何在不被封禁的情况下抓取网站。如果您希望将赞同票排名转化为关键词或话题地图,如何提取和分析 Google SEO 数据中的工作流程与本文输出搭配得很好。

抓取 Quora 是否合法?

抓取 Quora 是否被允许,取决于 Quora 的服务条款、您所在的司法管辖区以及您如何使用数据。Quora 的条款限制自动化访问和批量抓取,因此无论您的工具多么谨慎,抓取都可能违反这些条款。请阅读 Quora 的服务条款及其 robots.txt,尊重其暗示的速率限制,并将两者视为您收集内容的边界。这里的代码不会改变这一点;它只是让技术部分得以实现,且仅针对任何人无需账户即可阅读的公开问题页面。

在 Quora 这样的平台上,更大的问题是个人数据。作者姓名、个人主页链接以及人们附在回答上的信誉凭证都是个人数据,而用户撰写的回答是其本人的内容。这就是为什么本指南中的爬虫只保留问题文本、用于聚合分析的回答正文、赞同数和回答位置,并刻意删除作者姓名和个人主页链接。请将输出用于趋势分析、话题频率研究,以及哪些表达方式引发互动的洞察。不要为可识别的个人构建画像,不要将某人的回答与其姓名关联后再发布,也不要建立针对特定个人的数据集。如果您在欧盟或加利福尼亚州,一旦涉及个人数据,GDPR 和 CCPA 即适用:您需要合法处理依据,且必须响应删除请求,这是聚合并丢弃姓名而非存储的有力理由。

对于经过授权的结构化访问,请优先选择官方途径。本指南刻意将范围限定在公开问答页面,因为这是使工作具有可辩护性的边界。它不涵盖任何需要登录的内容、私人或匿名作者的详情、私信,以及任何绕过身份验证或内容墙的尝试。如果您的项目需要可识别的用户数据,或超出轻量级公开研究所需的数量,正确的路径是与平台签订正式数据协议或合作,而不是设计更聪明的爬虫。

回顾

核心要点

  • Quora 在客户端渲染帖子。普通请求返回空框架,因此您必须在解析之前使用 JS token 渲染页面。
  • 一次调用完成渲染和可信 IP。使用 JS token 的 Crawling API 同时处理两者;ajax_waitpage_wait 控制等待回答加载的时长。
  • cheerio 提取字段。读取问题文本,然后将每条回答块映射到其正文、赞同数和位置,并预期混淆的选择器会发生漂移。
  • 聚合而非构建画像。保留问题文本、回答正文和赞同数用于话题研究;删除作者姓名和个人主页链接,以避免为可识别个人建立档案。
  • 坚守公开数据的边界。尊重 Quora 的服务条款和 robots.txt,保持请求量适中,涉及个人数据时注意 GDPR 和 CCPA,对于超出轻量级公开研究的需求,优先选择官方协议。

常见问题

为什么普通请求从 Quora 返回空页面?

因为 Quora 使用 JavaScript 在客户端渲染问题和所有回答。初始 HTML 在浏览器中脚本运行之前几乎是空的,而未经身份验证的自动化请求通常会被重定向到登录页或内容墙。要获取完整帖子,您必须在可信 IP 背后渲染它,这正是 Crawling API 的 JS token 为您处理的事情。

抓取 Quora 需要普通 token 还是 JS token?

请使用 JS token。普通 token 获取静态 HTML,在 Quora 上只会返回没有任何回答的空框架。JS token 在将 HTML 传回之前先在真实浏览器中渲染页面,因此当 cheerio 解析时,问题正文、回答和赞同数都已存在。

我可以用现成的 Quora 数据爬虫代替编写 cheerio 吗?

可以。Crawling API 提供了 quora-question 数据爬虫。在选项对象中传入 { scraper: 'quora-question' },API 会在 response.json.body 中返回解析后的 JSON,完全无需编写 cheerio。当您想严格控制保留哪些字段时(这与将作者个人数据排除在数据集之外密切相关),手动编写解析器依然值得。

我的选择器返回空值。什么发生了变化?

几乎可以肯定是 Quora 的标记发生了变化。其类名(q-boxq-textpuppeteer_test_ 标记)是混淆过的,且会不经通知地更改,因此上个月有效的选择器可能突然失效。在浏览器开发者工具中重新检查实时问题页面并更新选择器。定期维护选择器是任何生产环境爬虫的正常工作。

存储回答中看到的作者姓名是否可以?

请将作者姓名、个人主页链接和信誉凭证视为个人数据,避免存储。本指南中的爬虫故意删除这些信息,只保留问题、用于聚合分析的回答文本和赞同数。如果您必须接触个人数据,GDPR 和 CCPA 适用:您需要合法处理依据,且必须响应删除请求,因此聚合并丢弃身份信息是更安全的默认选择。

如何避免在抓取 Quora 时被封禁?

保持较低的请求频率,分散请求而不是全速循环,并通过轮换住宅 IP 路由,使任何单一地址都不会触发速率限制。Crawling API 为您管理轮换和可信 IP 池;如果您自建方案,这是最值得投入的环节。密切关注状态码,当开始收到重定向或挑战响应时及时回退。

开始构建

大规模爬取任何站点,无需与基础设施对抗。

Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。

自助开通 · 无需销售通话 · 提供企业级爬取量