Using Django with GAE Python 后台抓取多个网站的页面全文

http://blog.sina.com.cn/s/blog_6266e57b010128l4.html


序,引子


谨以此文,感谢那些在自己的博客上提供优质的问题解答的寂寞英雄们。。我要是女的,就嫁给默默的你们中的一个

一直想做个能帮我过滤出优质文章和博客的平台 给它取了个名 叫Moven。。 把实现它的过程分成了三个阶段:
1. Downloader: 对于指定的url的下载 并把获得的内容传递给Analyser--这是最简单的开始
2. Analyser: 对于接受到的内容,用Regular Expression 或是 XPath 或是BeautifulSoup/lxml 进行过滤和简化--这部分也不是太难
3. Smart Crawler: 去抓取优质文章的链接--这部分是最难的:
      Crawler的话可以在Scrapy Framework的基础上快速的搭建
      但是判断一个链接下的文章是不是优质 需要一个很复杂的算法

最近就先从Downloader 和 Analyser 开始: 最近搭了一个 l2zstory  并且还有一个  ZLife  和  ZLife@Sina  还有一个她的博客 做为一个对Downloader 和Analyser的练习 我就写了这个东西来监听以上四个站点 并且把它们的内容都同步到这个站上:


Z <wbr>Story <wbr>: <wbr>Using <wbr>Django <wbr>with <wbr>GAE <wbr>Python <wbr>后台抓取多个网站的页面全文

App 的特色

这个站上除了最上面的黑色导航条 和 最右边的About This Site 部分外, 其他的内容都是从另外的站点上自动获得
原则上, 可以添加任何博客或者网站地址到这个东西。。。当然因为这个是L2Z Story..所以只收录了四个站点在里面
特点是: 只要站点的主人不停止更新, 这个东西就会一直存在下去---这就是懒人的力量

值得一提的是, Content 菜单是在客户端用JavaScript 自动生成的--这样就节约了服务器上的资源消耗

Z <wbr>Story <wbr>: <wbr>Using <wbr>Django <wbr>with <wbr>GAE <wbr>Python <wbr>后台抓取多个网站的页面全文

这里用的是html全页面抓取 所以对那些feed没有全文输出的站点来说, 这个app可以去把它要隐藏的文字抓来

在加载的时候会花很多时间因为程序会自动到一个没有全文输出的页面上抓取所有的文章列表,作者信息,更新时间,以及文章全文。。所以打开的时候请耐心。。。下一步会加入数据存储部分,这样就会快了。。


技术准备
   
前端:

1. CSS 在信奉简单之上的原则上 twitter的bootstrap.css满足了我大多数的要求 个人超喜欢它的 GridSystem 
2. Javascript上, 当然选用了jQuery 自从我开始在我的第一个小项目上用了jQuery 后 我就爱上了它  那个动态的目录系统就是用jQuery快速生成的
    为了配合bootstrap.css,bootstrap-dropdown.js 也用到了

服务器:

这个app有两个版本:
      一个跑在我的Apache上, 但是因为我的网络是ADSL, 所以ip一直会变基本上只是我在我的所谓的局域网内自测用的。。这个版本是纯Django的
      另一个跑在GoogleApp Engine上 地址是  http://l2zstory.appspot.com  在把Django配置到GAE的时候我花了很多功夫才把框架搭起来

后台:

主要语言是Python--不解释, 自从认识Python后就没有离开它

主要用到的module是 

1. BeautifulSoup.py 用于html 的解析--不解释
2. feedparser.py 用于对feedxml的解析--网上有很多人说GAE不支持feedparser..这里你们得到答案了。。可以。。这里我也是花了很久才弄明白到底是怎么回事。。总之简单讲就是: 可以用!但是feedparser.py这个文件必须放到跟app.yaml同一个目录中 不然会出现网上众人说的不可以importfeedparser的情况

数据库:
Google Datastore: 在下一步中, 这个程序会每隔30分钟醒来 逐一查看各个站点有没有更新并抓取更新后的文章并存入Google 的Datastore中 


App 的配置


遵循Google的规则, 配置文件app.yaml 如下:
这里主要是定义了一些static directory--css 和 javascript的所在地

application: l2zstory
version: 1
runtime: python
api_version: 1

handlers:
     
- url: /images
  static_dir:l2zstory/templates/template2/images
- url: /css
  static_dir:l2zstory/templates/template2/css
- url: /js
  static_dir:l2zstory/templates/template2/js   
- url: /js
  static_dir:l2zstory/templates/template2/js
- url: /.*
  script:  main.py

URL的配置

这里采用的是Django 里的正则表达式
from django.conf.urls.defaults import *

