added shannon_H entropy metric for each revision
[wikiq] / wikiq.c
1 /* 
2  * An XML parser for Wikipedia Data dumps.
3  * Converts XML files to tab-separated values files readable by spreadsheets
4  * and statistical packages.
5  */
6
7 #include <stdio.h>
8 #include <string.h>
9 #include <ctype.h>
10 #include <stdlib.h>
11 #include "expat.h"
12 #include <getopt.h>
13 #include "disorder.h"
14
15 // timestamp of the form 2003-11-07T00:43:23Z
16 #define DATE_LENGTH 10
17 #define TIME_LENGTH 8
18 #define TIMESTAMP_LENGTH 20
19
20 #define MEGABYTE 1048576
21 #define FIELD_BUFFER_SIZE 1024
22 // 2048 KB in bytes + 1
23 //#define TEXT_BUFFER_SIZE 2097153
24 //#define TEXT_BUFFER_SIZE 10485760
25
26 enum elements { 
27     TITLE, ARTICLEID, REVISION, REVID, TIMESTAMP, CONTRIBUTOR, 
28     EDITOR, EDITORID, MINOR, COMMENT, UNUSED, TEXT
29 }; 
30
31 enum block { TITLE_BLOCK, REVISION_BLOCK, CONTRIBUTOR_BLOCK, SKIP };
32
33 enum outtype { FULL, SIMPLE };
34
35 typedef struct {
36
37     // pointers to once-allocated buffers
38     char *title;
39     char *articleid;
40     char *revid;
41     char *date;
42     char *time;
43     char *timestamp;
44     char *anon;
45     char *editor;
46     char *editorid;
47     char *comment;
48     char *text;
49
50     // track string size of the elements, to prevent O(N^2) processing in charhndl
51     // when we have to take strlen for every character which we append to the buffer
52     size_t title_size;
53     size_t articleid_size;
54     size_t revid_size;
55     size_t date_size;
56     size_t time_size;
57     size_t timestamp_size;
58     size_t anon_size;
59     size_t editor_size;
60     size_t editorid_size;
61     size_t comment_size;
62     size_t text_size;
63
64     bool minor;
65     
66     enum elements element;
67     enum block position;
68     enum outtype output_type;
69     
70 } revisionData;
71
72
73 /* free_data and clean_data
74  * Takes a pointer to the data struct and an integer {0,1} indicating if the 
75  * title data needs to be cleared as well.
76  * Also, frees memory dynamically allocated to store data.
77  */ 
78 static void
79 clean_data(revisionData *data, int title)
80 {
81     // reset title (if we are switching articles)
82     if (title) {
83         data->title[0] = '\0';
84         data->articleid[0] = '\0';
85         data->title_size = 0;
86         data->articleid_size = 0;
87     }
88
89     // reset text fields
90     data->revid[0] = '\0';
91     data->date[0] = '\0';
92     data->time[0] = '\0';
93     data->timestamp[0] = '\0';
94     data->anon[0] = '\0';
95     data->editor[0] = '\0';
96     data->editorid[0] = '\0';
97     data->comment[0] = '\0';
98     data->text[0] = '\0';
99
100     // reset length tracking
101     data->revid_size = 0;
102     data->date_size = 0;
103     data->time_size = 0;
104     data->timestamp_size = 0;
105     data->anon_size = 0;
106     data->editor_size = 0;
107     data->editorid_size = 0;
108     data->comment_size = 0;
109     data->text_size = 0;
110
111     // reset flags and element type info
112     data->minor = false;
113     data->element = UNUSED;
114
115 }
116
117 // presently unused
118 static void
119 free_data(revisionData *data, int title)
120 {
121     if (title) {
122         //printf("freeing article\n");
123         free(data->title);
124         free(data->articleid);
125     }
126     free(data->revid);
127     free(data->date);
128     free(data->time);
129     free(data->timestamp);
130     free(data->anon);
131     free(data->editor);
132     free(data->editorid);
133     free(data->comment);
134     free(data->text);
135 }
136
137 void cleanup_revision(revisionData *data) {
138     clean_data(data, 0);
139 }
140
141 void cleanup_article(revisionData *data) {
142     clean_data(data, 1);
143 }
144
145
146 static void 
147 init_data(revisionData *data, outtype output_type)
148 {
149     data->text = (char*) malloc(4 * MEGABYTE);  // 2MB is the article length limit, 4MB is 'safe'?
150     data->comment = (char*) malloc(FIELD_BUFFER_SIZE);
151     data->title = (char*) malloc(FIELD_BUFFER_SIZE);
152     data->articleid = (char*) malloc(FIELD_BUFFER_SIZE);
153     data->revid = (char*) malloc(FIELD_BUFFER_SIZE);
154     data->date = (char*) malloc(FIELD_BUFFER_SIZE);
155     data->time = (char*) malloc(FIELD_BUFFER_SIZE);
156     data->timestamp = (char*) malloc(FIELD_BUFFER_SIZE);
157     data->anon = (char*) malloc(FIELD_BUFFER_SIZE);
158     data->editor = (char*) malloc(FIELD_BUFFER_SIZE);
159     data->editorid = (char*) malloc(FIELD_BUFFER_SIZE);
160     data->minor = false;
161
162     // resets the data fields, null terminates strings, sets lengths
163     clean_data(data, 1);
164
165     data->output_type = output_type;
166 }
167
168 /* for debugging only, prints out the state of the data struct
169  */
170 static void
171 print_state(revisionData *data) 
172 {
173     printf("element = %i\n", data->element);
174     printf("output_type = %i\n", data->output_type);
175     printf("title = %s\n", data->title);
176     printf("articleid = %s\n", data->articleid);
177     printf("revid = %s\n", data->revid);
178     printf("date = %s\n", data->date);
179     printf("time = %s\n", data->time);
180     printf("anon = %s\n", data->anon);
181     printf("editor = %s\n", data->editor);
182     printf("editorid = %s\n", data->editorid);
183     printf("minor = %s\n", (data->minor ? "1" : "0"));
184     printf("comment = %s\n", data->comment); 
185     printf("text = %s\n", data->text);
186     printf("\n");
187
188 }
189
190 /* Write a header for the comma-separated output
191  */
192 static void
193 write_header()
194 {
195  //   printf("title, articleid, revid, date, time, anon, editor, editorid, minor, comment\n");
196 //    printf("title\tarticleid\trevid\tdate time\tanon\teditor\teditorid\tminor\n");
197
198 }
199
200
201 /* 
202  * write a line of comma-separated value formatted data to standard out
203  * follows the form:
204  * title,articleid,revid,date,time,anon,editor,editorid,minor,comment
205  * (str)  (int)    (int) (str)(str)(bin)(str)   (int)   (bin) (str)
206  *
207  * it is called right before cleanup_revision() and cleanup_article()
208  */
209 static void
210 write_row(revisionData *data)
211 {
212
213     // TODO: make it so you can specify fields to output
214     // note that date and time are separated by a space, to match postgres's 
215     // timestamp format
216     printf("%s\t%s\t%s\t%s %s\t%s\t%s\t%s\t%s",
217         data->title,
218         data->articleid,
219         data->revid,
220         data->date,
221         data->time,
222         (data->editor[0] != '\0') ? "0" : "1",  // anon?
223         data->editor,
224         data->editorid,
225         (data->minor) ? "1" : "0");
226     switch (data->output_type)
227     {
228         case SIMPLE:
229             printf("\t%i\t%f\n", (unsigned int) strlen(data->text), shannon_H(data->text, data->text_size));
230             //printf("\n");
231             break;
232         case FULL:
233             printf("\t%s\t%s\n", data->comment, data->text);
234             break;
235     }
236
237 }
238
239 void
240 split_timestamp(revisionData *data) 
241 {
242     char *t = data->timestamp;
243     strncpy(data->date, data->timestamp, DATE_LENGTH);
244     char *timeinstamp = &data->timestamp[DATE_LENGTH+1];
245     strncpy(data->time, timeinstamp, TIME_LENGTH);
246 }
247
248 // like strncat but with previously known length
249 char*
250 strlcatn(char *dest, const char *src, size_t dest_len, size_t n)
251 {
252    //size_t dest_len = strlen(dest);
253    size_t i;
254
255    for (i = 0 ; i < n && src[i] != '\0' ; i++)
256        dest[dest_len + i] = src[i];
257    dest[dest_len + i] = '\0';
258
259    return dest;
260 }
261
262 static void
263 charhndl(void* vdata, const XML_Char* s, int len)
264
265     revisionData* data = (revisionData*) vdata;
266     if (data->element != UNUSED && data->position != SKIP) {
267         //char t[len];
268         //strncpy(t,s,len);
269         //t[len] = '\0'; // makes t a well-formed string
270         switch (data->element) {
271             case TEXT:
272                    // printf("buffer length = %i, text: %s\n", len, t);
273                     strlcatn(data->text, s, data->text_size, len);
274                     data->text_size += len;
275                     break;
276             case COMMENT:
277                     strlcatn(data->comment, s, data->comment_size, len);
278                     data->comment_size += len;
279                     break;
280             case TITLE:
281                     strlcatn(data->title, s, data->title_size, len);
282                     data->title_size += len;
283                     break;
284             case ARTICLEID:
285                    // printf("articleid = %s\n", t);
286                     strlcatn(data->articleid, s, data->articleid_size, len);
287                     data->articleid_size += len;
288                     break;
289             case REVID:
290                    // printf("revid = %s\n", t);
291                     strlcatn(data->revid, s, data->revid_size, len);
292                     data->revid_size += len;
293                     break;
294             case TIMESTAMP: 
295                     strlcatn(data->timestamp, s, data->timestamp_size, len);
296                     data->timestamp_size += len;
297                     if (strlen(data->timestamp) == TIMESTAMP_LENGTH)
298                         split_timestamp(data);
299                     break;
300             case EDITOR:
301                     strlcatn(data->editor, s, data->editor_size, len);
302                     data->editor_size += len;
303                     break;
304             case EDITORID: 
305                     //printf("editorid = %s\n", t);
306                     strlcatn(data->editorid, s, data->editorid_size, len);
307                     data->editorid_size += len;
308                     break;
309             /* the following are implied or skipped:
310             case MINOR: 
311                     printf("found minor element\n");  doesn't work
312                     break;                   minor tag is just a tag
313             case UNUSED: 
314             */
315             default: break;
316         }
317     }
318 }
319
320 static void
321 start(void* vdata, const XML_Char* name, const XML_Char** attr)
322 {
323     revisionData* data = (revisionData*) vdata;
324     
325     if (strcmp(name,"title") == 0) {
326         cleanup_article(data); // cleans up data from last article
327         data->element = TITLE;
328         data->position = TITLE_BLOCK;
329     } else if (data->position != SKIP) {
330         if (strcmp(name,"revision") == 0) {
331             data->element = REVISION;
332             data->position = REVISION_BLOCK;
333         } else if (strcmp(name, "contributor") == 0) {
334             data->element = CONTRIBUTOR;
335             data->position = CONTRIBUTOR_BLOCK;
336         } else if (strcmp(name,"id") == 0)
337             switch (data->position) {
338                 case TITLE_BLOCK:
339                     data->element = ARTICLEID;
340                     break;
341                 case REVISION_BLOCK: 
342                     data->element = REVID;
343                     break;
344                 case CONTRIBUTOR_BLOCK:
345                     data->element = EDITORID;
346                     break;
347             }
348     
349         // minor tag has no character data, so we parse here
350         else if (strcmp(name,"minor") == 0) {
351             data->element = MINOR;
352             data->minor = true; 
353         }
354         else if (strcmp(name,"timestamp") == 0)
355             data->element = TIMESTAMP;
356
357         else if (strcmp(name, "username") == 0)
358             data->element = EDITOR;
359
360         else if (strcmp(name,"ip") == 0) 
361             data->element = EDITORID;
362
363         else if (strcmp(name,"comment") == 0)
364             data->element = COMMENT;
365
366         else if (strcmp(name,"text") == 0)
367             data->element = TEXT;
368
369         else if (strcmp(name,"page") == 0 
370                 || strcmp(name,"mediawiki") == 0
371                 || strcmp(name,"restrictions") == 0
372                 || strcmp(name,"siteinfo") == 0)
373             data->element = UNUSED;
374     }
375
376 }
377
378
379 static void
380 end(void* vdata, const XML_Char* name)
381 {
382     revisionData* data = (revisionData*) vdata;
383     if (strcmp(name, "revision") == 0 && data->position != SKIP) {
384         write_row(data); // crucial... :)
385         cleanup_revision(data);  // also crucial
386     } else {
387         data->element = UNUSED; // sets our state to "not-in-useful"
388     }                           // thus avoiding unpleasant character data 
389                                 // b/w tags (newlines etc.)
390 }
391
392 void print_usage(char* argv[]) {
393     fprintf(stderr, "usage: <wikimedia dump xml> | %s [options]\n", argv[0]);
394     fprintf(stderr, "\n");
395     fprintf(stderr, "options:\n");
396     fprintf(stderr, "  -t   print text and comments after each line of tab separated data\n");
397     fprintf(stderr, "\n");
398     fprintf(stderr, "Takes a wikimedia data dump XML stream on standard in, and produces\n");
399     fprintf(stderr, "a tab-separated stream of revisions on standard out:\n");
400     fprintf(stderr, "\n");
401     fprintf(stderr, "title, articleid, revid, date, time, anon, editor, editorid, minor, revlength\n");
402     fprintf(stderr, "\n");
403     fprintf(stderr, "author: Erik Garrison <erik@hypervolu.me>\n");
404 }
405
406
407 int
408 main(int argc, char *argv[])
409 {
410     
411     enum outtype output_type;
412     int dry_run = 0;
413     // in "simple" output, we don't print text and comments
414     output_type = SIMPLE;
415     char c;
416
417     while ((c = getopt(argc, argv, "ht")) != -1)
418         switch (c)
419         {
420             case 'd':
421                 dry_run = 1;
422                 break;
423             case 't':
424                 output_type = FULL;
425                 break;
426             case 'h':
427                 print_usage(argv);
428                 exit(0);
429                 break;
430         }
431
432     if (dry_run) { // lets us print initialization options
433         printf("simple_output = %i\n", output_type);
434         exit(1);
435     }
436
437     // create a new instance of the expat parser
438     XML_Parser parser = XML_ParserCreate("UTF-8");
439
440     // initialize the user data struct which is passed to callback functions
441     revisionData data;  
442     // initialize the elements of the struct to default values
443     init_data(&data, output_type);
444
445
446     // makes the parser pass "data" as the first argument to every callback 
447     XML_SetUserData(parser, &data);
448     void (*startFnPtr)(void*, const XML_Char*, const XML_Char**) = start;
449     void (*endFnPtr)(void*, const XML_Char*) = end;
450     void (*charHandlerFnPtr)(void*, const XML_Char*, int) = charhndl;
451
452     // sets start and end to be the element start and end handlers
453     XML_SetElementHandler(parser, startFnPtr, endFnPtr);
454     // sets charhndl to be the callback for character data
455     XML_SetCharacterDataHandler(parser, charHandlerFnPtr);
456
457     bool done;
458     char buf[BUFSIZ];
459     
460     // shovel data into the parser
461     do {
462         
463         // read into buf a bufferfull of data from standard input
464         size_t len = fread(buf, 1, BUFSIZ, stdin);
465         done = len < BUFSIZ; // checks if we've got the last bufferfull
466         
467         // passes the buffer of data to the parser and checks for error
468         //   (this is where the callbacks are invoked)
469         if (XML_Parse(parser, buf, len, done) == XML_STATUS_ERROR) {
470             fprintf(stderr,
471                 "%s at line %d\n",
472                 XML_ErrorString(XML_GetErrorCode(parser)),
473                 (int) XML_GetCurrentLineNumber(parser));
474             return 1;
475         }
476     } while (!done);
477    
478
479     XML_ParserFree(parser);
480
481     return 0;
482 }

Benjamin Mako Hill || Want to submit a patch?