内容


构建一个文本可视化和分析应用程序

使用 Eclipse 和其他开源软件开发一个词汇波应用程序

Comments

本文可视化是一个功能强大到令人膛目结舌的方法,它可以快速确定特定文本所指的内容。作为一个副产品,可视化也提供了一种方法进行文本实时分析。本文将介绍如何使用开源工具和库开发文本可视化和分析软件。本文的应用程序比较和分析了两个具有相同或类似内容的文本,支持用户获取关于这些文本及其内容的新见解。

您构建的应用程序是基于词汇云 可视化的。词汇云可视化可分析特定文本,并将其单词按出现频率进行排序。对单词进行排名,然后根据其排名来设置字体大小。排名最高的单词用最大字体显示。可视化单词布局可以变化,但它通常看起来像一片云,如 所示:

图 1. 一个词汇云
使用 IBM Many Eyes 从 IBM 2011 年度报告中的总裁和 CEO 致辞生成的云词汇
使用 IBM Many Eyes 从 IBM 2011 年度报告中的总裁和 CEO 致辞生成的云词汇

为了生成 所示的词汇云,我使用了 IBM Many Eyes(参阅 参考资料)来分析 IBM 2011 年度报告中的总裁和 CEO 致辞。

在本文中,应用程序生成了一个词汇波,即一个形状像波浪的文本可视化。词汇波将排名最高的单词放在左上角。 展示了一个示例,可视化与 相同的本文:

图 2. 样例词汇波
可视化与图 1 相同文本的词汇波
可视化与图 1 相同文本的词汇波

可视化文本显示了排名较高的词汇。基于可视化的本文分析,假设高排名单词有一个重要性层次结构。当两个文本可视化同时显示时,需要进行比较。如果两个文本的内容是相同的或相似的,比较尤为重要。例如,对那些描述同一行业中两个企业的策略的文本进行比较,就会显示这两家公司关注重点的相似之处和不同之处。

是两个文本的最终比较草图。第一个文本可视化位于顶部,第二个位于底部。排名较高的单词位于左侧。

图 3. 可视化文本比较草图
两个文本的最终比较草图
两个文本的最终比较草图

本文及其代码将向您展示如何:

  • 实用开源工具和库开发一个命令行应用程序来实现文本可视化,并进行文本比较。
  • 使用词汇波可视化创建一个特定文本的可视化(类似 )。
  • 将两个可视化结合到同一图像中进行比较和分析。
  • 从可视化中创建一个可视的、引人注目的视频。

本文并没有深入探索开发的细节,所以熟悉 Java™ 开发和 Eclipse 编程模型对读者很有帮助。所有这些应用程序源代码(应用程序的 Eclipse 项目和准备部署更新的网站)均可 下载

我首先将会简要介绍一下开发环境组件。

开发环境

开发环境由各种开源工具和库组成,结合使用它们可使得创建词汇波和视频更加容易。应用代码本身相对较短。工具和库能够处理大量的图像、视频和命令行界面。

Eclipse

当您将 Eclipse 作为一个 IDE 使用时,可以利用命令行程序。

命令行程序

命令行程序是一个第三方 Eclipse 富客户端平台 (RCP) 应用程序,用于创建命令行应用程序。它使用 Eclipse 插件编写模型,包括特性更新网站(参阅 参考资料,获取 Eclipse 概念的链接)。命令行程序为命令行应用程序提供了基础架构,例如,命令行解析、帮助命令、日志和其他基础功能。我们计划为用户开发可视化和分析软件,将它作为一个命令行程序的扩展命令

Processing 和 WordCram

Processing 既是一种语言也是一个开发环境,用于创建图像和动画。关于 Processing 的介绍,请参阅 “使用 Processing 的数据可视化,第 1 部分”。Processing 容易学习和使用,您也可以使用它完成了不起的工作(参阅 参考资料,获取 OpenProcessing.org 中的示例链接)。WordCram 是一个针对 Processing 的可自定义的库,用于创建词汇云。

Monte Media Library