# Uncomment the next two lines to enable the admin:
# from django.contrib import admin
# admin.autodiscover()

urlpatterns = patterns('',
      #Example:
      #(r'^l2zstory/', include('l2zstory.foo.urls')),

      #Uncomment the admin/doc line below and add'django.contrib.admindocs'
      # toINSTALLED_APPS to enable admin documentation:
      #(r'^admin/doc/', include('django.contrib.admindocs.urls')),

      #Uncomment the next line to enable the admin:
      #(r'^admin/(.*)', admin.site.root),
      (r'^$','l2zstory.stories.views.L2ZStory'),
      (r'^YukiLife/','l2zstory.stories.views.YukiLife'),
        (r'^ZLife_Sina/','l2zstory.stories.views.ZLife_Sina'),
        (r'^ZLife/','l2zstory.stories.views.ZLife')
)

Views的细节

对Django比较熟悉的人应该会从url的配置中看到view的名字了 我只把L2ZStory的这个view贴出来因为其他的在view里的架构至少是差不多的
#from BeautifulSoup import BeautifulSoup
from PyUtils import getAboutPage
from PyUtils import getPostInfos

def L2ZStory(request):
      about_url=" http://l2zstory.wordpress.com/about/"
      blog_type="wordpress"
      htmlpages={}
      aboutContent=getAboutPage(about_url,blog_type)
      ifaboutContent=="Not Found":
              aboutContent="We use this to tell those past stories..."
      htmlpages['about']={}
      htmlpages['about']['content']=aboutContent
      htmlpages['about']['title']="About This Story"
      htmlpages['about']['url']=about_url
      PostInfos=getPostInfos(url,blog_type,order_desc=True)
      returnrender_to_response('l2zstory.html',
{'PostInfos':PostInfos,
'htmlpages':htmlpages
})

这里主要是构建一个dictionary of dictionary   htmlpages 和一个list of dictionary PostInfos
htmlpages 主要是存贮站点的 About, Contact US 之类的页面
PostInfos 会存贮所有文章的 内容, 作者, 发布时间 之类的

这里面最重要的是PyUtils。。这是这个app的核心

PyUtils的细节

我把一些我认为比较重要的细节加深了 并加了评论

import feedparser
import urllib2
import re
from BeautifulSoup import BeautifulSoup
header={
'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7;rv:8.0.1) Gecko/20100101 Firefox/8.0.1',
}

#用来欺骗网站的后台。。象新浪这类的网站对我们这类的app十分不友好。。。希望它们可以多象被墙掉的wordpress学一学。。


timeoutMsg="""
The Robot cannot connect to the desired page due to either ofthese reasons:
1. Great Fire Wall
2. The Blog Site has block connections made by Robots.
"""

def getPageContent(url,blog_type):
      try:
              req=urllib2.Request(url,None,header)
              response=urllib2.urlopen(req)
              html=response.read()
              html=BeautifulSoup(html).prettify()
              soup=BeautifulSoup(html)
              Content=""
                ifblog_type=="wordpress":
           try:
               for Sharesection in soup.findAll('div',{'class':'sharedaddysd-like-enabled sd-sharing-enabled'}):
                   Sharesection.extract()
               for item in soup.findAll('div',{'class':'post-content'}):
                   Content+=unicode(item)
           except:
               Content="No Post Content Found"
       elif blog_type=="sina":
           try:
               for item insoup.findAll('div',{'class':'articalContent '}):
                   Content+=unicode(item)
           except:
               Content="No Post Content Found"

       #对于不同的网站类型 应用不同的过滤器

      except:
              Content=timeoutMsg
      returnremoveStyle(Content)

def removeStyle(Content):
        #addthis to remove all the img tag : (<img+(\w=\".*\")>)|(</img>)|(src=\".*\")|
   patn=re.compile(r"(align=\".*\")|(id=\".*\")|(class=\"*\")|(style=\".*\")|(</font>)|(<font.*\">)|(<embed+(\w*=\".*\")>)|(</embed>)")
   replacepatn=""

   Content=re.sub(patn,replacepatn,Content)
   #运用正则表达式把抓取的内容中那些格式通通去掉 这样得到的文字比较纯粹
      returnContent
     
def getPostInfos(url,blog_type,order_desc=False):
      feeds=feedparser.parse(url)
      PostInfos=[]
      iforder_desc:
              items=feeds.entries[::-1]
      else:
              items=feeds.entries
      Cnt=0
      for  item in items:
              PostInfo={}
              PostInfo['title']=item.title
              PostInfo['author']=item.author
              PostInfo['date']=item.date
              PostInfo['link']=item.link
             
              if blog_type=="wordpress":
                      Cnt+=1
                      if Cnt<=8:
                              PostInfo['description']=getPageContent(item.link,blog_type)
                      else:
                              PostInfo['description']=removeStyle(item.description)
              elif blog_type=="sina":
                      PostInfo['description']=removeStyle(item.description)
                     
             
              PostInfos.append(PostInfo)
             
      returnPostInfos

