返回博客
指南
Raluca PenciucLast updated on Mar 31, 20262 min read

从情感分析到营销:Twitter网络爬取的诸多优势

从情感分析到营销:Twitter网络爬取的诸多优势

Twitter 是一个广受欢迎的微博客和社交网站,用户可以在该平台上发布并互动名为“推文”的消息。这些推文可以包含多种信息,包括文本、图片和链接,因此成为各种应用场景中宝贵的数据来源。

从个人研究者到企业,抓取 Twitter 数据具有多种实际应用:趋势与新闻监测、消费者情绪分析、广告活动优化等。

尽管 Twitter 提供了用于访问数据的 API,但其中存在一些注意事项,您应予以留意:

  • 速率限制:您在指定时间段内只能发起一定数量的请求。若超出限制,您的 API 访问权限可能会被暂时暂停;
  • 数据可用性:您仅能访问有限的数据集,例如推文、用户资料和私信。部分数据(如已删除的推文)无法通过 API 获取。

本文将探讨如何使用 TypeScript 和 Puppeteer 进行 Twitter 网页抓取。我们将涵盖必要环境的搭建、数据的定位与提取,以及这些数据的潜在用途。

此外,我们还将探讨为何在抓取 Twitter 数据时,使用专业的爬虫工具比仅使用 Twitter API 更为优越。本文将提供一份关于如何有效抓取 Twitter 数据的分步指南,助您轻松获取所需数据。

先决条件

在开始之前,请确保已准备好必要的工具。

首先,请从官方网站下载并安装 Node.js,务必选择长期支持(LTS)版本。此操作将自动安装 Node 包管理器(NPM),我们后续将通过它安装其他依赖项。

在本教程中,我们将使用 Visual Studio Code 作为集成开发环境(IDE),但您也可以选择使用任何其他 IDE。为您的项目创建一个新文件夹,打开终端并运行以下命令来创建一个新的 Node.js 项目:

npm init -y

这将在项目目录中生成一个 package.json 文件,用于存储项目及其依赖项的相关信息。

接下来,我们需要安装 TypeScript 以及 Node.js 的类型定义。TypeScript 提供了可选的静态类型检查功能,有助于防止代码中的错误。为此,请在终端中运行:

npm install typescript @types/node --save-dev

您可以通过运行以下命令验证安装是否成功:

npx tsc --version

TypeScript 使用名为 tsconfig.json 的配置文件来存储编译器选项及其他设置。要在项目中创建此文件,请运行以下命令:

npx tsc -init

请确保将“outDir”的值设置dist”。这样,我们将把 TypeScript 文件与编译后的文件分开。您可以在 TypeScript 官方文档中找到有关此文件及其属性的更多信息。

现在,在项目中创建一个“src”目录,并新建一个“index.ts”文件。我们将在此保存爬取代码。要执行 TypeScript 代码,必须先进行编译,因此为了确保不会遗漏这一步,我们可以使用自定义命令。

请打开“package.json”文件,并将“scripts”部分修改如下:

"scripts": {

    "test": "npx tsc && node dist/index.js"

}

这样,当你需要运行脚本时,只需在终端中输入“npm run test”即可。

最后,为了从网站抓取数据,我们将使用 Puppeteer。这是一个适用于 Node.js 的无头浏览器库,允许您通过编程方式控制网页浏览器并与网站进行交互。要安装它,请在终端中运行以下命令:

npm install puppeteer

强烈建议在确保数据完整性时使用该工具,因为如今许多网站都包含动态生成的内容。如果您感兴趣,可以在继续之前查阅 Puppeteer 文档,全面了解其功能。

数据来源

现在环境已配置完毕,我们可以开始探讨数据提取了。本文中,我选择抓取 Netflix 的 Twitter 个人主页:https://twitter.com/netflix

我们将提取以下数据:

  • 个人资料名称;
  • 个人资料用户名;
  • 用户简介;
  • 用户所在地;
  • 用户网站;
  • 用户注册日期;
  • 关注人数;
  • 用户的关注者数量;
  • 关于用户推文的信息 - 作者姓名 - 作者用户名 - 发布日期 - 文本内容 - 媒体(视频或照片) - 回复数 - 转发数 - 点赞数 - 浏览量。

