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篇文章

Poetry

共有2篇文章

Movie

共有22篇文章

Read

共有9篇文章

Repost

共有4篇文章

Review

共有11篇文章

Tech

共有28篇文章

Trade

共有25篇文章

Travel

共有8篇文章

2019

共有3篇文章

2018

共有34篇文章

2017

共有23篇文章

2016

共有89篇文章

2015

共有18篇文章