template 的概览

在简单之上的原则的鼓舞下, 所有的站点都统一使用一个template 这个template只接受两个变量--前文中提到的htmlpages 和 PostInfos
重要的片断是:
<div class="page-header">
                                                          <a href=" {{htmlpages.about.url}}"name=" {{htmlpages.about.title}}"><h3>{{htmlpages.about.title}}</h3></a>
                                                         
                                                </div>
                                                <p>
                                                            {{htmlpages.about.content}}
                                                </p>
                                                  {%foritem in PostInfos%}
                                                <div class="page-header">
                                                          <a href=" {{item.link}}"name=" {{item.title}}"><h3> {{item.title}}</h3></a>
                                                         
                                                </div>
                                                <p><i>author:  {{item.author}}  &nbsp;&nbsp;date:  {{item.date}}</i></p>
                                                <p> {{item.description}}</p>
                                                  {%endfor%}
                                      </div>



序,引子

谨以此文,感谢那些在自己的博客上提供优质的问题解答的寂寞英雄们。。我要是女的,就嫁给默默的你们中的一个

回来有了快5天了,冷的很 因为Google App Engine 被墙,我无法继续完善我的 Movenproject  还有20+天才回去,怕到时候会忘记project的进度和细节就趁着个冷的什么都不想干的时候, 大概的总结一下:

1.增加了Cron: 用来告诉程序每隔30分钟 让一个task 醒来, 跑到指定的那几个博客上去爬取最新的更新
2.用google 的 Datastore 来存贮每次爬虫爬下来的内容。。只存贮新的内容。。

就像上次说的那样,这样以来 性能有了大幅度的提高: 原来的每次请求后, 爬虫才被唤醒 所以要花大约17秒的时间才能从后台输出到前台而现在只需要2秒不到

3.对爬虫进行了优化

1. Cron.yaml 来安排每个程序醒来的时间

经过翻文档, 问问题终于弄明白google的cron的工作原理--实际上只是google每隔指定的时间虚拟地访问一个我们自己指定的url…
因此在Django 下, 根本不需要写一个纯的python 程序   一定不要写:
      if__name__=="__main__": 
只需要自己配置一个url 放在views.py里:

def updatePostsDB(request):

    #deleteAll()

   SiteInfos=[]

   SiteInfo={}

   SiteInfo['PostSite']="L2ZStory"

   SiteInfo['feedurl']="feed://l2zstory.wordpress.com/feed/"

   SiteInfo['blog_type']="wordpress"

   SiteInfos.append(SiteInfo)

   SiteInfo={}

   SiteInfo['PostSite']="YukiLife"

   SiteInfo['feedurl']="feed://blog.sina.com.cn/rss/1583902832.xml"

   SiteInfo['blog_type']="sina"

   SiteInfos.append(SiteInfo)

   SiteInfo={}

   SiteInfo['PostSite']="ZLife"

   SiteInfo['feedurl']="feed://ireallife.wordpress.com/feed/"

   SiteInfo['blog_type']="wordpress"

   SiteInfos.append(SiteInfo)

   SiteInfo={}

   SiteInfo['PostSite']="ZLife_Sina"

   SiteInfo['feedurl']="feed://blog.sina.com.cn/rss/1650910587.xml"

   SiteInfo['blog_type']="sina"

   SiteInfos.append(SiteInfo)

    

   try:

       for site in SiteInfos:

           feedurl=site['feedurl']

           blog_type=site['blog_type']

           PostSite=site['PostSite']

           PostInfos=getPostInfosFromWeb(feedurl,blog_type)

           recordToDB(PostSite,PostInfos)

       Msg="Cron Job Done..." 

    exceptException,e:

       Msg=str(e)   

      returnHttpResponse(Msg)


cron.yaml 要放在跟app.yaml同一个级别上:

cron:

- description: retrievenewest posts

  url:/task_updatePosts/

  schedule: every 30 minutes

在url.py 里只要指向这个把task_updatePostsDB 指向url就好了

调试这个cron的过程可以用惨烈来形容。。。在stackoverflow上有很多很多人在问为什么自己的cron不能工作。。。我一开始也是满头是汗,找不着头脑。。。最后侥幸弄好了,大体步骤也是空泛的很。。但是很朴实:
      首先,一定要确保自己的程序没有什么syntaxerror….然后可以自己试着手动访问一下那个url 如果cron 正常的话,这个时候任务应该已经被执行了 最后实在不行的话多看看log…

