# /usr/bin/python
# -*- coding: utf-8 -*-
"""
Version: 2012年9月30日 01:17:59
Author: YFdyh000
License: AGPL v3
./iw.py
iw.py
iw.py -num:500
iw.py -cycle:3
iw.py -multiple:2
./iw.py -cycle:10 -autorun
./iw.py -cycle:5 -multiple:2 -autorun
./iw.py -cycle:50 -multiple:2 -autorun
我自己写的第一个比较正式的Python脚本,边学边写的。有些渣代码/渣英语见谅。
待优化结构,减少全局变量使用。(class Global(object))
异常检测机制的位置现在有点乱,待整理。
可选增加锁定自身文件.iw文件 作为互斥锁。缺点是脚本运行时不能更新iw.py内容了。(可采用自带的def checkMultiplicity(self) ,待学习)
学习、改为使用套件包本身的模块函数,一些函数功能有重复,比如睡眠/节流,参数解析。
异常处理还是有点乱,待清理改进
学习进程间通讯,然后增加适时延伸/中止功能(经过100个页面仍无需更新时终止)
变量名待整理
每天第一次运行可采用双倍检查量以避免遗漏。
可使用-langlist手动检测,寻找当前新条目多的维基加入脚本配置或手动运行。
"""
import datetime, platform, time, os, socket, sys
import wikipedia as pywikibot
from subprocess import *
from ctypes import *
###优先级常量
#HIGH_PRIORITY_CLASS = c_int(0x00000080)
ABOVE_NORMAL_PRIORITY_CLASS = 0x00008000
NORMAL_PRIORITY_CLASS = 0x00000020
BELOW_NORMAL_PRIORITY_CLASS = 0x00004000
#IDLE_PRIORITY_CLASS = c_int(0x00000040)
###运行参数配置
num=60 #默认基准运行数量。推荐为60的倍数(无来源无根据待证实)
run_time_number=3 #默认代码循环运行次数。至少为1,否则无法运行。可命令行参数指定
sleep_time=int(1.0*60*60) #代码循环每运行一轮后的睡眠时间。单位秒。1/2/3小时(彻夜自动运行。注意无人全自动可靠性,理论上不会有问题,且目前也从未出现过问题)。已废弃
each_round_time=int(4.0*60*60) #每轮至少消耗的时间,若一轮消耗时间不足则通过睡眠补足。此配置每轮通常消耗2~4小时。
multiple=1 #脚本配置的num倍数,支持小数
fastmode=False #快速模式下所有脚本配置的num均强制为基准数量,以便快速发现新条目多的维基。可考虑放弃,现在有了langlist,快速模式用途不大且目前仅可用于脚本配置。
defaultPriority=BELOW_NORMAL_PRIORITY_CLASS #进程运行的默认优先级。暂未实现Linux的
#defaultPriority=NORMAL_PRIORITY_CLASS #正常优先级,即不变
mutex_prot=58888 #进程互斥端口,要选一个不常用不会冲突的端口。目前弃用。
Running=False #任务是否正在运行,延缓退出。
noasync=False #是否强制不带-async
puttime=-1 #指定提交间隔。默认-1不指定
force_multiple=False
p=None #子进程对象
round_str='' #全局变量,存储运行命令时的轮数标记,默认空不显示。待改进
###运行指令项配置
def get_command_parameter(num):
num=int(num)
param = '-auto -async -quiet -namespace:0 -namespace:14'
Parameter = [ #方括号则内容可变化,圆括号则只读
#('en', num*5, 'new', param), #数量最多,比例低
('zh', num*3, 'new', param), #medium/high
('ar', num*3, 'new', param), #数量中(1天约200),比例高
('ro', num*3, 'new', param), #数量低~高(1天约100~XXX),比例高
('ko', num*3, 'new', param), #high,比例中/高
('id', num*3, 'new', param), #high/medium,比例中/高
('vi', num*3, 'new', param), #high
('uk', num*3, 'new', param), #high,比例高
('sv', num*3, 'new', param), #数量高(200~600+),比例高
('sh', num*3, 'new', param), #数量中?(页面数1天约200~X),比例高
('tr', num*2, 'new', param), #数量中(1天约150|300),比例高
('cs', num*3, 'new', param), #数量中(1天约250),比例高
('fa', num*2, 'new', param), #数量低~高(1天50~500+),比例高
('simple', num*1, 'new', param), #数量低(1天约50),比例高
('de', num*4, 'new', param), #high/low
('ja', num*3, 'new', param), #medium
('es', num*3, 'new', param), #medium
('ru', num*3, 'new', param), #medium
('ka', num*1, 'new', param), #数量低(页面数1天约100),比例高
('sk', num*1, 'new', param), #数量低(1天50),比例中高
('nl', num*4, 'new', param), #medium/high?
('fr', num*3, 'new', param), #medium?
('ca', num*3, 'new', param), #medium
('it', num*4, 'new', param), #low/high
('he', num*2, 'new', param), #数量中(1天约200),比例中
('pl', num*3, 'new', param), #medium/low
('fi', num*2, 'new', param), #数量中(1天约150),比例中
('hu', num*2, 'new', param), #数量中(1天100~250),比例中
('pt', num*2, 'new', param), #medium/low
('no', num*2, 'new', param), #medium?
('kk', num*3, 'new', param), #
('th', num*1, 'new', param), #数量低(页面数1天约100),比例中/高
('la', 30, 'new', param), #数量低(1天50)
('eo', num*1, 'new', param), #数量低(1天50~100),比例中/高
('da', num*1, 'new', param), #数量低(1天不足100)
('sr', num*1, 'new', param), #数量中(1天约100~150)
('gl', 30, 'new', param), #数量低(1天50)
('az', num*1, 'new', param), #数量低(1天约50)
('et', 30, 'new', param), #数量低(1天约50),本地无IWBOT操作者。
('kk', 30, 'new', param), #数量低(1天约50)
('ms', 30, 'new', param), #数量低(1天50)
('eu', 30, 'new', param), #数量极低(1天约50)
('sl', 30, 'new', param), #数量极低(1天不足50)
('bg', 30, 'new', param), #数量低(1天50)
('lt', 30, 'new', param), #数量低(1天约50)。比例中
('lt', 30, 'new', param), #数量极低(1天不足20)
]
#high/medium/low是指需维护新条目出现比率的参考,可能随时更改变化。
#倍数条目数排名前十通常可3或4,10~20可2或3,20~30通常按情况1或2。其他通常放弃。
#运行一轮时间(5秒间隔)。2012.07.04:约2~3小时。2012.07.24,同上。
#num参数值暂时按一天运行2次计算,即每天新条目数除以2。数量指每天新条目数量(估算,不准确),比例指需要维护iw的比例(同样不准确),随心更改,无规则,大多只取了一次样。
#发现正确的数量应该算所有命名空间的新建量,而不仅是条目,因为-new + 过滤0&14命名空间参数获取时的数值是所有的命名空间的数值,如果去除过滤分类的参数则是仅条目的。
return(Parameter)
###程序代码:函数块
def start_iwpy(lang='zh', num=60, type='new', additional=''): #启动单次任务
global Running
#print(lang,type,num)
wait_other()
Running=True #开始运行
command_str="interwiki.py -%s:%d %s -lang:%s" % (type, num, additional, lang)
if (platform.system()=="Linux"):
#command_str="python2.7 " + command_str # for earlier versions of pythonanywhere.com
command_str="python " + command_str
elif (platform.system()=="Windows"):
os.system('title !pywikipedia interwiki.py %s %s %s' % (lang, type, num))
exitcode = -1 #占位,防止没能启动返回的0误以为正常
exitcode=run_system(command_str) #新方式,支持指定启动优先级
#exitcode=run_system(command_str, use_os=True) #旧方式,似乎更稳定
Running=False #已运行完
if exitcode==2: #检测退出码。似乎Ctrl+C是2。
time.sleep(0.1) #避免控制台输出内容顺序错乱
exit_callback()
elif exitcode==0: # 正常完成退出
#print "code testing"
pass
#elif exitcode==3: #此处不应出现。跳出一次内循环
elif exitcode==10000:
pass #本脚本内部发生异常
elif exitcode==-1:
print 'Error: The command did not start.'
else: #意外退出码
print "Error: The run exitcode is "+str(exitcode)
exit()
def run_system(command, showcommand=True, pyfile=True, use_os=False): #运行程序并回显指令参数
global round_str
if showcommand:
#print(command)
#对Windows禁用,因为长度正好超过了cmd的长度而另起一行不美观,且通常不需要
#P.S:右键-属性,调整布局的宽度,默认80,可调至100。
#pywikibot.output('\03{lightaqua}%s%s\03{default}' % (round_str, command))
if platform.system()!="Windows":
pywikibot.output('\03{lightaqua}%s%s\03{default}' % (round_str, command))
else:
pywikibot.output('\03{lightaqua}%s\03{default}' % command)
# color: lightaqua lightyellow lightpurple lightred lightblue
if (platform.system()=="Windows" and pyfile and use_os==False): #Windows平台下,运行.py文件,不使用旧方式,才可加参数。
command='python '+command #暂时解决方案,分出来放是为了更清晰
commands=command.split()
#print commands
global p #为了程序终止时能检查和终止子进程
if use_os: # 为了 svn update 等命令正常,使用os模块
exitcode=os.system(command) #旧方式。缺点,会纪录到命令历史(按方向键-上)
return(exitcode) #运行完直接返回,不管下面的代码
try: #这里好像也要拦截异常,不然好像会出现异常错误。待检查测试。
if platform.system()!="Windows" or defaultPriority==0x00000020:
p = Popen(commands)
else:
p = Popen(commands, creationflags=defaultPriority) #若shell=True则获得的是shell的PID。
#setPriority(p.pid,defaultPriority)
p.wait()
except KeyboardInterrupt:
p.wait() #可能必须,是程序未完成但未处理异常而返回到上层的异常
time.sleep(0.5) #给程序一个时间结束
if p.poll() is None: #None是运行中,已终止是0
p.terminate() #会给程序一点时间完成结束的吧?
#p.terminate() #以防万一没能退出。如果上面的wait理解正常应该不会发生。不行,每次都[Errno 3] No such process
return(2) #键盘中止退出似乎应该是2
except Exception,data: #测试中,不确定是否对。
if p.poll() is None: #None是运行中,已终止是0
p.kill()
print Exception,data
return(10000) #这是随便写的错误号
# while True: #待测试,不确定这样的逻辑是否正常,待改进
# p.wait() #似乎有逻辑错误?Ctrl+C后子进程仍然在运行而选择提示后会立即启动下个程序而不是等待上个程序运行完成退出后再启动。待检查,待修复。
exitcode=p.returncode
#print 'exitcode:'+str(exitcode)
return(exitcode)
#time.sleep(10)
def exit_callback(): #键盘中止事件处理 (Ctrl+C)
try:
choice = pywikibot.inputChoice(u'Exit all command of the script?',
['Yes', 'No', 'Sleep', 'Reset index'],
['Y', 'N', 'S', 'R'])
except KeyboardInterrupt:
choice = 'n' #默认为n,防止出现提前引用变量错误
if choice == 'y':
#这里还有缺陷,等待子进程前没有好的明确提示
#wait_other() 这个有问题,会导致阻塞。暂时放弃,现在会直接退出
#这里如果字程序也在同时询问那这个询问会挡住输入接口,并且导致异常,待解决。
print('The script exiting...')
exit()
elif choice == 'n':
pass #理论上子程序应该已退出,无需做任何事情
if p and p.poll() is None: #但目前对仍有队列的处理仍有问题,强制终止
p.kill()
elif choice == 's': #立即开始睡眠,推迟到下次继续运行。不会重置当前for维基位置
time = pywikibot.input(u'本任务已被终止。要立即睡眠多长时间再开始下个任务?单位秒。支持运算。结果为0则取消睡眠操作:')
sleeptime=int(eval(time,{},{})) #支持运算符。未实际验证安全性
if sleeptime < 1:
return(exit_callback()) #不确定逻辑是否正确
else:
start_sleep(sleeptime)
elif choice == 'r': #跳出内循环,重头开始(循环完成次数+1)
return(3) #待测试。目前返回值无任何意义
#Restart this lang,不增加循环完成次数的方式尚未实现
else:
print('Error: choice is ', choice)
def start_sleep(sleep_time): # 开始一次睡眠
#ltime=time.time()
if sleep_time <= 0:
return
starttimeStr=time.strftime("%H:%M:%S", time.localtime()) # %Y-%m-%d %H:%M:%S
EndtimeStr=time.strftime("%H:%M:%S", time.localtime(time.time()+sleep_time))
pywikibot.output('\03{lightaqua}The script will sleep: %s, Start at %s, end at %s.\03{default}' % (str(datetime.timedelta(seconds=(sleep_time))), starttimeStr, EndtimeStr))
try:
time.sleep(sleep_time)
except KeyboardInterrupt:
print('The sleep %s has been canceled.' % str(datetime.timedelta(seconds=(sleep_time))))
exit_callback()
"""
def setPriority(PID=0,WindowsPriority=BELOW_NORMAL_PRIORITY_CLASS, LinuxPriority=10): #设置指定进程优先级。目前仅适用Windows。暂时未使用
if (platform.system()=="Windows"):
if PID==0:
PID = windll.kernel32.GetCurrentProcess() #待改进
print PID
bool = windll.kernel32.SetPriorityClass(c_int(PID), c_int(WindowsPriority))
print bool
elif (platform.system()=="Linux"):
#os.system('nice -n 12') #以指定优先级运行程序-20最高,19最低,默认10
#os.system('renice +12') #改变运行中进程优先级。非root权限只能降低。
pass #暂不需要故禁用。未测试,未实现
"""
"""
def totimestr(timestamp=0, offset=0): #将时间戳转为想要的时间字符串,不含日期
#目前缺点是超过24小时的时间戳不能正常显示
if timestamp==0:
#timestamp=time.time() #这里不确定正确,也许应该引发异常而不是当前时间
return(-1)
if timestamp>=24*60*60: #24小时问题。
returnstr = time.strftime("%d-%H:%M:%S", time.gmtime(timestamp))
#return(-2) #已解决,输出日,但不输出月,即30天问题,但通常不会遇到。仅在超过1日时使用此代码,所以日至少是1的问题无影响
#if (platform.system()=="Linux" and offset != 0): #输出非消耗时间而是开始/结束时间时可指定时区偏移。目前不需要,禁用
# timestamp+=offset*60*60 #临时无奈解决之法,手动转换时区。仅适用PAW UTC+8
returnstr = time.strftime("%H:%M:%S", time.gmtime(timestamp))
return(returnstr)
#2012.07.19 刚发现原来可以这样直接用 datetime.timedelta(seconds=600)
"""
"""
def socket_isRunnig(prot): #按端口号检测是否正在运行
global socketobj #直接建立互斥体,节省代码
socketobj = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
socketobj.bind(('127.0.0.1',prot))
except socket.error, e:
if e[0] == 10048: #10048 #端口被占用
return True
else:
(ErrorType, ErrorValue, ErrorTB) = sys.exc_info()
print "failed: ", ErrorValue #待改进,会被之后的清屏冲掉
return True #发生其他异常也退出。
#return False #未运行。请建立互斥体
"""
def run_svn_update():
os.chdir('/home/yfdyh000/pywikipedia') #曾准备用于PAW计划任务
run_system('svn update', pyfile=False, use_os=True) #每天自动运行时更新套件包版本以适应最新环境。可靠率较高,但要考虑万一当天的新套件包出问题了呢
def run_backup(): #运行备份命令,for pythonanywhere.com
#run_system('cd ~/pywikipedia/', pyfile=False, use_os=True) #必须切换,计划任务默认在~/运行。
os.chdir('/home/yfdyh000/pywikipedia') #好像是要这样做才对 for PAW website。这里不支持使用~符号。
run_system('cp -Rfuv ~/pywikipedia/user-config.py ~/pywikipedia/logs/ ~/pywikipedia/cache/ ~/pywikipedia/autonomous_problems.dat ~/Dropbox/pythonanywhere/', pyfile=False, use_os=True)
run_system('cp -Rfuv ~/pywikipedia/iw.py ~/.bash_history ~/.bashrc ~/Dropbox/pythonanywhere/', pyfile=False, use_os=True)
def wait_other(): #等待其他子进程运行完毕再继续
global Running
while Running:
try:
time.sleep(1) #若运行中则延缓
except KeyboardInterrupt:
exit_callback()
def main():
###主命令块
###声明变量
lang=[] #手动运行指令语言
autorun=False #是否为日程自动启动。
current_time_number=1 #当前代码循环已运行次数
global num #待改进
global run_time_number #同上
global sleep_time #同上...
global fastmode
global multiple
global noasync
global force_multiple
global puttime
global round_str
thistime_nosleep=False #此次不睡眠。待改进,很乱
total_starttime = datetime.datetime.now()
###清屏。待改进,这之前的警告信息会被冲掉
if (platform.system()=="Windows"):
os.system('cls')
pass
elif (platform.system()=="Linux"):
os.system('clear')
if len(lang) == 0:
run_time_number=1 #若为手动运行模式则默认为1轮
###解析命令行参数 可参考readOptions改善,还需定义Globalvar类
for arg in pywikibot.handleArgs(): # get循环索引要用enumerate
#print arg
if arg.startswith('-') == False:
num=int(arg) #非-开头则当作纯数字参数
#可改为作为额外参数,但又担心输入错误导致运行错误。
elif arg.startswith('-num:'):
num=int(arg[5:])
elif arg.startswith('-langlist:'): #-lang不可用,会冲突。
#lang=arg[6:] #旧代码
#print arg[10:]
lang=arg[10:].split(',') #支持,分隔的列表。
#print lang
elif arg.startswith('-autorun'): #计划任务自动运行。
#目前仅对计划任务自动运行的情况检测避免重复运行。
"""突然发现 pythonanywhere.com 不允许随意开端口,会error 13,所以代码目前无用了……
if socket_isRunnig(mutex_prot): #检测是否正在运行,如是则退出
pywikibot.output('\03{lightred}The script is Running, the script will now exit.\03{default}')
exit()
"""
#else:
# socketobj = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #建立互斥体占位。待检查作用域是否正确。待测试。改为直接建立互斥体
autorun=True # 目前无作用
run_svn_update()
#运行日志备份到Dropbox的命令。
run_backup()
elif arg.startswith('-cycle:'): #指定循环运行多少次。目前不支持手动指定模式
run_time_number=int(arg[7:])
elif arg.startswith('-fast'):
fastmode=True
elif arg.startswith('-multiple:'): #支持小数
multiple=float(arg[10:])
elif arg.startswith('--multiple:'): #支持小数。强制每轮都倍数
multiple=float(arg[11:])
force_multiple=True
#elif arg.startswith('-'): #静默运行。用于计划任务不输出到控制台记录
#-daemonize #未实现,不确定需要
elif arg.startswith('-backup'):
run_backup()
exit() #备份命令仅用于备份,然后就退出
elif arg.startswith('-sleep:'): #运行后先睡眠指定秒数再运行。
start_sleep(int(arg[7:]))
elif arg.startswith('-noasync'):
noasync=True
elif arg.startswith('-puttime:'):
puttime=int(arg[9:])
if puttime==0:
puttime=-1 #严禁0间隔提交
else:
print('Unknown parameters:'+arg)
###手动指定语言列表参数运行的模式。
if int(num) < 1:
print('num must be > 0!')
if len(lang) != 0:
while current_time_number <= run_time_number:
for lang_i in lang:
langstr=lang_i.strip() #去除左右空格
if len(langstr)== 0: #去除左右空格后长度是否为0
continue
try:
#tempstr='-auto -quiet -namespace:0 -namespace:14' #拆分出默认参数。增加了维护难度,放弃
tempstr=''
if not noasync: #默认为带上异步参数
tempstr += '-async'
if puttime != -1:
tempstr += ' -putthrottle:'+str(puttime)
start_iwpy(langstr, num, additional=tempstr)
except KeyboardInterrupt:
returncode = exit_callback()
current_time_number += 1
sys.exit()
###上为手动指定语言运行。下为按脚本配置运行
#setPriority(,defaultPriority) #不是设置子进程而是本进程。放弃
Parameter = get_command_parameter(num) #仅获取一次。让-num仅适用手动指定语言模式
###循环运行批代码
while current_time_number <= run_time_number:
if current_time_number > 1 and thistime_nosleep==False: # 非首次运行
#if costtime > each_round_time: #上一轮耗时超过3小时则不需再睡眠
# start_sleep(sleep_time)
start_sleep(each_round_time-costtime) #用睡眠补足3小时间隔
elif thistime_nosleep:
pywikibot.output('\03{lightaqua}The script index has been reset, current round: %s\03{default}' % (current_time_number))
thistime_nosleep=False
elif run_time_number>1 and current_time_number==1:
pywikibot.output('\03{lightaqua}The script will Repeat run %s times, each time sleep: %s, Start at %s.\03{default}' % (run_time_number, str(datetime.timedelta(seconds=(sleep_time))), str(total_starttime)[0:19]))
#Parameter = get_command_parameter() #若每次都获取,有-num:乘法会影响效果
if run_time_number>1:
starttime = datetime.datetime.now() #记录每轮时间
round_str='['+str(current_time_number)+'] ' #显示轮数标记,待改进
if (current_time_number % 5) == 0 and autorun: #每5轮(约1天)检查更新一次
run_svn_update()
run_backup()
for p in Parameter: # 单次批次任务主体。为了except而合并回来,原为run_a_batch()
returncode=0
if fastmode:
real_num=num
else:
real_num=p[1]
if real_num < num and current_time_number>1: #如果原始值(不算倍数)就小于基准数量,则仅运行在首次,第二次及以后直接跳过。
continue
if current_time_number==1 or force_multiple: #首轮或特殊参数时
real_num = int(real_num*multiple) #使用倍数。此处必须转为整数
additional=p[3]
if noasync:
additional.replace('-async', '') #去掉异步参数,保留其他参数
try:
#print p
returncode = start_iwpy(p[0], real_num, p[2], additional) # 这段不优美,不知道有没有什么方法更美观的达到同样效果
#raise KeyboardInterrupt
except KeyboardInterrupt:
returncode = exit_callback()
except Exception,data:
print Exception,data
#print returncode
if returncode == 3:
thistime_nosleep=True #待改进,变量太多了
break #跳出一整次循环运行,从头开始下一轮
if run_time_number>1:
endtime = datetime.datetime.now()
costtime = int((endtime - starttime).total_seconds())
costtimestr = str(datetime.timedelta(seconds=costtime))
pywikibot.output('\03{lightaqua}No.%s round cost of %s\03{default}' % (current_time_number, costtimestr)) #输出此轮消耗时间
current_time_number += 1
total_endtime = datetime.datetime.now()
total_costtime = datetime.timedelta(seconds=int((total_endtime - total_starttime).total_seconds())) #总消耗时间
pywikibot.output('\03{lightaqua}The script run is completed. Start at %s, end at %s, Total cost of %s\03{default}' % (str(total_starttime)[0:19], str(total_endtime)[0:19], str(total_costtime)))
if __name__ == "__main__":
try:
#pywikibot.setLogfileStatus(False) #不保存本脚本自身的输出
#print(time.strftime('%Z', time.localtime()))
os.environ['TZ'] = 'Asia/Shanghai' #转换程序环境变量时区为CST(中国标准时间),用于time和datetime模块获取及输出。
main()
finally:
pass
if p and p.poll() is None: #None是运行中,已终止是0。结束当前子进程
p.kill()