您可以在下方的截图中看到所有这些信息已被高亮显示:

通过在每个元素上打开开发者工具,您将能够注意到我们将用于定位 HTML 元素的 CSS 选择器。如果您对 CSS 选择器的运作方式还不太熟悉,欢迎查阅这篇入门指南

提取数据

在编写脚本之前,让我们先验证 Puppeteer 是否已正确安装:

import puppeteer from 'puppeteer';

async function scrapeTwitterData(twitter_url: string): Promise<void> {

    // Launch Puppeteer

    const browser = await puppeteer.launch({

        headless: false,

    	  args: ['--start-maximized'],

    	  defaultViewport: null

    })

    // Create a new page

    const page = await browser.newPage()

    // Navigate to the target URL

    await page.goto(twitter_url)

    // Close the browser

    await browser.close()

}

scrapeTwitterData("https://twitter.com/netflix")

这里我们打开一个浏览器窗口,新建一个页面,导航至目标 URL,然后关闭浏览器。为了简化操作和便于可视化调试,我以非无头模式全屏打开了浏览器窗口。

现在,让我们查看一下网站的结构,并逐步提取前面的数据列表:

乍看之下,您可能已经注意到该网站的结构相当复杂。类名是随机生成的,且极少有 HTML 元素具有唯一标识。

幸运的是,当我们遍历目标数据的父元素时,发现了“data-testid”属性。在 HTML 文档中快速搜索即可确认,该属性能唯一标识我们目标的元素。

因此,要提取用户名和用户ID,我们将提取那个“data-testid”属性值为“UserName”的“div”元素。代码如下所示:

// Extract the profile name and handle

const profileNameHandle = await page.evaluate(() => {

    const nameHandle = document.querySelector('div[data-testid="UserName"]')

    return nameHandle ? nameHandle.textContent : ""

})

const profileNameHandleComponents = profileNameHandle.split('@')

console.log("Profile name:", profileNameHandleComponents[0])

console.log("Profile handle:", '@' + profileNameHandleComponents[1])

由于个人资料名称和用户名具有相同的父元素,最终结果会显示为拼接在一起。为解决此问题,我们使用“split”方法将数据拆分。

随后,我们采用相同的逻辑来提取个人资料简介。在此情况下,“data-testid”属性的值为“UserDescription”:

// Extract the user bio

const profileBio = await page.evaluate(() => {

    const location = document.querySelector('div[data-testid="UserDescription"]')

    return location ? location.textContent : ""

})

console.log("User bio:", profileBio)

最终结果由 HTML 元素的“textContent”属性描述。

继续查看个人资料数据的下一部分,我们会发现位置、网站和加入日期也遵循相同的结构。

// Extract the user location

const profileLocation = await page.evaluate(() => {

    const location = document.querySelector('span[data-testid="UserLocation"]')

    return location ? location.textContent : ""

})

console.log("User location:", profileLocation)

// Extract the user website

const profileWebsite = await page.evaluate(() => {

    const location = document.querySelector('a[data-testid="UserUrl"]')

    return location ? location.textContent : ""

})

console.log("User website:", profileWebsite)

// Extract the join date

const profileJoinDate = await page.evaluate(() => {

    const location = document.querySelector('span[data-testid="UserJoinDate"]')

    return location ? location.textContent : ""

})

console.log("User join date:", profileJoinDate)

要获取关注数和粉丝数,我们需要采用略微不同的方法。请查看下图:

此处没有“data-testid”属性,且类名仍是随机生成的。一个解决方案是定位锚点元素,因为它们具有唯一的“href”属性。

// Extract the following count

const profileFollowing = await page.evaluate(() => {

    const location = document.querySelector('a[href$="/following"]')

    return location ? location.textContent : ""

})

console.log("User following:", profileFollowing)

// Extract the followers count

const profileFollowers = await page.evaluate(() => {

    const location = document.querySelector('a[href$="/followers"]')

    return location ? location.textContent : ""

})

