本教程的代码可在 GitHub 上获取。你也可以在 GitHub 上查看该应用的在线演示。
全新 Twitter API 的第一批端点已经推出,等待你探索并运用它们构建应用,以了解公共对话中发生的情况。
构建应用后,你将了解如何把它应用到一些真实示例中,以激发你的入门灵感,例如:
发现新的音乐视频:想象一下,你有一个面板可以查看当前在 Twitter 上分享的音乐视频
寻找远程开发者职位空缺:想象一下,创建一个远程开发者职位列表应用,列出当前在 Twitter 上分享的远程开发者职位空缺
学习个人理财和储蓄:展示 Twitter 上有关个人理财和储蓄的公开对话
npx create-react-app real-time-tweet-streamer
在 create-react-app 执行完毕后,更改到新创建的 real-tweet-streamer 目录下,并将 package.json 中的脚本块替换为 package.json 中的以下脚本块。这几行将提供一个命令快捷方式,以便在开发或生产中根据需要同时运行客户端和服务器后台代码。
cd real-time-tweet-streamer
"scripts": {
"start": "npm run development",
"development": "NODE_ENV=development concurrently --kill-others \"npm run client\" \"npm run server\"",
"production": "npm run build && NODE_ENV=production npm run server",
"client": "react-scripts start",
"server": "node server/server.js",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
更新脚本部分后,你的 package.json 现在应该如下所示。
package.json
{
"name": "real-time-tweet-streamer",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1"
},
"scripts": {
"start": "npm run development",
"development": "NODE_ENV=development concurrently --kill-others \"npm run client\" \"npm run server\"",
"production": "npm run build && NODE_ENV=production npm run server",
"client": "react-scripts start",
"server": "node server/server.js",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
接下来,删除 src/ 子目录中的所有文件。
rm src/*
然后,在 src/ 子目录下创建一个名为 index.js 的新文件。该文件的代码将如下所示。
import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";
ReactDOM.render(<App />, document.querySelector("#root"));
凭据
若要连接到已筛选的流端点,需要你在 Twitter 开发者门户中使用应用的不记名令牌进行身份验证。要使用你的不记名令牌,需要设置以下环境变量。如果你使用 bash 作为 shell,则可以在终端窗口中发出以下命令来实现此目的。将 <YOUR BEARER TOKEN HERE>(包括左右尖括号)替换为你的不记名令牌。
export TWITTER_BEARER_TOKEN=<YOUR BEARER TOKEN HERE>
服务器端代码
首先,你需要开始实现 Node 服务器,它将负责向 Twitter API 发出实际请求。此 Node 服务器将充当基于浏览器的 React 客户端和 Twitter API 之间的代理。在 Node 服务器上,你将需要创建连接到已筛选的流端点的 API 端点。反过来,来自 React 客户端的请求将通过代理发送到本地 Node 服务器。
在继续操作之前,请先转到项目根目录,并安装以下的依赖项
npm install concurrently express body-parser util request http socket.io path http-proxy-middleware request react-router-dom axios socket.io-client react-twitter-embed
接下来,在仍处于项目根目录下时,新建一个名为“服务器”的子目录,并在该子目录下创建一个名为“server.js”的新文件。
mkdir server
touch server/server.js
该源代码文件将包含你所有的后端逻辑,用于连接到已筛选的流端点并从中接收推文。你的 server.js 文件内容将如下所示。
server.js
const express = require("express");
const bodyParser = require("body-parser");
const util = require("util");
const request = require("request");
const path = require("path");
const socketIo = require("socket.io");
const http = require("http");
const app = express();
let port = process.env.PORT || 3000;
const post = util.promisify(request.post);
const get = util.promisify(request.get);
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
const server = http.createServer(app);
const io = socketIo(server);
const BEARER_TOKEN = process.env.TWITTER_BEARER_TOKEN;
let timeout = 0;
const streamURL = new URL(
"https://api.twitter.com/2/tweets/search/stream?tweet.fields=context_annotations&expansions=author_id"
);
const rulesURL = new URL(
"https://api.twitter.com/2/tweets/search/stream/rules"
);
const errorMessage = {
title: "Please Wait",
detail: "Waiting for new Tweets to be posted...",
};
const authMessage = {
title: "Could not authenticate",
details: [
`Please make sure your bearer token is correct.
If using Glitch, remix this app and add it to the .env file`,
],
type: "https://developer.twitter.com/en/docs/authentication",
};
const sleep = async (delay) => {
return new Promise((resolve) => setTimeout(() => resolve(true), delay));
};
app.get("/api/rules", async (req, res) => {
if (!BEARER_TOKEN) {
res.status(400).send(authMessage);
}
const token = BEARER_TOKEN;
const requestConfig = {
url: rulesURL,
auth: {
bearer: token,
},
json: true,
};
try {
const response = await get(requestConfig);
if (response.statusCode !== 200) {
if (response.statusCode === 403) {
res.status(403).send(response.body);
} else {
throw new Error(response.body.error.message);
}
}
res.send(response);
} catch (e) {
res.send(e);
}
});
app.post("/api/rules", async (req, res) => {
if (!BEARER_TOKEN) {
res.status(400).send(authMessage);
}
const token = BEARER_TOKEN;
const requestConfig = {
url: rulesURL,
auth: {
bearer: token,
},
json: req.body,
};
try {
const response = await post(requestConfig);
if (response.statusCode === 200 || response.statusCode === 201) {
res.send(response);
} else {
throw new Error(response);
}
} catch (e) {
res.send(e);
}
});
const streamTweets = (socket, token) => {
let stream;
const config = {
url: streamURL,
auth: {
bearer: token,
},
timeout: 31000,
};
try {
const stream = request.get(config);
stream
.on("data", (data) => {
try {
const json = JSON.parse(data);
if (json.connection_issue) {
socket.emit("error", json);
reconnect(stream, socket, token);
} else {
if (json.data) {
socket.emit("tweet", json);
} else {
socket.emit("authError", json);
}
}
} catch (e) {
socket.emit("heartbeat");
}
})
.on("error", (error) => {
// Connection timed out
socket.emit("error", errorMessage);
reconnect(stream, socket, token);
});
} catch (e) {
socket.emit("authError", authMessage);
}
};
const reconnect = async (stream, socket, token) => {
timeout++;
stream.abort();
await sleep(2 ** timeout * 1000);
streamTweets(socket, token);
};
io.on("connection", async (socket) => {
try {
const token = BEARER_TOKEN;
io.emit("connect", "Client connected");
const stream = streamTweets(io, token);
} catch (e) {
io.emit("authError", authMessage);
}
});
console.log("NODE_ENV is", process.env.NODE_ENV);
if (process.env.NODE_ENV === "production") {
app.use(express.static(path.join(__dirname, "../build")));
app.get("*", (request, res) => {
res.sendFile(path.join(__dirname, "../build", "index.html"));
});
} else {
port = 3001;
}
server.listen(port, () => console.log(`Listening on port ${port}`));
使用规则筛选已筛选的流端点上的推文
已筛选的流有两个端点,一个是流式传输端点,用于接收数据;另一个是规则端点,用于创建和删除规则。已筛选的流端点需要你定义称为规则的搜索查询,以便让它知道要发送给你的推文类型。你可借助规则使用一组运算符缩小范围,只搜索你要找的推文。当你完成应用构建后,将看到一些稍后可使用的示例用例及相应规则。
另一个已筛选的流端点是流式传输端点,它使用一个简单的 GET 连接。建立连接后,推文将通过持续的 HTTP 流式传输连接以 JSON 格式传送。你只会在连接到流时收到与您的规则匹配的推文。
客户端代码
下一步是使用以下 React 组件
App.js - 父组件,它反过来将提供所有其他组件
NavBar.js - 显示导航栏,用于在推文流之间进行导航并管理规则
Tweet.js - 在页面上显示推文
TweetFeed.js - 以类似“流”的方式同时提供多个推文组件
Rule.js - 在你的流上提供一个单独的规则
RuleList.js - 提供多个 Rule 组件并显示一个输入字段来添加规则
ErrorMessage.js - 在屏幕上提供任何状态或错误信息
Spinner.js - 为任何挂起的 API 调用提供一个加载指示器
若要为你的所有组件设置样式,将使用 Semantic UI。在位于 ~/real-tweet-streamer/public 目录下的 index.html 文件中的 head
标签中添加以下一行,以便在你的项目中包含 Semantic UI 的 CSS。
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css" />
现在,你需要开始创建 React 组件。在 /src 子目录下,创建一个名为“组件”的目录。上面的源代码文件将存储在这个新目录中。首先,创建应用程序的父组件。该组件将负责提供其他所有组件。
App.js
import React from "react";
import { BrowserRouter, Route } from "react-router-dom";
import Navbar from "./Navbar";
import TweetFeed from "./TweetFeed";
import RuleList from "./RuleList";
class App extends React.Component {
render() {
return (
<div className="ui container">
<div className="introduction"></div>
<h1 className="ui header">
<div className="content">
Real Time Tweet Streamer
<div className="sub header">Powered by Twitter data</div>
</div>
</h1>
<div className="ui container">
<BrowserRouter>
<Navbar />
<Route exact path="/" component={RuleList} />
<Route exact path="/rules" component={RuleList} />
<Route exact path="/tweets" component={TweetFeed} />
</BrowserRouter>
</div>
</div>
);
}
}
export default App;
接下来,创建导航栏的组件
Navbar.js
import React from "react";
import { NavLink } from "react-router-dom";
const Navbar = () => {
return (
<div className="ui two item menu">
<NavLink to="/tweets" className="item" target="_blank">
New Tweets
</NavLink>
<NavLink to="/rules" className="item" target="_blank">
Manage Rules
</NavLink>
</div>
);
};
export default Navbar;
接下来,创建父组件来提供所有职位列表。
TweetFeed.js
import React, { useEffect, useReducer } from "react";
import Tweet from "./Tweet";
import socketIOClient from "socket.io-client";
import ErrorMessage from "./ErrorMessage";
import Spinner from "./Spinner";
const reducer = (state, action) => {
switch (action.type) {
case "add_tweet":
return {
...state,
tweets: [action.payload, ...state.tweets],
error: null,
isWaiting: false,
errors: [],
};
case "show_error":
return { ...state, error: action.payload, isWaiting: false };
case "add_errors":
return { ...state, errors: action.payload, isWaiting: false };
case "update_waiting":
return { ...state, error: null, isWaiting: true };
default:
return state;
}
};
const TweetFeed = () => {
const initialState = {
tweets: [],
error: {},
isWaiting: true,
};
const [state, dispatch] = useReducer(reducer, initialState);
const { tweets, error, isWaiting } = state;
const streamTweets = () => {
let socket;
if (process.env.NODE_ENV === "development") {
socket = socketIOClient("http://localhost:3001/");
} else {
socket = socketIOClient("/");
}
socket.on("connect", () => {});
socket.on("tweet", (json) => {
if (json.data) {
dispatch({ type: "add_tweet", payload: json });
}
});
socket.on("heartbeat", (data) => {
dispatch({ type: "update_waiting" });
});
socket.on("error", (data) => {
dispatch({ type: "show_error", payload: data });
});
socket.on("authError", (data) => {
console.log("data =>", data);
dispatch({ type: "add_errors", payload: [data] });
});
};
const reconnectMessage = () => {
const message = {
title: "Reconnecting",
detail: "Please wait while we reconnect to the stream.",
};
if (error && error.detail) {
return (
<div>
<ErrorMessage key={error.title} error={error} styleType="warning" />
<ErrorMessage
key={message.title}
error={message}
styleType="success"
/>
<Spinner />
</div>
);
}
};
const errorMessage = () => {
const { errors } = state;
if (errors && errors.length > 0) {
return errors.map((error) => (
<ErrorMessage key={error.title} error={error} styleType="negative" />
));
}
};
const waitingMessage = () => {
const message = {
title: "Still working",
detail: "Waiting for new Tweets to be posted",
};
if (isWaiting) {
return (
<React.Fragment>
<div>
<ErrorMessage
key={message.title}
error={message}
styleType="success"
/>
</div>
<Spinner />
</React.Fragment>
);
}
};
useEffect(() => {
streamTweets();
}, []);
const showTweets = () => {
if (tweets.length > 0) {
return (
<React.Fragment>
{tweets.map((tweet) => (
<Tweet key={tweet.data.id} json={tweet} />
))}
</React.Fragment>
);
}
};
return (
<div>
{reconnectMessage()}
{errorMessage()}
{waitingMessage()}
{showTweets()}
</div>
);
};
export default TweetFeed;
接下来,为前面提供单条推文的组件创建子组件。
Tweet.js
import React from "react";
import { TwitterTweetEmbed } from "react-twitter-embed";
const Tweet = ({ json }) => {
const { id } = json.data;
const options = {
cards: "hidden",
align: "center",
width: "550",
conversation: "none",
};
return <TwitterTweetEmbed options={options} tweetId={id} />;
};
export default Tweet;
接下来,创建负责提供流上所有规则以及显示用于创建新规则的输入控件的组件。在本例中,我们将只使用一条规则。
RuleList.js
import React, { useEffect, useReducer } from "react";
import axios from "axios";
import Rule from "./Rule";
import ErrorMessage from "./ErrorMessage";
import Spinner from "./Spinner";
const reducer = (state, action) => {
switch (action.type) {
case "show_rules":
return { ...state, rules: action.payload, newRule: "" };
case "add_rule":
return {
...state,
rules: [...state.rules, ...action.payload],
newRule: "",
errors: [],
};
case "add_errors":
return { ...state, rules: state.rules, errors: action.payload };
case "delete_rule":
return {
...state,
rules: [...state.rules.filter((rule) => rule.id !== action.payload)],
};
case "rule_changed":
return { ...state, newRule: action.payload };
case "change_loading_status":
return { ...state, isLoading: action.payload };
default:
return state;
}
};
const RuleList = () => {
const initialState = { rules: [], newRule: "", isLoading: false, errors: [] };
const [state, dispatch] = useReducer(reducer, initialState);
const exampleRule = "from:twitterdev has:links";
const ruleMeaning = `This example rule will match Tweets posted by
TwtterDev containing links`;
const operatorsURL =
"/content/developer-twitter/en/docs/labs/filtered-stream/operators";
const rulesURL = "/api/rules";
const createRule = async (e) => {
e.preventDefault();
const payload = { add: [{ value: state.newRule }] };
dispatch({ type: "change_loading_status", payload: true });
try {
const response = await axios.post(rulesURL, payload);
if (response.data.body.errors)
dispatch({ type: "add_errors", payload: response.data.body.errors });
else {
dispatch({ type: "add_rule", payload: response.data.body.data });
}
dispatch({ type: "change_loading_status", payload: false });
} catch (e) {
dispatch({
type: "add_errors",
payload: [{ detail: e.message }],
});
dispatch({ type: "change_loading_status", payload: false });
}
};
const deleteRule = async (id) => {
const payload = { delete: { ids: [id] } };
dispatch({ type: "change_loading_status", payload: true });
await axios.post(rulesURL, payload);
dispatch({ type: "delete_rule", payload: id });
dispatch({ type: "change_loading_status", payload: false });
};
const errors = () => {
const { errors } = state;
if (errors && errors.length > 0) {
return errors.map((error) => (
<ErrorMessage key={error.title} error={error} styleType="negative" />
));
}
};
const rules = () => {
const { isLoading, rules } = state;
const message = {
title: "No rules present",
details: [
`There are currently no rules on this stream. Start by adding the rule
below.`,
exampleRule,
ruleMeaning,
],
type: operatorsURL,
};
if (!isLoading) {
if (rules && rules.length > 0) {
return rules.map((rule) => (
<Rule
key={rule.id}
data={rule}
onRuleDelete={(id) => deleteRule(id)}
/>
));
} else {
return (
<ErrorMessage
key={message.title}
error={message}
styleType="warning"
/>
);
}
} else {
return <Spinner />;
}
};
useEffect(() => {
(async () => {
dispatch({ type: "change_loading_status", payload: true });
try {
const response = await axios.get(rulesURL);
const { data: payload = [] } = response.data.body;
dispatch({
type: "show_rules",
payload,
});
} catch (e) {
dispatch({ type: "add_errors", payload: [e.response.data] });
}
dispatch({ type: "change_loading_status", payload: false });
})();
}, []);
return (
<div>
<form onSubmit={(e) => createRule(e)}>
<div className="ui fluid action input">
<input
type="text"
autoFocus={true}
value={state.newRule}
onChange={(e) =>
dispatch({ type: "rule_changed", payload: e.target.value })
}
/>
<button type="submit" className="ui primary button">
Add Rule
</button>
</div>
{errors()}
{rules()}
</form>
</div>
);
};
export default RuleList;
接下来,创建 RuleList.js 的子组件,用于负责显示一条规则和删除一条规则。
Rule.js
import React from "react";
export const Rule = ({ data, onRuleDelete }) => {
return (
<div className="ui segment">
<p>{data.value}</p>
<div className="ui label">tag: {data.tag}</div>
<button
className="ui right floated negative button"
onClick={() => onRuleDelete(data.id)}
>
Delete
</button>
</div>
);
};
export default Rule;
接下来,创建一个显示任何状态或错误信息的组件。
ErrorMessage.js
import React from "react";
const ErrorMessage = ({ error, styleType }) => {
const errorDetails = () => {
if (error.details) {
return error.details.map(detail => <p key={detail}>{detail}</p>);
} else if (error.detail) {
return <p key={error.detail}>{error.detail}</p>;
}
};
const errorType = () => {
if (error.type) {
return (
<em>
See
<a href={error.type} target="_blank" rel="noopener noreferrer">
{" "}
Twitter documentation{" "}
</a>
for further details.
</em>
);
}
};
return (
<div className={`ui message ${styleType}`}>
<div className="header">{error.title}</div>
{errorDetails()}
{errorType()}
</div>
);
};
export default ErrorMessage;
最后,创建一个在任何挂起的 API 调用期间显示加载指示器的组件。
Spinner.js
import React from "react";
const Spinner = () => {
return (
<div>
<div className="ui active centered large inline loader"></div>
</div>
);
};
export default Spinner;
代理设置
最后一步是将来自客户端的请求代理到后端服务器。为此,请在你的 src/ 目录下创建一个名为“setupProxy.js”的新文件,并添加以下代码。
setupProxy.js
const { createProxyMiddleware } = require("http-proxy-middleware");
// This proxy redirects requests to /api endpoints to
// the Express server running on port 3001.
module.exports = function (app) {
app.use(
["/api"],
createProxyMiddleware({
target: "http://localhost:3001",
})
);
};
现在,你可以进入项目根目录并键入以下内容来启动服务器和客户端。
npm start
该命令完成后,你的默认 Web 浏览器应该会自动启动,并导航到 http://localhost:3000。然后,你将进入应用的规则管理部分。
现在,你已部署了一个项目来捕捉你需要的任何类型的推文,接下来让我们通过一些现实生活中的示例来了解如何使用此应用,例如
发现新的音乐视频
寻找远程开发者职位空缺
学习个人理财和储蓄
对于下面列出的每个示例和附带的规则,你可以导航到应用的规则部分,只需将规则复制粘贴到输入字段中即可将其添加到你的流。添加了该规则后,它将在几秒钟内生效,并且只会将符合该规则标准的推文发送给你。
远程开发者职位空缺
在这第一个示例中,假设你对寻找远程开发者职位
空缺感兴趣。要展示此类推文,你可以使用以下规则。
(developer OR engineer) remote (context:66.961961812492148736 OR context:66.850073441055133696)
要了解此规则的作用,你可以将其分解为两个部分。关键词部分和推文注释部分。
使用关键词运算符
(developer OR engineer) remote
规则中的关键词部分将匹配包含关键词“开发者”或“工程师”和关键词“远程”的推文。单单是这些关键词肯定会匹配包含远程开发者职位招聘的推文,但它也会匹配不相关的推文。例如,下面的推文将匹配此规则。
https://twitter.com/EraserFarm/status/1220013392766947332
由于这不是所需的推文,因此你需要进一步改进此规则。虽然这条规则匹配不相关的推文,但它也能成功匹配与实际职位招聘相匹配的推文。例如,你截至目前拥有的规则也会导致匹配以下推文。
https://twitter.com/plataformatec/status/1225460423761317888
推文注释:使用上下文操作符
你现在面临的挑战是,虽然你收到的推文中包含了职位招聘,但你还是要去查看不相关的推文。如果有一种方法可以尽可能地只匹配包含职位招聘的推文就好了。你是否可以在规则中使用一些只匹配这些相关推文的操作符?
幸运的是,这正是推文注释的强大之处。仔细查看在已筛选的流端点上为该推文发送的推文对象有效负荷示例。在有效负荷中,存在嵌套的“context_annotations”字段。上下文注释作为有效负荷中的“context_annotations”字段进行传递。这些注释是根据推文文本推断得出的,会生成域和/或实体标签,这些标签可用于发现以前可能很难发现的话题的相关推文。请注意,这些字段只有在数据存在的情况下才会被包含在内,因为不是所有的推文都包含此数据。
"context_annotations": [
{
"domain": {
"id": "66",
"name": "Interests and Hobbies Category",
"description": "A grouping of interests and hobbies entities, like Novelty Food or Destinations"
},
"entity": {
"id": 961961812492148736,
"name": "Recruitment",
"description": "Recruitment"
}
},
{
"domain": {
"id": "66",
"name": "Interests and Hobbies Category",
"description": "A grouping of interests and hobbies entities, like Novelty Food or Destinations"
},
"entity": {
"id": "850073441055133696",
"name": "Job search",
"description": "Job search"
}
}
],
要在 context_annotations 字段中匹配域和实体 ID,可以使用“context”操作符。“context”操作符可用于指示已筛选的流端点匹配包含特定域和实体名称的推文。该情况如下所示。
(context:66.961961812492148736 OR context:66.850073441055133696)
以上操作符遵循“context:<domain id>.<entity id>”格式。从上面的有效负荷示例中可以看出,域 ID 66 表示“兴趣和爱好类”。实体 ID 961961812492148736 表示“招聘”实体,实体 ID 850073441055133696 表示“职位搜索”实体。有关完整的域列表,请查看推文注释文档,里面有一张包含 50 多个域名的表格。
处理好运算符解释后,规则的第 2 部分将匹配包含实体名称“招聘”或“职位搜索”的推文。
概括地说,将此规则的两个部分合在一起,它将匹配包含关键词“开发者”或“工程师”和关键词“远程”的推文,但前提是这些推文也包含实体名称“招聘”或“职位搜索”
发现新的音乐视频
如果你需要新的音乐视频建议,你可以从使用一条简单的规则开始,该规则匹配包含关键词“歌曲”和“YouTube”的
推文。此外,你还希望获得实际链接到外部视频的推文
song youtube has:links
仔细观察这个推文的有效负荷,你会发现它有一些注释,可以帮助你匹配更相关的推文。请注意注释的实体标签为“流行音乐”,域名为“音乐风格”。
"context_annotations": [
{
"domain": {
"id": "10",
"name": "Person",
"description": "Named people in the world like Nelson Mandela"
},
"entity": {
"id": "871815676998033408",
"name": "Ally Brooke",
"description": "Ally Brooke"
}
},
{
"domain": {
"id": "54",
"name": "Musician",
"description": "A musician in the world, like Adele or Bob Dylan"
},
"entity": {
"id": "871815676998033408",
"name": "Ally Brooke",
"description": "Ally Brooke"
}
},
{
"domain": {
"id": "55",
"name": "Music Genre",
"description": "A category for a musical style, like Pop, Rock, or Rap"
},
"entity": {
"id": "810938279801470977",
"name": "Pop",
"description": "Pop"
}
}
],
为了改进这条规则,并缩小你的推文范围来使其更具相关性,你可以按如下所示更新规则。
song youtube has:links context:55.810938279801470977
这将缩小范围,仅搜索使用“音乐风格”域标签和“流行音乐”实体标签标记的推文,从而使你使用的原始规则更进一步。
学习个人理财和储蓄
最后一个例子,假设你对学习个人理财感兴趣,想要更好地管理你的支出和储蓄。同样,你只想搜索包含提供更多信息的在线文章链接的原创推文。
如果你只添加以下规则,就会经历与前面类似的过程,只会将包含“个人”、“金融”和“储蓄”字词的推文发送给你。
“personal finance savings”
查看其中某个推文有效负荷,嵌套的注释中包含了一个关于个人理财的实体标签,它将帮助你把搜索范围缩小到相关性最高的推文
"annotations": {
"context": [
{
"context_domain_id": 66,
"context_domain_id_str": "66",
"context_domain_name": "Interests and Hobbies Category",
"context_domain_description": "A grouping of interests and hobbies entities, like Novelty Food or Destinations",
"context_entity_id": 847888632711061504,
"context_entity_id_str": "847888632711061504",
"context_entity_name": "Personal finance",
"context_entity_description": "Personal finance"
},
结论
通过使用已筛选的流端点将公开可用的推文流式传输到你的服务器和注释,你创建了一个应用来更轻松地围绕感兴趣的话题展示推文。已筛选的流端点以推文的形式为你提供了大量的数据,而推文注释则可帮助你在这些数据中找到所需信息。
你是否发现了使用此应用的其他有趣方法?请在 Twitter 上关注我,并发送推文告诉我。我使用了 Twitter API 以外的多个库来制作本教程,但你可能有不同的需求和要求,所以应评估这些工具是否适合你。
在 GitHub 上查看该应用的在线演示。
后续步骤