聚沙成塔--爬虫系列(四)(爬取糗事百科段子)

字数 2847阅读 161

版权声明:本文为作者原创文章,可以随意转载,但必须在明确位置表明出处!!!

通过上一遍文章我们对python的基础语法和正则表达式有了一定的了解,从这边文章开始我们将进入实战,大家不要害怕,学习爬虫我们首先要做的是定一个目标,要相信没有爬不下来的东西,毕竟网页都是人写得,它们也遵循html规则,关于html标签语言可以到菜鸟教材·去稍微了解一下。这里我们通过爬取糗事百科的段子来作为python3爬虫系列的第一个教程,因为糗事百科基本上是静态网页,所以这类网页是是最好爬取的网页,非常适合爬虫的入门教程。

urllib和re(正则表达式模块)

爬虫,爬虫,就是把网上的内容爬到本地来,urllib模块的作用就是把网页上的内容去回到本地来,urllib是封装了http协议,至于http协议是怎么回事本篇内容不做讨论,http协议会在该系列之后的文章中对它做出详细的讲解。这里我们只需要知道urllib的作用是将网页上的内容取回到本地。取回到本地后它就是文本信息了,而要从文本信息中提取我们需要的信息就需要我们使用re模块(正则表达式模块)。

查看页面元素

首先我们来看一下糗事百科的主页长什么样子


4-1 糗百主页

从主页上可以看到每一个用户发表的一条段子都是一块,一块的分割开的。这就是html标签语言<div>...</div>的作用,如果你对htm标签语言不熟习,可以花个半天时间去了解了解,了解html标签语言有助于更好的理解网页的布局,当然对你编写正则表达式规则也有莫大的帮助。

F12开发则工具

浏览器上都自带了开发则工具,只要你是web开发人员,这个工具你肯定会使用到,不管你是调试JS,还是css都会用到此工具。当然我们爬虫程序更需要用到此工具了,下面我们在糗事百科的页面按下键盘上的F12将会出现下面界面。


4-2 开发者工具

当然你看到的界面可能和我的不一样,这是浏览器不同造成的,我使用的是Chrome浏览器,如果你们使用的是IE或者Firefox浏览器按下F12肯定和我的是不一样的。

Elements、Network

按下F12后在出现的界面中有一行菜单栏,这里我们主要用到Elements、Network两个菜单

  • Elements: 元素,指的是当前网页元素(如果是静态网页那么在当前页面右键鼠标-->查看网页源代码所看到的页面元素和这里看到的页面元素是一样的,只是这里的看到的元素是格式化了的,方便开发人员查看)
  • Network: 网络,指的是浏览器和web服务器发生的所有网络交互

快速定位元素

在Elements菜单下移动鼠标到<div class="article block untagged mb15 typs_long" id="qiushi_tag_119595801"></div>项,可以看到页面上有“遇奸”这个用户的块区域被选中了。

4-3 快速定位

可以看到Elements菜单下有很多类是<div class="...">...</div>这样的标签,每一对这样的标签表示包含一个用户的所有信息,所以我们要查看“遇奸”这个用户发布的段子,那么肯定是在<div class="article block untagged mb15 typs_long" id="qiushi_tag_119595801"></div>之间了。

快速的定位到发表的段子

  1. 点击菜单栏最左边的按钮。
  2. 滑动鼠标到发表的段子上。
    是的只需要两步,你可以看到开发则工具界面就定位到发布的段子上了。
4-4 快速定位指定元素

是时候表演真正的技术了

通过上面的介绍我们对页面的基本元素已经有所了解了,那么如何从页面元素中获取用户发表的段子呢,这里我们定一个目标,我们需要获取用户名用户ID段子好笑数、如果用户发表的是图片我们还需要获取图片的url地址,下面正则表达式这位英雄就要登场了。

获取段子

当然最重要的就是段子了,我们的目的不就是为了爬取段子吗。首先我们在糗百页面右键鼠标-->产看页面源代码,然后Ctrl + a(全选)Ctrl + c(复制), 然后粘贴到正则表达式在线测试工具中。

4-5 正则表达式在线工具

我们先通过在线工具来测试我们写的正则表达式是否正确。

爬取思想

在解决一个事情之前我们首先要做的是先在脑子里想一想该怎么去解决这个问题,第一步做什么、第二步做什么、最后做什么、爬虫也一样我们首先要去想一想怎么去爬到我们需要的信息。从文章的前半部分开发者工具截图的Elements可以看出每个用户的信息都是包含在一对<div class="...">...</div>之中的,所以我们的第一步是把每个用户的所有信息取出来。

  • 第一步:取出包含在<div class="..">...</div>中的所有信息
  • 第二步:从第一步的结果中取出该用户发布的段子、用户名、用户ID、url图片地址、好笑数