Monte Media Library 是一个简单而又优秀的开源库,用于视频和图像的写入、读取和控制。Monte Media Library 的作者 Werner Randelshofer 在其网站上宣布,尽管 Monte Media Library 是他个人的研究成果,但他还是决定将其公开,以便其他人可以使用。与其他提供的库不同(这些库往往很繁琐且需要本地代码),Monte Media Library 易于使用并且是纯 Java 代码。

开发应用程序

现在您已经对工具有了一定的了解,可以开始开发名为 TextVisualizationAndAnalysis 的应用程序,并向命令行程序添加一个名为 Compare 扩展命令 。

您需要提供一个命令行程序项目,以便可为其开发扩展。下载 CLP_Plugin_Main 项目,并将它导入 Eclipse 工作空间。这个项目包括命令行程序的源代码,并提供扩展点来在您自己的项目中开发扩展。

创建一个插件项目

为了开发一个 Command Line Program 扩展 ,与其他 Eclipse 扩展项目类似,首先要创建一个插件项目:

  1. 创建一个新的插件项目,并输入 TextVisualizationAndAnalysis 作为项目名称。
  2. 选择 3.5 或更高版本 作为 Eclipse 版本,单击 Next
  3. 输入 0.0.1 作为版本号,清空(如果已选择)This plug-in will make contributions to the UI,其他字段保留默认值。单击 Next 到达最终屏幕。
  4. 清空(如果已选择)Create a plug-in using one of the templates,然后单击 Finish 创建项目。
  5. 新的项目立刻就会出现在 Eclipse 工作区,并且 plugin.xml 文件打开在屏幕上。(如果没有,且 plugin.xml 并不存在于项目目录中,请打开 META-INF/MANIFEST.MF 文件)。
  6. 打开 Dependencies 选项卡,在 Required Plug-ins 部分中将 com.softabar.clpp.application 添加到项目。这个插件是您导入 Eclipse 工作空间的 Command Line Program Eclipse 项目附带。
  7. 打开 Extensions 选项卡,添加一个扩展 com.softabar.clpp.application.command 的扩展点,这个点由 Command Line Program 提供。
  8. 输入扩展详情。在 name 字段输入 compare。在 help 字段输入 Visualize and compare two text files。在 class 字段输入 textvisualizationandanalysis.Compare Command。请查看 :
    图 4. 命令行程序的新命令
    Extension Element Details 对话框屏幕截图
    Extension Element Details 对话框屏幕截图

稍后创建一个类。另外,如果需要的话,请将新的信息添加到插件。

添加所需的库

为了创建 Compare 命令,请将这些库添加到插件中:

  • Processing 的 core.jar
  • WordCram 的 WordCram.jar、jsoup-1.3.3.jar 和(可选)cue.language.jar
  • Monte Media Library 的 monte-cc.jar

要添加这些库:

  1. 在插件项目中创建一个库目录,并将 JAR 文件添加到该目录。
  2. 添加库目录到插件构建,打开 plugin.xml 并选择 构建 选项卡。要选择 Binary Build 对话框中的库目录,请单击其复选框,如 所示:
    图 5. 插件 Binary Build
    Binary Build 对话框屏幕截图
    Binary Build 对话框屏幕截图
  3. 打开 Runtime 选项卡,在 Classpath 对话中,选择 Add,将库添加到插件类路径。 显示了添加所有库(不包含 cue.language.jar)的类路径对话框:
    图 6. 将库添加到插件类路径
    类路径对话框的屏幕截图
    类路径对话框的屏幕截图
  4. 保存 plugin.xml 文件。

编写代码

现在可以编写该命令的实际代码了。该命令的源代码位于后面的清单中(包和输入语句刻意省略)。您还必须添加一些信息到 plugin.xml 文件中,这将在浏览了代码清单之后向您展示。

包含类的声明和变量:

