一、工作背景

放弃之前的计划
经过开会讨论,仅爬取心理学领域的知识来构建心理沙盘的知识图谱是不可取的(事实上,项目的目标是根据用户设计的沙盘场景推理出用户的心理状态,而不是做心理学百科知识的科普)。
这一知识图谱构建方向上的改变归功于我们小组的讨论和 《知识图谱-概念与技术》 这本书对我的启发,一些知识要点如下(可以跳过直接查看项目细节):

1、知识图谱广义概念

为了讲明白我对项目的理解,还要从人工智能说起(想在这里一次性的的捋清楚,也许将来会加入一个链接):
image
图:知识图谱的学科地位

人工智能有3个学派:符号主义连接主义行为主义
知识工程源于符号主义,为了有效的应用知识,首先要在计算机系统中合理的表示知识;
知识表示的一个重要方式就是知识图谱
注意:知识图谱只是知识表示中的一种,除了语义网络以外,谓词逻辑、产生式规则、本体、框架、决策树、贝叶斯网络、马尔可夫逻辑网都是知识表示的形式;

2、KG研究意义

KG是认知智能的基石

  • 机器理解数据的本质是从数据到知识图谱中的知识要素(实体概念、关系)的映射
  • 理解过程可以视作建立从数据(文本、图片、语音、视频)到KG(脑海)中实体、概念、属性之间的映射过程;

几点KG对于认知智能的重要性:
①机器语言认知
人类对语言的理解建立在认知能力基础之上,所以解释了我们听不懂西方的笑话故事。
语言理解需要背景知识,机器理解自然语言当然也需要背景知识了。
②赋能可解释人工智能
“解释”与符号化知识图谱密切相关,人只能理解符号而无法理解数值,书中有三个很好的例子:

问鲨鱼为什么可怕?你可能解释因为鲨鱼是肉食动物——用概念在解释;
问鸟为什么能飞翔?你可能会解释因为它有翅膀——用属性解释;
问前段时间鹿晗和关晓彤为什么会刷屏?你可能会解释因为她是他女朋友——用关系在解释;

③有助于增强机器学习的能力
人类的学习高效、健壮,并不需要机器学习那样庞大的样本量,根本原因在于人类很少从零开始学习,人类擅长结合丰富的先验知识。

3、常识知识图谱

知识图谱可以根据其所涵盖的知识分为四类:事实知识、概念知识、词汇知识和常识知识
有了1和2的铺垫,我们选择为心理沙盘项目建立一个常识知识图谱的理由就明确了,每一个沙盘实体都来自现实世界,我们要做的是高度还原人脑中实体与实体、实体与概念、概念与概念这些映射关系,从而能够让心理测试结果更强健和有说服力
以沙盘描述中的虾为例:

①虾看似慢悠悠的游,遇到对手时它勇猛奔上去伸展双臂与它博斗,
打断双臂在所不惜,象征充满阳刚之气。
②虾须流畅,飘逸,虾尾随移其形而动,象征个性和目标。

我们可以构建这样一种联系:
image.png

二、工作思路

沙盘心理分析师提供了一个entity文件,里边有大约600个实体,长这样:
image.png
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’
来专注于分析这个页面,显然每一页都有共性,页面部分截图如下:
image.png
其中’edges‘是一个list形式,每一项是一个关系,连接着与apple相关的实体;
在网页底部,有一个键值是‘view’,是管前后页的,也就是说里边有下一页的url;
image.png
网页解析:

# 将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()

略多
1 声望0 粉丝

可傻了!