今天用Python3改写一片名为《Python爬虫实战二之爬取百度贴吧帖子》的博客,文章很好,基本就按照这个文章的结构将整个程序重写的一边,收获很大,感谢名为崔庆才的网友(附上他的博客地址:http://cuiqingcai.com/)。并且改用Pycharm编程,很痛快,调试也很方便。
首先,我们先观察一下百度贴吧的任意一个帖子。
比如:http://tieba.baidu.com/p/3138733512?see_lz=1&pn=1,这是一个关于NBA50大的盘点,分析一下这个地址。
1
2
3
4
|
http
:
// 代表资源传输使用http协议
tieba
.
baidu
.
com
是百度的
二级域名,指向百度贴吧的服务器。
/
p
/
3138733512
是
服务器某个资源,即这个帖子的地址定位符
see
_lz和
pn是该
URL的两个参数,分别代表了只看楼主和帖子页码,等于
1表示该条件为真
|
所以我们可以把URL分为两部分,一部分为基础部分,一部分为参数部分。
例如,上面的URL我们划分基础部分是 http://tieba.baidu.com/p/3138733512,参数部分是 ?see_lz=1&pn=1
熟悉了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
.
baseURL
=
baseUrl
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
=
BDTB
(
baseURL
,
1
)
bdtb
.
getPage
(
1
)
|
运行代码,我们可以看到屏幕上打印出了这个帖子第一页楼主发言的所有内容,形式为HTML代码。
首先,让我们提取帖子的标题。
在浏览器中审查元素,或者按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
|
同样地,帖子总页数我们也可以通过分析页面中的共?页来获取。所以我们的获取总页数的方法如下
1
2
3
4
5
6
7
8
9
10
|
#获取帖子一共有多少页
def
getPageNum
(
self
)
:
page
=
self
.
getPage
(
1
)
pattern
=
re
.
compile
(
'<li class="l_reply_num.*?</span>.*?<span.*?>(.*?)</span>'
,
re
.
S
)
result
=
re
.
search
(
pattern
,
page
)
if
result
:
#print result.group(1) #测试输出
return
result
.
group
(
1
)
.
strip
(
)
else
:
return
None
|
审查元素,我们可以看到百度贴吧每一层楼的主要内容都在<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方法即可。
现在将整体的代码写到下面:
#!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() #--------程序入口---------