首先取出包含在<div class="..">...</div>中的所有信息,正则表达式如下:

<div class="article block untagged mb15.*?</div>

鼠标点击[测试匹配]发现匹配结果为[没有匹配],如下图

4-6 正则表达式在线工具

这是怎么回事呢,点符号表示匹配任何字符除换行符外,星号表示匹配前面出现的正则表达式零次或多次,问号表示非贪婪匹配,非贪婪匹配的意思是尽可能少的匹配,我会在后面的章节中详细说明。按理说我们的正则表达式是没有问题的啊?这里需要特别主要了从上一篇聚沙成塔--爬虫系列(三)(正则表达式)文章介绍我们知道点符号匹配任何字符串(除换行符外),所以这里我们不能用点符号来匹配了,我们需要用到特殊符号\s表示匹配所有的空白符,\S表示匹配左右的非空白符,所以这两个符号的组合就表示匹配所以字符包括换行符,下面我们改写正则如下:

<div class="article block untagged mb15[\s\S]*</div>

鼠标点击[测试匹配]发现匹配结果为[没有匹配],如下图

4-7 正则表达式在线工具

结果显示了我们匹配到了25个结果,这25个结果正好是糗百一页发布的数量,也表示一页上有25个用户, 但是这个时候我们查看结果的时候发现并没有包含段子内容,为什么呢?是因为我们的正则表达式匹配到</div>结束,但是在最外层的</div class="...">...</div>之中还有类似的div对,所以就不会匹配到最外层的</div>结束标记,修改正则表达式如下:

<div class="article block untagged mb15[\s\S]*?class="stats-vote".*?</div>

取用户名、用户ID、段子、图片url地址

通过上一步我们已经取到了25个用户的所有信息,那么用户名和用户ID等等需要从这25个用户信息中获取到。这里我们又要回到开发者工具,通过上面所讲的操作,可以轻松定位到用户名,用户ID,段子

4-8 定位用户名,用户ID,段子内容

可以发现用户ID是在herf后面,用户名是在<h2>...</h2>之间,段子是在<span>...</span>之间的,所以我们需要的正则表达式如下:

<a href="(.*?)".*?<h2>(.*?)</h2>.*?<div class="content">(.*?)</div>

代码实现

from urllib import request
import re
url = 'https://www.qiushibaike.com'
user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36'
headers = {'User-Agent': user_agent}

# 构建一个请求对象
req = request.Request(url, headers=headers)
# 打开一个url
response = request.urlopen(req)
# 读取服务器返回的页面数据内容
content = response.read().decode('utf-8')
# 匹配所有用户信息
#pattern  = re.compile(r'<div class="article block untagged mb15[\s\S]*?<a href="(.*?)".*?<h2>(.*?)</h2>.*?<div class="content">(.*?)</div>',re.S)
pattern = re.compile(r'<div class="article block untagged mb15[\s\S]*?class="stats-vote".*?</div>', re.S)
userinfos = re.findall(pattern, content)
print(len(userinfos))

pattern = re.compile(r'<a href="(.*?)".*?<h2>(.*?)</h2>.*?<div class="content">(.*?)</div>', re.S)
for userinfo in userinfos:
    item = re.findall(pattern, userinfo)
    #print(item)
    userid, name, content = item[0]
    # 去掉换行符,<span></span>,<br/>符号
    userid = re.sub(r'\n|<span>|</span>|<br/>', '', userid)
    name = re.sub(r'\n|<span>|</span>|<br/>', '', name)
    content = re.sub(r'\n|<span>|</span>|<br/>', '', content)
    print((userid, name, content))

运行结果

