网络爬虫的概念

网络爬虫(Web Spider)是一种自动获取网页内容的程序,是搜索引擎的重要组成部分。 初次深入了解爬虫的概念是在吴军博士的《数学之美》中,其中描述互联网本质上就是一张无形的大网,我们可以把每一个网页当做网中的节点,超链接作为连接节点的弧。这样网络爬虫就可以从任何一个网页出发,通过图的遍历算法,自动的访问每一个网页并将它们存起来。

实际上程序的目的都是代替人来完成大量的重复操作,爬虫亦是如此。假设现有一个需求是需要获取中国石油大学新闻网的所有新闻标题及发布日期来进行统计分析,那么用人来实现这个需求需要大概以下几个步骤。

  1. 通过浏览器访问中国石油大学新闻网
  2. 根据网页内容分析出哪些地方是我们需要获取的标题和日期。
  3. 将获取的数据存储到文件中。
  4. 跳转到下一页重复执行2和3步骤直到所有内容获取完毕。

接下来我们就会按照以上思路用Python编写一个简单的爬虫。

urllib2抓取网页内容

网页抓取就是把URL地址指定的网络资源读取出来,保存到本地。相当于我们平时在浏览器中通过网址浏览网页,只不过我们看到的是解析过的页面效果,而程序获取到的是源代码等资。 Python的标准库和组件非常强大,可以处理包括数学计算、网络传输、正则表达式等诸多操作。urllib2就是使用各种协议打开url的一个扩展包。最简单的方法就是调用urlopen的方法,比如:

import urllib2

url="http://news.upc.edu.cn/sdyw/"
response=urllib2.urlopen(url)
html=response.read()
print html

在Sublime中输入以上代码,按Ctrl+B执行代码(IDLE中F5执行代码)可以看到运行结果。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/loose.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>中国石油大学新闻网</title>
<meta name="keywords" content='石大门户网,石大资讯,石大新闻,中国石油大学新闻网,创造太阳网,石油之光' />
<!--省略以下运行结果-->

我们在浏览器中打开该网址,右键选择查看源代码发现和输出的内容是一致的,这时我们第一步已经完成了。

正则表达式返回匹配信息

第二步中分析出哪些信息是我们需要的信息这个过程对于人来说非常简单,但是对于电脑就没那么容易了,这里需要引入正则表达式的概念。

正则表达式,又称正规表示法、常规表示法(英语:Regular Expression,在代码中常简写为regex、regexp或RE),计算机科学的一个概念。正则表达式使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。在很多文本编辑器里,正则表达式通常被用来检索、替换那些符合某个模式的文本。

简而言之,正则表达式就是为了处理相对复杂的文本查询或替换。 我们在新闻标题上单击右键,选择查看元素,这样Chrome浏览器下方就会出现开发者工具,并定位新闻标题所在HTML的位置。 enter image description here

<a href="/sdyw/2014/04/17/15361630123.shtml" target="_blank">1项目入选团中央学校共青团重点工作创新试点</a>

我们分析HTML可以得知<a href="/sdyw/是标题的固定前缀,2014/04/17是新闻发布日期,15361630123.shtml是随机生成的静态页面名称。1项目入选团中央学校共青团重点工作创新试点是我们需要获取的新闻标题,最后以</a>结尾,实现代码如下。

# -*- coding: utf-8 -*- 
import urllib2
import re
# 1.获取访问页面的HMTL
url="http://news.upc.edu.cn/sdyw/"
response=urllib2.urlopen(url)
html=response.read()
# 2.根据正则表达式抓取特定内容
r=re.compile(r'<a href="/sdyw/(?P<Date>.{10}).*" target="_blank">(?P<Title>.+)</a>')
news=r.findall(html)
print news

在上方加入import re来导入python自带的正则表达式模块,通过re.compile(strPattern[, flag])方法将字符串形式的正则表达式编译为Pattern对象,第二个参数flag是匹配模式,我们此处省略填写。

正则表达式字符串开头有一个前缀r,r是raw(原始)的意思。因为在表示字符串中有一些转义符,如表示回车'\n'。如果要表示\需要写为'\\'。但如果我就是需要表示一个'\'+'n',不用r方式要写为:'\\n'。但使用r方式则为r'\n'这样清晰多了。

固定字符串<a href="/sdyw/后跟了一个用括号包起来的字符串(?P<Date>.{10})(?P<name>...) 是定义一个命名组,(?P=name)则是对命名组的逆向引用。而后面的.匹配任意除换行符"\n"以外的字符。.{10}匹配10个任意字符,<a href="/sdyw/(?P<Date>.{10}).*的意思也就很清楚了,是将/sdyw/后面的10个字符命名为Date,后面的Title也是同样的道理。

最后通过news=r.findall(html)将在html中所有匹配的字符串以列表的形式返回。输出news就可以看到返回结果了,但是输出的内容好像存在一些问题,输出的中文全部为\xe9\xa1\xb9\xe7这种样式。这是因为存在于list,dict等容器中的字符是以unicode字符编码方式存在的,单独打印某一项的时候,会显示成中文字符。

我们将print news替换成以下代码就可以再控制台看见内容正常输出了,如果仍然报错,查看最上方是否加入了# -*- coding: utf-8 -*-

for i in range(len(news)):
    date=news[i][0]
    title=news[i][1]
    print title+" "+date

至此我们已经完成了第二步获取标题和日期。由于本文并不是主要介绍正则表达式,建议读者通过正则表达式30分钟入门教程来深入了解正则表达式。

将获取的字段存入MySQL

本节涉及到Python操作MySQL的操作,请读者在Python IDLE中执行import MySQLdb as mdb来检查 Python连接MySQL模块是否可用,如果执行报错请先检查MySQL和相关模块是否安装。