清单 1. 类的声明和变量
public class CompareCommand extends PApplet implements ICommand, WordColorer {
    private static final long serialVersionUID = -188003470351748783L;
    private static CLPPLogger logger = CLPPLogger.getLogger(CompareCommand.class);
    private static boolean testing = true;
    private static boolean processingDone = false;
    private static String fileName;
    private static String outputDir;
    private static File inputTextFile1;
    private static File inputTextFile2;
    private static boolean drawTitle;
    private static String title1;
    private static String title2;
    private static boolean createVideo = false;
    private static int frameRate;
    private int frameWidth = 1280;
    private int frameHeight = 720;
    private int maxWords = 50;
    // font to be used
    private String font = "c:/windows/fonts/georgiab.ttf";
    private WordCram wordCram1;
    private PGraphics buffer1;
    private WordCram wordCram2;
    private PGraphics buffer2;
    // colors used in word waves

    private int[] colors = { 0x22992A, 0x9C3434, 0x257CCD, 0x950C9E };

在 中,可以扩展 processing.core.PApplet 类来利用 Processing 方法。然后,可以实现两个接口:com.softabar.clpp.program.ICommandwordcram.WordColorercom.softabar.clpp.program.ICommand 接口是针对命令行程序的;当命令运行时,由命令行程序调用它。wordcram.WordColorer 接口处理词汇云(或者波)的颜色。部分变量被声明为 static,因为它们在执行过程中必须对 Processing 代码可视。

显示了 ICommand 接口的 execute() 方法:

清单 2. execute() 方法
public void execute(CommandLine commandLine, IProgramContext programContext) {
    testing = false;
    String inputFileStr = commandLine.getOptionValue("input1");
    inputTextFile1 = new File(inputFileStr);
    if (!inputTextFile1.exists()) {
      Output.error(inputFileStr + " does not exist.");
      return;
    }
    inputFileStr = commandLine.getOptionValue("input2");
    inputTextFile2 = new File(inputFileStr);
    if (!inputTextFile2.exists()) {
      Output.error(inputFileStr + " does not exist.");
      return;
    }
    drawTitle = commandLine.hasOption("title");
    fileName = commandLine.getOptionValue("filename", "results");
    outputDir = commandLine.getOptionValue("outputdir", ".");
    if (!outputDir.endsWith("/")) {
      outputDir = outputDir + "/";
    }
    title1 = commandLine.getOptionValue("title1", inputTextFile1.getName());
    title2 = commandLine.getOptionValue("title2", inputTextFile2.getName());
    String frate = commandLine.getOptionValue("framerate", "5");
    frameRate = Integer.parseInt(frate);
    createVideo = commandLine.hasOption("video");
    Output.println("Generating comparison word waves...");
    generateWordCloud();
    createVideo();
}

在 中,execute() 方法接收了一个 org.apache.commons.cli.CommandLine 实例作为参数;这个参数能够获取命令的选项。稍后,将支持的选项添加到 plugin.xml。在获取和设置选项后,调用 generateWordCloud() 方法创建一个词汇云。然后,通过调用 createVideo() 方法创建视频。

显示了 generateWordCloud() 方法,它在 processing.PApplet 类中调用了 main 方法,一直等到 Processing/WordCram 完成词汇云呈现:

清单 3. generateWordCloud() 方法
private void generateWordCloud() {
    try {
      main(new String[] { "--present", getClass().getName() });
      // wait until word wave is finished
      while (!processingDone) {
        try {
          Thread.sleep(0, 1);
        } catch (InterruptedException e) {
        }
      }
    } catch (Exception e) {
      logger.error(e.toString(), e);
      Output.error(e.toString());
    }
}

显示了生成词汇云的设置:

清单 4. setup() 方法
public void setup() {
    if (testing) {
      logger.debug("testing");
      inputTextFile1 = new File("c:/CocaCola_MissionVisionValues.txt");
      inputTextFile2 = new File("c:/PepsiCo_MissionVisionValues.txt");
      outputDir = "c:/output/";
      fileName = "results";
      drawTitle = true;
      createVideo = false;
      title1 = "Coke";// inputTextFile1.getName();
      title2 = "Pepsi";// inputTextFile2.getName();
    }
    logger.debug("frameWidth: {}, frameHeight: {}", frameWidth, frameHeight);
    size(frameWidth, frameHeight);
    background(255);
    logger.debug("setup");
    // create buffer to draw the upper word wave
    buffer1 = createGraphics(frameWidth, frameHeight / 2, JAVA2D);
    buffer1.beginDraw();
    buffer1.background(255);
    wordCram1 = initWordCram(inputTextFile1, buffer1);
    // create buffer to draw the lower word wave
    buffer2 = createGraphics(frameWidth, frameHeight / 2, JAVA2D);
    buffer2.beginDraw();
    buffer2.background(255);
    wordCram2 = initWordCram(inputTextFile2, buffer2);
    // set up font for titles
    fill(0);
    textFont(createFont(font, 40));
    textAlign(CENTER);
}

在 中,setup() 方法在开始绘制之前就被 Processing 调用。您可以在这里初始化屏幕尺寸和背景颜色,创建图形缓存,在缓存中绘制由 WordCram 生成的词汇云。该代码还指定了 Eclipse 中的测试变量。

WordCram 库负责生成词汇云,在 中初始化。您可以指定词汇云的各个方面,比如位置和颜色。WordCram 提供了一些占位符(例如,这里使用的波),以及一些词汇颜色标记。在这里,您可以使用自己的颜色标记。

清单 5. initWordCram() 方法
private WordCram initWordCram(File inputFile, PGraphics buffer) {
    WordCram wordCram = new WordCram(this);
    if (buffer != null) {
      wordCram = wordCram.withCustomCanvas(buffer);
    }
    // initialize WordCram with specified placer, text file,
    // colorer, and other details
    wordCram = wordCram.fromTextFile(inputFile.getPath());
    wordCram = wordCram.withColorer(this);
    wordCram = wordCram.withWordPadding(2);
    wordCram = wordCram.withPlacer(Placers.wave());
    wordCram = wordCram.withAngler(Anglers.randomBetween(-0.15f, 0.15f));
    wordCram = wordCram.withFont(createFont(font, 40));
    wordCram = wordCram.sizedByWeight(7, 52);
    wordCram = wordCram.maxNumberOfWordsToDraw(maxWords);
    return wordCram;
}

显示了 draw() 方法生成的词汇云:

清单 6. draw() 方法
public void draw() {
    logger.debug("Draw..");
    // draw one word at a time
    if (wordCram1.hasMore()) {
      // draw next word in upper word wave
      wordCram1.drawNext();
      buffer1.endDraw();
      image(buffer1, 0, 0);

      buffer1.beginDraw();
      // draw next word in lower word wave
      wordCram2.drawNext();
      buffer2.endDraw();
      image(buffer2, 0, frameHeight / 2);
      buffer2.beginDraw();
    } else {
      buffer1.endDraw();
      buffer2.endDraw();
      image(buffer1, 0, 0);
      image(buffer2, 0, frameHeight / 2);
      listSkippedWords(inputTextFile1.getName(), wordCram1);
      listSkippedWords(inputTextFile2.getName(), wordCram2);
      noLoop();
      // if no video then
      // save only last frame result
      if (!createVideo) {
        saveFrame(outputDir + fileName + ".png");
      }
      // for testing purposes within Eclipse
      if (testing) {
        createVideo();
      }
      processingDone = true;
    }
    if (drawTitle) {
      color(0);
      textSize(20);
      text(title1, 0, 0, frameWidth, 50);
      text(title2, 0, frameHeight / 2, frameWidth, 50);
    }
    if (createVideo) {
      saveFrame(outputDir + fileName + "-####.png");
    }
}

一个词汇一次生成一个词汇云。 使用了两个不同的词汇云的缓存,这两个缓存都可以绘制在屏幕上。生成词汇云之后,停止绘制并列出所有漏掉的词汇。如果您要创建一个视频,可以将各个帧保存为图像;这些图像用于生成视频。

在 中,listSkippedWords() 方法用于打印一个词汇清单,这些词汇不能置于可视化场景中:

清单 7. listSkippedWords() 方法
private void listSkippedWords(String desc, WordCram wordcram) {
    Word[] words = wordcram.getWords();
    int skipped = 0;
    // for each word check whether it was skipped
    List<String> skippedWords = new Vector<String>();
    for (Word word : words) {
      if (word.wasSkipped()) {
        int skippedBecause = word.wasSkippedBecause();
        if (skippedBecause == WordCram.NO_SPACE) {
          // increase number of skipped words
          // only if no space for word
          skippedWords.add(word.word);
          skipped++;
        }
      }
    }
    // print number of skipped words
    if (skipped > 0) {
      logger.debug("skippedWords: {}, {}", desc, skippedWords);
      Output.println(desc + ": no space for " + skipped + " words: "
          + skippedWords);
    }
}

如果返回任何漏掉的词汇,那么这很可能就意味着可视化遗漏了一些重要词汇。这可能会误导之后基于可视化的分析,甚至导致失败。如果返回任何漏掉的词汇,那么可以再次运行程序,尝试将所有的词汇放置到词汇云中。

中的 colorFor() 方法实现了 WordCram 的 WordColorer 接口。这个方法能够从预定义的清单中随机返回选中的颜色。

清单 8. colorFor() 方法
public int colorFor(Word w) {
    int index = (int) random(colors.length);
    int colorHex = colors[index];
    int r = colorHex >> 16;
    int g = (colorHex >> 8) & 0x0000ff;
    int b = colorHex & 0x0000ff;
    logger.debug("R: {}, G: {}, B: {}", new Integer[] { r, g, b });
    return color(r, g, b);
}

createVideo() 如 所示,最终方法在 中被调用:

清单 9. createVideo() 方法
private void createVideo() {
    if (createVideo) {
      Output.println("Generating video...");
      try {
        File aviFile = new File(outputDir, fileName + ".avi");
        // format specifies the type of video we are creating
        // video encoding, frame rate, and size is specified here
        Format format = new Format(org.monte.media.FormatKeys.EncodingKey,
            org.monte.media.VideoFormatKeys.ENCODING_AVI_PNG,
            org.monte.media.VideoFormatKeys.DepthKey, 24,
            org.monte.media.FormatKeys.MediaTypeKey, MediaType.VIDEO,
            org.monte.media.FormatKeys.FrameRateKey,
            new Rational(frameRate, 1),
            org.monte.media.VideoFormatKeys.WidthKey, width,
            org.monte.media.VideoFormatKeys.HeightKey, height);
        logger.debug("Framerate: {}", frameRate);
        AVIWriter out = null;
        try {
          // create new AVI writer with previously specified format
          out = new AVIWriter(aviFile);
          out.addTrack(format);
          int i = 1;
          // read the first image file
          String frameFileName = String.format(fileName + "-%04d.png", i);
          File frameFile = new File(outputDir, frameFileName);
          while (frameFile.exists()) {
            logger.debug("Frame filename: {}", frameFileName);
            // while frame images exist
            // create a Buffer and write it to AVI writer
            Buffer buf = new Buffer();
            buf.format = new Format(org.monte.media.FormatKeys.EncodingKey,
                org.monte.media.VideoFormatKeys.ENCODING_BUFFERED_IMAGE,
                org.monte.media.VideoFormatKeys.DataClassKey,
                BufferedImage.class).append(format);
            buf.sampleDuration = format.get(
                org.monte.media.FormatKeys.FrameRateKey).inverse();
            buf.data = ImageIO.read(frameFile);
            out.write(0, buf);
            // read next frame image
            i++;
            frameFileName = String.format(fileName + "-%04d.png", i);
            frameFile = new File(outputDir, frameFileName);
          }
        } finally {
          if (out != null) {
            out.close();
          }
          Output.println("Done.");
        }
      } catch (IOException e) {
        logger.error(e.toString(), e);
        Output.error(e.toString());
      }
    }
}

从 中可以看出,从图像创建视频比较简单。您只需要指定一个视频格式,然后一次一个图像创建视频即可。

将命令行选项添加到 plugin.xml

现在,代码部分已经完成了。在部署之前,添加一个命令行选项到 plugin.xml 文件,这样命令行程序就可以对它们进行解析:

