made changes necessary to build a static version
[wikiq] / wikiq.cpp
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 <iostream>
9 #include <string.h>
10 #include <ctype.h>
11 #include <stdlib.h>
12 #include "expat.h"
13 #include <getopt.h>
14 #include "disorder.h"
15 #include "md5.h"
16 #include "dtl/dtl.hpp"
17 #include <vector>
18 #include <map>
19 #include <pcrecpp.h>
20
21
22 using namespace std;
23
24 // timestamp of the form 2003-11-07T00:43:23Z
25 #define DATE_LENGTH 10
26 #define TIME_LENGTH 8
27 #define TIMESTAMP_LENGTH 20
28
29 #define MEGABYTE 1048576
30 #define FIELD_BUFFER_SIZE 1024
31
32 // this can be changed at runtime if we encounter an article larger than 10mb
33 size_t text_buffer_size = 10 * MEGABYTE;
34
35 enum elements { 
36     TITLE, ARTICLEID, REVISION, REVID, TIMESTAMP, CONTRIBUTOR, 
37     EDITOR, EDITORID, MINOR, COMMENT, UNUSED, TEXT
38 }; 
39
40 enum block { TITLE_BLOCK, REVISION_BLOCK, CONTRIBUTOR_BLOCK, SKIP };
41
42 enum outtype { FULL, SIMPLE };
43
44 typedef struct {
45
46     // pointers to once-allocated buffers
47     char *title;
48     char *articleid;
49     char *revid;
50     char *date;
51     char *time;
52     char *timestamp;
53     char *anon;
54     char *editor;
55     char *editorid;
56     char *comment;
57     char *text;
58     vector<string> last_text_tokens;
59     vector<pcrecpp::RE> regexes;
60     vector<pcrecpp::RE> wp_namespace_res;
61     vector<string> regex_names;
62     map<string, string> revision_md5; // used for detecting reversions
63
64     // track string size of the elements, to prevent O(N^2) processing in charhndl
65     // when we have to take strlen for every character which we append to the buffer
66     size_t title_size;
67     size_t articleid_size;
68     size_t revid_size;
69     size_t date_size;
70     size_t time_size;
71     size_t timestamp_size;
72     size_t anon_size;
73     size_t editor_size;
74     size_t editorid_size;
75     size_t comment_size;
76     size_t text_size;
77
78     bool minor;
79     
80     enum elements element;
81     enum block position;
82     enum outtype output_type;
83     
84 } revisionData;
85
86
87 /* free_data and clean_data
88  * Takes a pointer to the data struct and an integer {0,1} indicating if the 
89  * title data needs to be cleared as well.
90  * Also, frees memory dynamically allocated to store data.
91  */ 
92 static void
93 clean_data(revisionData *data, int title)
94 {
95     // reset title (if we are switching articles)
96     if (title) {
97         data->title[0] = '\0';
98         data->articleid[0] = '\0';
99         data->title_size = 0;
100         data->articleid_size = 0;
101     }
102
103     // reset text fields
104     data->revid[0] = '\0';
105     data->date[0] = '\0';
106     data->time[0] = '\0';
107     data->timestamp[0] = '\0';
108     data->anon[0] = '\0';
109     data->editor[0] = '\0';
110     data->editorid[0] = '\0';
111     data->comment[0] = '\0';
112     data->text[0] = '\0';
113
114     // reset length tracking
115     data->revid_size = 0;
116     data->date_size = 0;
117     data->time_size = 0;
118     data->timestamp_size = 0;
119     data->anon_size = 0;
120     data->editor_size = 0;
121     data->editorid_size = 0;
122     data->comment_size = 0;
123     data->text_size = 0;
124
125     // reset flags and element type info
126     data->minor = false;
127     data->element = UNUSED;
128
129 }
130
131 // presently unused
132 static void
133 free_data(revisionData *data, int title)
134 {
135     if (title) {
136         //printf("freeing article\n");
137         free(data->title);
138         free(data->articleid);
139     }
140     free(data->revid);
141     free(data->date);
142     free(data->time);
143     free(data->timestamp);
144     free(data->anon);
145     free(data->editor);
146     free(data->editorid);
147     free(data->comment);
148     free(data->text);
149     data->last_text_tokens.clear();
150 }
151
152 void cleanup_revision(revisionData *data) {
153     clean_data(data, 0);
154 }
155
156 void cleanup_article(revisionData *data) {
157     clean_data(data, 1);
158     data->last_text_tokens.clear();
159     data->revision_md5.clear();
160 }
161
162
163 static void 
164 init_data(revisionData *data, outtype output_type)
165 {
166     data->text = (char*) malloc(text_buffer_size);
167     data->comment = (char*) malloc(FIELD_BUFFER_SIZE);
168     data->title = (char*) malloc(FIELD_BUFFER_SIZE);
169     data->articleid = (char*) malloc(FIELD_BUFFER_SIZE);
170     data->revid = (char*) malloc(FIELD_BUFFER_SIZE);
171     data->date = (char*) malloc(FIELD_BUFFER_SIZE);
172     data->time = (char*) malloc(FIELD_BUFFER_SIZE);
173     data->timestamp = (char*) malloc(FIELD_BUFFER_SIZE);
174     data->anon = (char*) malloc(FIELD_BUFFER_SIZE);
175     data->editor = (char*) malloc(FIELD_BUFFER_SIZE);
176     data->editorid = (char*) malloc(FIELD_BUFFER_SIZE);
177     data->minor = false;
178
179     // resets the data fields, null terminates strings, sets lengths
180     clean_data(data, 1);
181
182     data->output_type = output_type;
183 }
184
185 /* for debugging only, prints out the state of the data struct
186  */
187 static void
188 print_state(revisionData *data) 
189 {
190     printf("element = %i\n", data->element);
191     printf("output_type = %i\n", data->output_type);
192     printf("title = %s\n", data->title);
193     printf("articleid = %s\n", data->articleid);
194     printf("revid = %s\n", data->revid);
195     printf("date = %s\n", data->date);
196     printf("time = %s\n", data->time);
197     printf("anon = %s\n", data->anon);
198     printf("editor = %s\n", data->editor);
199     printf("editorid = %s\n", data->editorid);
200     printf("minor = %s\n", (data->minor ? "1" : "0"));
201     printf("comment = %s\n", data->comment); 
202     printf("text = %s\n", data->text);
203     printf("\n");
204
205 }
206
207
208 /* 
209  * write a line of comma-separated value formatted data to standard out
210  * follows the form:
211  * title,articleid,revid,date,time,anon,editor,editorid,minor,comment
212  * (str)  (int)    (int) (str)(str)(bin)(str)   (int)   (bin) (str)
213  *
214  * it is called right before cleanup_revision() and cleanup_article()
215  */
216 static void
217 write_row(revisionData *data)
218 {
219
220     // get md5sum
221     md5_state_t state;
222     md5_byte_t digest[16];
223     char md5_hex_output[2 * 16 + 1];
224     md5_init(&state);
225     md5_append(&state, (const md5_byte_t *)data->text, data->text_size);
226     md5_finish(&state, digest);
227     int di;
228     for (di = 0; di < 16; ++di) {
229         sprintf(md5_hex_output + di * 2, "%02x", digest[di]);
230     }
231
232     string reverted_to;
233     map<string, string>::iterator prev_revision = data->revision_md5.find(md5_hex_output);
234     if (prev_revision != data->revision_md5.end()) {
235         reverted_to = prev_revision->second; // id of previous revision
236     }
237     data->revision_md5[md5_hex_output] = data->revid;
238
239     string text = string(data->text, data->text_size);
240     vector<string> text_tokens;
241     size_t pos = 0;
242     size_t start = 0;
243     while ((pos = text.find_first_of(" \n\t\r", pos)) != string::npos) {
244         //cout << "\"\"\"" << text.substr(start, pos - start) << "\"\"\"" << endl;
245         text_tokens.push_back(text.substr(start, pos - start));
246         start = pos;
247         ++pos;
248     }
249
250     // skip this if the wp_namespace is not in the proscribed list of
251     // namespaces
252     bool wp_namespace_found = false;
253     if (!data->wp_namespace_res.empty()) {
254         for (vector<pcrecpp::RE>::iterator r = data->wp_namespace_res.begin(); r != data->wp_namespace_res.end(); ++r) {
255             pcrecpp::RE& wp_namespace_re = *r;
256             if (wp_namespace_re.PartialMatch(data->title)) {
257                 wp_namespace_found = true;
258                 break;
259             }
260         }
261         if (!wp_namespace_found) {
262             return;
263         }
264     }
265
266     //vector<string> additions;
267     //vector<string> deletions;
268     string additions;
269     string deletions;
270
271     vector<bool> regex_matches_adds;
272     vector<bool> regex_matches_dels;
273
274     if (data->last_text_tokens.empty()) {
275         additions = data->text;
276     } else {
277         // do the diff
278         
279         dtl::Diff< string, vector<string> > d(data->last_text_tokens, text_tokens);
280         //d.onOnlyEditDistance();
281         d.compose();
282
283         vector<pair<string, dtl::elemInfo> > ses_v = d.getSes().getSequence();
284         for (vector<pair<string, dtl::elemInfo> >::iterator sit=ses_v.begin(); sit!=ses_v.end(); ++sit) {
285             switch (sit->second.type) {
286             case dtl::SES_ADD:
287                 //cout << "ADD: \"" << sit->first << "\"" << endl;
288                 additions += sit->first;
289                 break;
290             case dtl::SES_DELETE:
291                 //cout << "DEL: \"" << sit->first << "\"" << endl;
292                 deletions += sit->first;
293                 break;
294             }
295         }
296     }
297     
298     if (!additions.empty()) {
299         //cout << "ADD: " << additions << endl;
300         for (vector<pcrecpp::RE>::iterator r = data->regexes.begin(); r != data->regexes.end(); ++r) {
301             pcrecpp::RE& regex = *r;
302             regex_matches_adds.push_back(regex.PartialMatch(additions));
303         }
304     }
305
306     if (!deletions.empty()) {
307         //cout << "DEL: " << deletions << endl;
308         for (vector<pcrecpp::RE>::iterator r = data->regexes.begin(); r != data->regexes.end(); ++r) {
309             pcrecpp::RE& regex = *r;
310             regex_matches_dels.push_back(regex.PartialMatch(deletions));
311         }
312     }
313
314     data->last_text_tokens = text_tokens;
315
316
317     // print line of tsv output
318     cout
319         << data->title << "\t"
320         << data->articleid << "\t"
321         << data->revid << "\t"
322         << data->date << " "
323         << data->time << "\t"
324         << ((data->editor[0] != '\0') ? "FALSE" : "TRUE") << "\t"
325         << data->editor << "\t"
326         << data->editorid << "\t"
327         << ((data->minor) ? "TRUE" : "FALSE") << "\t"
328         << (unsigned int) data->text_size << "\t"
329         << shannon_H(data->text, data->text_size) << "\t"
330         << md5_hex_output << "\t"
331         << reverted_to << "\t"
332         << (int) additions.size() << "\t"
333         << (int) deletions.size();
334
335     for (int n = 0; n < data->regex_names.size(); ++n) {
336         cout << "\t" << ((!regex_matches_adds.empty() && regex_matches_adds.at(n)) ? "TRUE" : "FALSE")
337              << "\t" << ((!regex_matches_dels.empty() && regex_matches_dels.at(n)) ? "TRUE" : "FALSE");
338     }
339     cout << endl;
340
341     // 
342     if (data->output_type == FULL) {
343         cout << "comment:" << data->comment << endl
344              << "text:" << endl << data->text << endl;
345     }
346
347 }
348
349 void
350 split_timestamp(revisionData *data) 
351 {
352     char *t = data->timestamp;
353     strncpy(data->date, data->timestamp, DATE_LENGTH);
354     char *timeinstamp = &data->timestamp[DATE_LENGTH+1];
355     strncpy(data->time, timeinstamp, TIME_LENGTH);
356 }
357
358 // like strncat but with previously known length
359 char*
360 strlcatn(char *dest, const char *src, size_t dest_len, size_t n)
361 {
362    size_t i;
363
364    for (i = 0 ; i < n && src[i] != '\0' ; i++)
365        dest[dest_len + i] = src[i];
366    dest[dest_len + i] = '\0';
367
368    return dest;
369 }
370
371 static void
372 charhndl(void* vdata, const XML_Char* s, int len)
373
374     revisionData* data = (revisionData*) vdata;
375     size_t bufsz;
376     if (data->element != UNUSED && data->position != SKIP) {
377         switch (data->element) {
378             case TEXT:
379                     // check if we'd overflow our buffer
380                     bufsz = data->text_size + len;
381                     if (bufsz + 1 > text_buffer_size) {
382                         data->text = (char*) realloc(data->text, bufsz + 1);
383                         text_buffer_size = bufsz + 1;
384                     }
385                     strlcatn(data->text, s, data->text_size, len);
386                     data->text_size = bufsz;
387                     break;
388             case COMMENT:
389                     strlcatn(data->comment, s, data->comment_size, len);
390                     data->comment_size += len;
391                     break;
392             case TITLE:
393                     strlcatn(data->title, s, data->title_size, len);
394                     data->title_size += len;
395                     break;
396             case ARTICLEID:
397                    // printf("articleid = %s\n", t);
398                     strlcatn(data->articleid, s, data->articleid_size, len);
399                     data->articleid_size += len;
400                     break;
401             case REVID:
402                    // printf("revid = %s\n", t);
403                     strlcatn(data->revid, s, data->revid_size, len);
404                     data->revid_size += len;
405                     break;
406             case TIMESTAMP: 
407                     strlcatn(data->timestamp, s, data->timestamp_size, len);
408                     data->timestamp_size += len;
409                     if (strlen(data->timestamp) == TIMESTAMP_LENGTH)
410                         split_timestamp(data);
411                     break;
412             case EDITOR:
413                     strlcatn(data->editor, s, data->editor_size, len);
414                     data->editor_size += len;
415                     break;
416             case EDITORID: 
417                     //printf("editorid = %s\n", t);
418                     strlcatn(data->editorid, s, data->editorid_size, len);
419                     data->editorid_size += len;
420                     break;
421             /* the following are implied or skipped:
422             case MINOR: 
423                     printf("found minor element\n");  doesn't work
424                     break;                   minor tag is just a tag
425             case UNUSED: 
426             */
427             default: break;
428         }
429     }
430 }
431
432 static void
433 start(void* vdata, const XML_Char* name, const XML_Char** attr)
434 {
435     revisionData* data = (revisionData*) vdata;
436     
437     if (strcmp(name,"title") == 0) {
438         cleanup_article(data); // cleans up data from last article
439         data->element = TITLE;
440         data->position = TITLE_BLOCK;
441     } else if (data->position != SKIP) {
442         if (strcmp(name,"revision") == 0) {
443             data->element = REVISION;
444             data->position = REVISION_BLOCK;
445         } else if (strcmp(name, "contributor") == 0) {
446             data->element = CONTRIBUTOR;
447             data->position = CONTRIBUTOR_BLOCK;
448         } else if (strcmp(name,"id") == 0)
449             switch (data->position) {
450                 case TITLE_BLOCK:
451                     data->element = ARTICLEID;
452                     break;
453                 case REVISION_BLOCK: 
454                     data->element = REVID;
455                     break;
456                 case CONTRIBUTOR_BLOCK:
457                     data->element = EDITORID;
458                     break;
459             }
460     
461         // minor tag has no character data, so we parse here
462         else if (strcmp(name,"minor") == 0) {
463             data->element = MINOR;
464             data->minor = true; 
465         }
466         else if (strcmp(name,"timestamp") == 0)
467             data->element = TIMESTAMP;
468
469         else if (strcmp(name, "username") == 0)
470             data->element = EDITOR;
471
472         else if (strcmp(name,"ip") == 0) 
473             data->element = EDITORID;
474
475         else if (strcmp(name,"comment") == 0)
476             data->element = COMMENT;
477
478         else if (strcmp(name,"text") == 0)
479             data->element = TEXT;
480
481         else if (strcmp(name,"page") == 0 
482                 || strcmp(name,"mediawiki") == 0
483                 || strcmp(name,"restrictions") == 0
484                 || strcmp(name,"siteinfo") == 0)
485             data->element = UNUSED;
486     }
487
488 }
489
490
491 static void
492 end(void* vdata, const XML_Char* name)
493 {
494     revisionData* data = (revisionData*) vdata;
495     if (strcmp(name, "revision") == 0 && data->position != SKIP) {
496         write_row(data); // crucial... :)
497         cleanup_revision(data);  // also crucial
498     } else {
499         data->element = UNUSED; // sets our state to "not-in-useful"
500     }                           // thus avoiding unpleasant character data 
501                                 // b/w tags (newlines etc.)
502 }
503
504 void print_usage(char* argv[]) {
505     cerr << "usage: <wikimedia dump xml> | " << argv[0] << "[options]" << endl
506          << endl
507          << "options:" << endl
508          << "  -v   verbose mode prints text and comments after each line of tab separated data" << endl
509          << "  -n   name of the following regex (e.g. -n name -r \"...\")" << endl
510          << "  -r   regex to check against additions and deletions" << endl
511          << "  -t   regex(es) to check title against as a way of limiting output to specific namespaces" << endl
512          << endl
513          << "Takes a wikimedia data dump XML stream on standard in, and produces" << endl
514          << "a tab-separated stream of revisions on standard out:" << endl
515          << endl
516          << "title, articleid, revid, timestamp, anon, editor, editorid, minor," << endl
517          << "text_length, text_entropy, text_md5, reversion, additions_size, deletions_size" << endl
518          << ".... and additional fields for each regex executed against add/delete diffs" << endl
519          << endl
520          << "Boolean fields are TRUE/FALSE except in the case of reversion, which is blank" << endl
521          << "unless the article is a revert to a previous revision, in which case, it" << endl
522          << "contains the revision ID of the revision which was reverted to." << endl
523          << endl
524          << "author: Erik Garrison <erik@hypervolu.me>" << endl;
525 }
526
527
528 int
529 main(int argc, char *argv[])
530 {
531     
532     enum outtype output_type;
533     int dry_run = 0;
534     // in "simple" output, we don't print text and comments
535     output_type = SIMPLE;
536     char c;
537     string regex_name;
538
539     // the user data struct which is passed to callback functions
540     revisionData data;
541
542     while ((c = getopt(argc, argv, "hvn:r:t:")) != -1)
543         switch (c)
544         {
545             case 'd':
546                 dry_run = 1;
547                 break;
548             case 'v':
549                 output_type = FULL;
550                 break;
551             case 'n':
552                 regex_name = optarg;
553                 break;
554             case 'r':
555                 data.regexes.push_back(pcrecpp::RE(optarg, pcrecpp::UTF8()));
556                 data.regex_names.push_back(regex_name);
557                 if (!regex_name.empty()) {
558                     regex_name.clear();
559                 }
560                 break;
561             case 'h':
562                 print_usage(argv);
563                 exit(0);
564                 break;
565             case 't':
566                 data.wp_namespace_res.push_back(pcrecpp::RE(optarg, pcrecpp::UTF8()));
567                 break;
568         }
569
570     if (dry_run) { // lets us print initialization options
571         printf("simple_output = %i\n", output_type);
572         exit(1);
573     }
574
575     // create a new instance of the expat parser
576     XML_Parser parser = XML_ParserCreate("UTF-8");
577
578     // initialize the elements of the struct to default values
579     init_data(&data, output_type);
580
581
582     // makes the parser pass "data" as the first argument to every callback 
583     XML_SetUserData(parser, &data);
584     void (*startFnPtr)(void*, const XML_Char*, const XML_Char**) = start;
585     void (*endFnPtr)(void*, const XML_Char*) = end;
586     void (*charHandlerFnPtr)(void*, const XML_Char*, int) = charhndl;
587
588     // sets start and end to be the element start and end handlers
589     XML_SetElementHandler(parser, startFnPtr, endFnPtr);
590     // sets charhndl to be the callback for character data
591     XML_SetCharacterDataHandler(parser, charHandlerFnPtr);
592
593     bool done;
594     char buf[BUFSIZ];
595
596     // write header
597
598     cout << "title" << "\t"
599         << "articleid" << "\t"
600         << "revid" << "\t"
601         << "date" << " "
602         << "time" << "\t"
603         << "anon" << "\t"
604         << "editor" << "\t"
605         << "editor_id" << "\t"
606         << "minor" << "\t"
607         << "text_size" << "\t"
608         << "text_entropy" << "\t"
609         << "text_md5" << "\t"
610         << "reversion" << "\t"
611         << "additions_size" << "\t"
612         << "deletions_size";
613
614     int n = 0;
615     if (!data.regexes.empty()) {
616         for (vector<pcrecpp::RE>::iterator r = data.regexes.begin(); r != data.regexes.end(); ++r, ++n) {
617             if (data.regex_names.at(n).empty()) {
618                 cout << "\t" << "regex_" << n << "_add"
619                      << "\t" << "regex_" << n << "_del";
620             } else {
621                 cout << "\t" << data.regex_names.at(n) << "_add"
622                      << "\t" << data.regex_names.at(n) << "_del";
623             }
624         }
625     }
626     cout << endl;
627     
628     // shovel data into the parser
629     do {
630         
631         // read into buf a bufferfull of data from standard input
632         size_t len = fread(buf, 1, BUFSIZ, stdin);
633         done = len < BUFSIZ; // checks if we've got the last bufferfull
634         
635         // passes the buffer of data to the parser and checks for error
636         //   (this is where the callbacks are invoked)
637         if (XML_Parse(parser, buf, len, done) == XML_STATUS_ERROR) {
638             cerr << "XML ERROR: " << XML_ErrorString(XML_GetErrorCode(parser)) << " at line "
639                  << (int) XML_GetCurrentLineNumber(parser) << endl;
640             return 1;
641         }
642     } while (!done);
643    
644
645     XML_ParserFree(parser);
646
647     return 0;
648 }

Benjamin Mako Hill || Want to submit a patch?