数据挖掘 文本分类 知乎问题单分类(二):爬取知乎某话题下的问题(数据爬取)

  • 爬虫目标
  • Scrapy框架介绍
    • Scrapy框架原理 [^1]
    • Scrapy工作流程 [^2]
  • 具体实现
    • 安装Scrapy
    • 创建项目
    • 定义item
    • 编写存储MySQL的Pipeline
    • spider编写
    • 总结
    • 反反爬(选修)[^5]
  • 参考

爬虫目标

由于我们打算对知乎某些话题下的问题和问题描述中的文按话题进行分类,所以使用了Python的Scrapy对知乎进行爬取,并将爬下来的数据保存在到mysql数据库中。


Scrapy框架介绍

Scrapy是用纯Python实现一个为了爬取网站数据、提取结构性数据而编写的应用框架,用途非常广泛。而且Scrapy 使用了 Twiste异步网络框架来处理网络通讯,可以加快我们的下载速度,不用自己去实现异步框架,并且包含了各种中间件接口,可以灵活的完成各种需求。

Scrapy框架原理 1


Scrapy Engine(引擎): 负责Spider、ItemPipeline、Downloader、Scheduler中间的通讯,信号、数据传递等。

Scheduler(调度器): 它负责接受引擎发送过来的Request请求,并按照一定的方式进行整理排列,入队,当引擎需要时,交还给引擎。

Downloader(下载器):负责下载Scrapy Engine(引擎)发送的所有Requests请求,并将其获取到的Responses交还给Scrapy Engine(引擎),由引擎交给Spider来处理,

Spider(爬虫):它负责处理所有Responses,从中分析提取数据,获取Item字段需要的数据,并将需要跟进的URL提交给引擎,再次进入Scheduler(调度器),

Item Pipeline(管道):它负责处理Spider中获取到的Item,并进行进行后期处理(详细分析、过滤、存储等)的地方.

Downloader Middlewares(下载中间件):你可以当作是一个可以自定义扩展下载功能的组件。

Spider Middlewares(Spider中间件):你可以理解为是一个可以自定扩展和操作引擎和Spider中间通信的功能组件(比如进入Spider的Responses;和从Spider出去的Requests)

Scrapy工作流程 2

  1. 引擎:Hi!Spider, 你要处理哪一个网站?

  2. Spider:老大要我处理xxxx.com。

  3. 引擎:你把第一个需要处理的URL给我吧。

  4. Spider:给你,第一个URL是xxxxxxx.com。

  5. 引擎:Hi!调度器,我这有request请求你帮我排序入队一下。

  6. 调度器:好的,正在处理你等一下。

  7. 引擎:Hi!调度器,把你处理好的request请求给我。

  8. 调度器:给你,这是我处理好的request

  9. 引擎:Hi!下载器,你按照老大的下载中间件的设置帮我下载一下这个request请求

  10. 下载器:好的!给你,这是下载好的东西。(如果失败:sorry,这个request下载失败了。然后引擎告诉调度器,这个request下载失败了,你记录一下,我们待会儿再下载)

  11. 引擎:Hi!Spider,这是下载好的东西,并且已经按照老大的下载中间件处理过了,你自己处理一下(注意!这儿responses默认是交给def parse()这个函数处理的)

  12. Spider:(处理完毕数据之后对于需要跟进的URL),Hi!引擎,我这里有两个结果,这个是我需要跟进的URL,还有这个是我获取到的Item数据。

  13. 引擎:Hi !管道 我这儿有个item你帮我处理一下!调度器!这是需要跟进URL你帮我处理下。然后从第四步开始循环,直到获取完老大需要全部信息。


具体实现

安装Scrapy

  1. 安装Python或者直接安装Anaconda(推荐)
  2. 将pip升级最新版本:pip install --upgrade pip
  3. 通过pip 安装 Scrapy 框架:pip install Scrapy

创建项目

使用以下命令创建一个Scrapy项目:scrapy startproject mySpider