  1. 打开 plugin.xml,并打开 Extensions 标签。
  2. 选择 compare-command,单击右键然后选择 New > option
  3. input1input2video 选项是应用程序必需的。input1input2 指定两个您想要比较的文本文件的名称。video 生成视频(如果省略,就不生成视频)。 显示了 input1 选项定义(Text 1 file to compare),以及其他您可以随意添加的选项:
图 7. 命令选项
Extensions 标签中 Extension Element 详情对话框的屏幕截图
Extensions 标签中 Extension Element 详情对话框的屏幕截图

命令行程序的 Compare 命令现已完成,可以对其进行部署。

部署应用程序

为了使用 TextVisualizationAndAnalysis 应用程序,您必须将其部署到 Command Line Program。为 TextVisualizationAndAnalysis 插件创建一个特性和更新网站,然后将其安装到 Command Line Program 上。

创建一个特性

Eclipse 中的特性是可安装且可更新的插件集合。命令行程序使用标准 Eclipse 特性功能来支持新扩展命令的安装、更新和删除。为 Compare 命令创建特性的快速步骤是:

  1. 创建一个特性项目,将其命名为 TextVisualizationAndAnalysisFeature
  2. 将版本号设置为 0.0.1,然后单击 Next
  3. 选择 TextVisualizationAndAnalysis 插件,然后单击 Finish

生成一个更新站点

更新站点包括您可以安装在应用中的特性。更新站点可以是一个本地目录或者远程网络服务器。要为 TextVisualizationAndAnalysis 程序生成更新站点,请执行以下操作:

