The Disambot source code is divided into three scripts:

  • enwp.py provides the framework for interfacing with the English Wikipedia. It uses a combination of API calls and regular HTTP requests.
  • disambot.py extracts a list of disambiguation pages (or more precisely, their titles) from working list.txt and puts each one through an inspection function which loads the page content, makes various changes, and saves any changes.
  • private.py stores the username and password of the bot account.

These scripts are shown below:

enwp.py

edit
 import urllib, urllib2, ClientCookie, time
 
 
 debug_mode = False
 <nowiki>base_url = 'http://en.wikipedia.org/'</nowiki>
 api_url = base_url + 'w/api.php'
 
 
 def login(username, password):
 	url = globals()['api_url']
 	data = {
 		'action'     : 'login',
 		'lgname'     : username,
 		'lgpassword' : password,
 		'format'     : 'xml'
 	}
 	
 	if globals()['debug_mode']: print 'Logging in...'
 	response = ClientCookie.urlopen(url, urllib.urlencode(data)).read()
 	if globals()['debug_mode']: print 'Done'
 
 
 def grab_page(title, render=False, expand_templates=False):
 	if render: ren_param = '&action=render'
 	else:      ren_param = '&action=raw'
 	if expand_templates: expand_param = '&templates=expand'
 	else:                expand_param = ''
 	
 	url = globals()['base_url'] + 'w/index.php?title=' + title.replace(' ', '_') + ren_param + expand_param
 	if globals()['debug_mode']: print 'Fetching ' + url
 	
 	response = ClientCookie.urlopen(url).read()
 	if globals()['debug_mode']: print str(len(response)) + ' bytes received'
 	
 	return response
 	
 
 def edit_page(title, new_content, summary=''):
 	# First, obtain the required editing token and the timestamp of the last page edit
 	url = globals()['api_url']
 	data = {
 		'action'  : 'query',
 		'prop'    : 'info|revisions',
 		'intoken' : 'edit',
 		'titles'  : title,
 		'format'  : 'xml'
 	}
 	if globals()['debug_mode']: print 'Fetching ' + url
 	response = ClientCookie.urlopen(url, urllib.urlencode(data)).read()
 	if globals()['debug_mode']: print str(len(response)) + ' bytes received'
 	
 	# Grab the supplied token from the XML-formatted response
 	token_start = response.find('edittoken="') + len('edittoken="')
 	token_end   = response.find('"', token_start)
 	token = response[token_start : token_end]
 	if globals()['debug_mode']: print 'Token: ' + token
 	
 	# Grab the last revision timestamp as well
 	ts_start = response.find('timestamp="') + len('edittoken="')
 	ts_end   = response.find('"', ts_start)
 	ts = response[ts_start : ts_end]
 	if globals()['debug_mode']: print 'Base timestamp: ' + ts
 	
 	# We just fetched a (last edit) timestamp of the form 2008-06-18T07:18:06Z; convert it to 20080618071806
 	edit_time = ts[0:4] + ts[5:7] + ts[8:10] + ts[11:13] + ts[14:16] + ts[17:19]
 	if globals()['debug_mode']: print 'Time of last edit: ' + str(edit_time)
 	
 	# Get the current time and convert it to the 20080618071806 format as well
 	ct = time.gmtime()[0:6] # tuple of the form (year, month, day, hour, minute, second)
 	start_time = str(ct[0]).zfill(4) + str(ct[1]).zfill(2) + str(ct[2]).zfill(2) + str(ct[3]).zfill(2) + str(ct[4]).zfill(2) + str(ct[5]).zfill(2)
 	if globals()['debug_mode']: print 'Time of token retreival: ' + str(start_time)
 	
 	# Next, use the API to push the new page content
 	'''
 	data = {
 		'action'        : 'edit',
 		'title'         : title,
 		'section'       : 0,
 		'text'          : new_content,
 		'token'         : token,
 		'summary'       : summary,
 		'bot'           : True,
 		'basetimestamp' : ts,
 		'nocreate'      : True,
 		'format'        : 'xml'
 	}
 	'''
 	url = globals()['base_url'] + 'w/index.php?' + urllib.urlencode({ 'title':title, 'action':'submit' }, True)
 	data = {
 		'wpAntispam'    : '',
 		'wpSection'     : '',
 		'wpStarttime'   : start_time,
 		'wpEdittime'    : edit_time,
 		'wpScrolltop'   : 0, # WTF does this do?
 		'wpTextbox1'    : new_content,
 		'wpSummary'     : summary,
 		'wpAutoSummary' : 'd41d8cd98f00b204e9800998ecf8427e', # not sure how this works
 		'wpSave'        : 'Save page',
 		'wpEditToken'   : token
 	}
 	data = urllib.urlencode(data)
 	req = urllib2.Request(url, data, { 'User-Agent' : 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9) Gecko/2008060309 Firefox/3.0' }, True)
 	
 	if globals()['debug_mode']: print 'Sending data to ' + url
 	try:
 		response = ClientCookie.urlopen(req).read()
 	except urllib2.HTTPError, response:
 		if globals()['debug_mode']: print 'HTTP error encountered...'
 	except AttributeError: pass # seems to be a small of bug in ClientCookie
 	if globals()['debug_mode']: globals()['response'] = response
 	
 	'''
 	result_start = response.find('result="') + len('result="')
 	result_end   = response.find('"', result_start)
 	result = response[result_start : result_end]
 	if globals()['debug_mode']: print 'Result: ' + result
 	
 	if result.lower() is 'failure':
 		return False
 	'''
 	
 	return True
 
 def sandbox_test():
 	edit_page('Wikipedia:Sandbox', 'Hello! This is a sandbox edit done using a [[Python (programming language)|Python]] script.')

disambot.py

edit
 import enwp, private
 
 abbreviations = ( 'ac.', 'Co.', 'Corp.', 'deg.', 'ft.', 'Inc.', 'kg.', 'km.' 'mi.', 'mo.', 'oz.', 'qr.', 'qt.', 'yd.' )
 
 # Log in to en-wp account
 enwp.login(private.username, private.password)
 
 
 def inspect(title):
 	print 'Inspecting ' + title + '...'
 	
 	# Defaults
 	changed = False
 	complex_errors = ()
 	
 	article_body = enwp.grab_page(title).strip()
 	article_body_orig = article_body
 	
 	raw_html = enwp.grab_page(title, True)
 	
 	# Skip set indices
 	if article_body.lower().find('[[category:set indices') is not -1:
 		return false
 	
 	lines = article_body.splitlines()
 	
 	# Main loop -- cycle through lines
 	for i, line in enumerate(lines):
 		# Skip short/empty lines
 		if len(line) < 5:
 			continue
 		
 		# Strip extra whitespace
 		line = line.strip()
 		line_orig = line
 		
 		# Replace ordered list items with unordered list items
 		if line[0] is '#':
 			line = '*' + line[1:]
 		
 		# Handle list items
 		if line[0] is '*': # if this line is a list item
 			# Fix punctuation at the end
 			if line[-1] is '.' or line[-1] is ',' or line[-1] is ';': # if there is punctuation at the end
 				if line.count('.') >= 2 and line[line.find('.')+1] == ' ' and line[line.find('.')+2] is line[line.find('.')+2].upper(): # if multiple sentences
 					complex_errors += ('item with multiple sentences detected (line '+str(i)+')',)
 				else:
 					# Remove the punctuation, unless it's a proper abbreviation
 					abbrev = False
 					for a in globals()['abbreviations']:
 						if ' '+a.lower() is line[-1*(len(a)+1):].lower(): # if this abbreviation is at the end of the line
 							abbrev = True
 							break;
 					if not abbrev and line[-2] is line[-2].lower(): # not an abbreviation and not an acronym
 						line = line[0:-1] # remove punctuation (last character)
 			
 			# Remove any bullets to assess the item itself
 			line_content = line
 			while line_content[0] is '*':
 				line_content = line_content[1:].strip()
 			line_content_orig = line_content
 			
 			# Remove outer boldness if necessary
 			if line_content[0:3] is "'''":
 				count = 0
 				while line_content[0] is "'":
 					line_content = line_content[1:]
 					count += 1
 				if count is 3 and line_content[count:count+2] is '[[':
 					line_content.replace("'"*count, '', 1)
 			
 			# Correct piped links
 			<nowiki>if line.find('|') is not -1 and line_content.find('[[') is 0 and line.find(']]') is not -1 and line.find('|') < line.find(']]'):</nowiki>
 				# There is a piped link at the beginning of this line -- remove it
 				# Get rid of pipe, checking for italics
 				p1 = line_content.find('|')
 				p2 = line_content.find(']]')
 				p3 = line_content.find("''", p1, p2)
 				if p3 is not -1 and line_content[p3+2] is not "'": # there are italics inside pipe
 					pass ####
 					#p4 = line_content.find("''", p3+2) # closing ''
 					#if p4 is -1:
 						#complex_errors += ('italicized text seems misformatted (line '+str(i)+')',)
 					#else:
 						#italicized = line_content[p3+2:p4]
 				else: # no italics --> simply remove pipe
 					line_content = line_content[:p1] + line_content[p2:]
 			
 			# Check for wikilinks that are not the first word
 			if line_content.find('[[', 3) is not -1:
 				p1 = line_content.find('[[')
 				p2 = line_content.find('|')
 				p3 = line_content.find(']]')
 				if p2 is -1:
 					article_title = line_content[p1+2:p3]
 				else:
 					article_title = line_content[p2+1:p3]
 				p4 = raw_html.find(article_title+' (page does not exist)')
 				if (p1 is 0 or p1 is 2) and p4 is -1:
 					# The first word is wikilinked as it should be and not a red link, but there are other links that shouldn't be here
 					firstlink_end = line_content.find(']]')
 					if firstlink_end is -1:
 						# No closing "]]" ... something must be screwy
 						complex_errors += ('error in wikilink syntax (line '+str(i)+')',)
 					else:
 						firstlink_end += 2 # skip the ]]
 						<nowiki>while line_content.find('[[', firstlink_end) is not -1 and line_content.find(']]', firstlink_end) is not -1:</nowiki> # links remain
 							link_start = line_content.find('[[', firstlink_end)
 							link_pipe  = line_content.find('|' , firstlink_end)
 							link_end   = line_content.find(']]', firstlink_end)
 							
 							if link_start > link_end:
 								complex_errors += ('error in wikilink syntax (line '+str(i)+')',)
 								break
 							
 							new = line_content[:link_start]
 							if link_pipe is -1 or link_pipe > link_end: # no pipe in link of interest
 								new += line_content[link_start+2:link_end] + line_content[link_end+2:]
 							else: # there is a pipe in link of interest
 								new += line_content[link_pipe+1:link_end] + line_content[link_end+2:]
 							line_content = new # update
 				else:
 					# There are inappropriate wikilinks, but if we remove them we'll be left with no links. Human review needed.
 					complex_errors += ('item contains link, but not in the proper place (line '+str(i)+')',)
 			
 			# Update the line without screwing with its spacing
 			line = line[:len(line)-len(line_content_orig)] + line_content
 		
 		# Replace old version of this line with new one if we've changed anything
 		if line is not line_orig:
 			lines[i] = line
 			changed = True
 	
 	# Implode lines back into one big string
 	article_body = "\n".join(lines)
 	
 	# Check for external links
 	links = article_body.count('[http')
 	if links > 0:
 		complex_errors += ('contains '+str(links)+' external link'+('s'*(links!=1)),)
 	
 	# Finish up
 	if lines is not article_body_orig.splitlines(False):
 		# Update the article
 		print "\tMaking changes..."
 		<nowiki>enwp.edit_page(title, article_body, 'Cleaning up disambiguation page in accordance with [[Wikipedia:Manual of Style (disambiguation pages)]]')</nowiki>
 	if len(complex_errors) > 0:
 		# Add the article to list of potential atrocities, along with notes, unless it's already there
 		atrocities = enwp.grab_page('User:Disambot/Potential atrocities')
 		<nowiki>if atrocities.find("[[" + title + "]]") == -1: # if not already listed</nowiki>
 			<nowiki>atrocities += "\n\n[[" + title + "]]"</nowiki>
 			for this in complex_errors:
 				atrocities += "\n* " + this
 			print "\tListing on potential atrocities..."
 			<nowiki>enwp.edit_page('User:Disambot/Potential atrocities', atrocities, 'Adding [['+title+']]')</nowiki>
 
 
 def go():
 	article_list = open('working list', 'r')
 	for title in article_list: inspect(title.strip())
 	article_list.close()

private.py

edit
 username = '(not shown)'
 password = '(not shown)'