SKYue

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代码。

爬虫抓取网页的过程可简化为两步:

  1. 向服务器发出请求,一般通过url
  2. 获得服务器的响应内容,通常就是我们看到的网页

这个过程与我们直接通过浏览器访问一个网址是一致的。对应到上面的代码中,就是reqres那两行。先通过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

参考文章:

Python爬虫入门七之正则表达式
Python爬虫实战一之爬取糗事百科段子

Code

共有7篇文章

Diary

共有51篇文章

Movie

共有22篇文章

Poetry

共有2篇文章

Read

共有9篇文章

Review

共有9篇文章

Tech

共有27篇文章

Trade

共有25篇文章

Travel

共有7篇文章

Weekly

共有0篇文章

2018

共有29篇文章

2017

共有23篇文章

2016

共有89篇文章

2015

共有18篇文章