  1. 创建一个更新站点项目,将其命名为 TextVisualizationAndAnalysisUpdateSite
  2. 在 Update Site Map 页面中,选择 Managing the Site 下的 Add Feature 来添加 TextVisualizationAndAnalysisFeature,如 所示:
    图 8. 更新站点路线图页面
    Update Site Map 图页面的屏幕截图
    Update Site Map 图页面的屏幕截图
  3. 单击 Build All

安装命令

您无法运行命令本身,因为您必须将其安装在命令行程序中。命令行程序安装已经完成了。输入 admin 命令(这里假设创建的更新站点在 c:/workspace/ 目录下):

clp.cmd admin --install --dir='c:\workspace\TextVisualizationAndAnalysisUpdateSite'

运行应用程序

这里列出了一些示例命令,说明了如何运行应用程序。

生成并展示一个文本比较图像:

clp.cmd compare --input1='c:/path/file1.txt' --input2='c:/path/file2.txt'

使用输入文件名作为可视化标题:

clp.cmd compare --input1='c:/path/file1.txt' --input2='c:/path/file2.txt' --title

使用自定义标题:

clp.cmd compare --input1='c:/path/file1.txt' --input2='c:/path/file2.txt' 
   --title --title1='Text1' --title2='Text2'

生成一个图像和视频

clp.cmd compare --input1='c:/path/file1.txt' -–input2='c:/path/file2.txt' -title --video

结果和分析

现在您可以运用 TextVisualizationAndAnalysis 应用程序了。我用它来可视化两个类似的文本:同一行业的两家企业的公开价值声明:可口可乐公司和百事可乐(参阅 参考资料)。然后,我使用可视化来进行分析。假设词汇波将会可视化这两家公司认为现在和将来对它们身最为重要的事件。(为了获取源文本以及插件项目中完整的应用程序代码,请参见 下载 部分。)

可视化

我使用了前面小节 中的最后一个命令来生成图像和视频。 是两个文本的可视化比较:

图 9. 两个类似文本的可视化
可口可乐公司和百事可乐公司的价值声明可视化比较
可口可乐公司和百事可乐公司的价值声明可视化比较
static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source, Java technology
ArticleID=936875
ArticleTitle=构建一个文本可视化和分析应用程序
publish-date=07082013