如果上面代码没有报错,那我们可以开始学习如何用Python操作MySQL了。首先,我们新建一个文件通过Python来创建一个表来熟悉一下MySQL的操作流程(此处有一定基础可以跳过)。

# -*- coding: utf-8 -*-
import MySQLdb as mdb
import sys

try:
    con=mdb.connect('localhost','user','password','python',charset='utf8')
    cur = con.cursor()
    cur.execute("CREATE TABLE IF NOT EXISTS News\
    (`ID`  INT PRIMARY KEY NOT NULL AUTO_INCREMENT ,`PublishDate`  date NOT NULL ,`Title`  varchar(255) NOT NULL);")
except mdb.Error, e:
    print "Error %d: %s" % (e.args[0],e.args[1])
    sys.exit(1)
finally:
    if con:    
        con.close()

下面代码中,我们连接到名为python的数据库并执行新建News表的操作。 con=mdb.connect('localhost','user','password','python',charset='utf8') 其中connect()方法一般只需要填写四个参数。第一个是MySQL数据库所在的主机地址,在我们的例子中为'localhost';第二个参数是数据库的用户名,第三个是该用户的密码;第四个参数是数据库的名称。我们这里额外填写了charset='utf8'是为了避免写入数据库中出现乱码,charset是要跟你数据库的编码一样,如果是数据库是gb2312 ,则写charset='gb2312'。

备注:写入数据库编码的问题纠结了我很久,请大家一定要注意

cur = con.cursor()
    cur.execute("CREATE TABLE IF NOT EXISTS News\
    (`ID`  INT PRIMARY KEY NOT NULL AUTO_INCREMENT ,`PublishDate`  date NOT NULL ,`Title`  varchar(255) NOT NULL);")

一旦连接成功,我们将会得到一个cursor(游标)对象,我们通过调用该cursor对象的execute()方法来执行SQL语句。

except mdb.Error, e:
    print "Error %d: %s" % (e.args[0],e.args[1])
    sys.exit(1)

这个地方是用来抛出异常。

finally:            
    if con:    
        con.close()

最后释放掉连接资源。

我们可以通过SQL查询或者客户端查看表是否创建成功。回到正题——如何将获取的日期和标题存入MySQL中的news表?因为上一节我们已经获取到数据了,现在只需要调用`cusor.excute()执行INSERT INTO news2(PublishDate,Title) VALUES(%s,%s)",(date,title))即可。 最终代码如下:

# -*- coding: utf-8 -*- 
import urllib2
import re
import MySQLdb as mdb

url="http://news.upc.edu.cn/sdyw/"
response=urllib2.urlopen(url)
html=response.read()
r=re.compile(r'<a href="/sdyw/(?P<Date>.{10}).*" target="_blank">(?P<Title>.+)</a>')
news=r.findall(html)
con=mdb.connect('localhost','root','root','Python',charset='utf8')
with con:
    curs=con.cursor()
    for i in range(len(news)):
        date=news[i][0]
        title=news[i][3]
        print title+" "+date
        curs.execute("INSERT INTO News(PublishDate,Title) VALUES(%s,%s)",(date,title))

这里用with关键字来代替try-catch,Python会在with结束的时候调用con自带的__exit__方法提交事务和释放连接资源。同样,with也可以处理异常。

因为这里获取的时间格式不需要处理就直接可以插入到数据库中,而实际的例子中可能会复杂很多,需要对获取的数据进行处理,排重等操作。

在数据库中查看,我们已经将数据保存在News表了! enter image description here

遍历所有新闻页面

上一节我们获取到了第一页的新闻数据,但是我们的目的是需要获取所有的内容。 那我们跳转到其他分页看看有没有什么规律可循。通过分析发现,第一页的url为http://news.upc.edu.cn/sdyw/List_171.shtml,第二页的url为http://news.upc.edu.cn/sdyw/List_170.shtml,最后一页的url为http://news.upc.edu.cn/sdyw/List_1.shtml。那么遍历所有新闻页面获取内容这件事就变的非常简单了,直接通过for循环就可以实现,废话不说了,直接上代码吧。

# -*- coding: utf-8 -*- 
import urllib2
import re
import MySQLdb as mdb

if __name__=="__main__":
    url="http://news.upc.edu.cn/sdyw/List_"
    con=mdb.connect('localhost','root','root','Python',charset='utf8')
    with con:
        for x in xrange(1,171):
            response=urllib2.urlopen(url+str(x)+'.shtml')
            html=response.read()
            r=re.compile(r'<a href="/sdyw/(?P<Date>.{10}).*" target="_blank">(?P<Title>.+)</a>')
            news=r.findall(html)
            curs=con.cursor()
            for i in range(len(news)):
                date=news[i][0]
                title=news[i][5]
                curs.execute("INSERT INTO news2(PublishDate,Title) VALUES(%s,%s)",(date,title))
                print title+" "+date

到此,本文已经结束了,短短的20行代码就可以实现一个简单的爬虫,这就是Python的魅力。执行import this就可以看到Zen of Python

The Zen of Python, by Tim Peters

Beautiful is better than ugly.

Explicit is better than implicit.

Simple is better than complex.

Complex is better than complicated.

Flat is better than nested.

Sparse is better than dense.

Readability counts.

Special cases aren't special enough to break the rules.

Although practicality beats purity.

Errors should never pass silently.

Unless explicitly silenced.

In the face of ambiguity, refuse the temptation to guess.

There should be one-- and preferably only one --obvious way to do it.

Although that way may not be obvious at first unless you're Dutch.

Now is better than never.

Although never is often better than right now.

If the implementation is hard to explain, it's a bad idea.

If the implementation is easy to explain, it may be a good idea.

Namespaces are one honking great idea -- let's do more of those!