console.log("User followers:", profileFollowers)

为了使代码适用于任何 Twitter 个人资料,我们定义了 CSS 选择器,分别定位“href”属性以“/following”或“/followers”结尾的锚点元素。

接下来是推文列表,我们仍可通过“data-testid”属性轻松识别每条推文,如下所示:

这段代码与此前所做的并无二致,唯一的区别在于使用了“querySelectorAll”方法,并将结果转换为 JavaScript 数组:

// Extract the user tweets

const userTweets = await page.evaluate(() => {

    const tweets = document.querySelectorAll('article[data-testid="tweet"]')

    const tweetsArray = Array.from(tweets)

    return tweetsArray

})

console.log("User tweets:", userTweets)

然而,尽管 CSS 选择器无疑是正确的,您可能已经注意到生成的列表几乎总是为空。这是因为推文会在页面加载完成后几秒钟才加载。

解决此问题的简单方法是在跳转至目标 URL 后增加额外的等待时间。一种方案是尝试固定数秒的等待时间,另一种则是等待特定的 CSS 选择器出现在 DOM 中:

await page.waitForSelector('div[aria-label^="Timeline: "]')

因此,这里我们指示脚本等待,直到页面上出现一个“aria-label”属性以“Timeline:”开头的“div”元素。现在,之前的代码片段应该能完美运行。

接下来,我们可以像之前一样,通过“data-testid”属性识别推文作者的信息。

在算法中,我们将遍历 HTML 元素列表,并对每个元素应用“querySelector”方法。这样,由于目标范围更小,我们可以更好地确保所使用的选择器是唯一的。

// Extract the user tweets

const userTweets = await page.evaluate(() => {

    const tweets = document.querySelectorAll('article[data-testid="tweet"]')

    const tweetsArray = Array.from(tweets)

    return tweetsArray.map(t => {

        const authorData = t.querySelector('div[data-testid="User-Names"]')

        const authorDataText = authorData ? authorData.textContent : ""

        const authorComponents = authorDataText.split('@')

        const authorComponents2 = authorComponents[1].split('·')

        return {

            authorName: authorComponents[0],

            authorHandle: '@' + authorComponents2[0],

            date: authorComponents2[1],

        }

    })

})

console.log("User tweets:", userTweets)

作者相关数据在此处也会以拼接形式呈现,因此为了确保结果合理,我们对每个部分都应用了“split”方法。

推文的文本内容处理非常简单:

const tweetText = t.querySelector('div[data-testid="tweetText"]')

对于推文中的图片,我们将提取一组“img”元素,其父元素是“data-testid”属性设置为“tweetPhoto”的“div”元素。最终结果将是这些元素的“src”属性。

const tweetPhotos = t.querySelectorAll('div[data-testid="tweetPhoto"] > img')

const tweetPhotosArray = Array.from(tweetPhotos)

const photos = tweetPhotosArray.map(p => p.getAttribute('src'))

最后是推文的统计部分。通过识别带有“data-testid”属性的元素,并获取其“aria-label”属性的值,即可同样获取回复、转发和点赞的数量。

要获取浏览量,我们需要定位那个“aria-label”属性以“Views. View Tweet analytics”字符串结尾的锚点元素。

const replies = t.querySelector('div[data-testid="reply"]')

const repliesText = replies ? replies.getAttribute("aria-label") : ''

const retweets = t.querySelector('div[data-testid="retweet"]')

const retweetsText = retweets ? retweets.getAttribute("aria-label") : ''

const likes = t.querySelector('div[data-testid="like"]')

const likesText = likes ? likes.getAttribute("aria-label") : ''

const views = t.querySelector('a[aria-label$="Views. View Tweet analytics"]')

const viewsText = views ? views.getAttribute("aria-label") : ''

由于最终结果中也会包含字符,因此我们使用“split”方法来提取并仅返回数值部分。提取推文数据的完整代码片段如下所示:

// Extract the user tweets

