显示并绘制文本文档中a~z出现的次数

这是《Learning Processing》第十八章“数据输入”的练习18-10:遍历一个来自URL的txt文本(本练习内我使用的是http://processing.org/download/revisions.txt)中的文字,统计它们在字母表a~z中出现的频率,并显示为柱状比例图(当然你也可以将其表示为其他可视化效果)。如图

虽然还没读到Ben Fry写的<可视化数据>,但是这不就是一个活生生的有趣的练习吗?

在编写之初,首先我想到的是用什么类型的文本来比较。我们知道文本可以由两种类型被p5表述:String和char。

对于字母表,很好办,我们可以很方便地以char或String或字符串数组的类型得到它。
以字符串显示:

String alphabet = "abcdefghijklmnopqrstuvwxyz";

以字符顺序显示:

for(int i=0; i < alphabet.length(); i++) {
  char letter = alphabet.charAt(i);
}

以字符串数组显示:

String alphabet = "a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z"
String[] letter = split(alphabet, ",");

对于那份网络txt,我们可以使用loadStrings()将其载入为一个数组,再使用join()将其中的元素组合为一个长句,再用splitTokens()剔除其中我们不需要的标点后重新将其分配入一个新的数组(由于是仅仅关乎于字母的问题,因此不做这一步也没问题,估计仅仅与运算速度有关吧,但是我发现两种方式得到的最后数字有细微的差别,目前不清楚是什么原因造成的)。

// 按txt顺序显示
char tletter;
String delimiters = " ,.?!;:[]+/()\"-#=_'";

String url = "http://processing.org/download/revisions.txt";
String[] rawtext = loadStrings(url);
String everything = join(rawtext, "" );
String[] revisions = splitTokens(everything,delimiters);
String onelongstring = join(revisions,"");

for (int i = 2000; i < onelongstring.length(); i++ ) {
  tletter = onelongstring.charAt(i);
  print(tletter);
}

在这里,我们需要注意,各种函数返回的数据类型,因为String只能和String比,char只能和char比,在比较之前,我们必须要弄清楚我们的变量是什么类型的:
loadStrings()——返回字符串数组;
join()——返回一个字符串;
split()、splitTokens()——返回字符串数组;
charAt()——返回字符(char)。

在进一步的思考中,我们会发现很难将txt中的文字由整合的一个无分隔符的长字符串拆分为一个个单个字母的字符串,尝试以下异想天开的代码失败(返回内存不足错误):

String delimiters = " ,.?!;:[]+/()\"-#=_'";
String url = "http://processing.org/download/revisions.txt";
String[] rawtext = loadStrings(url);
String everything = join(rawtext, "" );
String[] revisions = splitTokens(everything,delimiters);
String onelongstring = join(revisions,"");
// 事实证明下面这关键一步无法执行
// 意味着我们无法得到txt中单个字母组成的字符串数组
String[] letter = split(onelongstring,"");

于是自然而然的,放弃做字符串比较的尝试。现在的唯一选择就是比较char。于是想到在draw()里边这么写:

// 遍历字母表字符串的每一个字母
letter = alphabet.charAt(counter);
counter++;

// 初始化每个字母出现的计数
int total = 0;
// 遍历txt字符串中的每个字母,同时与字母表中的每个字母比较
// 如果相同,增加相同次数total的值
// 我们从txt的第两千个字符开始数,以此加快程序运行的速度
for(int i=2000;i < everything.length();i++) {
  tletter = everything.charAt(i);
    if (letter==tletter) {
      total++;
    }
}

这里有一个关键的思维,你是每次用字母表中的一个字母遍历一次txt文档,得出其在txt中出现的次数。以此类推,a,b,c,d,e,f,g........z的进行下去(这并不是一个很高效的方式,因为每比对一个字母,都需要遍历一次txt文档,但我认为更易于初学者学习。高级的方式见下文)。
还有请注意,对比两个字符串是否“相等”可以使用equals()函数,而对比两个char,只能使用“==”。

下一步,用得到的total来显示柱状图。
1)在每根柱子上方显示对应字母:

int x; // x轴位置
fill(150-total/10); // 出现次数越高,字母颜色越深
textAlign(CENTER); // 居中显示文本
text(letter,x+width/26/2,height-total/4-4); // 在柱子上方的统一位置居中显示文本

2)显示柱状图:

stroke(0); // 设置边框颜色为黑
fill(total/6,total/5,total/6); // 出现次数越多的字母,对应柱子的颜色越浅
rect(x,height-total/4,width/26,total/4); // // 出现次数越多的字母,对应柱子的高度越高
x+=width/26; // 依size()的宽度平均画26根柱子

最后不要忘记,在显示完z之后,停止draw()的循环,否则这个程序将会永远的跑下去。

if(counter==26){
  noLoop();
}

全部代码(没有做分隔符剔除工作,你可以取消那几行注释来对比两者运行效率和结果):

PFont f;
int x;
int counter = 0;
char tletter;
char letter;
String alphabet;
String everything;
//String onelongstring;
//String delimiters = " ,.?!;:[]+/()\"-#=_";

void setup() {
  size(520,320);
  background(255);
  f = createFont("Arial", 16);
  textFont(f);

  alphabet = "abcdefghijklmnopqrstuvwxyz";
  String url = "http://processing.org/download/revisions.txt";
  String[] rawtext = loadStrings(url);
  everything = join(rawtext, "" );
  //String[] revisions = splitTokens(everything,delimiters);
  //onelongstring = join(revisions,"");
}

