爬虫初探02-爬取糗事百科段子

学习了简单爬虫后的练手项目,爬取糗事百科的热门段子在控制台显示

修正

2016.7.25 段子正文去掉了原有的<– –>导致程序失效,新正则为

1
2
3
'<div.*?author.*?">.*?<a.*?<h2>(.*?)</h2>.*?</a>.*?<div.*?' +
'content">(.*?)</div>(.*?)<div class="stats.*?' +
'class="number">(.*?)</i>.*?<span class="dash.*?class="number">(.*?)</i>'

说明

  • 本文章为学习《Python爬虫实战一之爬取糗事百科段子》的二次实现,所以在文章布局和代码上都有参考
  • 原文代码基于python 2.7版本,本文基于python3.5版本
  • 本文对原博中已失效的正则部分进行更新,对功能、代码重构上的添加扩展和实现

目标

  • 抓取糗事百科热门段子
  • 过滤带有图片的段子
  • 实现在控制台每按一次回车显示一个段子、作者、点赞数和评论数

实现步骤

1. 确定URL和抓取页面代码

首先打开http://www.qiushibaike.com

点击第二页观察网址,可以分析出8小时热门和24小时热门的网址分别对应http://www.qiushibaike.com/8hr/page/2http://www.qiushibaike.com/hot/page/2,其中最后一位代表页数,通过改变最后一位可以获取到不同的页面。

尝试使用

1
urllib.request.urlopen('http://www.qiushibaike.com/hot/page/2')

执行出错无法获取,添加header头试试

1
2
3
4
5
url = 'http://www.qiushibaike.com/8hr/page/1'
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
headers = {'User-Agent': user_agent}
req = urllib.request.Request(url, headers=headers)
response = urllib.request.urlopen(req)

成功获取到页面。

2. 提取一页中的所有段子

这一步需要构造网页解析器,本次将采用正则方式解析。

2.1 提取段子

通过审查元素看下段子的布局结构

段子页面结构

根据页面框架构造出如下解析代码,其中 item[0]匹配段子正文, item[1]匹配作者, item[2]对于有图片的段子内有图片地址。由于本次输出在控制台下进行,所以item[2]仅用于过滤有图片的段子。

1
2
3
4
5
6
7
8
pattern = re.compile('<div.*?author.*?">.*?<a.*?<h2>(.*?)</h2>.*?</a>.*?<div.*?' +
'content">(.*?)<!--.*?-->.*?</div>(.*?)<div class="stats.*?' +
'class="number">(.*?)</i>.*?<span class="dash.*?class="number">(.*?)</i>', re.S)
items = re.findall(pattern, content)
for item in items:
haveImg = re.search("img", item[2])
if not haveImg:
print('%s\n作者:%s %s好笑 %s评论\n' % (item[1], item[0], item[3], item[4]))

看下输出结果

1
2
3
4
5
6
7
8
9
妹妹:哥哥你的愿望是什么?<br/>哥哥:我们家族凋零,我要壮大我们的家族,娶上十七八个老婆,生上七八十个儿子。你呢?<br/>妹妹:我也想壮大家族,可是我就算娶了十七八个老公,也生不出七八十个孩子啊。

作者:翔→梦之堡 224好笑 4评论


N年前到一小商店,偶尔去买东西,店主和我都是面熟但算不上朋友,这是前提。有次买东西是女老板卖的,后因某种问题回去交涉,只有男老板在,我一急反复说是你妈妈卖的,这东西就是他家的。男老板一脸黑色,等事情交涉好走后,回头一想,他们是夫妻店呀,难快男店主不开心。真是罪过!

作者:转过街角 222好笑 1评论
。。。

2.2 提取最大页数

最大页数用于判断可以爬取的最大页数,对于超过最大页数的段子将采用循环方式重新从第一页开始获取。

看下页面结构

最大页数页面结构

观察到最后一个class=”page-numbers”的span标签的值就是最大页数,这里通过贪婪模式来实现

1
max_page_index = re.findall('<a.*<span class="page-numbers">\n(.*?)\n</span>', html_contents, re.S)

3. 完善细节交互,优化代码结构

进一步完善,结构细化到方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# encoding: utf-8
# require python version 3.x
# from tuzhis.github.io
# by zhisheng reference http://cuiqingcai.com/990.html

import re
import urllib.error
import urllib.request


class QSBK:
def __init__(self):
"""
初始化方法
:return:
"""
# 糗事百科8小时和24小时热门网址
self.url1 = 'http://www.qiushibaike.com/8hr/page/'
self.url2 = 'http://www.qiushibaike.com/hot/page/'
# 初始页码
self.page_index = 1
# 原始最大页码
self.max_page_index = 255
# 程序继续运行标志
self.enable = True

def get_url(self):
"""
生成待爬取地址
:return: 待爬取地址
"""
# 采用取余的方式实现超过最大页码的重新载入第一页
index = self.page_index % self.max_page_index
if index == 0: # 对于余数为0的修正处理
index = self.max_page_index
url = self.url1 + str(index)
self.page_index += 1
return url

@staticmethod
def download_page(url):
"""
根据传入的地址下载页码内容
:param url:
:return:对应的html页面内容
"""
try:
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
headers = {'User-Agent': user_agent}
req = urllib.request.Request(url, headers=headers)
response = urllib.request.urlopen(req)
return response.read().decode('utf-8')
except urllib.error.URLError as e:
if hasattr(e, 'reason'):
print('Failed to reach a server.')
print('Reason: ', e.reason)
elif hasattr(e, 'code'):
print("The server couldn't fulfill the request.")
print('Error code: ', e.code)
return None

def parse(self, html_contents):
"""
解析传入的html内容
:param html_contents:
:return: 当前页所有不带图片的段子
"""
# 初次解析写入最大页数
if self.max_page_index == 255:
max_index = re.findall('<a.*<span class="page-numbers">\n(.*?)\n</span>', html_contents, re.S)
if max_index:
self.max_page_index = int(max_index[0])

stories = []
stories_pattern = re.compile('<div.*?author.*?">.*?<a.*?<h2>(.*?)</h2>.*?</a>.*?<div.*?' +
'content">(.*?)<!--.*?-->.*?</div>(.*?)<div class="stats.*?' +
'class="number">(.*?)</i>.*?<span class="dash.*?class="number">(.*?)</i>', re.S)

items = re.findall(stories_pattern, html_contents)
for item in items:
have_img = re.search("img", item[2])
if not have_img:
# br标签替换为换行
text = re.sub('<br/>', "\n", item[1])
stories.append([text.strip(), item[0].strip(), item[3].strip(), item[4].strip()])
return stories

def print_story(self, stories):
"""
输出一条段子回车继续显示
:param stories:
:return:
"""
for story in stories:
print('\n%s\n作者:%s %s好笑 %s评论' % (story[0], story[1], story[2], story[3]))
input_a = input('回车继续输出段子,按Q结束\n')
if input_a == 'Q' or input_a == 'q':
self.enable = False
return

def craw(self):
"""
爬虫总调度方法
:return:
"""
while self.enable:
url = self.get_url()
html_contents = self.download_page(url)
if not html_contents:
print("Failed to load the page")
return
stories = self.parse(html_contents)
self.print_story(stories)


if __name__ == '__main__':
obj_spider = QSBK()
obj_spider.craw()

运行结果

运行结果