const userTweets = await page.evaluate(() => {

    const tweets = document.querySelectorAll('article[data-testid="tweet"]')

    const tweetsArray = Array.from(tweets)

    return tweetsArray.map(t => {

        

        // Extract the tweet author, handle, and date

        const authorData = t.querySelector('div[data-testid="User-Names"]')

        const authorDataText = authorData ? authorData.textContent : ""

        const authorComponents = authorDataText.split('@')

        const authorComponents2 = authorComponents[1].split('·')

        // Extract the tweet content

        const tweetText = t.querySelector('div[data-testid="tweetText"]')

        // Extract the tweet photos

        const tweetPhotos = t.querySelectorAll('div[data-testid="tweetPhoto"] > img')

        const tweetPhotosArray = Array.from(tweetPhotos)

        const photos = tweetPhotosArray.map(p => p.getAttribute('src'))

        // Extract the tweet reply count

        const replies = t.querySelector('div[data-testid="reply"]')

        const repliesText = replies ? replies.getAttribute("aria-label") : ''

        // Extract the tweet retweet count

        const retweets = t.querySelector('div[data-testid="retweet"]')

        const retweetsText = retweets ? retweets.getAttribute("aria-label") : ''

        // Extract the tweet like count

        const likes = t.querySelector('div[data-testid="like"]')

        const likesText = likes ? likes.getAttribute("aria-label") : ''

        // Extract the tweet view count

        const views = t.querySelector('a[aria-label$="Views. View Tweet analytics"]')

        const viewsText = views ? views.getAttribute("aria-label") : ''

        return {

            authorName: authorComponents[0],

            authorHandle: '@' + authorComponents2[0],

            date: authorComponents2[1],

            text: tweetText ? tweetText.textContent : '',

            media: photos,

            replies: repliesText.split(' ')[0],

            retweets: retweetsText.split(' ')[0],

            likes: likesText.split(' ')[0],

            views: viewsText.split(' ')[0],

        }

    })

})

console.log("User tweets:", userTweets)

运行整个脚本后,终端应显示类似以下内容:

Profile name: Netflix

Profile handle: @netflix

User bio:

User location: California, USA

User website: netflix.com/ChangePlan

User join date: Joined October 2008

User following: 2,222 Following

User followers: 21.3M Followers

User tweets: [

  {

    authorName: 'best of the haunting',

    authorHandle: '@bestoffhaunting',

    date: '16 Jan',

    text: 'the haunting of hill house.',

    media: [

      'https://pbs.twimg.com/media/FmnGkCNWABoEsJE?format=jpg&name=360x360',

  	'https://pbs.twimg.com/media/FmnGkk0WABQdHKs?format=jpg&name=360x360',

  	'https://pbs.twimg.com/media/FmnGlTOWABAQBLb?format=jpg&name=360x360',

  	'https://pbs.twimg.com/media/FmnGlw6WABIKatX?format=jpg&name=360x360'

    ],

    replies: '607',

    retweets: '37398',

    likes: '170993',

    views: ''

  },

  {

    authorName: 'Netflix',

    authorHandle: '@netflix',

    date: '9h',

    text: 'The Glory Part 2 premieres March 10 -- FIRST LOOK:',

    media: [

  	'https://pbs.twimg.com/media/FmuPlBYagAI6bMF?format=jpg&name=360x360',

  	'https://pbs.twimg.com/media/FmuPlBWaEAIfKCN?format=jpg&name=360x360',

  	'https://pbs.twimg.com/media/FmuPlBUagAETi2Z?format=jpg&name=360x360',

  	'https://pbs.twimg.com/media/FmuPlBZaEAIsJM6?format=jpg&name=360x360'

    ],

    replies: '250',

    retweets: '4440',

    likes: '9405',

    views: '656347'

  },

  {

    authorName: 'Kurtwood Smith',

    authorHandle: '@tahitismith',

    date: '14h',

    text: 'Two day countdown...more stills from the show to hold you over...#That90sShow on @netflix',

    media: [

  	'https://pbs.twimg.com/media/FmtOZTGaEAAr2DF?format=jpg&name=360x360',

  	'https://pbs.twimg.com/media/FmtOZTFaUAI3QOR?format=jpg&name=360x360',

  	'https://pbs.twimg.com/media/FmtOZTGaAAEza6i?format=jpg&name=360x360',

  	'https://pbs.twimg.com/media/FmtOZTGaYAEo-Yu?format=jpg&name=360x360'

    ],

    replies: '66',

    retweets: '278',

    likes: '3067',

    views: ''

  },

  {

    authorName: 'Netflix',

    authorHandle: '@netflix',

    date: '12h',

    text: 'In 2013, Kai the Hatchet-Wielding Hitchhiker became an internet sensation   -- but that viral fame put his questionable past squarely on the radar of authorities. \n' +

  	'\n' +

  	'The Hatchet Wielding Hitchhiker is now on Netflix.',

    media: [],

    replies: '169',

    retweets: '119',

    likes: '871',

    views: '491570'

  }

]

