今天第一次写爬虫,感觉非常有趣!,中途也遇到了许多问题,所以写篇博客~
目标:爬取豆瓣编程类书籍中9分以上的
刚接触爬虫,说下我的认识(不一定准确^_^)
我们知道网页的呈现也是用编程语言写出来的,有源码,每个网页我们都可以查看它的源码,我的浏览器快捷键是Ctrl+U,
一般点击右键就可以看见查看源码。因为要爬取豆瓣的数据,那看看下图豆瓣图书页面的部分源码
它所对应的数据是这样的
那么我们知道了,网页上所能看见的每个数据在源码上都能找到,有的点击会跳转也是因为源码上链接着其他地方。
所以我们直接分析源码即可。
思路
其实很简单,我们想要爬取编程类9分以上的书籍。那么从编程类的首页开始爬取数据。
用正则表达式来过滤数据,取出9分以上的数据,关于正则表达式如果不熟悉可参考我的上篇文章^_^
然后我们在每个网页的最后可以发现跳转到下一个网页的链接,通过正则表达式过滤出这个链接即可。
保存这个链接作为下一次爬取的url。
我设置的结束条件是爬取当前页的9分以上书籍为0时跳出循环,为了简单起见。
看代码:
#!/usr/bin/env python #coding:UTF-8 import urllib import re #得到一个网页的源码 def gethtml(url): #返回类似文件描述副,可进行读操作,read返回str page = urllib.urlopen(url) html = page.read() return html #根据正则得到我们想要的数据 def getdata(obj): patt = r'(<dl>.+?>(9\.\d).+?</dl>)' #编译正则模块 pattern = re.compile(patt, re.DOTALL) #findall查询此页面所有符合条件的数据,返回list m = pattern.findall(obj) Len = len(m) print 'len: %d' % Len if m is not None: for i in m: print i[1], print else: print 'not found' #如果此页面的9分数据为0,则认为没有9分数据 if Len == 0: return False else: return True #得到下一个页面 def getnextpage(obj): #我们要爬去的每个页面url前面都一样,所以只换后面的数据即可 nextpage = 'http://www.douban.com/tag/%E7%BC%96%E7%A8%8B/book?start=' #根据正则匹配后面的数据,然后连接到一起 patt = '<span class="break">...</span>.+start=(\d+)' pattern = re.compile(patt, re.DOTALL) #下一个页面只有一个符合数据,所以用search m = pattern.search(obj) if m is not None: nextpage += m.group(1) else: print 'not found' return nextpage if __name__ == '__main__': #起始页面 html = gethtml("http://www.douban.com/tag/%E7%BC%96%E7%A8%8B/book") while 1: if getdata(html) == False: break ret = getnextpage(html) html = gethtml(ret)
注释写的比较详细。运行结果我输出了每个页面的书籍分数,len是此页面9分以上书籍的数量
运行结果:部分数据
遇到的问题:貌似都是正则表达式的问题 - - 。
1.
网页源码中每个书籍是这样的
可以看见我们要分析的数据中间包含中文,过滤中文首先必须要在开头加上
#coding:UTF-8,‘ . ‘符号在正则中可以过滤任何字符,\w只能是字母和数字
其次这段数据中包含了许多换行符号,在正则表达式中\s只能过滤空白字符,不能过滤回车。
我们需要在
#编译正则模块 pattern = re.compile(patt, re.DOTALL)
里面设则,DOTALL的意思就是忽略回车符号
2.findall的问题
findall是正则匹配中一次匹配多个数据的函数
它返回的是一个list。
先来看看遇到的问题吧
在getdata中,我的需求是获取每本书的整个标签,也就是上面的图片,但是我还想顺便过滤出分数,就是把分数也作为一个分组可以查看。
类似m = search.( ),m.group(0),m.group(1)。
但是findall返回的是list,里面的数据是str。
和小伙伴讨论后又仔细了书,结论:
当正则表达式只有一个子组的时候(()是一个子组),findall( )返回子组匹配的字符串组成的列表,如果表达式有多个子组,返回的是一个元组的列表
元组中的每个元素都是一个子组的匹配内容,像这样的元组构成了返回列表中的元素
这也是为什么代码中我用的是m[1],因为返回的是一个元组呗!
顺便吐槽下python核心编程这本书,我目前发现的印刷错误至少十几处了!
而且findall( )这部分除了简单的介绍外没有给任何例子,结尾还说了句 “这些内容初次听到可能感到费解,但如果你看看各种例子,就会明白了”
它没给例子...
3.贪婪匹配
很重要,当时我只是简单的看了看,没太在意,结果实战就出错了
原本我的getdata正则是这样写的
def getdata(obj): patt = r'(<dl>.+>(9\.\d).+</dl>)'
怎么匹配也出不来
于是发现了贪婪匹配的问题
正则表达式默认是贪心匹配的,简单来说,如果正则表达式用到通配符(*, +, ?)等,它在从左到右匹配的时候会尽量匹配
最长的字符串。
看个例子:
#!/usr/bin/env python #coding:UTF-8 import re s = 'helloworld800-333-3333' patt = '.+(\d+-\d+-\d+)' m = re.search(patt, s) print m.group(1)
我们想得出结果800-333-3333这个号码
结果却是
因为patt前面的‘.+‘默认是贪婪匹配的,它会匹配它能匹配尽量多的字符,所以我们后面的\d+只能匹配了一个0而已
解决办法是用非贪婪操作符号 ‘ ? ‘
那么‘.+‘就不会尽量读取多的字符
patt = '.+?(\d+-\d+-\d+)'
可以运行下试试,结果为800-333-3333
正则表达式问题的却比较多,一个学长建议我用BeautifulSoup来解析,会方便很多,感兴趣可以试试。
简单的入门,见笑了 ^_^