今天用Python3改写一片名为《Python爬虫实战二之爬取百度贴吧帖子》的博客,文章很好,基本就按照这个文章的结构将整个程序重写的一边,收获很大,感谢名为崔庆才的网友(附上他的博客地址:http://cuiqingcai.com/)。并且改用Pycharm编程,很痛快,调试也很方便。
1.URL格式的确定
首先,我们先观察一下百度贴吧的任意一个帖子。
比如:http://tieba.baidu.com/p/3138733512?see_lz=1&pn=1,这是一个关于NBA50大的盘点,分析一下这个地址。
所以我们可以把URL分为两部分,一部分为基础部分,一部分为参数部分。
例如,上面的URL我们划分基础部分是 http://tieba.baidu.com/p/3138733512,参数部分是 ?see_lz=1&pn=1
2.页面的抓取
熟悉了URL的格式,那就让我们用urllib2库来试着抓取页面内容吧。上一篇糗事百科我们最后改成了面向对象的编码方式,这次我们直接尝试一下,定义一个类名叫BDTB(百度贴吧),一个初始化方法,一个获取页面的方法。
其中,有些帖子我们想指定给程序是否要只看楼主,所以我们把只看楼主的参数初始化放在类的初始化上,即init方法。另外,获取页面的方法我们需要知道一个参数就是帖子页码,所以这个参数的指定我们放在该方法中。
综上,我们初步构建出基础代码如下:
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
|
__author__
=
'BALUW'
import
urllib
import
urllib.request
import
re
#百度贴吧爬虫类
class
BDTB
:
#初始化,传入基地址,是否只看楼主的参数
def
__init__
(
self
,
baseUrl
,
seeLZ
)
:
self
.
seeLZ
=
'?see_lz='
+
str
(
seeLZ
)
#传入页码,获取该页帖子的代码
def
getPage
(
self
,
pageNum
)
:
try
:
url
=
self
.
baseURL
+
self
.
seeLZ
+
'&pn='
+
str
(
pageNum
)
request
=
urllib.request
.
Request
(
url
)
response
=
urllib.request
.
urlopen
(
request
)
print
response
.
read
(
)
return
response
except
urllib.error
.
URLError
as
e
:
if
hasattr
(
e
,
"reason"
)
:
print(
"连接百度贴吧失败,错误原因:"
,
e
.
reason)
return
None
baseURL
=
'http://tieba.baidu.com/p/3138733512'
bdtb
.
getPage
(
1
)
|
运行代码,我们可以看到屏幕上打印出了这个帖子第一页楼主发言的所有内容,形式为HTML代码。
1)提取帖子标题
首先,让我们提取帖子的标题。
在浏览器中审查元素,或者按F12,查看页面源代码,我们找到标题所在的代码段,可以发现这个标题的HTML代码是
1
|
<
h1
class
=
"core_title_txt "
title
=
"纯原创我心中的NBA2014-2015赛季现役50大"
style
=
"width: 396px"
>纯原创我心中的
NBA2014
-
2015赛季现役
50大
<
/
h1
>
|
所以我们想提取<h1>标签中的内容,同时还要指定这个class确定唯一,因为h1标签实在太多啦。
正则表达式如下
1
|
<
h1
class
="
core_title_txt
.
*
?
>
(
.
*
?
)
<
/
h1
>
|
所以,我们增加一个获取页面标题的方法
1
2
3
4
5
6
7
8
9
10
|
#获取帖子标题
def
getTitle
(
self
)
:
page
=
self
.
getPage
(
1
)
pattern
=
re
.
compile
(
'<h1 class="core_title_txt.*?>(.*?)</h1>'
,
re
.
S
)
result
=
re
.
search
(
pattern
,
page
)
if
result
:
#print result.group(1) #测试输出
return
result
.
group
(
1
)
.
strip
(
)
else
:
return
None
|
2)提取帖子页数
同样地,帖子总页数我们也可以通过分析页面中的共?页来获取。所以我们的获取总页数的方法如下
1
2
3
4
5
6
7
8
9
10
|
#获取帖子一共有多少页
def
getPageNum
(
self
)
:
page
=
self
.
getPage
(
1
)
result
=
re
.
search
(
pattern
,
page
)
if
result
:
#print result.group(1) #测试输出
return
result
.
group
(
1
)
.
strip
(
)
else
:
return
None
|
3)提取正文内容
审查元素,我们可以看到百度贴吧每一层楼的主要内容都在<div id=”post_content_xxxx”></div>标签里面,所以我们可以写如下的正则表达式
1
|
<
div
id
="
post_content_
.
*
?
>
(
.
*
?
)
<
/
div
>
|
相应地,获取页面所有楼层数据的方法可以写成如下方法
1
2
3
4
5
6
|
#获取每一层楼的内容,传入页面内容
def
getContent
(
self
,
page
)
:
pattern
=
re
.
compile
(
'<div id="post_content_.*?>(.*?)</div>'
,
re
.
S
)
items
=
re
.
findall
(
pattern
,
page
)
for
item
in
items
:
print
item
|
好,我们运行一下结果看一下
我们就要对这些文本进行处理,把各种各样复杂的标签给它剔除掉,还原精华内容,把文本处理写成一个方法也可以,不过为了实现更好的代码架构和代码重用,我们可以考虑把标签等的处理写作一个类。
那我们就叫它Tool(工具类吧),里面定义了一个方法,叫replace,是替换各种标签的。在类中定义了几个正则表达式,主要利用了re.sub方法对文本进行匹配后然后替换。具体的思路已经写到注释中,大家可以看一下这个类:
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
|
import
re
#处理页面标签类
class
Tool
:
#去除img标签,7位长空格
removeImg
=
re
.
compile
(
'<img.*?>| {7}|'
)
#删除超链接标签
removeAddr
=
re
.
compile
(
'<a.*?>|</a>'
)
#把换行的标签换为\n
replaceLine
=
re
.
compile
(
'<tr>|<div>|</div>|</p>'
)
#将表格制表<td>替换为\t
replaceTD
=
re
.
compile
(
'<td>'
)
#把段落开头换为\n加空两格
replacePara
=
re
.
compile
(
'<p.*?>'
)
#将换行符或双换行符替换为\n
replaceBR
=
re
.
compile
(
'<br><br>|<br>'
)
#将其余标签剔除
removeExtraTag
=
re
.
compile
(
'<.*?>'
)
def
replace
(
self
,
x
)
:
x
=
re
.
sub
(
self
.
removeImg
,
""
,
x
)
x
=
re
.
sub
(
self
.
removeAddr
,
""
,
x
)
x
=
re
.
sub
(
self
.
replaceLine
,
"\n"
,
x
)
x
=
re
.
sub
(
self
.
replaceTD
,
"\t"
,
x
)
x
=
re
.
sub
(
self
.
replacePara
,
"\n "
,
x
)
x
=
re
.
sub
(
self
.
replaceBR
,
"\n"
,
x
)
x
=
re
.
sub
(
self
.
removeExtraTag
,
""
,
x
)
#strip()将前后多余内容删除
return
x
.
strip
(
)
|
在使用时,我们只需要初始化一下这个类,然后调用replace方法即可。
3.程序实现
现在将整体的代码写到下面:
#!C:\Python34\python.exe
__author__ = 'baluw'
import urllib
import urllib.request
import re
class Tool:
# 去除img标签,7位长空格
removeImg = re.compile('<img.*?>| {7}|')
# 删除超链接标签
removeAddr=re.compile('<a.*?>|</a>')
# 把换行的标签换为\n
replaceLine=re.compile('<tr>|<div>|</div>|</p>')
# 将制表符<td>换成\n
replaceTD=re.compile('<td>')
# 将段落开头换为\n加空两格
replacePara=re.compile('<p.*?>')
# 将换行符或双换行符替换为\n
replaceBR=re.compile('<br><br>|<br>')
# 将其余标签剔除
rmExtraTag=re.compile('<.*?>')
def replace(self,x):
x=re.sub(self.removeImg,"",x)
x=re.sub(self.removeAddr,"",x)
x=re.sub(self.replaceLine,"\n",x)
x=re.sub(self.replaceTD,"\t",x)
x=re.sub(self.replacePara,"\n",x)
x=re.sub(self.replaceBR,"\n",x)
x=re.sub(self.rmExtraTag,"",x)
# strip()将前后内容删除
return x.strip()
class TBSpider:
# 初始化,传入基地地址,是否只看楼主得参数
def __init__(self, baseUrl, seeLZ,floorTag):
self.user_agent = 'Mozilla/4.0(compatible:MSIE 5.5;Windows NT)'
#初始化headers
self.headers = {'User-Agent': self.user_agent}
self.baseURL = baseUrl
self.seeLZ = "?see_lz=" + str(seeLZ)
# 初始化Tool工具
self.tool=Tool()
self.file=None
self.floor=1
self.defaultTitle="百度贴吧"
self.floorTag=floorTag
#传入页码,获取该页面帖子的代码
def getPage(self, pageNum) -> object:
try:
url = self.baseURL + self.seeLZ + "&pn=" + str(pageNum)
request = urllib.request.Request(url, headers=self.headers)
reqonse = urllib.request.urlopen(request)
content = reqonse.read().decode("utf-8")
return content
except urllib.error.URLError as e:
if hasattr(e, "reason"):
print("连接百度贴吧失败,错误原因:", e.reason)
return None
#获取帖子标题
def getTitle(self,page):
pattern = re.compile('<h1 class="core_title_txt.*?>(.*?)</h1>', re.S)
result = re.search(pattern, page)
if result:
return result.group(1).strip()
else:
return None
#提取帖子页数
def getPageNum(self,page):
pattern = re.compile('<li class="l_reply_num.*?</span>.*?<span.*?>(.*?)</span>', re.S)
result = re.search(pattern, page)
if result:
return result.group(1).strip()
else:
return None
def getConetent(self, page):
pattern = re.compile('<div id="post_content_.*?>(.*?)</div>', re.S)
items = re.findall(pattern, page)
# 楼层
contents=[]
self.floor=1
for item in items:
content="\n"+self.tool.replace(item)+"\n"
#contents.append(content.encode('utf-8'))就是这句出问题了,整了一个小时,终于排查出错误了
contents.append(content)
return contents
def setFileTitle(self,title):
if title is not None:
self.file=open(title+".txt","w+")
else:
self.file=open(self.defaultTitle+".txt","w+")
def writeData(self,contents):
for item in contents:
if self.floorTag=='1':
floorLine="\n-------------"+str(self.floor)+"楼--------------\n"
self.file.write(floorLine)
self.file.write(item)
self.floor+=1
def start(self):
indexPage=self.getPage(1)
pageNum=self.getPageNum(indexPage)
title=self.getTitle(indexPage)
self.setFileTitle(title)
if pageNum==None:
print("URL链接已经失效,请重试")
return
try:
print("该帖子共有{0}页".format(str(pageNum)))
for i in range(1,int(pageNum)):
print("正在写入第{0}页数据".format(str(i)))
page=self.getPage(1)
contents=self.getConetent(page)
self.writeData(contents)
except IOError as e:
print("写入异常原因:",e.reason)
finally:
print("写入任务完成!")
#--------程序入口---------
print("""
------------------------------------
程序:百度贴吧爬虫
版本:1.0
作者:{0}
日期:2015-04-19
语言:Python 3.4
功能:百度贴吧里的内容,分段现实
------------------------------------
""".format(__author__))
print("请输入帖子代号")
baseURL = 'http://tieba.baidu.com/p/' + str(input('http://tieba.baidu.com/p/'))
seeLZ = input("是否只获取楼主发言,是输入1,否输入0\n")
floorTag = input("是否写入楼层信息,是输入1,否输入0\n")
bdtb = TBSpider(baseURL,seeLZ,floorTag)
bdtb.start()
#--------程序入口---------