该命令将会创建包含下列内容的 tutorial 目录:

这些文件分别是:

  • scrapy.cfg: 项目的配置文件
  • tutorial/: 该项目的python模块。之后您将在此加入代码。
  • tutorial/items.py: 项目中的item文件.
  • tutorial/pipelines.py: 项目中的pipelines文件.
  • tutorial/settings.py: 项目的设置文件.
  • tutorial/spiders/: 放置spider代码的目录.

定义item

我们要爬取知乎的某话题下的所有问题,将每个问题作为实体,并且具有以下属性:

  • zhihu_id :该问题在之中的id
  • label :该问题所属的话题在知乎中的id
  • url :该问题对应的url
  • title :该问题的文字描述
  • content :该问题的问题描述
  • keywords :该问题的在之中的标签
  • crawl_time = scrapy.Field()

下面是问题在item中的定义:

class ZhihuQuestionItem(scrapy.Item):# 知乎问题 itemzhihu_id = scrapy.Field()label = scrapy.Field()url = scrapy.Field()title = scrapy.Field()content = scrapy.Field()keywords = scrapy.Field()crawl_time = scrapy.Field()# 构造item的插入语句并返回def get_sql(self):insert_sql = """insert into question(zhihu_id,label,url,title,content,keywords,crawl_time)VALUES(%s,%s,%s,%s,%s,%s,%s)ON DUPLICATE KEY UPDATE title=VALUES(title), content=VALUES(content), crawl_time=VALUES(crawl_time)"""params = (self["zhihu_id"],self["label"],self["url"],self["title"],self["content"],self["keywords"], datetime.datetime.now().strftime(SQL_DATETIME_FORMAT))return insert_sql, params

编写存储MySQL的Pipeline

这里使用异步的twisted进行进行存储,主要过程如下:

  • 从设置文件获取数据库相关设置(要在settings.py编写相关设置)
  • 获取item的插入语句
  • 将item和插入语句放入dbpool中等待插入

代码如下:

class MysqlTwistedPipeline(object):# 采用异步机制写入mysqldef __init__(self, dbpool):self.dbpool = dbpool@classmethoddef from_settings(cls, settings):dbparms = dict(# 启动时就会被scrapy调用,将settings传进来,固定的函数,自定义自己的组件时很有用# 注意:host、db等参数名称是固定的host=settings["MYSQL_HOST"],db=settings["MYSQL_DBNAME"],user=settings["MYSQL_USER"],passwd=settings["MYSQL_PASSWORD"],charset='utf8',cursorclass=MySQLdb.cursors.DictCursor,use_unicode=True)dbpool = adbapi.ConnectionPool("MySQLdb", **dbparms)return cls(dbpool)def process_item(self, item, spider):# 使用twisted将mysql插入变成异步执行query = self.dbpool.runInteraction(self.do_insert, item)# 处理异常 ,加上item, spider更方便排查异常query.addErrback(self.handle_error, item, spider)def handle_error(self, failure, item, spider):# 处理异步插入的异常print(failure)def do_insert(self, cursor, item):# 执行插入操作insert_sql, params = item.get_sql()cursor.execute(insert_sql, params)

spider编写

下面就spider编写就是最重要的部分了首相我将简单介绍一下spider工作的流程然后在后面附上代码。

逻辑上:

  1. 使用知乎API3: https://www.zhihu.com/api/v4/topics/19556498/feeds/top_question 可以获得某个话题下面的问题相关信息。其中可以在url中传入 offset 参数来检测设定要从第几个问题开始。另外该api每次最多能获取10条问题,需要迭代不段获取
  2. 从上面的api中获取到用json中,获取问题的问题id,并将其与url拼接成某个问题的如:https://www.zhihu.com/question/267791727
  3. 对上面的问题页面进行解析获取获得问题相关信息如:问题文字标识,问题描述等
  4. 将获取到的信息存入数据库

代码实现:

Scrapy爬虫的入口时 start_requests 函数,在该函数中我们需要构建url,header以及回调函数等,将它们放到scrapy的请求队列 scrapy.Request 中。然后我们就可以在回调函数中对相应进行处理(解析json或html)。

另外由于不需要登陆也能获得我们想要的信息(后来才发现),登录部分就不介绍了,有兴趣小伙伴可以自行另外查找相关资料。

具体到本项目,在 start_requests 中,我们首先从数据库中查询出我们所要爬取的所有话题的话题id、话题名称以及当前已经爬取的数目cnt(对应url参数offset)放入字典topics,然后依次构造每个话题的url topic_url,将该话题url放入Request队列中。代码如下:

def start_requests(self):# 从数据库获取每个话题的id,名称以及已爬取数目zhihuDB = ZhihuDB()topics = zhihuDB.get_types_num_cnt()# 依次构造每个话题的url `topic_url`,讲该url放入`Request`队列中。for topic in topics:label_tyepe = topic["label"]if label_tyepe not in self.uncrawl_type:topic_url = "http://www.zhihu.com/api/v4/topics/" \+ str(label_tyepe) + "/feeds/top_question?include=data%5B%3F%28target.type%3Dtopic_sticky_module%29%5D.target.data%5B%3F%28target.type%3Danswer%29%5D.target.content%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%3Bdata%5B%3F%28target.type%3Dtopic_sticky_module%29%5D.target.data%5B%3F%28target.type%3Danswer%29%5D.target.is_normal%2Ccomment_count%2Cvoteup_count%2Ccontent%2Crelevant_info%2Cexcerpt.author.badge%5B%3F%28type%3Dbest_answerer%29%5D.topics%3Bdata%5B%3F%28target.type%3Dtopic_sticky_module%29%5D.target.data%5B%3F%28target.type%3Darticle%29%5D.target.content%2Cvoteup_count%2Ccomment_count%2Cvoting%2Cauthor.badge%5B%3F%28type%3Dbest_answerer%29%5D.topics%3Bdata%5B%3F%28target.type%3Dtopic_sticky_module%29%5D.target.data%5B%3F%28target.type%3Dpeople%29%5D.target.answer_count%2Carticles_count%2Cgender%2Cfollower_count%2Cis_followed%2Cis_following%2Cbadge%5B%3F%28type%3Dbest_answerer%29%5D.topics%3Bdata%5B%3F%28target.type%3Danswer%29%5D.target.annotation_detail%2Ccontent%2Chermes_label%2Cis_labeled%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%3Bdata%5B%3F%28target.type%3Danswer%29%5D.target.author.badge%5B%3F%28type%3Dbest_answerer%29%5D.topics%3Bdata%5B%3F%28target.type%3Darticle%29%5D.target.annotation_detail%2Ccontent%2Chermes_label%2Cis_labeled%2Cauthor.badge%5B%3F%28type%3Dbest_answerer%29%5D.topics%3Bdata%5B%3F%28target.type%3Dquestion%29%5D.target.annotation_detail%2Ccomment_count%3B&" \"limit=10&offset=" \+ str(topic["count(*)")yield scrapy.Request(topic_url,meta={"topic_id": str(topic["label"]), "crawl_cnt": topic["count(*)"]},headers=self.headers,dont_filter=True,callback=self.get_question_url)

之后Scrapy会将url对应的页面爬取下来,以参数response的形式传入回调函数 get_question_url 。由于返回的数据是json格式,我们使用python json 库对返回结果进行解析。

在该函数中主要做了两件事情:

  1. 根据json数据中is_end属性判断,当前是否已经爬完该话题下的所有问题,如果是则停止,如果不是,则将offset+ 10,然后从新构造话题url进行爬取。如此反复,直到该话题的所有问题都被爬取(is_end = True)或者爬取数量已经大于需要的数。
  2. 根据json数据中 target属性获取某个问题的相关内容,然后根据问题id构造问题url,将其放入放入Request队列中等待处理。

原理介绍完了,具体细节可以参考下面的代码:

def get_question_url(self, response):topic_id = response.meta.get("topic_id", "")crawl_cnt = response.meta.get("crawl_cnt", 0)# 每个话题最多爬取100000(十万)条数据if crawl_cnt < self.num_bound:# 睡眠 防止被封time.sleep(random.uniform(0, 2))print("------------------------topic = " + str(topic_id) + ":" + str(self.topics_info.get(topic_id)))print("------------------------crawl_cnt = " + str(crawl_cnt))ans_json = json.loads(response.text)# 爬虫每次最多只能获取10条记录,所以需要根据paging中# 的is_end判断是否加载到最后,并将下一页的urlnext_question_url放入Request中crawl_cnt += 10if 'paging' in ans_json:is_end = ans_json['paging']['is_end']print("------------------------is_end = " + str(is_end))next_page_url = ans_json['paging']['next']# 根据返回json的结构获取问题的id从而构造# 形如https://www.zhihu.com/question/******的urlfor data in ans_json['data']:target = data["target"]if "type" in target and target["type"] == "question":# 根据问题id构造问题urlid = target["id"]question_url = 'https://www.zhihu.com/question/' + str(id)yield scrapy.Request(question_url, meta={"topic_id": topic_id, "question_id": str(id)},priority=1, headers=self.headers, callback=self.parse_question)if not is_end:yield scrapy.Request(next_page_url, meta={"topic_id": topic_id, "crawl_cnt": crawl_cnt},dont_filter=True, headers=self.headers, callback=self.get_question_url)

最后就是对爬取的问题页面进行解析,从对页面源码的解析中我们可以看到我们可以从class="QuestionPage"的这个div标签中获取到问题的name、url、keywords信息。

同时也可以从class="RichText ztext"的这个span标签中获取到问题的问题描述信息。


那我们就可以使用python的xpath这个库4对页面进行解析,然后构造成为一个ZhihuQuestionItem对象,并返回。由于该类继承了scrapy.Item,Scrapy框架会自动调用上面定义的MysqlTwistedPipeline管道对ZhihuQuestionItem对象进行存储。具体代码如下:

def parse_question(self, response):time.sleep(random.uniform(0, 1))topic_id = response.meta.get("topic_id", "")question_id = response.meta.get("question_id", "")item_loader = QuestionItemLoader(item=ZhihuQuestionItem(), response=response)item_loader.add_value("zhihu_id", question_id)item_loader.add_value("label", topic_id)item_loader.add_xpath("title", '//div[@class="QuestionPage"]/meta[@itemprop="name"]/@content')item_loader.add_xpath("url", '//div[@class="QuestionPage"]/meta[@itemprop="url"]/@content')content = response.xpath('//span[@class="RichText ztext"]/text()').extract()item_loader.add_xpath("keywords", '//div[@class="QuestionPage"]/meta[@itemprop="keywords"]/@content')question_item = item_loader.load_item()question_item["crawl_time"] = datetime.datetime.now()if len(content) != 0:question_item["content"] = content[0]else:question_item["content"] = " "return question_item

总结

下面用流程图的形式来总结一下,爬取的整个过程:

以上就是爬虫爬取知乎某些话题下所有(大部分)问题的方法,希望能对你有所帮助。看似简单,但是也花费了我很长时间来研究和编写,有问题可以在评论下进行交流(可能因为我比较菜吧)。下面的主要是一些反反爬(禁止套娃)内容,没兴趣的同学可以跳过了。


反反爬(选修)5

事实上,上面的内容已经足够我们的需要了。那为什么还需要这部分内容呢?主要原因是知乎有反爬策略,会对爬虫进行一定的限制,导致我们爬取失败。

那知乎为什么要反爬虫呢?除了数据安全之外,最主要的原因就是爬虫的请求频率太高了(可能相当于普通用户的几万倍?),容易对知乎的服务器造成压力(可能会影响正常用户的使用,而且对于知乎来说还没产生利润)。

反爬手段主要包括:验证码、根据User-Agent判断是否浏览器请求、限制高请求ip等。

由于我不需要登录,所以验证码的问题我们不需要担心。

随机切换user-agent

middlewares.py中重载UserAgentDownloadMiddleware函数,返回process_request,并q其中的User-Agent设置每次请求随机切换。代码如下:

class UserAgentDownloadMiddleware(object):# user-agent 设置随机的请求头中间键USER_AGENTS = ['Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36'"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36""Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362""Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246""Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9""Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36""Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36"'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:64.0) Gecko/20100101 Firefox/64.0','Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.28) Gecko/20120410 Firefox/3.6.28 Lunascape/6.7.1.25446']def process_request(self, request, spider):# random.choice()在列表中随机选择一个user_agent = random.choice(self.USER_AGENTS)# print("this is USER_AGENTS :" + user_agent)request.headers['User-Agent'] = user_agent

限制高请求IP问题

对于这个问题,最简单的解决办法就是在setting.py中设置如下内容用来限制爬虫的请求速率以解决反爬的问题。

AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 1  # 初始下载延迟
DOWNLOAD_DELAY = 1  # 每次请求间隔时间

但是这样的话,要么你需要很长的时间进行爬取,要么就是需要多台机器一起爬取。

当然还有其他办法,不过比较麻烦,而且忙活半天不一定能用(不要问我是怎么知道的)。跟前面一样,我们也可以在每次请求时设置随机IP代理(IPProxy)。

要想实现此能功能,首先需要一个IP池。可以在一些网站上(如站大爷、快代理等)购买付费的IP,这种方式获得IP一般都可以用而且比较稳定。但是要钱啊,穷学生没有钱,而且还挺贵的,老师肯定也不会报销。遂放弃。

另外就是爬取网络上一些免费IP,由于跟本次文本分类实验的内容离得有点偏,就没有自己编写。而是在github上找了一个爬取免费代理IP的项目:proxy_pool。

项目是搭起来了,IP也爬下来,但是这些IP爬取百度什么的没有问题,但是一到知乎就不行了。我猜测是,知乎每天也在爬取这些IP,并且一爬下来就封禁了(gan!白费了那么大功夫)。

经过上述尝试之后,我们还是选择了最简单的降速的方法。不过为了能按时完成实验,又加上了队友的几台机器一起爬,爬了好几天才勉强爬完一百万条数据,太难了。


参考


  1. Scrapy文档. 架构概览. https://scrapy-chs.readthedocs.io/zh_CN/0.24/topics/architecture.html ↩︎

  2. Scrapy 框架入门简介. https://segmentfault.com/a/1190000013178839 ↩︎

  3. 知乎 API v4 整理. https://juejin.im/entry/5c6d620e518825621f2a6e85 ↩︎

  4. XPath 教程. https://www.w3school.com.cn/xpath/index.asp ↩︎

  5. Scrapy爬虫——突破反爬虫最全策略解析. https://www.jianshu.com/p/a94d7de5560f ↩︎

数据挖掘 文本分类 知乎问题单分类(二):爬取知乎某话题下的问题(数据爬取)相关推荐

  1. 单分类算法:One Class SVM

    安全检测常用算法有:Isolation Forest,One-Class Classification等,孤立森林参见另一篇,今天主要介绍One-Class Classification单分类算法. ...

  2. 【一学就会】爬取知乎热榜话题下的回答及评论点赞数

    最近印度新冠疫情爆发,连我国都有好几个城市出现了印度的变异病毒.为此,我特意去知乎上逛了逛关于印度疫情的话题 [如何看待全球新冠确诊超 1.5 亿,印度单日新增确诊连续 9 天超 30 万例,未来国际 ...

  3. 北邮数据挖掘文本分类实验

    首先需要说明的是,这是北邮王晓茹老师的数据挖掘与数据仓库这门课的文本分类的实验.实验要求如下 实验一文本数据的分类与分析 [实验目的] 1.掌握数据预处理的方法,对训练集数据进行预处理: 2.掌握文本 ...

  4. 机器学习-文本处理之电影评论多分类情感分析

    一.背景 文本处理是许多ML应用程序中最常见的任务之一.以下是此类应用的一些示例 语言翻译:将句子从一种语言翻译成另一种语言 情绪分析:从文本语料库中确定对任何主题或产品等的情绪是积极的.消极的还是中 ...

  5. 【深度学习】单标签多分类问题之新闻主题分类

    # -*- coding: utf-8 -*- """单标签多分类问题之新闻主题分类.ipynbAutomatically generated by Colaborato ...

  6. 数据挖掘_基于balance-scale数据集的简单分类任务验证性实验

    目录 一,题目描述 二,实验准备 1,数据集下载 1.1 常用数据集链接 1.2 UCI数据集 1.3 Kaggle 2,所需基础知识 2.1 分类的方法 2.2 去重 2.3 扩增数据集 2.4 特 ...

  7. R语言使用table函数计算单分类变量的频率表(frequency table)、使用prop.table函数将table函数计算获得的频率表转化为比率表、返回单分类变量每一个类别的比率、或者百分比

    R语言使用table函数计算单分类变量的频率表(frequency table).使用prop.table函数将table函数计算获得的频率表转化为比率表.返回单分类变量每一个类别的比率.或者百分比. ...

  8. 机器学习(二十一)——Optimizer, 单分类SVM多分类SVM, 时间序列分析

    http://antkillerfarm.github.io/ Optimizer 在<机器学习(一)>中,我们已经指出梯度下降是解决凸优化问题的一般方法.而如何更有效率的梯度下降,就是本 ...

  9. 机器学习之单标签多分类及多标签多分类

    单标签二分类算法 Logistic算法 单标签多分类算法 Softmax算法 One-Versus-One(ovo):一对一 One-Versus-All / One-Versus-the-Rest( ...

最新文章

  1. C语言程序设计之回调函数实现方法
  2. Android:打造“万能”Adapter与ViewHolder
  3. mysql5.7 archive安装_对于Mysql 5.7.19 winx64 ZIP Archive的运用安装详细说明
  4. vue - blog开发学习6
  5. WIN8 启用虚拟AP 以共享网络,使手机电脑一起网上冲浪
  6. java socket编程实现聊天程序_java Socket编程 聊天程序 服务器端和客户端
  7. 质问微软 WP8.1开发HTTPS 真费劲
  8. 界面原型设计工具Balsamiq、墨刀、Axure、Mockplus
  9. Android (cocos2dx 网络访问)访问权限设置
  10. 排序算法——随机快速排序
  11. python 验证码识别 阿里云_python3调用阿里云图像识别OCR-实现验证码识别
  12. Bootstrap 按钮
  13. POJ 3253 Fence Repair 贪心
  14. Python教程大纲
  15. C语言 · 判断回文
  16. 播放视频时有残影、水纹的原因
  17. Text B:What’s wrong with copying?剽窃何罪
  18. 练习记录-用FSL工具对DTI数据进行FDT预处理
  19. 利用同义词简化SQL Server 2005开发
  20. macOS微信客户端插件,支持免认证登录、多账号登录以及防撤回

热门文章

  1. CoinCola研究院 | 平台币暴涨,背后推手IEO是何方神圣?
  2. 最近对IT行业收入的话题火起来了,我也来说说毕业三年混到20万的经历
  3. 基于JavaWeb的体育赛事平台的设计与实现
  4. python竖排版输出_Python 让美文竖排显示-高大上
  5. linux 光功率 模块_基于国产芯片的千兆皮秒脉冲激光器模块研制
  6. html()函数用法
  7. Adblock浏览器插件屏蔽百度热搜
  8. 音频服务未启动的症状及解决办法
  9. 基于深度学习的海洋生物声音信号智能识别技术与实现
  10. 时间序列学习笔记(1)