void draw() {
  letter = alphabet.charAt(counter);
  counter++;

  int total = 0;
  for (int i=2000; i < everything.length(); i++) {
    tletter = everything.charAt(i);
    if (letter==tletter) {
      total++;
    }
  }
  print(total+" ");
  
  fill(150-total/10);
  textAlign(CENTER);
  text(letter,x+width/26/2,height-total/4-4);
  stroke(0);
  fill(total/6,total/5,total/6);
  rect(x,height-total/4,width/26,total/4);
  x+=width/26;
  
  if(counter==26){
    noLoop();
  }
}

控制台返回total的值:

543 282 401 475 1199 163 565 491 836 8 69 317 193 681 814 415 6 710 823 798 389 149 263 84 94 4

水平有限,不对、不足、代码冗余之处欢迎留言或来信指教。

不走寻常路
下边是做相同的事更高级的代码,出自豆瓣p5小组老陶。让作为p5初学者的我大有天外飞仙之感,放这里供大家参考学习借鉴养眼,与我做的效果不同,他一次性显示全部直方:

PFont f;

String letters;
String onelongstring;
String delimiters = " ,.?!;:[]+/()\"-#=_";

int[] frequency = new int[26];

void setup() {
  size(520,350);
  background(255);
  f = createFont("Arial", 16);
  textFont(f);
  initialize();
  stats();
  histoAll();
}

void draw() {
}

void stats(){ // 计数字母在txt中出现的次数
  for(int iter = 0; iter < onelongstring.length(); iter++)
    for(int iter1 = 0; iter1 < letters.length(); iter1++)
      if (onelongstring.charAt(iter) == letters.charAt(iter1))
        frequency[iter1]++;

  for(int iter = 0; iter < frequency.length; iter++)
    print(frequency[iter]+" ");
}

void initialize(){ // 获取文本并加入一个字符串
  letters = "abcdefghijklmnopqrstuvwxyz";
  String url = "http://processing.org/download/revisions.txt";
  String[] rawtext = loadStrings(url);
  String everything = join(rawtext, "" );
  String[] revisions = splitTokens(everything,delimiters);
  onelongstring = join(revisions,"");
}

void histo(int ithLetter){ // 为一个字母画柱状图
  fill(150-frequency[ithLetter]/10);
  textAlign(CENTER);
  text(letters.charAt(ithLetter),ithLetter*20+10,height-frequency[ithLetter]/4-4);
  stroke(0);
  fill(frequency[ithLetter]/6,frequency[ithLetter]/5,frequency[ithLetter]/6);
  rect(ithLetter*20,height-frequency[ithLetter]/4,20,frequency[ithLetter]/4);
}

void histoAll(){ // 为所有字母画柱状图
  for(int iter = 0; iter < frequency.length; iter++)
    histo(iter);
}

Daniel的做法
因为书本网站上没有这一题的答案,因此我给Daniel写了邮件询问,以下是他给我的答案,对比我的做法,有几点不同:

  • 他将整合后的长文本字符串用toLowerCase()全部降为了小写,使得统计的结果更准确。
  • 他用a~z的ASCII码(数字)替代了我繁琐的从a输入到z的过程,并巧妙地将它们映射到0-25的范围内(数组索引)(其实这个我也试过,但后来由于控制力差、容易混淆,还是放弃了..)。
  • 他引入了最大计数(maxCount)的概念,设置如果字母表数组的第i个字母大于maxCount,则将这个值赋给数组中的第i个字母。这个挺聪明的。
PFont f; // A variable to hold onto a font

String all;
int[] letterCounts = new int[26];
int maxCount = 0;

void setup() {
  size(390,200);
  // Load the font
  f = createFont("Georgia",16,true);
  String url = "http://processing.org/download/revisions.txt";
  String[] rawtext = loadStrings(url);
  all = join(rawtext," ");
  // 将全部字符降为小写 
  all = all.toLowerCase();


  for (int i = 0; i < all.length(); i++) {
    // 遍历每个字符
    char c = all.charAt(i);
    // 如果是a-z的话
    if (c > 96 && c < 123) {
      // 减去97 ('a'的ASCII码)
      // 这一步将字母映射到0-25的范围内(关键一步)
      int index = c - 97;
      // 通过增加索引增加计数
      letterCounts[index]++;
    } 
  }

  // 找到字母计数的最大值
  // 我们用它来给柱状图赋值
  for (int i = 0; i < letterCounts.length; i++) {
    if (letterCounts[i] > maxCount) {
      maxCount = letterCounts[i];
    }
  }
}

void draw() {
  background(255);
  textFont(f);
  fill(0);
  
  // 画一根柱子
  for (int x = 0; x < letterCounts.length; x++) {
    // 高度基于count
    float h = 180*(letterCounts[x] / (float)maxCount);
    stroke(0);
    strokeWeight(1);
    fill(175);
    rect(x*15,height-h,15,h); 
    fill(0);
    textAlign(CENTER);
    text((char)(x+97),x*15+15/2.0,height-h-5);
  }
}
Be Sociable, Share!

《显示并绘制文本文档中a~z出现的次数》有一个想法

发表评论

电子邮件地址不会被公开。 必填项已用*标注