数据
Daniel Shiffman
本教程将学习字符串和绘图文本教程的结尾部分,并研究如何使用字符串对象作为读取和写入数据的基础。我们将首先学习更复杂的方法来操作字符串,搜索字符串,将它们切碎,并将它们连接在一起。之后,我们将看到这些技能如何允许我们使用来自数据源(如文本文件、web页面、xml提要和第三方api)的输入,并在数据可视化领域迈出一步。
字串操作
在字符串和绘图文本中,我们讨论了Java字符串中可用的一些基本函数,如charAt()、toUpperCase()、equals()和length()。这些函数记录在字符串的处理引用页上。不过,为了执行一些更高级的数据解析技术,我们需要研究Java API中记录的一些额外的字符串操作函数。
让我们仔细看看以下两个字符串函数:indexOf()和substring()。
indexOf()定位字符串中的字符序列。它接受一个参数-一个搜索字符串-并返回一个数值,该数值对应于要搜索的字符串对象中搜索字符串的第一个匹配项。
String search = "def";
String toBeSearched = "abcdefghi";
int index = toBeSearched.indexOf(search); // 本例中index的值为3。
字符串就像数组一样,第一个字符是索引号0,最后一个字符是字符串的长度减去1。如果找不到搜索字符串,indexOf()将返回-1。这是一个不错的选择,因为-1不是字符串本身的合法索引值,因此可以指示“未找到”。字符串或数组中没有负索引。
在字符串中找到搜索短语后,我们可能希望将字符串的一部分分离出来,并将其保存在其他变量中。字符串的一部分称为子字符串,子字符串由substring()函数生成,该函数接受两个参数:开始索引和结束索引。substring()返回两个索引之间的子字符串。
String alphabet=“abcdefghi”;
String sub=alphabet.substring(3,6);//String sub现在是“def”。
注意,子字符串从指定的开始索引(第一个参数)开始,扩展到结束索引(第二个参数)减去1的字符。我知道,我知道。把子字符串从开始索引一直带到结束索引不是更容易吗?虽然这一点最初看起来可能是真的,但实际上在结束索引减一处停下来是非常方便的。例如,如果您想创建一个扩展到字符串末尾的子字符串,只需一直转到string.length()。此外,当结束索引减去一个标记结束时,子字符串的长度很容易计算为结束索引减去开始索引。
分裂和连接字符串
在字符串和绘图文本中,我们看到了如何使用“+”运算符将字符串连接在一起(称为“连接”)。让我们用一个使用连接从键盘获取用户输入的示例来回顾一下。
PFont f;
// 用于存储当前正在键入的文本的变量
String typing = "";
// 返回时存储已保存文本的变量
String saved = "";
void setup() {
size(300, 200);
f = createFont("Arial", 16);
}
void draw() {
background(255);
int indent = 25;
// 设置文本的字体和填充
textFont(f);
fill(0);
// 显示所有内容
text("Click in this sketch and type. \nHit return to save what you typed.", indent, 40);
text(typing, indent, 90);
text(saved, indent, 130);
}
void keyPressed() {
// 如果按下返回键,则保存字符串并将其清除
if (key == '\n') {
saved = typing;
typing = "";
// 否则,连接字符串
} else {
typing = typing + key;
}
}
处理有两个额外的函数,使连接字符串(或者相反,将它们分开)变得容易。在涉及解析来自文件或web的数据的草图中,您可能以字符串数组或长字符串的形式获得该数据。根据您想要完成的任务,了解如何在这两种存储模式之间切换是很有用的。这就是这两个新函数split()和join()的用武之地" ←→ {"one", "long", "string", "or" ,"array", "of", "strings"}
让我们看看split()函数。split()根据一个称为分隔符的拆分字符,将较长的字符串分隔成字符串数组。它有两个参数,要拆分的字符串对象和分隔符。(分隔符可以是单个字符或字符串。)在下面的代码中,句点未设置为分隔符,因此将包含在数组的最后一个字符串中:“dog”。请注意print array()如何用于将数组的内容及其相应的索引打印到消息控制台。
// 基于空格拆分字符串
String spaceswords = "The quick brown fox jumps over the lazy dog.";
String[] list = split(spaceswords, " ");
printArray(list);
下面是一个使用逗号作为分隔符的示例(这次传入单个字符:',')
// 基于逗号拆分字符串
String commaswords = "The,quick,brown,fox,jumps,over,the,lazy,dog.";
String[] list = split(commaswords, ",");
printArray(list);
如果要使用多个分隔符拆分文本,则必须使用处理函数splitTokens()。splitTokens()的工作方式与split()相同,但有一个例外:在传递的字符串中出现的任何字符都限定为分隔符。)在下面的代码中,句点被指定为分隔符,因此不会包含在数组“dog”的最后一个字符串中。
// Splitting a String based on multiple delimiters
String stuff = "hats & apples, cars + phones % elephants dog.";
String[] list = splitTokens(stuff, " &,+." );
printArray(list);
如果要拆分字符串中的数字,则可以使用Processing的int()函数将结果数组转换为整数数组。字符串中的数字不是数字,除非先转换它们,否则不能用于数学运算。
// 计算字符串中数字列表的和
String numbers = "8,67,5,309";
// 将字符串数组转换为int数组
int[] list = int(split(numbers, ','));
int sum = 0;
for (int i = 0; i<list.length; i++ ) {
sum = sum + list[i];
}
println(sum);
split()的反面是join()。join()接受一个字符串数组并将它们连接到一个长字符串对象中。函数还接受两个参数,一个是要联接的数组,另一个是分隔符。分隔符可以是单个字符或字符串。
考虑以下数组:
String[] lines = {"It", "was", "a", "dark", "and", "stormy", "night."};
//使用“+”运算符和for循环,可以将字符串连接在一起,如下所示:
// 手动连接
String onelongstring = "";
for (int i = 0; i < lines.length; i++) {
onelongstring = onelongstring + lines[i] + " ";
}
//然而,join()函数允许您绕过此过程,仅在一行代码中获得相同的结果。
// 使用处理的join()
String onelongstring = join(lines, " ");
处理数据
数据可以来自许多不同的地方:网站、新闻源、电子表格、数据库等等。假设你决定画一张世界花卉的地图。在线搜索之后,您可能会找到一个PDF版本的花卉百科全书、一个花卉属的电子表格、一个花卉数据的JSON提要、一个提供地理位置lat/lon坐标的REST API、或者一些有人将美丽的花卉照片放在一起的网页等等。不可避免地会出现这样的问题:“我发现了所有这些数据;我应该使用哪些数据,以及如何将它们进行处理?”
如果你真的很幸运的话,你可能会发现一个处理库直接把数据和代码交给你。也许答案是下载这个库并编写一些代码,比如:
import flowers.*;
void setup() {
FlowerDatabase fdb = new FlowerDatabase();
Flower sunflower = fdb.findFlower("sunflower");
float h = sunflower.getAverageHeight();
}
在这种情况下,其他人已经为你做了所有的工作。他们收集了有关花卉的数据,并构建了一个处理库,其中包含一组函数,这些函数以易于理解的格式向您提供数据。遗憾的是,这个图书馆还不存在,但有些图书馆确实存在。例如,YahooWeather是Marcel Schwittlick为您从Yahoo获取天气数据的库,允许您编写诸如weather.getWindSpeed()或weather.getSunrise()等代码。在使用图书馆的情况下还有很多工作要做。
让我们换个场景。假设你想建立一个大联盟棒球统计的可视化。你找不到一个处理库来提供数据,但是你可以在mlb.com上看到你想要的一切。如果数据是在线的,并且您的web浏览器可以显示它,那么您不应该能够在处理中获取数据吗?将数据从一个应用程序(如web应用程序)传递到另一个应用程序(如处理草图)是软件工程中一次又一次出现的事情。实现这一点的一种方法是API或“应用程序编程接口”:两个计算机程序可以相互通信的一种方法。现在您知道了这一点,您可能决定在线搜索“MLB API”。不幸的是,mlb.com没有通过API提供数据。在这种情况下,您必须加载网站本身的原始源并手动搜索您要查找的数据。尽管有可能,但考虑到阅读HTML源代码和解析它的程序算法所需的大量时间,这种解决方案是不太可取的。
每种获取数据的方法都有自己的挑战。处理库的易用性取决于是否存在清晰的文档和示例。但在几乎所有的情况下,如果你能找到为计算机设计的格式(电子表格、XML、JSON等)的数据,你就能在一天中节省一些时间出去走走。
另一个值得一提的关于使用数据的注意事项。在开发涉及数据源(如数据可视化)的应用程序时,有时使用“伪”或“伪”数据进行开发是很有用的。您不希望在调试数据检索过程的同时解决与绘图算法相关的问题。按照我一步一个脚印的口头禅,一旦程序的肉用虚拟数据完成,那么您就可以只关注如何从真实源中检索实际数据。当你尝试一个可视化的想法并在以后连接真实的数据时,你总是可以在代码中使用随机或硬编码的数字。
使用文本文件
让我们从使用最简单的数据检索方法开始:从文本文件中读取。文本文件可以用作一个非常简单的数据库(您可以存储程序的设置、高分列表、图形的数字等)或模拟更复杂的数据源。
为了创建文本文件,可以使用任何简单的文本编辑器。Windows记事本或Mac OS X TextEdit就可以了;只需确保将文件格式化为“纯文本”即可。建议使用“.txt”扩展名命名文本文件,以避免混淆。与图像文件一样,这些文本文件应该放在草图的“数据”目录中,以便处理草图识别它们。
文本文件就位后,使用Processing的loadStrings()函数将文件内容读入字符串数组。文件中的每一行文本都成为数组中的一个单独元素。
// 此代码将打印源文本文件中的所有行。
String[] lines = loadStrings("file.txt");
println("There are " + lines.length + " lines.");
printArray(lines);
要运行代码,请创建一个名为“file.txt”的文本文件,在该文件中键入一堆行,并将其放置在草图的数据目录中。
文件中的文本可用于生成简单的可视化效果。获取以下数据文件。
可视化此数据的结果如下所示。
从文本文件中绘制逗号分隔的数字
int[] data;
void setup() {
size(200, 200);
// 以字符串形式加载文本文件
String[] stuff = loadStrings("data.csv");
// 使用“,”作为分隔符将字符串转换为整数数组
data = int(split(stuff[0], ','));
}
void draw() {
background(255);
stroke(0);
for (int i = 0; i<data.length; i++) {
// 使用整数数组设置每个矩形的颜色和高度。
rect(i*20, 0, 20, data[i]);
}
noLoop();
}
研究如何使用split()解析csv文件是一个很好的学习练习。实际上,处理csv文件(可以很容易地从电子表格软件(如Google docs)生成)是一种常见的活动,处理过程有一个称为Table的完整内置类来为您处理解析。
表格数据
表格由一组行和列组成,也称为“表格数据”。如果你曾经使用过电子表格,这就是表格数据。处理的loadTable()函数接受逗号分隔(csv)或制表符分隔(tsv)值,并自动将内容放入一个表对象中,该对象将数据存储在列和行中。这比使用split()手动解析大型数据文件要方便得多。它的工作原理如下。假设您有一个数据文件,如下所示:
而不是说:
String[] stuff = loadStrings("data.csv");
我们现在可以说:
Table table = loadTable("data.csv");
现在我错过了一个重要的细节。再次查看上面的data.csv文本文件。请注意,第一行文本不是数据本身,而是标题行。此行包括描述每个后续行中包含的数据的标签。好消息是,如果在加载表时传入“header”选项,那么处理过程可以自动为您解释和存储头。(除了“header”之外,还可以指定其他选项。例如,如果您的文件名为data.txt,但是逗号分隔的数据,则可以传入选项“csv”。如果它也有一个标题行,那么您可以指定两个选项,如“header,csv”)。选项的完整列表可以在loadTable()文档页上找到。
Table table = loadTable("data.csv", "header");
现在加载了表,我可以展示如何获取单个数据片段或遍历整个表。让我们看看可视化为网格的数据。
在上面的网格中,您可以看到数据是按行和列组织的。因此,访问数据的一种方法是通过数值行和列位置请求一个值(零是第一行或第一列)。这类似于在给定的(x,y)位置访问像素颜色,尽管在这种情况下,y位置(行)优先。下面的代码请求给定位置(行、列)的一段数据。
int val1 = table.getInt(2, 1); // val现在的值是235
float val2 = table.getFloat(3, 2); // val2现在的值是44.758068
String s = table.getString(0, 3); // s现在有“快乐”的价值
虽然数字索引有时很有用,但通常按列名访问每一段数据会更方便。例如,我可以从表中提取特定行。
TableRow row = table.getRow(2); // 获取第三行(索引2)
请注意,在上面的代码行中,表对象引用整个数据表,而TableRow对象处理表中的单个数据行。
一旦有了TableRow对象,就可以从部分或全部列中请求数据。
int x = row.getInt("x"); // //x的值是273
int y = row.getInt("y"); // y的值是235
float d = row.getFloat("diameter"); // d值为61.14072
String s = row.getString("name"); // s有“欢乐”的价值
方法getRow()从表中返回一行。如果要获取所有行并对其进行迭代,可以在循环中执行此操作,计数器一次访问一行。可以使用getRowCount()检索可用行的总数。
for (int i = 0; i<table.getRowCount(); i++) {
// 在循环中一次访问表的每一行。
TableRow row = table.getRow(i);
float x = row.getFloat("x");
float y = row.getFloat("y");
float d = row.getFloat("diameter");
String n = row.getString("name");
// 处理每一行的数据
}
如果要在表中搜索选定数量的行,可以使用findRows()和matchRows()进行搜索。
除了被读取之外,还可以在运行草图时动态更改或创建表对象。可以调整单元格值、删除行和添加新行。例如,要在单元格中设置新值,有函数setInt()、setFloat()和setString()。
row.setInt("x", mouseX); // 在给定的表行中将列“x”的值更新为mouseX。
要向表中添加新行,只需调用addRow()方法并设置每列的值。
//创建新行。
TableRow row = table.addRow();
//设置该行中所有列的值。
row.setFloat("x", mouseX);
row.setFloat("y", mouseY);
row.setFloat("diameter", random(40, 80));
row.setString("name", "new label");
要删除行,只需调用removeow()方法并传入要删除的行的数字索引。例如,当表的大小大于10行时,下面的代码将删除第一行。
// 如果表有10行以上
if (table.getRowCount()>10) {
//删除第一行(索引0)。
table.removeRow(0);
}
下面的示例将以上所有代码放在一起。请注意表中的每一行是如何包含气泡对象的数据的
// 表对象中的数据将填充气泡对象数组
Table table;
Bubble[] bubbles;
void setup() {
size(480, 360);
loadData();
}
void draw() {
background(255);
// 显示所有气泡
for (int i = 0; i<bubbles.length; i++) {
bubbles[i].display();
}
}
void loadData() {
//“header”表示文件有header行。数组的大小
//然后由表中的行数确定。
table = loadTable("data.csv", "header");
bubbles = new Bubble[table.getRowCount()];
for (int i = 0; i<table.getRowCount(); i++) {
// 遍历表中的所有行。
TableRow row = table.getRow(i);
//通过字段的列名(或索引)访问字段。
float x = row.getFloat("x");
float y = row.getFloat("y");
float d = row.getFloat("diameter");
String n = row.getString("name");
// 从每一行的数据中创建一个气泡对象。
bubbles[i] = new Bubble(x, y, d, n);
}
}
void mousePressed() {
// 当按下鼠标时,创建一个新行并为该行的每一列设置值。
TableRow row = table.addRow();
row.setFloat("x", mouseX);
row.setFloat("y", mouseY);
row.setFloat("diameter", random(40, 80));
row.setString("name", "Blah");
// 如果表有超过10行,请删除最早的行。
if (table.getRowCount()>10) {
table.removeRow(0);
}
//这会将表写回原始CSV文件
//并重新加载文件,以便绘制的内容匹配。
saveTable(table, "data/data.csv");
loadData();
}
//这个简单的Bubble类向窗口绘制一个圆
//并在鼠标悬停时显示文本标签。
class Bubble {
float x, y;
float diameter;
String name;
boolean over = false;
// 创建气泡
Bubble(float tempX, float tempY, float tempD, String s) {
x = tempX;
y = tempY;
diameter = tempD;
name = s;
}
// 检查鼠标是否在气泡上方
void rollover(float px, float py) {
float d = dist(px, py, x, y);
if (d<diameter/2) {
over = true;
} else {
over = false;
}
}
// 显示气泡
void display() {
stroke(0);
strokeWeight(2);
noFill();
ellipse(x, y, diameter, diameter);
if (over) {
fill(0);
textAlign(CENTER);
text(name, x, y+diameter/2+20);
}
}
}
这里,将给定点和圆的中心之间的距离与该圆的半径进行比较,如图所示:
在下面的代码中,函数根据点(mx,my)是否在圆内返回布尔值(true或false)。注意半径是如何等于直径的一半。
boolean rollover(int mx, int my) {
if (dist(mx, my, x, y)<diameter/2) {
return true;
} else {
return false;
}
}
非标准格式的数据
如果您的数据不是标准格式(如表),那么您如何处理它呢?loadStrings()的一个很好的特性是,除了从文件中提取文本外,还可以获取URL。例如:
String[] lines = loadStrings("http://www.yahoo.com");
当您将URL路径发送到loadStrings()时,您将获得所请求网页的原始HTML(超文本标记语言)源。这和从浏览器菜单选项中选择“查看源”时显示的内容是一样的。你不需要是一个HTML专家来理解这一节,但是如果你对HTML一点都不熟悉,你可能想阅读http://en.wikipedia.org/wiki/HTML。
与处理草图中使用的特殊格式文本文件的逗号分隔数据不同,将生成的原始HTML存储在字符串数组(每个元素表示源代码中的一行)中是不实际的。将数组转换成一个长字符串可以使事情变得简单一些。正如您在本章前面看到的,这可以使用join()实现。
String onelongstring = join(lines, " ");
从网页中提取原始HTML时,很可能不需要所有源代码,而只需要其中的一小部分。也许你在寻找天气信息、股票报价或新闻标题。您可以利用所学的文本操作函数-indexOf()、substring()和length()-在一大块文本中查找数据片段。例如,以下字符串对象:
String stuff = "Number of apples:62. Boy, do I like apples or what!";
假设我想从上面的文本中提取出苹果的数量。我的算法如下:
1。找到子字符串“apples:”的结尾,称之为start。
2。找到“苹果”后面的第一个句号:结束。
3。在开始和结束之间生成字符的子字符串。
4。将字符串转换为数字(如果我想这样使用的话)。
在代码中,这看起来像:
int start = stuff.indexOf("apples:" ) + 7; // STEP 1
//字符串结尾的索引可以通过
//搜索该字符串并添加其长度(此处为8)。
int end = stuff.indexOf(".", start); // STEP 2
String apples = stuff.substring(start, end); // STEP 3
int apple_no = int(apples); // STEP 4
上面的代码可以做到这一点,但我应该更加小心,以确保我不会遇到任何错误,如果我没有找到我正在搜索的字符串。我可以添加一些错误检查并将代码泛化为函数:
//返回两个子字符串之间的子字符串的函数。
//如果找不到结束“tag”的开头,则函数返回空字符串。
String giveMeTextBetween(String s, String startTag, String endTag) {
// 查找开始标记的索引
int startIndex = s.indexOf(startTag);
// If I don't find anything
if (startIndex == -1) {
return "";
}
// 移到开始标记的结尾
startIndex += startTag.length();
// 查找结束标记的索引
int endIndex = s.indexOf(endTag, startIndex);
// 如果我找不到结束标记,
if (endIndex == -1) {
return "";
}
// 返回中间的文本
return s.substring(startIndex, endIndex);
}
使用此技术,您可以从处理内部连接到网站,并获取要在草图中使用的数据。例如,你可以从nytimes.com上阅读HTML源代码,查找今天的头条新闻,在finance.yahoo.com上搜索股票报价,计算“花”这个词出现在你最喜欢的博客上的次数,等等。然而,HTML是一个丑陋、可怕的地方,其格式不一致的页面很难进行有效的逆向工程和解析。更不用说公司经常更改网页的源代码了,所以我在写这一段时可能会做的任何例子都可能在你阅读这一段时被打断。
为了从web上获取数据,XML(可扩展标记语言)或JSON(JavaScript对象表示法)提要将被证明更可靠、更易于解析。XML和JSON不同于HTML(它的设计目的是让人们的眼睛可以看到内容),它的设计目的是让计算机可以看到内容,并促进不同系统之间的数据共享。大多数数据(新闻、天气等)都是通过这种方式提供的,我将以#初学者xml和#JSON格式查看示例。尽管不太理想,但是手动HTML解析仍然很有用,原因有两个。首先,实践文本操作技术来强化关键编程概念是无害的。但更重要的是,有时有些数据是你真正想要的,而这些数据并不是以API格式提供的,而获得这些数据的唯一方法就是使用这样一种技术。(我还应该提到正则表达式,一种在文本模式匹配方面非常强大的技术,也可以在这里使用。虽然我很喜欢regex,但不幸的是它超出了本教程的范围。)
Internet电影数据库就是一个仅以HTML形式提供的数据示例。IMDb包含按年份、类型、收视率等排序的电影信息。对于每部电影,您可以找到演员和剧组列表、情节摘要、运行时间、电影海报图像,列表将继续。但是,IMDb没有API,也不提供XML或JSON格式的数据。因此,将数据拉入处理过程需要一点侦查工作。让我们看看《肖恩羊》电影的页面
从上面的URL中查找HTML源代码,我发现了一大堆标记。
这取决于我仔细研究原始数据源,并找到我正在寻找的数据。假设我想知道电影的运行时间,并抓取电影海报图像。经过一番挖掘,我发现这部电影的长度是139分钟,如下面的HTML所列。
<div class="txt-block">
<h4 class="inline">Runtime:</h4>
<time itemprop="duration" datetime="PT139M">139 min</time>
</div>
对于任何给定的电影,运行时间本身都是可变的,但是页面的HTML结构将保持不变。因此,我可以推断,运行时间总是出现在以下两者之间:
<time itemprop="duration" datetime="PT139M"> </time>
知道数据在哪里开始和结束,我可以使用giveMeTextBetween()提取运行时间。Java中的引号标记字符串的开头或结尾。那么如何在String对象中包含实际的引号呢?答案是通过一个“转义”序列。引号可以用反斜杠括起来,后跟引号。例如:
String q = "This String has a quote \"in it";
String url = "http://www.imdb.com/title/tt0058331";
String[] lines = loadStrings(url);
//去掉数组以便搜索整个页面
String html = join(lines, " ");
// 寻找运行时间
String start = "";
String end = "";
String runningtime = giveMeTextBetween(html, start, end);
println(runningtime);
下面的代码从IMDb中检索运行时间和电影海报图像,并将其显示在屏幕上
String runningtime;
PImage poster;
void setup() {
size(300, 350);
loadData();
}
void draw() {
// 显示所有我想显示的内容
background(255);
image(poster, 10, 10, 164, 250);
fill(0);
text("Shaun the Sheep", 10, 300);
text(runningtime, 10, 320);
}
void loadData() {
String url = "http://www.imdb.com/title/tt2872750/";
//将原始HTML源代码放入字符串数组(数组中的每一行都是一个元素)。
//下一步是使用join()将数组变成一个长字符串。
String[] lines = loadStrings(url);
String html = join(lines, "");
String start = "";
String end = "";
runningtime = giveMeTextBetween(html, start, end);Searching for running time.
start = "";
// 搜索海报图像的URL。
String imgUrl = giveMeTextBetween(html, start, end);
// 现在,加载图像!
poster = loadImage(imgUrl);
}
String giveMeTextBetween(String s, String before, String after) {
//此函数返回两个子字符串(前后)之间的子字符串。
//如果找不到任何内容,则返回空字符串。
String found = "";
// 查找以前的索引
int start = s.indexOf(before);
if (start == -1) {
return "";
}
//移到开始标记的结尾
//找到“after”字符串的索引
start += before.length();
int end = s.indexOf(after, start);
if (end == -1) {
return "";
}
// 返回中间的文本
return s.substring(start, end);
}
文本分析
从URL加载文本不仅需要解析小部分信息。通过处理可以分析从新闻源、文章和演讲到整本书的大量网络文本。一个很好的来源是古腾堡计划,它提供了成千上万的公共领域的文本。分析文本的算法值得一整本书,但是让我们看看一些基本的技术。
文本一致性是一个按字母顺序排列的单词列表,这些单词与上下文信息一起出现在书或正文中。复杂的一致性可能会保留一个列表,其中列出每个单词出现的位置(如索引),以及哪些单词出现在其他单词旁边。在本例中,我将创建一个简单的协和式,它只存储单词列表和相应的计数,即它们在文本中出现的次数。一致性可用于文本分析应用,如垃圾邮件过滤或情感分析。为了完成这项任务,我将使用处理内置类IntDict。
正如前面所学,数组是变量的有序列表。数组的每个元素都有编号,并由其数字索引访问。
但是,如果不给数组的元素编号,而是给它们命名呢?这个元素叫做“Sue”,这个叫做“Bob”,这个叫做“Jane”,等等。在编程中,这种数据结构通常被称为关联数组、映射或字典。它是(键,值)对的集合。想象一下你有一本关于人们年龄的字典。当你查找“Sue”(键)时,定义或值是她的年龄,24岁。
关联数组可以非常方便地用于各种应用。例如,您可以在字典中保留学生id列表(学生姓名,id)或价格列表(产品名称,价格)。这里字典是保持一致性的完美数据结构。字典中的每一个元素都是一个与其计数成对的词。
尽管Java中有许多类用于处理高级数据结构(如maps),但Processing为您提供了一组易于使用的三个内置字典类:IntDict、FloatDict和StringDict。在所有这些类中,当值是变量(整数、浮点数或字符串)时,键始终是字符串。为了协调一致,我要用一个IntDict。
创建IntDict就像调用空构造函数一样简单。假设你想要一本字典来记录用品的库存。
IntDict inventory = new IntDict();
Values can be paired with their keys using the set() method.
// set()为字符串指定整数。
inventory.set("pencils", 10);
inventory.set("paper clips", 128);
inventory.set("pens, 16");
可以调用多种其他方法来更改与特定键关联的值。例如,如果要添加五支铅笔,可以使用add()。
inventory.add("pencils", 5); // “铅笔”的价值现在是15英镑。
协和示例的一个特别方便的方法是increment(),它将一个值添加到键的值中。
inventory.increment("pens"); // “铅笔”的价值现在是16。
要检索与特定键关联的值,请使用get()方法。
int num = inventory.get("pencils"); // num的值是16。
最后,字典可以使用sortKeys()、sortKeysReverse()、sortValues()和sortValuesReverse()方法按键(字母顺序)或值(从最小到最大或相反)排序。
协和现在变成了一个相当简单的程序来编写。我需要做的就是加载到一个文本文件中,用splitTokens()将它切碎成单词,并对文本中找到的每个单词调用IntDict上的increment()。下面的例子正是通过莎士比亚戏剧《仲夏夜之梦》的整个文本来实现这一点的,它显示了最常用单词的简单图表。
使用IntDict的文本一致性
String[] allwords;
// 任何标点符号都用作分隔符。
String delimiters = " ,.?!;:[]";
IntDict concordance;
void setup() {
size(360, 640);
// 把仲夏夜的梦装成一排弦
String url = "http://www.gutenberg.org/cache/epub/1514/pg1514.txt";
String[] rawtext = loadStrings(url);
// 将大数组作为一个长字符串连接在一起
String everything = join(rawtext, "" );
//仲夏夜之梦中所有的线都是先串成一根大绳
//然后分成一组单独的单词。
//注意splitTokens()的用法,因为我使用空格和标点符号作为分隔符。
allwords = splitTokens(everything, delimiters);
// 编一本新的空字典
concordance = new IntDict();
for (int i = 0; i<allwords.length; i++) {
//把每个单词都转换成小写很有用,
//例如,“The”和“The”都算作同一个词。
String s = allwords[i].toLowerCase();
// 对于每一个单词,增加它在字典中的数量。
concordance.increment(s);
}
// 把字典分类,使最常出现的单词排在第一位。
concordance.sortValuesReverse();
}
void draw() {
background(255);
//显示文本和单词出现的总次数
int h = 20;
//为了遍历字典中的每个单词,
//首先要一系列的钥匙。
String[] keys = concordance.keyArray();
for (int i = 0; i<height/h; i++) {
// 一次看一个键并检索其计数。
String word = keys[i];
int count = concordance.get(word);
fill(51);
// 将矩形与计数一起显示为简单图形。
rect(0, i*20, count/4, h-4);
fill(0);
text(word + ": " + count, 10+count/4, i*h+h/2);
stroke(0);
}
}
处理还包括数字和字符串列表的三个类:IntList、FloatList和StringList。换言之,如果您只需要一个单词列表(不包括它们的计数),那么可以使用StringList而不是IntDict。
如果您的数据可以通过标准格式(如XML或JSON)获得,则不再需要通过文本手动搜索单个数据片段的过程。XML旨在促进不同系统之间的数据共享,您可以使用内置的处理XML类检索该数据。
XML以树结构组织信息。让我们想象一个学生名单。每个学生都有一个身份证号码、姓名、地址、电子邮件和电话号码。每个学生的地址都有一个城市、州和邮政编码。此数据集的XML树可能如下所示:
XMl源本身(列出了两个学生)是:
<?xml version = "1.0" encoding = "UTF-8 "?>
<students>
<student>
<id>001</id>
<name>Daniel Shiffman</name>
<phone>555-555-5555</phone>
<email>daniel@shiffman.net</email>
<address>
<street>123 Processing Way</street>
<city>Loops</city>
<state>New York</state>
<zip>01234</zip>
</address>
</student>
<student>
<id>002</id>
<name>Zoog</name>
<phone>555-555-5555</phone>
<email>zoog@planetzoron.uni</email>
<address>
<street>45.3 Nebula 5</street>
<city>Boolean City</city>
<state>Booles</state>
<zip>12358</zip>
</address>
</student>
</students>
注意与面向对象编程的相似之处。可以用以下术语来考虑XML树。XML文档表示学生对象的数组。每个student对象都有多条信息、一个ID、一个名称、一个电话号码、一个电子邮件地址和一个邮件地址。邮件地址也是一个包含多个数据片段的对象,如街道、城市、州和邮政编码。
让我们来看看从一个web服务(如Yahoo Weather)提供的一些数据。这是原始XML源。(请注意,为了简化起见,我对其进行了轻微的编辑。)
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<rss version="2.0" xmlns:yweather="http://xml.weather.yahoo.com/ns/rss/1.0">
<channel>
<item>
<title>Conditions for New York, NY at 12:49 pm EDT</title>
<geo:lat>40.67</geo:lat>
<geo:long>-73.94</geo:long>
<link>http://us.rd.yahoo.com/dailynews/rss/weather/New_York__NY//link>
<pubDate>Thu, 24 Jul 2014 12:49 pm EDT</pubDate>
<yweather:condition text="Partly Cloudy" code="30" temp="76"/>
<yweather:forecast day="Thu" low="65" high="82" text="Partly Cloudy"/>
</item>
</channel>
</rss>
数据映射到树结构中,如下所示:
你可能想知道最高级的“RSS”是关于什么的。雅虎的XML天气数据是以RSS格式提供的。RSS代表“真正简单的联合”,是用于联合web内容(如新闻文章等)的标准XML格式。你可以在维基百科上阅读更多关于RSS的内容。
既然已经有了树结构的句柄,让我们看看该结构中的细节。除了第一行(它只是表示该页是XML格式的)之外,此XML文档包含一个嵌套的元素列表,每个元素都有一个开始标记,即<channel>,和一个结束标记,即<channel>。其中一些元素在标记之间有内容:
<title>Conditions for New York, NY at 12:49 pm EDT</title>
还有一些具有属性(由属性名格式化,等于引号中的属性值):
<yweather:forecast day="Thu" low="65" high="82" text="Partly Cloudy"/>
使用Processing XML类
由于XML的语法是标准化的,我当然可以使用split()、indexof()和substring()在XML源代码中找到所需的片段。不过,这里的要点是,因为XML是一种标准格式,所以我不必这样做。相反,我可以使用XML解析器。在处理过程中,可以使用内置的处理类XML解析XML。
XML xml = loadXML("http://xml.weather.yahoo.com/forecastrss?p=10003");
在这里,我现在调用loadXML()并传入XML文档的地址(URL或本地文件),而不是loadStrings()或loadTable()。XML对象表示XML树的一个元素。首次加载文档时,该XML对象始终是根元素。参考Yahoo Weather的XML树图,我通过以下路径找到当前温度:
1。树的根是RSS。
2。RSS有一个名为channel的子项。
3。通道有一个名为item的子项。
4。项有一个名为yweather:condition的子项。
5。温度作为属性temp存储在yweather:condition中。
元素的子元素通过getChild()函数访问。
//访问根元素的“channel”子元素-->
XML channel = xml.getChild("channel");
使用以下方法之一检索元素本身的内容:getContent()、getIntContent()或getFloatcContent()。getContent()用于一般用途,并始终将内容作为字符串提供给您。如果您打算将内容用作数字,处理将使用getIntContent()或getFloatcContent()为您转换它。属性也可以读取为numbers-getInt()、getFloat()-或text-getString()。
按照上面通过XML树概述的步骤1到5,我已经:
XML root = loadXML("http://xml.weather.yahoo.com/forecastrss?p=10003"); // Step 1
XML channel = root.getChild("channel"); // Step 2
XML item = channel.getChild("item"); // Step 3
XML yweather = item.getChild("yweather:condition"); // Step 4
int temperature = yweather.getInt("temp"); // Step 5
但是,这有点冗长,因此可以压缩成一行(或三行,如下所示)代码。
XML root = loadXML(http://xml.weather.yahoo.com/forecastrss?p=10003);
XML forecast =
root.getChild("channel").getChild("item").getChild("yweather:condition"); // Steps 2-4
int temperature = forecast.getInt("temp");
//最后,上面的第二行代码可以进一步压缩为:
XML forecast = xml.getChild("channel/item/yweather:condition"); // Steps 2-4
下面是通过解析Yahoo的XML提要为多个邮政编码检索天气数据的示例中的上述代码。
解析Yahoo天气XML
int temperature = 0; //温度以数字形式存储,天气的描述以字符串形式存储。
String weather = "";
// 邮政编码
String zip = "10003";
void setup() {
size(200, 200);
// XML文档的URL
String url = "http://xml.weather.yahoo.com/forecastrss?p=" + zip;
// 加载XML文档
XML xml = loadXML(url);
// 在这里,我获取所需的XML元素。
XML forecast = xml.getChild("channel/item/yweather:forecast");
// 然后从XML元素中提取属性。
temperature = forecast.getInt("high");
weather = forecast.getString("text");
}
void draw() {
background(255);
fill(0);
// Display all the stuff I want to display
text("Zip code: " + zip, 10, 50);
text("Today’s high: " + temperature, 10, 70);
text("Forecast: " + weather, 10, 90);
}
其他有用的XML函数包括:
•hasChildren()-检查元素是否有任何子元素
•getChildren()-返回包含所有子元素的数组
•getAttributeCount()-统计指定元素的属性数
•hasAttribute()-检查元素是否具有指定的属性
在本例中,我通过子节点的名称(即“channel”、“item”等)访问它们,但是也可以通过索引(从零开始,与数组相同)对它们进行数字访问。这在循环遍历子列表时很方便,就像我在遍历表中的行时对表格数据所做的那样。
在前面,我们使用一个表来存储与气泡对象相关的信息。XML文档也可以以同样的方式使用。以下是气泡对象的XML树的可能解决方案:
<?xml version="1.0" encoding="UTF-8"?>
<bubbles>
<bubble>
<position x="160" y="103"/>
<diameter>43.19838</diameter>
<label>Happy</label>
</bubble>
<bubble>
<position x="372" y="137"/>
<diameter>52.42526</diameter>
<label>Sad</label>
</bubble>
<bubble>
<position x="273" y="235"/>
<diameter>61.14072</diameter>
<label>Joyous</label>
</bubble>
<bubble>
<position x="121" y="179"/>
<diameter>44.758068</diameter>
<label>Melancholy</label>
</bubble>
</bubbles>
我可以使用getChildren()检索<bubble>元素的数组,并从每个元素中创建一个bubble对象。下面是一个使用前面(不包括在下面)的相同Bubble类的示例。新代码是粗体的。
使用处理的XML类
//气泡对象数组
Bubble[] bubbles;
// XML对象
XML xml;
void setup() {
size(480, 360);
loadData();
}
void loadData() {
// 加载XML文件
xml = loadXML("data.xml");
// 获取所有名为“bubble”的子节点
XML[] children = xml.getChildren("bubble");
bubbles = new Bubble[children.length];
// 气泡数组的大小由名为“Bubble”的XML元素总数决定
for (int i = 0; i < bubbles.length; i++) {
XML positionElement = children[i].getChild("position");
//position元素有两个属性:“x”和“y”。
//属性可以通过getInt()和getFloat()以整数或浮点形式访问。
float x = positionElement.getInt("x");
float y = positionElement.getInt("y");
//diameter是名为“diameter”的子级的内容
XML diameterElement = children[i].getChild("diameter");
float diameter = diameterElement.getFloatContent();
//但是,请注意,对于XML节点的内容,
//我通过getIntContent()和getFloatContent()检索。
//label是名为“label”的子项的内容
XML labelElement = children[i].getChild("label");
String label = labelElement.getContent();
// 使Bubble对象从数据中读取
bubbles[i] = new Bubble(x, y, diameter, label);
}
}
void draw() {
background(255);
// 显示所有气泡
for (int i = 0; i < bubbles.length; i++) {
bubbles[i].display();
bubbles[i].rollover(mouseX, mouseY);
}
}
除了loadXML()之外,处理还包括一个saveXML()函数,用于将XML文件写入草图文件夹。您可以通过使用addChild()或removeChild()添加或删除元素来修改XML树本身,也可以使用setContent()、setIntContent()、setFloatContent()、setString()、setInt()和setFloat()修改元素或属性的内容。
JSON格式
另一种越来越流行的数据交换格式是JSON(发音类似于Jason),它代表JavaScript对象表示法。它的设计基于JavaScript编程语言中对象的语法(最常用于在web应用程序之间传递数据),但已经变得相当普遍,而且与语言无关。虽然您不需要知道任何关于JavaScript的知识就可以进行处理,但是在学习JavaScript语法的同时了解一些基本的JavaScript语法也不会有什么坏处。
JSON是XML的替代品,可以用类似于树的方式查看数据。所有JSON数据都有以下两种方式:对象或数组。幸运的是,您已经知道这两个概念,只需要学习一种新的语法来编码它们。
我们先来看看JSON对象。JSON对象就像一个没有函数的处理对象。它只是一个带有名称和值(或“名称/值对”)的变量集合。例如,下面是描述一个人的JSON数据:
{
"name":"Olympia",
// 每个名称/值对用逗号分隔。
"age":3,
"height":96.5,
"state":"giggling"
}
Notice how this maps closely to classes in Processing.
class Person {
String name;
int age;
float height;
String state;
}
//JSON中没有类,只有对象文本本身。一个对象也可以包含另一个对象,作为其自身的一部分。
{
"name":"Olympia",
"age":3,
"height":96.5,
"state":"giggling",
// “brother”的值是一个包含两个名称/值对的对象。
"brother":{
"name":"Elias",
"age":6
}
}
//在XML中,前面的JSON数据如下所示(为了简单起见,我避免使用XML属性)。
<xml version="1.0" encoding="UTF-8"?>
<person>
<name>Olympia</name>
<age>3</age>
<height>96.5</height>
<state>giggling</state>
<brother>
<name>Elias</name>
<age>6</age>
</brother>
</person>
多个JSON对象可以作为数组出现在数据中。就像在处理中使用的数组一样,JSON数组只是值(原语或对象)的列表。但是,语法与方括号不同,方括号表示使用数组,而不是卷曲的数组。下面是一个简单的JSON整数数组:
[1, 7, 8, 9, 10, 13, 15]
//您可能会发现数组是对象的一部分。
{
"name":"Olympia",
// “最喜欢的颜色”的值是一个字符串数组。
"favorite colors":[
"purple",
"blue",
"pink"
]
}
或者一系列的物体。例如,下面是JSON中气泡的外观。请注意,这个JSON数据是如何组织为一个JSON对象“bubbles”的,它包含一个JSON对象的JSON数组bubbles。返回以与相同数据的CSV和XML版本进行比较。
{
"bubbles":[
{
"position":{
"x":160,
"y":103
},
"diameter":43.19838,
"label":"Happy"
},
{
"position":{
"x":372,
"y":137
},
"diameter":52.42526,
"label":"Sad"
},
{
"position":{
"x":273,
"y":235
},
"diameter":61.14072,
"label":"Joyous"
}
]
}
JSONObject和JSONArray
既然我已经介绍了JSON的语法,我就可以在处理中使用数据了。在处理过程中使用JSON有一点棘手,那就是我必须以不同的方式处理对象和数组。对于XML,我只需要一个XML类,其中包含我需要的所有解析功能。使用JSON,我有两个类:JSONObject和JSONArray,在解析过程中,我必须认真选择要使用的类。
第一步只是使用loadJSONObject()或loadJSONArray()加载数据。但是哪一个?我必须查看JSON文件的根、对象或数组。这可能有点棘手。让我们看看这两个JSON示例:
Sample 1:
[
{
"name":"Elias"
},
{
"name":"Olympia"
}
]
Sample 2:
{
"names":[
{
"name":"Elias"
},
{
"name":"Olympia"
}
]
}
看上面两个样本有多相似。它们都包含完全相同的数据,两个名字“Elias”和“Olympia”,但是,数据的格式有一个非常关键的区别:第一个字符。是“[”还是“{”?答案将决定您是加载数组(“[”)还是加载对象(“{”)。JSON对象以大括号开头,而JSON数组以方括号开头。
JSONObject json = loadJSONObject("file.json");
JSONArray json = JSONArray("file.json");
通常,即使数据最终被组织为一个对象数组(例如“bubble”对象数组),JSON数据的根元素将是包含该数组的对象。让我们再来看一次泡沫数据。
{
"bubbles":[
{
"position":{
"x":160,
"y":103
},
"diameter":43.19838,
"label":"Happy"
},
{
"position":{
"x":372,
"y":137
},
"diameter":52.42526,
"label":"Sad"
}
]
}
使用上面的方法,我首先加载一个对象,然后从该对象中拉出数组。
JSONObject json = loadJSONObject("data.json");
JSONArray bubbleData = json.getJSONArray("bubbles");
与XML一样,元素的数据是通过其名称访问的,在本例中是“bubbles”,然而,使用JSONArray,数组的每个元素都是通过其数字索引检索的。下面的代码迭代JSONArray。
for (int i = 0; i < bubbleData.size(); i++) {
JSONObject bubble = bubbleData.getJSONObject(i);
}
当您从JSONObject中查找特定的数据片段(如整数或字符串)时,这些函数与XML属性的函数相同。下面的代码从bubble对象中获取position对象,然后从position对象中获取x和y作为整数。直径和标签可直接从气泡对象获得。
JSONObject position = bubble.getJSONObject("position");
int x = position.getInt("x");
int y = position.getInt("y");
float diameter = bubble.getFloat("diameter");
String label = bubble.getString("label");
综上所述,我现在可以制作一个bubbles示例的JSON版本(不需要修改draw()函数和Bubble类)
使用Processing的JSON类
// 气泡对象数组
Bubble[] bubbles;
void setup() {
size(480, 360);
loadData();
}
void loadData() {
// 加载JSON文件并获取数组。
JSONObject json = loadJSONObject("data.json");
JSONArray bubbleData = json.getJSONArray("bubbles");
// 气泡对象数组的大小由JSON数组的长度决定。
bubbles = new Bubble[bubbleData.size()];
for (int i = 0; i<bubbleData.size(); i++) {
// 遍历数组,一次获取一个JSON对象。
JSONObject bubble = bubbleData.getJSONObject(i);
// 获取位置对象
JSONObject position = bubble.getJSONObject("position");
// 从JSON对象“position”获取(x,y)
int x = position.getInt("x");
int y = position.getInt("y");
// 获取直径和标签
float diameter = bubble.getFloat("diameter");
String label = bubble.getString("label");
// 将气泡对象放入数组中。
bubbles[i] = new Bubble(x, y, diameter, label);
}
}
螺纹
如您所见,各种加载函数loadStrings()、loadTable()、loadXML()和loadJSONObject()可用于从URL检索数据。尽管如此,除非草图在setup()过程中只需要加载一次数据,否则可能会出现问题。例如,考虑一个草图,它每五分钟从一个XML提要中获取一次AAPL股票的价格。每次调用loadXML()时,草图将在等待接收数据时暂停。任何动画都会结巴。这是因为这些加载函数是“阻塞”的,换言之,草图将在这行代码处等待,直到loadXML()完成其任务。对于本地数据文件,这是非常快的。尽管如此,处理中的URL请求(称为“HTTP请求”)是同步的,这意味着您的草图在继续之前等待服务器的响应。谁知道要怎么做?没有人;你受服务器的摆布!
这个问题的答案在于线程的概念。到现在为止,您已经非常熟悉编写一个遵循特定步骤序列的程序的想法了—先是setup()然后是draw(),一遍又一遍!线程也是一系列具有开始、中间和结束的步骤。处理草图是一个线程,通常称为动画线程。但是,其他线程的序列可以独立于主动画循环运行。实际上,一次可以启动任意数量的线程,它们都将并发运行。
处理经常这样做,例如使用captureEvent()和movieEvent()等库函数。这些函数是由后台运行的不同线程触发的,当它们有要报告的内容时,它们会向处理发出警报。当需要执行耗时太长且会减慢主动画的帧速率(例如从网络中获取数据)的任务时,这非常有用。在这里,您需要在不同的线程中异步处理请求。如果这个线程被卡住或有错误,整个程序不会停止,因为这个错误只会停止那个单独的线程,而不是主动画循环。
编写自己的线程可能是一项复杂的工作,涉及扩展Java线程类。但是,thread()方法是在处理过程中实现简单线程的一种快速而肮脏的方法。通过传入与草图中其他地方声明的函数名匹配的字符串,处理将在单独的线程中执行该函数。让我们来看看这是如何工作的。
void setup() {
thread("someFunction");
}
void draw() {
}
void someFunction() {
//当通过调用时,此函数将作为线程运行
//线程(“someFunction”)因为它是在设置!
}
函数的作用是:接收一个字符串作为参数。字符串应该与要作为线程运行的函数的名称匹配。在上面的例子中是“someFunction”。
让我们看一个更实际的例子。对于经常更改的数据示例,我将使用time.jsontest.com,它提供当前时间(以毫秒为单位)。虽然我可以从系统时钟中检索到这个,但这对于演示随时间变化的连续请求数据非常有效。不知道线程,我的第一反应可能是说:
void draw() {
// 代码将在此停止并等待接收数据,然后继续。
JSONObject json = loadJSONObject("http://time.jsontest.com/");
String time = json.getString("time");
text(time, 40, 100);
}
这将给我通过draw()的每个周期的当前时间。但是,如果我检查帧速率,我会注意到草图的运行速度非常慢(它只需要画一个字符串!)。在这里,将解析代码作为一个单独的线程调用将有很大帮助。
String time = "";
void draw() {
// 现在,当requestData()在单独的线程中执行时,代码将移到下一行。
thread("requestData");
text(time, 40, 100);
}
void requestData() {
JSONObject json = loadJSONObject("http://time.jsontest.com/");
time = json.getString("time");
}
逻辑是相同的,只是我没有直接在draw()中请求数据,而是作为单独的线程执行该请求。注意,我没有在requestData()中绘制任何图形。这是关键,因为在单独线程上运行的代码中执行绘图函数可能会导致与主动画线程(即draw())的冲突,从而导致奇怪的行为和错误。
在上面的例子中,我可能不想每秒请求60次数据(默认帧速率)。相反,我可以使用Timer类,每秒请求一次数据。下面是一个完整的例子,通过添加动画来显示draw()从不结巴。
螺纹
Timer timer = new Timer(1000);
String time = "";
void setup() {
size(200, 200);
// 首先在线程中异步请求数据。
thread("retrieveData");
timer.start();
}
void draw() {
background(255);
// 每隔一秒钟,就提出一个新的要求。
if (timer.isFinished()) {
retrieveData();
// 重新启动计时器。
timer.start();
}
fill(0);
text(time, 40, 100);
translate(20, 100);
stroke(0);
//画一个小动画来演示Draw()循环从不暂停。
rotate(frameCount*0.04);
for (int i = 0; i<10; i++) {
rotate(radians(36));
line(5, 0, 10, 0);
}
}
// 获取数据
void retrieveData() {
JSONObject json = loadJSONObject("http://time.jsontest.com/");
time = json.getString("time");
}
class Timer {
int savedTime;
boolean running = false;
int totalTime;
Timer(int tempTotalTime) {
totalTime = tempTotalTime;
}
void start() {
running = true;
savedTime = millis();
}
boolean isFinished() {
int passedTime = millis() - savedTime;
if (running && passedTime > totalTime) {
running = false;
return true;
} else {
return false;
}
}
}
APIs
鉴于本章大部分内容都是关于api中的数据,所以我把这一节称为“api”有点傻。不过,值得花点时间停下来反思一下。是什么使一些东西成为一个API,而不是你发现的一些数据,以及你在使用API时可能遇到的一些陷阱?
如我所述,API(应用程序编程接口)是一个应用程序可以通过它访问另一个应用程序的服务的接口。它们可以有多种形式。正如您前面看到的,Openweathermap.org是一个API,它提供JSON、XML和HTML格式的数据。使这个服务成为一个API的关键元素正是它提供的;openweathermap.org在生活中的唯一目的是为您提供它的数据。不仅提供它,还允许您以特定格式查询特定数据。让我们看一个简短的示例查询列表。
http://api.openweathermap.org/data/2.5/weather?lat=35&lon=139A 要求提供特定纬度和经度的当前天气数据.
http://api.openweathermap.org/data/2.5/forecast/daily?q=London&mode=xml&units=metric&cnt=7&lang=zh_cnA 要求提供一份7天的伦敦天气预报,格式为XML,公制单位,中文。
http://api.openweathermap.org/data/2.5/history/station?id=5091&type=dayA 请求给定气象站的历史数据。
关于openweathermap.org需要注意的一点是,它不要求您告诉API任何关于您自己的信息。你只需发送一个请求到一个URL并获取数据。但是,其他api要求您注册并获取访问令牌。纽约时报API就是这样一个例子。在处理请求之前,您需要访问纽约时报开发人员站点并请求一个API密钥。一旦你有了那个密钥,你就可以把它作为一个字符串存储在你的代码中。
/
//这不是真的钥匙
String apiKey = "40e2es0b3ca44563f9c62aeded4431dc:12:51913116";
您还需要知道API本身的URL是什么。此信息在开发人员网站上有文档记录,但此处仅作简单介绍:
String url = "http://api.nytimes.com/svc/search/v2/articlesearch.json";
最后,你必须告诉API你要找的是什么。这是通过一个“查询字符串”完成的,该字符串是一个名称-值对序列,描述了用与号连接的查询的参数。这个函数类似于在处理过程中如何将参数传递给函数。如果要从search()函数中搜索术语“processing”,可以说:
search("processing");
在这里,API充当函数调用,您通过查询字符串向它发送参数。下面是一个简单的例子,要求列出包含“处理”一词的最古老的文章(其中最古老的是1852年5月12日)。
// The name/value pairs that configure the API query are: (q,processing) and (sort,oldest)
String query = "?q=processing&sort=oldest";
这不仅仅是猜测。找出如何组合查询字符串需要阅读API的文档。对于《纽约时报》,这一切都在《纽约时报》的开发者网站上有所描述。查询完成后,可以将所有部分连接在一起,并将其传递给loadJSONObject()。下面是一个简单显示最新标题的小例子。
纽约时报API
void setup() {
size(200, 200);
String apiKey = "40e2ea0b3ca44563f9c62aeded0431dc:18:51513116";
String url = "http://api.nytimes.com/svc/search/v2/articlesearch.json";
String query = "?q=processing&sort=newest";
//进行API查询
//在这里,我通过将URL与API键和查询字符串连接来格式化对API的调用。
JSONObject json = loadJSONObject(url+query+"&api-key="+apiKey);
String headline = json.getJSONObject("response").getJSONArray("docs").
//从结果中获取一个标题。
getJSONObject(0).getJSONObject("headline").getString("main");
background(255);
fill(0);
text(headline, 10, 10, 180, 190);
}
有些API需要比API访问密钥更深层的身份验证。例如,Twitter使用名为“OAuth”的身份验证协议来提供对其数据的访问。编写OAuth应用程序不仅需要将字符串传递到请求中,而且超出了本教程的范围。但是,在这些情况下,如果幸运的话,您可以找到一个处理库来为您处理所有身份验证。有几个api可以通过库直接用于处理,您可以在libraires参考页面的“数据/协议”部分找到它们的列表,以获取一些想法。例如,Temboo提供了一个处理库,为您处理OAuth,并在处理过程中提供对许多api(包括Twitter)的直接访问。使用Temboo,您可以编写如下代码:
// Temboo充当了你和Twitter之间的中间人,所以首先你只需使用Temboo进行身份验证。
TembooSession session = new TembooSession("ACCOUNT_NAME", "APP_NAME", "APP_KEY");
Tweets tweets = new Tweets(session);
// 然后,您可以配置一个查询发送到Twitter本身并获取结果。
tweets.setCredential("your-twitter-name");
tweetsChoreo.setQuery("arugula");
TweetsResultSet tweetsResults = tweets.run();
JSONObject searchResults = parseJSONObject(tweetsResults.getResponse());
JSONArray statuses = searchResults.getJSONArray("statuses");
// 最后,您可以搜索结果并获取一条tweet。
JSONObject tweet = statuses.getJSONObject(0);
String tweetText = tweet.getString("text");