AppEngine之Cron Jobs应用实例
副标题: 将博客迁移到AppEngine的图片迁移处理问题
花了2天的时间将博客迁移到Google AppEngine平台,于今天正式迁移完成.留下此文备查之.
博客程序采用了Micolog这个开源程序.关于此程序的安装配置网上有不少的资料,这里就不作说明了.
但是图片的迁移问题很棘手,网上没有现成可用的方案.
于是考虑自行写代码处理之. 考虑到 AppEngine平台的一个限制: "应用程序必须在 30 秒钟内发出响应". 这说明一次性批量导入数据的方案肯定不可行, 必须要分批导入. 而又希望该程序能够自动进行导入,于是我想到到了:AppEngine的Cron Jobs功能应用能够解决这个问题.
基本思路:
前提1) 修改model.py: 为"class Entry(BaseModel)", 添加一个字段:imageloaded=db.BooleanProperty(default=False) 表示该片文章中的图片是否已经导入完成.
前提2) 已经将所有博客数据已经导入到micolog. 而图片链接全部以外链的方式存在.
通过Cron Jobs定时(比如每1分钟)驱动一次导入程序(/cron/imageloader),该导入程序每次执行只导入少量图片(比如4张图片).
而该导入程序的流程:
1)获取Entry中满足imageloaded=False的一条记录
2)获取内容文本中所有目标图片的链接(通过正则表达式: "(http://([^"~]+?\.(jpg|png|gif)))" 来抓取 )
3)循环下载图片
4)保存图片到Media表中,并且获取新的图片链接
5)将内容文本中的原图片链接置换成新的图片链接
6)设置imageloaded=True 并且保存新的内容文本到数据库. 本次导入结束
如果本次下载图片有失败的情况,或者本次下载图片数量大于指定量(比如4个图片),依然设置imageloaded=False并保存退出.
此方案的优点: 1)自动. 一旦配置成功, 不要手工干预. 2)比较独立, 如果采用非micolog的GAE程序,经过少量的修改,也可适用. 3)该程序不但在迁移阶段起作用,而且在 新发布或修改文章时,也会自动将外链图片保存到micolog本地中. 4)和目标博客程序耦合度很低,不影响原程序的稳定性
下面附上相关的配置和代码(配合micolog使用,其它GAE程序需要少量修改):
app.yaml ------ 修改, 新加配置
- url: /cron/imageloader(/.*)?
script: imageloader.py
login: admin
cron.yaml ---- 新增 放于app.yaml 同级目录
cron:
- description: image loader
url: /cron/imageloader
schedule: every 1 minutes
timezone: Asia/Shanghai
imageloader.py --- 新增 导入程序
# -*- coding: utf-8 -*-
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app
from google.appengine.ext import db
from google.appengine.api import memcache
import re,logging
import cgi
import urllibfrom model import Media
from model import EntryImagePattern=re.compile(r'"(http://(([^"~]+?\.(jpg|png|gif))|(www\.forkosh\.dreamhost\.com/mimetex\.cgi\?[^"]+)))"')
def downloadimage(url):
bits=None
mtype=None
try:
urlopen=urllib.URLopener()
fp = urlopen.open(url)
bits = fp.read()
mtype=fp.headers['Content-Type']
fp.close()
except :
logging.info('Download Error! :'+ url)
return None
logging.info('Download OK! :'+ url)
return (mtype,bits)
def imageloadertask(postid):
test = Entry.gql('LIMIT 1').get()
if test.imageloaded == None :
all=Entry.all()
for t in all :
t.imageloaded = False
t.put()
return '更新数据库,补充新添加的字段'
if postid == None or postid == '' :
entry = Entry.gql("WHERE imageloaded = False").get()
else:
entry = Entry.get_by_id(int(postid))
if entry == None :
return '没有要处理的文章'
title=entry.title
content = entry.content
entry.imageloaded=True
l=ImagePattern.findall(content)
count = 0 #计数器. 为了防止处理超时,限定每次最多下载4个图片
for e in l :
count += 1
if count > 4 :
entry.imageloaded=False
logging.info('Donwload Image Count > 4, Exit!')
break
#获取必要参数
url=e[0]
name=e[1]
#下载图片
rs=downloadimage(url)
if rs :
#保存图片
bits=db.Blob(str(rs[1]))
media=Media(name=name,mtype=rs[0],bits=bits)
media.put()
#置换图片链接
change='"/media/'+str(media.key())+'"'
logging.info('Save Image To : '+ change)
#tmpPattern=re.compile('"'+url.replace('?','\?')+'"')
#content=tmpPattern.sub(change,content)
content = content.replace('"'+url+'"',change)
else:
entry.imageloaded=False
entry.content = content
entry.put()
if len(l)>0 :
memcache.delete('/'+entry.link)
logging.info('Imageloader OK')
return titleclass imageloader(webapp.RequestHandler):
def get(self):
title=imageloadertask(self.request.get("postid"))
self.response.out.write('<html><head>')
self.response.out.write('<meta http-equiv="Content-Type" content="text/html;charset=utf-8;" />')
self.response.out.write('</head><body>')
self.response.out.write(cgi.escape(title + ' OK!'))
self.response.out.write('</body></html>')application = webapp.WSGIApplication([('/cron/imageloader', imageloader)],
debug=True)def main():
run_wsgi_app(application)if __name__ == "__main__":
main()