('/users/34093917/', '杨槐树的花香', '逛商场,一美女把一条大狗拉到商场里了。样子很吓人。估计碰到一个男的,吓得他大叫一声:我的个妈呀,拉好你老公行不?美女不愿意说男的骂她。两人纠缠看热闹的越来越多,大家看美女把狗拉到商城都反感一边倒支持男的。人群中有人说话,着男的怎么这样,说狗是人家老公,真没素质。美女听到有人替她说话又硬气三分。另一个人回答就是,哪狗明明是人家儿子……\x01[生气了]\x01[生气了]\x01[生气了]')
('/users/24703920/', '甜甜橙※', '去浴池洗澡,发现一个大姐老是偷偷看我,也没在意!等我准备穿衣服的时候,偷瞄一眼大姐已经穿好了!突然大姐“嗖”的飘到我跟前,用手揉了一把我 咪 咪,嘴里碎碎念:“这么大这么好看居然是真的?!真的?!”我擦。。。')
('/users/226122/', '想爱爱就别给老婆', '阴天,LZ白天在家一直睡觉,3岁儿子在客厅玩耍一会跑过来问我:妈妈,你睡觉,睡醒了吗?一会又跑回来问:妈妈,你可以起床了吗?好吧,知道你无聊没事做了')
('/users/21674803/', '小晓超人', '我这个人最怕别人跟我比!若果你满世界的乱拉屎,我就可以把屎糊满你的全世界!')
('/users/31240860/', '残落之辉', '不知道有几个人有类似的经历。当你走在大街上,突然听见一苍老又陌生的声音在叫你的小名,你以为是许久未见的长辈而满怀激动的心情转过身时……却看见一只小土狗撒着欢的冲向一位老人……别拦我,让我咬死那只狗!!!')
('/users/34710929/', '星舞飞碟', '上公交车遇到一奶奶带一小孩,小孩又哭又闹,吵的全车人不得安宁,后来一高中生问他奶奶:你家小孩多少钱?瞬间不哭了')
('/users/32215536/', '吃了两碗又盛', '那天一朋友被他儿子的班主任请去谈话,回来时一脸俄罗斯方块。问他怎么了,他说:马 勒戈壁的,这小兔 崽 子不知道跟哪个王 八犊 子学会了说脏话!')
('/users/30381978/', 'LOVE蓝可', '单身狗一枚 去参加朋友婚礼  结果把车炸成这样了,注定孤独一生吧')
('/users/30476775/', '我的个脑子呀', '分享#今天早上去帮弟弟买作业本的时候遇到弟弟的好朋友在骑自行车,速度很快结果没刹住车一头撞在了树上,我本来想关心他一下,可是想到了前几天发生的事,我竟大笑了起来。。。。。。弟弟的这个好朋友,刚刚买了一辆自行车勤学苦练终于学会了,可不久之后听弟弟说他掉进了粪坑,然后不久又钻进了水沟,有一次来我家找我弟弟玩就把车停在了大门外,我爸说让他推进家里来,可是他就是不听说没事,和弟弟玩了一会他们正准备出去玩,可是发现他的自行车不见啦,最后他们找啊找啊急得不得了就是找不到,吓得不敢回家,就去了他奶奶家,(他奶…<span class="contentForAll">查看全文')
('/users/21447817/', '嘟嘟豆豆5', '洗洗睡觉了!!')
('/users/30998801/', '红尘一笑醉红颜~', '深 夜 缠 绵,情到深处忍不住呻 吟 浅 唱,老公说你小声点,我说不要,那样憋着多难受啊……然后他直接吻住我的嘴,把我憋的“呜呜”叫,奶奶的,一脚给他踹下去了,他委屈的爬起来告诉我,他听到隔壁有说话的声音,怕人家听到.....我说“你不在家我每天听他们家叫,现在也该让他们尝尝我的厉害了!”.....')
('/users/21821649/', '卟溫柔', '闺蜜老公感冒了,让我开车捎他们两口子去医院打针。看完医生去打针,只见那个小护士很利索的把针头扎了进去,闺蜜问她老公有感觉没?她老公看了看那么细的针头,说这么细,扎进去能有啥感觉?闺蜜看了她老公一眼说:“老公,你终于体会到我的感受了......”\x01[惊讶]\x01[惊讶]\x01[惊讶]我是不是发现了什么\x01[doge]\x01[doge]\x01[doge]')
('/users/12679679/', 'o初恋o', '一个客户在我早上熟睡之际因为订单问题给我电话,我爱发嗲,加上没睡醒更是软绵绵的声音,然后连着5天他每天早上都给我打电话喊我起床,关键我凌晨三四点才睡觉的,可烦死人啦')
('/users/31973521/', '锤子是主谋', '背景:“国庆回家过了半个月荒诞的生活。一直没有运动,回到上海锻炼一小时以后喝了口冰水开始胃疼。”我:“医生我锻炼了以后胃疼什么情况?以前没发生过这种事。”医生:“你不用紧张,这种情况不用吃药,过一会自己就好了。”我:“谢谢,那耽误我晚上喝酒不?”医生:“那我还是给你开点药吧。”我:......')
('/users/16696217/', '黄半仙Demigo…', '男:,,,,,,女:。。。。。。男:好女:来接我')
('/users/13521795/', '哥裤裆有杀气', '万恶的马赛克(转)')
('/users/30683297/', '傻妞也', '老师:“凡事要争第一,第二和倒数第一没什么区别。”小宝:“为什么?”老师:“我举个例子,你说世界上最高的山峰是什么峰?”小宝:“不知道啊,啥峰啊?”')
('/users/31087078/', '大冰冰儿儿', '别再自欺欺人了,鹿晗关晓彤俩人就是在一起了,我和彭于晏也要宣布了请大家不要伤心我们会幸福的[微笑]       @所有人')
('/users/9005124/', '男人不将就', '同事儿子六岁,上幼儿园。经常被一同学欺负,老师训斥也不管用。同事气不过,又不能和小孩一般见识,晚上怒气冲冲对他儿子说“你明天上学不把他(欺负他的同学)揍一顿,回来你就要挨打,若是打赢了晚上回来还有奖励。”第二天他儿子一入园就找那小子报仇去了,还把他打的流鼻血。老师问为什么打架,他儿子理直气壮的说“不打他晚上回去我爸要打我的。”搞的老师有点哭笑不得。同事被请去学校,在办公室老师当着好几个老师的面讲给他听,同事说当时真觉得丢人,过后又觉得很赞。当晚同事就带他儿子肯德基去了,还奖励了20。之后再也没…<span class="contentForAll">查看全文')
('/users/28264578/', '屌丝小叔', '我想说在群里抢红包说少的发,抢了个最少的0.53元,到我发时想发个5.3元手一抖53元没了糗吧!这还不是经典,经典的刚洗澡时把洗面膏当洗发膏用了,半天不起沫才发现用错了!唉')
('/users/19216493/', 'CTR小诺', '小仙女请发言      过麦')
('/users/9942197/', '植哥哥', '太单纯,怪我不懂……')
('/users/27749688/', '无言,无缘', '两手抓咪咪')
('/users/33390668/', '吃土吃够了', '我爸要给我买火箭了,再过两天就是我27岁的生日了,我跟我爸说想出去玩,让他给我买张机票当礼物,我爸说∶“我给你买个火箭你直接上天,免得老嫁不出去呆在地球上丢人!”我∶\x01[捂脸]\x01[捂脸]……')
('/users/26260464/', '紫燕双飞飞飞的', '好像很拉风。')

