Python爬虫:抓取知乎某一话题下的全部问题
因为自己在做搜索产品,希望能对搜索技术有些了解,偶尔也会做些数据及文本方面的分析,所以今年给自己设了一个学习Python的任务,方向就是爬虫及数据分析。
在大学学过一点C,虽然从来没有写出过一个完整的程序,但对编程的基本概念(比如变量、函数、类型、对象)都有了解。去年就想着学Python,基本语法都看过,只是每当要实现一个完整功能的时候,都觉得挺难而放弃。所以这次不准备系统看语法了,直接开始练习小项目,不懂就Google。以问题为导向,边写代码边调试,这才是学习编程的正确姿势。然后在每个阶段做些总结,写成博文。本文就是第一篇。
我准备做的事情就是:抓取知乎上所有的问题、答案和用户并存入MySQL,然后基于这些数据做些分析。这个问题很大,估计够我学一年了吧。
第一步,怎么爬某一个话题下的所有问题。
爬虫的基本原理
先看一段代码,用Python实现爬虫的基本功能:抓取网页。
import urllib2
url = 'http://www.skyue.com'
req = urllib2.Request(url) #构建请求对象
res = urllib2.urlopen(req) #获得响应对象
html = res.read() #读取响应对象中的网页内容
print html #输出网页内容
代码非常的简单,才6行。放到IPython中运行,输出的结果就是本博客首页的HTML代码。
爬虫抓取网页的过程可简化为两步:
- 向服务器发出请求,一般通过url
- 获得服务器的响应内容,通常就是我们看到的网页
这个过程与我们直接通过浏览器访问一个网址是一致的。对应到上面的代码中,就是req
和res
那两行。先通过urllib2.Request
把url封闭为请求对象,再通过urllib2.urlopen
获得服务器的响应内容。
抓取知乎Python话题下的问题
Python话题链接:https://www.zhihu.com/topic/19552832/questions?page=1
将前面的代码中url换成知乎Python话题的链接,就可以抓取整个网页,但现在需要把网页中的问题ID(即问题页面URL中的数字)及问题标题抽出来。所以需要用正则表达去匹配(关于正则表达可参考《Python爬虫入门七之正则表达式》)。
先用chrome开发者工具,看下知乎话题页面的源代码,可以发现每个问题的链接和标题都在下面这段代码中
<a target="_blank" class="question_link" href="/question/39423081">theano里面的中间变量self.p_y_given_x如何追踪?</a>
匹配问题ID及标题的正则表达则为href="/question/(.*?)">(.*?)</a>
所以,抓取问题ID及问题标题的完整代码如下:
# 抓取知乎问题
import urllib2
import re
url = 'https://www.zhihu.com/topic/19552832/questions?page=1'
req = urllib2.Request(url)
res = urllib2.urlopen(req)
html = res.read()
#匹配问题ID及问题标题
pattern = re.compile(r'href="/question/(.*?)">(.*?)</a>',re.S)
items = re.findall(pattern,html)
#输出结果
for item in items:
print '问题ID:',item[0],
print '问题Title:', item[1]
部分运行结果:
问题ID: 39423081 问题Title: theano里面的中间变量self.p_y_given_x如何追踪?
问题ID: 39422374 问题Title: 如何用BeautifulSoup解析script中的内容?
问题ID: 39421438 问题Title: python包pandas中read_sql这个语句,查询(select)包含某个字符串的问题?
问题ID: 39419394 问题Title: python和易语言有什么相似之处?
完善代码
有几个问题
- 上面的代码只能抓取某一页,我们当然希望能抓取一个话题下所有的问题
- 抓取所有问题时,提供一些交互,比如每次抓取一页,输入回车抓取下一页,输入「Q」则退出
- 将代码封闭成类,再以类的实例去执行
完善后,完整代码如下:
# 爬取知乎一个主题下所有的问题
import urllib2
import re
class ZhiHu:
#初始化方法,定义变量
def __init__(self):
self.pageIndex = 1
self.user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
#初始化headers
self.headers = {'User-Agent': self.user_agent}
#存放问题的列表,每个元素是一页问题(20个)
self.questions = []
#爬虫运行状态
self.enable = False
#获取页面HTML
def getPage(self,pageIndex):
try:
url = 'https://www.zhihu.com/topic/19552832/questions?page=' + str(pageIndex)
#构建请求的request
request = urllib2.Request(url)
#获取请求的页面HTML代码
response = urllib2.urlopen(request)
#将页面代码转成UTF-8
pageCode = response.read().decode('utf-8')
return pageCode
except urllib2.Error, e:
if hasattr(e, 'reason'):
print u'连接知识错误,原因:',e.reason
return None
#传入某一页页码,返回问题列表
def getPageItem(self,pageIndex):
pageCode = self.getPage(pageIndex)
#存放每页问题的列表,每个元素包括问题的ID及标题
pageQuestions = []
#判断页面是否获取成功
if not pageCode:
print u'页面加载失败...'
return None
#正则匹配每个问题URL中的ID及标题
pattern = re.compile(r'href="/question/(.*?)">(.*?)</a>',re.S)
items = re.findall(pattern,pageCode)
#遍历正则表达匹配的结果,并存入到pageQuestions
for item in items:
#item[0]是问题的ID,item[1]问题的标题
pageQuestions.append([item[0],item[1]])
return pageQuestions
#加载并提取页面的内容加入到列表中
def loadPage(self):
if self.enable == True:
#如果当前未看的页数少于2页,则加载下一页
if len(self.questions) < 2:
#获取新的一页
pageQuestions = self.getPageItem(self.pageIndex)
if pageQuestions:
#将该页问题存放到全局列表中
self.questions.append(pageQuestions)
#获取完后,页面索引+1
self.pageIndex += 1
#打印一页问题
def getQuestion(self,pageQuestions,page):
for question in pageQuestions:
print u'第%s页,问题%s:%s' %(page,question[0],question[1])
#开始方法
def start(self):
print u'正在读取知乎问题,按回车查看,Q退出'
self.enable = True
nowPage = 0
while self.enable:
#输入命令
input = raw_input()
self.loadPage()
#输入「Q」则停止程序
if input == 'Q':
self.enable = False
break
elif len(self.questions)>0:
pageQuestions = self.questions[0]
nowPage += 1
del self.questions[0]
self.getQuestion(pageQuestions,nowPage)
print input
#实例化知乎爬虫
spider = ZhiHu()
#运行爬虫
spider.start()
小结
编程的基础语法看过很多次,写出完整功能的程序却是第一次,真的很爽。碰到了一些坑:
=
与==
,前者是赋值,后者是逻辑判断。用的时候注意点,if
语句后面,通常都是==
- 正则表达分组后,Python获得的是一个二维列表。
- 在类中,对类自身的变量及方法引用,都要带上
self
参考文章: