一、工作背景
放弃之前的计划:
经过开会讨论,仅爬取心理学领域的知识来构建心理沙盘的知识图谱是不可取的(事实上,项目的目标是根据用户设计的沙盘场景推理出用户的心理状态,而不是做心理学百科知识的科普)。
这一知识图谱构建方向上的改变归功于我们小组的讨论和 《知识图谱-概念与技术》 这本书对我的启发,一些知识要点如下(可以跳过直接查看项目细节):
1、知识图谱广义概念
为了讲明白我对项目的理解,还要从人工智能说起(想在这里一次性的的捋清楚,也许将来会加入一个链接):
图:知识图谱的学科地位
人工智能有3个学派:符号主义、连接主义和行为主义;
知识工程源于符号主义,为了有效的应用知识,首先要在计算机系统中合理的表示知识;
而知识表示的一个重要方式就是知识图谱;
注意:知识图谱只是知识表示中的一种,除了语义网络以外,谓词逻辑、产生式规则、本体、框架、决策树、贝叶斯网络、马尔可夫逻辑网都是知识表示的形式;
2、KG研究意义
KG是认知智能的基石
- 机器理解数据的本质是从数据到知识图谱中的知识要素(实体概念、关系)的映射;
- 理解过程可以视作建立从数据(文本、图片、语音、视频)到KG(脑海)中实体、概念、属性之间的映射过程;
几点KG对于认知智能的重要性:
①机器语言认知
人类对语言的理解建立在认知能力基础之上,所以解释了我们听不懂西方的笑话故事。
语言理解需要背景知识,机器理解自然语言当然也需要背景知识了。
②赋能可解释人工智能
“解释”与符号化知识图谱密切相关,人只能理解符号而无法理解数值,书中有三个很好的例子:
问鲨鱼为什么可怕?你可能解释因为鲨鱼是肉食动物——用概念在解释;
问鸟为什么能飞翔?你可能会解释因为它有翅膀——用属性解释;
问前段时间鹿晗和关晓彤为什么会刷屏?你可能会解释因为她是他女朋友——用关系在解释;
③有助于增强机器学习的能力
人类的学习高效、健壮,并不需要机器学习那样庞大的样本量,根本原因在于人类很少从零开始学习,人类擅长结合丰富的先验知识。
3、常识知识图谱
知识图谱可以根据其所涵盖的知识分为四类:事实知识、概念知识、词汇知识和常识知识
有了1和2的铺垫,我们选择为心理沙盘项目建立一个常识知识图谱的理由就明确了,每一个沙盘实体都来自现实世界,我们要做的是高度还原人脑中实体与实体、实体与概念、概念与概念这些映射关系,从而能够让心理测试结果更强健和有说服力。
以沙盘描述中的虾为例:
①虾看似慢悠悠的游,遇到对手时它勇猛奔上去伸展双臂与它博斗,
打断双臂在所不惜,象征充满阳刚之气。
②虾须流畅,飘逸,虾尾随移其形而动,象征个性和目标。
二、工作思路
沙盘心理分析师提供了一个entity文件,里边有大约600个实体,长这样:
1、以这600个实体为根节点,从ConceptNet上爬取实体的所有关系,再递归关系所牵的另一边实体,提取的内容有:
entity1、relation、entity2、att1、att2、weight、id
此七项内容
2、根据这些内容先构建一个知识图谱
三、scrapy实操
1、在之前创建的爬虫工程中,新建一个spider文件,还是通过命令来实现(工程项目创建的具体操作步骤参考scrapy(一) 爬取心理学领域词汇)
scrapy genspider -t basic conceptSpider api.conceptnet.io
2、分析页面,试探爬取——循环往复直到成型
①方法论:
当有一个新的爬虫任务时,我必然不能也绝不可能做到直接写成spider文件,解注释settings.py中的pipline,写items.py文件,写pipline.py文件。因为网页是否允许爬取、网页的格式、我们想要的内容在哪、是否需要递归等,都需要去debug,慢慢的磨出来。也就是先别解注释settings.py中的pipline,只消使用命令$ scrapy crawl conceptSpider --nolog
去一步步的迭代检验我们的规则是否写对了。
②页面分析:
查看ConceptNet官方文档,得知这种api类型的网页中,每一个key代表什么。
先给start_urls中只添加了一个‘http://api.conceptnet.io/c/en/apple’
来专注于分析这个页面,显然每一页都有共性,页面部分截图如下:
网页解析:
# 将json格式的api页面解析为用字符串表示的字典
js_str=response.xpath("//pre").xpath("string(.)").extract()[0].strip()
# 将字符串转化为字典
js=json.loads(js_str)
规则化提取:
for edge in js['edges']:
str=edge['@id'].replace('[','').replace(']','').replace('/a/','').split(',')
# 只取英文关系
if str[1][3:6]!='en/' or str[2][3:6]!='en/':
continue
# 取两个实体
e1=str[1].replace('/c/en/','').replace('/n','').replace('/wn','')[:-1].split('/',1)
e2=str[2].replace('/c/en/','').replace('/n','').replace('/wn','')[:-1].split('/',1)
# 取关系
r=str[0].replace('/r/','')[:-1]
# 摘除后边的标识,放到属性里
e1_att=''
e2_att=''
if len(e1)==2:
e1_att=e1[1]
if len(e2)==2:
e2_att=e2[1]
注:测试规则化提取正常后,就可以在上边的for循环里边用item传给pipline去输出了。
③页面跳转
if 'view' in js and 'nextPage' in js['view']:
relPos = js['view']['nextPage']
print("进入衍生页面执行parse:")
nexthref = self.url_std+relPos
print(nexthref)
yield scrapy.Request(nexthref, callback=self.parse)
注意:这里有坑,如果只判断'nextPage' in js['view']
,你就会发现有的实体页面中没有view这个键值,会产生错误,但不影响pipline输出的结果。
*3、解决网页访问频率限制问题
发现问题:
在只有两个url的start_urls列表上测试没有问题,但是将400多个url放入start_urls中后,得到的结果每次都只有几百,几千条,最多1万条。
细节: 观察输出文件triplets.txt和终端显示,发现每次在第602个页面,第1206个页面出现卡壳,最终可能停止输出。
曲折的解决过程:
以为是输出的时候内存爆了导致write文件终止或者是txt文件大小有限制导致提前关闭文件。结果换成csv格式也不好使,都要考虑使用数据库了;
去掉--nolog
后执行$ scrapy crawl conceptSpider
,发现问题在于爬取阶段而不是pipline的数据写出阶段。日志中有一些url反馈结果是 [429] ,也就是页面被访问频繁导致的拒绝访问。
解决办法的帖子 具体如下:
可使用 429 状态码,同时包含一个 Retry-After 响应头用于告诉客户端多长时间后可以再次请求服务。
middlewares.py: # 当状态码是429的时候 爬虫暂停60秒再爬取
import time
from scrapy.downloadermiddlewares.retry import RetryMiddleware
from scrapy.utils.response import response_status_message
class TooManyRequestsRetryMiddleware(RetryMiddleware):
def __init__(self, crawler):
super(TooManyRequestsRetryMiddleware, self).__init__(crawler.settings)
self.crawler = crawler
@classmethod
def from_crawler(cls, crawler):
return cls(crawler)
def process_response(self, request, response, spider):
if request.meta.get('dont_retry', False):
return response
elif response.status == 429:
self.crawler.engine.pause()
print("速度太快 暂停60秒")
time.sleep(60) # If the rate limit is renewed in a minute, put 60 seconds, and so on.
self.crawler.engine.unpause()
reason = response_status_message(response.status)
return self._retry(request, reason, spider) or response
elif response.status in self.retry_http_codes:
reason = response_status_message(response.status)
return self._retry(request, reason, spider) or response
return response
settings.py:
DOWNLOADER_MIDDLEWARES = { # 开启暂停中间件
'psySpider.middlewares.PsyspiderDownloaderMiddleware': 543,
}
RETRY_HTTP_CODES = [429, 500, 403] # 这个状态重试
DOWNLOAD_DELAY = 0.5
RANDOMIZE_DOWNLOAD_DELAY = True # 发完一个请求 随机暂停
4、piplines.py写法
# 直接照着爬虫名字补上就可以
class ConceptspiderPipline(object):
def __init__(self):
self.file = open("./triplets1.csv","w",encoding='utf-8')
self.writer = csv.writer(self.file, dialect="excel")
def process_item(self, item, spider):
theme=item['theme']
entity_1 = item['entity_1']
entity_2 = item['entity_2']
relation = item['relation']
att1=item['e1_att']
att2=item['e2_att']
weight=item['weight']
id=item['id']
self.writer.writerow([entity_1,entity_2,relation,att1,att2,weight,id])
return item
5、items.py
# 直接照着爬虫名字补上就可以
class ConceptspiderItem(scrapy.Item):
theme=scrapy.Field()
entity_1=scrapy.Field()
entity_2=scrapy.Field()
relation=scrapy.Field()
e1_att=scrapy.Field()
e2_att=scrapy.Field()
weight=scrapy.Field()
id=scrapy.Field()