扩展

虽然初看之下抓取 Twitter 似乎很简单,但随着项目规模的扩大,该过程会变得更加复杂且具有挑战性。该网站采用了多种技术来检测和防范自动化流量,因此当您的抓取工具规模扩大后,可能会开始受到速率限制,甚至被封禁。

要克服这些挑战并继续进行大规模抓取,一种方法是使用抓取 API。此类服务提供了一种简单可靠的方式,可访问 twitter.com 等网站的数据,而无需自行构建和维护抓取工具。

WebScrapingAPI 便是此类产品的典型代表。其代理轮换机制能彻底规避封禁风险,而丰富的知识库则支持对浏览器数据进行随机化处理,使其行为与真实用户无异。

配置过程快速简便。您只需注册一个账户,即可获得 API 密钥。该密钥可在控制面板中获取,并用于对您发送的请求进行身份验证。

既然您已经配置好了 Node.js 环境,我们就可以使用相应的 SDK。运行以下命令将其添加到项目依赖项中:

npm install webscrapingapi

现在只需发送一个 GET 请求,即可获取网站的 HTML 文档。请注意,这并非访问 API 的唯一方式。

import webScrapingApiClient from 'webscrapingapi';

const client = new webScrapingApiClient("YOUR_API_KEY");

async function exampleUsage() {

    const api_params = {

        'render_js': 1,

        'proxy_type': 'residential',

        'wait_for_css': 'div[aria-label^="Timeline: "]',

        'timeout': 30000

    }

    const URL = "https://twitter.com/netflix"

    const response = await client.get(URL, api_params)

    if (response.success) {

        console.log(response.response.data)

    } else {

        console.log(response.error.response.data)

    }

}

exampleUsage();

通过启用“render_js”参数,我们可以使用无头浏览器发送请求,就像您之前在本教程中做的那样。

获取 HTML 文档后,您可以使用其他库(如 Cheerio)来提取所需数据。没听说过它?请查看这篇指南,助您快速入门!

结论

本文提供了一份关于如何使用 TypeScript 有效抓取 Twitter 数据的全面指南。我们涵盖了配置必要环境、定位和提取数据,以及这些信息潜在用途的各个步骤。

对于希望深入了解消费者情绪、进行社交媒体监测以及获取商业情报的人士而言,Twitter 是一个宝贵的数据来源。但需要注意的是,仅使用 Twitter API 可能无法获取您所需的所有数据,因此使用专业的爬虫工具才是更好的解决方案。

总体而言,抓取 Twitter 数据能够提供宝贵的洞察,对于任何希望获得竞争优势的企业或个人而言,都是一项极具价值的资产。

关于作者
Raluca Penciuc, 全栈开发工程师 @ WebScrapingAPI
Raluca Penciuc全栈开发工程师

Raluca Penciuc 是 WebScrapingAPI 的全栈开发工程师,主要负责开发爬虫、优化规避机制,并探索可靠的方法以降低在目标网站上的被检测概率。

开始构建

准备好扩展您的数据收集规模了吗?

加入2,000多家企业,使用WebScrapingAPI在无需任何基础设施开销的情况下,以企业级规模提取网络数据。