2. Datastore的配置和利用--Using Datastore with Django

我的需求在这里很简单--没有join…所以我就直接用了最简陋的django-helper..
这个models.py 是个重点:

fromappengine_django.models import BaseModel

fromgoogle.appengine.ext import db


classPostsDB(BaseModel):

   link=db.LinkProperty()

   title=db.StringProperty()

   author=db.StringProperty()

   date=db.DateTimeProperty()

   description=db.TextProperty()

    postSite=db.StringProperty()

前两行是重点中的重点。。。。我一开始天真没写第二行。。。结果我花了2个多小时都没明白是怎么回事。。得不偿失。。。
读写的时候, 千万别忘了。。。PostDB. put()

一开始的时候,我为了省事,就直接每次cron被唤醒, 就删除全部的数据, 然后重新写入新爬下来的数据。。。
结果。。。一天过后。。。有4万条读写纪录。。。。而每天免费的只有5万条。。。。
所以就改为在插入之前先看看有没有更新, 有的话就写,没的话就不写。。总算把数据库这部分搞好了。。。

3.爬虫的改进:
一开始的时候,爬虫只是去爬feed里给的文章。。这样一来,如果一个博客有24*30篇文章的话。。。最多只能拿到10篇。。。。
这次,改进版能爬所有的文章。。我分别拿孤独川陵, 韩寒,Yuki和Z的博客做的试验。。成功的很。。。其中孤独川陵那里有720+篇文章。。。无遗漏掉的被爬下来了。。

import urllib

#from BeautifulSoupimport BeautifulSoup

from pyquery import PyQueryas pq

def getArticleList(url):

   lstArticles=[]

    url_prefix=url[:-6]

   Cnt=1

    

   response=urllib.urlopen(url)

   html=response.read()

   d=pq(html)

    try:

       pageCnt=d("ul.SG_pages").find('span')

       pageCnt=int(d(pageCnt).text()[1:-1])

   except:

       pageCnt=1

    for i inrange(1,pageCnt+1):

       url=url_prefix+str(i)+".html"

        #printurl

       response=urllib.urlopen(url)

       html=response.read()

        d=pq(html)

        title_spans=d(".atc_title").find('a')

       date_spans=d('.atc_tm')

        

       for j in range(0,len(title_spans)):

           titleObj=title_spans[j]

           dateObj=date_spans[j]

           article={}

           article['link']= d(titleObj).attr('href')

           article['title']= d(titleObj).text()

           article['date']=d(dateObj).text()

           article['desc']=getPageContent(article['link'])

           lstArticles.append(article)

    returnlstArticles

    

def getPageContent(url):

    #getPage Content

   response=urllib.urlopen(url)

   html=response.read()

   d=pq(html)

   pageContent=d("div.articalContent").text()

    #printpageContent

    returnpageContent

def main():

   url='http://blog.sina.com.cn/s/articlelist_1191258123_0_1.html'#HanHan

   url="http://blog.sina.com.cn/s/articlelist_1225833283_0_1.html"#GuDu Chuan Ling

   url="http://blog.sina.com.cn/s/articlelist_1650910587_0_1.html"#Feng

   url="http://blog.sina.com.cn/s/articlelist_1583902832_0_1.html"#Yuki

   lstArticles=getArticleList(url)

    forarticle in lstArticles:

        f=open("blogs/"+article['date']+"_"+article['title']+".txt",'w')

       f.write(article['desc'].encode('utf-8')) #特别注意对中文的处理

       f.close()

        #printarticle['desc']

        

if__name__=='__main__':

    main()
 
 对PyQuery的推荐。。
很遗憾的说,BueautifulSoup让我深深的失望了。。。在我写上篇文章的时候,当时有个小bug..一直找不到原因。。在我回家后,又搭上了很多时间试图去弄明白为什么BueautifulSoup一直不能抓到我想要的内容。。。后来大体看了看它selector部分的源代码觉得应该是它对于很多还有<script>tag的不规范html页面的解析不准确。。。

我放弃了这个库, 又试了lxml..基于xpath 很好用。。但是xpath的东西我老是需要查文档。。。所以我又找了个库PyQuery…可以用jQuery选择器的工具。。。非常非常非常好用。。。。具体的用法就看上面吧。。。这个库有前途。。。

 隐忧 
因为pyquery基于lxml…而lxml的底层又是c…所以估计在gae上用不了。。。我这个爬虫只能现在在我的电脑上爬好东西。。。然后push到server上。。。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值