代码解读

  • import: 关键字,是导入python库文件的,当然也可以导入你自己写得文件,它的意思是要引用哪个模块,这里我们需要引用两个模块,一个是urllib模块,一个是re(正则表达式模块),引用了这两个模块后我们就能使用这两个模块提供的方法

  • url: 变量,这里的变量赋值为糗事百科的网址

  • user_agent: 用户代理,这个参数很关键,爬虫所有的动作都需要模拟人怎么去操作,这样服务器才不会拦截你的访问,user_agent表示我们使用的是什么浏览器去访问的,这里需要说明一下反爬策略,常见的反爬策略是服务器端会监测你的user_agent,ip, 访问频率,如果你同一个ip地址短时间内访问过多,服务器端会认为你不是人为操作,它会认为你是个机器人,因为人为操作没有这么频繁。所以爬和反爬是一对天敌,后面我会讲爬虫的高级应用,所谓“道高一尺,魔高一丈”是也。user_agent参数可以通过开发者工具Network菜单下的Headers查看

    4-9 user_agent参数查看

  • Request(...):该函数是创建一个请求实例,我们可以看看开发文档的描述

    4-10 python3开发文档urllib.request模块定义

  • urlopen(...): 打开一个url,该函数的定义如下
    urllib.request.urlopen(url, data=None, [timeout, ]*, cafile=None, capath=None, cadefault=False, context=None)
    url参数可以是一个字符串也可以是一个Request对象,更多的介绍可以查看开发文档

  • read(): 读取页面返回的内容。

  • decode():解码,我们读取回来的内容是经过编码后的字符串,如果不解码就看不明白都会来的字符串,如何查看页面是什么编码呢?我们可以通过开发者工具查看,也可以通过查看页面源代码查看网页编码。

    4-11 页面编码查看

  • compile(): 编译正则表达式模块,这个函数将返回一个匹配模式对象,这里推荐大家写正则表达式的时候先把正则表达式预编译成一个对象,这样在查找的时候程序就不需要每次都再去编译一次正则表达式了,特别是对于在海量文本中查找的时候预编译正则规则尤为重要,预编译正则规则能提高你的代码执行效率
    findall,sub等函数上一篇聚沙成塔--爬虫系列(三)(正则表达式)文章已经介绍过了,这里不在累诉。

note: 这里我们用到了前面章节介绍到的python的两种数据结果,列表和元组,还有for循环语法,函数findall返回的是一个list(列表),所以我们需要循环列表里的每个元素,每个元素代表一个被匹配上的用户信息,正则表达式中用()括号括起来的表示你要取的元素,它将会以元组的形式方法。到这里我们就完成了一个最基本的爬虫程序了,你是不是可以收集海量段子了,时不时的给妹子发个段子,是不是可以在妹子面前展示一下你的幽默诙谐了,哈哈。。。。,骚年这还不够,继续努力吧。。。。

更多的文章可以关注我的blog:http://www.gavinxyj.com


欢迎关注我的公众号:爱做饭的老谢,老谢一直在努力...

推荐阅读更多精彩内容