使用 Python、AWS 和艺术构建 Twitter 机器人
为什么要制造一个艺术机器人?
将项目分解成多个部分
需求与设计:高级机器人架构
开发:从 WikiArt 提取绘画作品
开发:本地处理绘画作品和元数据
开发:使用 S3 和 Lambda 函数
部署:机器人推文!
接下来去哪儿?
结论
原文发布在我的博客上。
简而言之:我用 Python 和 AWS Lambda 构建了一个 Twitter 机器人@SovietArtBot ,它每 6 小时发布一次 WikiArt 社会主义现实主义类别的画作。点击此处查看机器人的网站和代码。
这篇文章概述了我为什么决定这样做、我做出的架构决策、机器人的工作原理的技术细节以及我对机器人的下一步计划。
目录
- 为什么要制造一个艺术机器人?
- 将项目分解成多个部分
- 需求与设计:高级机器人架构
- 开发:从 WikiArt 提取绘画作品
- 开发:本地处理绘画作品和元数据
- 开发:使用 S3 和 Lambda 函数
- 部署:机器人推文!
- 接下来去哪儿?
- 结论
为什么要制造一个艺术机器人?
通常,当你刚开始从事数据科学家或开发人员工作时,人们会出于好意给你建议“随便选个项目去做”,以此来学习你需要的技能。
这样的建议可能很难做到,而且很模糊,尤其是在你没有太多经验可以借鉴,无法根据你所掌握的知识来判断什么是可行的,以及整个过程应该如何运作的时候。
通过详细写下我的过程,我希望能够帮助更多的人理解:
1) 软件项目从开始到结束的步骤。
2) 推出一个“足够好”的最小可行项目,并迭代现有代码以添加功能的过程。
3)选择一个你乐于从事的项目。
4)社会主义现实主义艺术的乐趣。
技术目标
最近,我在数据科学工作流程中增加了软件开发环节,我发现:
1)我非常喜欢数据科学项目中的分析和开发工作。2
)数据科学家掌握的开发技能越多,他们的价值就越高,因为这意味着他们最终可以构建生产工作流程的原型,并比等待数据工程师更快地将模型投入生产。
我最近的一个目标是能够独立完成一个完整的软件开发项目,重点是了解现代生产最佳实践,尤其是在云领域。
个人目标
但是,一个仅仅是“云架构交付”的项目实在太无聊了。事实上,我读到最后一句话就睡着了。我做的项目必须要有有趣且具体的目标。
为此,我对将 Twitter 作为开发平台非常感兴趣。我最近撰文指出,修复互联网最重要的途径之一就是停止使用 Twitter。
说起来容易做起来难,因为推特仍然是我最喜欢的网络平台之一。我大部分新闻都来自推特,在那里我了解到了新的博客文章,参与了关于数据科学的讨论,而且我还在那里结识了很多现实生活中的朋友。
但是,推特最近非常嘈杂,甚至到了令人反感的地步。推特可以通过一些系统性的方法解决这个问题,但我决定自己尝试解决这个问题,于是创建了#devart这个话题标签,人们可以在上面发布古典艺术作品,并配上自己创作的科技相关文字,以此来缓解令人焦虑的内容。
能够清晰地描述技术问题,并用视觉隐喻来表达,然后与同样欣赏这种视觉隐喻、觉得它有趣且有共鸣的人分享,这本身就具有极大的宣泄作用。
Liquid 错误:内部
Liquid 错误:内部
Liquid 错误:内部
Liquid 错误:内部
Liquid 错误:内部
Liquid 错误:内部
Liquid 错误:内部
Liquid 错误:内部
有时候,你只想用一些能打动你的艺术作品来打破文字带来的愤怒和单调。结果发现,我并不是唯一一个这么想的人。
Liquid 错误:内部
随着我发布的 #devart 越来越多,我意识到我喜欢欣赏原画几乎和琢磨配文一样多,而且我喜欢像Archillect、Rabih Almeddine和Soviet Visuals这样的账号,他们都发布了很多精美的视觉内容,并附有一些解释。
我决定开发一个能发布绘画作品的机器人。我尤其对社会主义现实主义艺术作品感兴趣。
为什么是社会主义现实主义
社会主义现实主义是俄国革命后发展起来的一种艺术形式。随着俄国君主制的垮台,社会界限逐渐消融,人们开始尝试各种新的艺术形式,包括未来主义和抽象主义。我之前曾在这里写过关于这种转变的文章。
随着布尔什维克巩固权力,他们成立了国家人民委员会(Narkompros),该机构负责控制教育和文化价值观,以决定新政权下哪些内容是可接受的。政府还制定了可接受的苏联艺术的新标准。
纵观社会主义现实主义艺术,其根本目的显然是宣扬共产主义。但是,即便这些作品带有明显的宣传色彩,也丝毫不影响我对这一流派的喜爱,因为它确实反映了真实生活中人们的真实行为。
Liquid 错误:内部
这些人工作、睡觉、欢笑、皱眉、争论,展现出我们在艺术作品中不常看到的真实情感。他们平易近人,充满人性,映照出我们自身的人性。我尤其喜爱这种艺术流派的一点是,它描绘的女性不再只是静静地坐着与艺术家对视,而是在做着各种各样的事情。
所以我决定做一个推特机器人,每隔几个小时发布一条推文。
最终结果如下:
传统软件开发包含以下几个步骤:
- 要求
- 设计
- 发展
- 测试
- 部署
- 维护
将项目分解成多个部分
这信息量太大了。刚开始的时候,我列了一张清单,上面写满了所有需要做的事情:设置 AWS 凭证、角色和权限,版本控制,编写实际代码,学习如何使用 requests 下载图片,如何让机器人按计划发布推文等等。
从上往下看,它令人眼花缭乱。但在我最喜欢的关于写作过程(但实际上也适用于任何创作过程)的书之一《鸟瞰人生》(Bird by Bird)中,安妮·拉莫特写道:
三十年前,我哥哥当时十岁,正努力完成一份关于鸟类的报告,这份报告他有三个月的时间来完成,而且第二天就要交了。当时我们正在博利纳斯的家庭小木屋里,他坐在厨房的桌子旁,眼眶里噙满了泪水,周围堆满了活页纸、铅笔和未开封的鸟类书籍,面对如此艰巨的任务,他几乎动弹不得。这时,我父亲在他旁边坐下,搂着他的肩膀说:“一只一只地写,孩子。就一只一只地写。”
我对软件开发的看法也是如此。一次只做一件事,完成它之后再进行下一部分。因此,考虑到这一点,我决定将传统瀑布式开发方法中的上述步骤与敏捷开发理念相结合,即通过许多小而快速的步骤循环来实现最终目标。
需求与设计:高级机器人架构
我首先根据自己的需求,反向推导,开始构建这个应用程序:
Twitter 上的一个机器人,按照定时任务(可能是cron或类似工具)从某种数据库中提取绘画图像和元数据。
这帮助我理清了设计思路。由于最后一步是发布到 Twitter,所以将数据预先存储在云端是合理的。我也知道最终会用到 AWS,因为我不希望代码和数据依赖于本地机器的运行状态。
我知道我还需要版本控制和持续集成,以确保机器人在我开发时的本地机器上以及我将代码推送到 AWS 时都能保持稳定,这样我就不必手动将代码放入 AWS 控制台中。
最后,我知道我会用 Python,因为我喜欢 Python,也因为它能通过 Twython API 很好地连接到 Twitter(感谢 Timo 向我推荐了Twython而不是已经弃用的 Tweepy),还能通过Boto 库很好地连接到 AWS 。
我会先从一个有很多优秀的、不受版权限制的社会主义现实主义绘画作品的网站上获取这些画作及其元数据。然后,我会对这些画作进行一些处理,提取出画作的名称、画家和标题,以便我能把这些信息发布到 Twitter 上。最后,我会在 AWS 上完成剩下的工作。
我的总体流程大致如下:
最终,我会完全重构掉对本地机器的依赖,并将所有内容推送到 S3,但在弄清楚 JSON 返回的元数据类型之前,我不想在 AWS 上花任何钱。
除此之外,我并没有具体的想法需要哪些工具,随着我的中期目标逐渐清晰,我才做出设计和架构方面的选择。
开发:从 WikiArt 提取绘画作品
现在,开发工作开始了。
WikiArt拥有一个令人惊叹的、编目完善的艺术作品集,涵盖你能想到的所有艺术流派。它做得如此出色,以至于一些研究人员也将其用于他们关于深度学习的论文中。
有时候,我只是去逛逛,看看有什么新鲜玩意儿,然后沉浸在一些艺术作品中。(如果您喜欢他们的作品,请捐赠支持他们。)
WikiArt还有两个对该项目至关重要的方面:
1)他们专门设立了社会主义现实主义艺术类别,收录了相当数量的作品。社会主义现实主义视角下有500件作品,数量不算多(如果我每天想发多张图片的话),但作为起点已经足够了。
2) 每件作品都有图片、标题、艺术家和年份,这对于在 Twitter 上正确标注作品出处非常重要。
我的第一步是尝试通过 API 访问该网站,这是如今从网站以编程方式获取内容的最常用方法。WikiArt 的问题在于它实际上并没有现成的公共 API,因此人们只能采用一些非常巧妙的方法来抓取该网站的内容。
但是,我真的非常不想抓取数据,尤其是因为该网站有无限滚动的 Javascript 元素,而BeautifulSoup(大多数人在 Python 中用于抓取数据的工具)很难抓取这些元素。
所以我进行了一些调查,发现WikiArt 确实有一个 API,即使它不是官方的,而且目前来说也有些过时了。
它包含一些关于 API 速率限制的重要信息,告诉我们您可以访问 API 的频率,而不会导致网站崩溃并终止您的访问:
API 调用:每 2.5 秒 10 次请求
图片下载速度:每秒 20 次请求
更重要的是,如何通过基于 JSON 的查询参数访问特定类别。然而,他们现有的文档大多只针对艺术家层面:
http://www.wikiart.org/en/salvador-dali/by-style/Neoclassicism&json=2
所以我不得不反复尝试才找到我想要的正确链接,那就是:
https://www.wikiart.org/en/paintings-by-style/socialist-realism?json=2&page=1
就这样,我准备提取数据了。
我首先使用 Python Requests 库连接到网站并拉取两项数据:
1)包含所有元数据的 JSON 文件;
2)所有实际的画作png/jpg/jpeg文件。
开发:本地处理绘画作品和元数据
我收到的JSON数据如下所示:
{
ArtistsHtml: null,
CanLoadMoreArtists: false,
Paintings: [],
Artists: null,
AllArtistsCount: 0,
PaintingsHtml: null,
PaintingsHtmlBeta: null,
AllPaintingsCount: 512,
PageSize: 60,
TimeLog: null
}
在这些画作中,每幅画看起来都像这样:
{
"id": "577271cfedc2cb3880c2de61",
"title": "Winter in Kursk",
"year": "1916",
"width": 634,
"height": 750,
"artistName": "Aleksandr Deyneka",
"image": "https://use2-uploads8.wikiart.org/images/aleksandr-deyneka/winter-in-kursk-1916.jpg",
"map": "0123**67*",
"paintingUrl": "/en/aleksandr-deyneka/winter-in-kursk-1916",
"artistUrl": "/en/aleksandr-deyneka",
"albums": null,
"flags": 2,
"images": null
}
response.raw我还通过从 JSON返回数据并使用该方法下载了所有图像文件shutil.copyfileobj。
我决定不再在本地进行任何处理,因为我的目标无论如何最终都要将所有内容迁移到云端,但现在我已经有了可以进行测试的文件,这样我就不需要再访问 WikiArt 并给网站造成过载了。
然后我使用boto 客户端将 JSON 文件和图像文件都上传到了同一个 S3 存储桶中,该客户端允许你写入:
def upload_images_to_s3(directory):
for f in directory.iterdir():
if str(f).endswith(('.png', '.jpg', '.jpeg')):
full_file_path = str(f.parent) + "/" + str(f.name)
file_name = str(f.name)
s3_client.upload_file(full_file_path, settings.BASE_BUCKET, file_name)
print(f,"put")
顺便提一下,.iterdir()这里用到的方法来自一个非常棒的pathlib库,它是 Python 3 新增的,文件操作比操作系统本身更出色。点击这里了解更多信息。
开发:使用 S3 和 Lambda 函数
现在我的文件已经上传到 S3,接下来需要一种方法让 Twitter 读取它们。为了实现定期读取,我决定使用 AWS Lambda 函数(注意不要与 Python Lambda 函数混淆,它们完全不同)。因为我已经熟悉 Lambda 函数及其功能——参见我之前关于 AWS 的文章——所以它对我来说是一个无需太多时间就能上手使用的工具(这是架构决策的关键因素)。
Lambda 函数是一小段代码,您可以直接运行它,而无需了解运行它的机器的任何信息。它们由 AWS 生态系统中发生的其他事件触发。或者,它们可以像cron 一样按计划运行,这正符合我的需求。这正是我需要的,因为我需要安排机器人按一定时间间隔发布内容。
在 Python 中,Lambda 表达式看起来像这样:
def handler_name(event, context):
return some_value
这event是您决定执行的操作,用于触发该函数;上下文设置与 AWS 交互和运行该函数所需的所有运行时信息。
因为我希望我的机器人能够同时发布艺术作品及其相关背景信息,所以我需要一种方法,通过将图片与元数据匹配,来同时发布图片和元数据。
为此,我需要创建键值对(一种常见的编程数据模型),其中键是image属性的文件名部分,值是文件名、title文件名year和artistName文件名,以便我可以像这样匹配两者:
总而言之,我希望我的 lambda 函数能够完成几项任务。我为这部分编写的所有代码都在这里。
1)打开 S3 存储桶对象并检查元数据文件的内容
在 Lambda 函数中打开 S3 存储桶通常如下所示:
def handler(event, context):
for record in event['Records']:
bucket = record['s3']['bucket']['name']
key = record['s3']['object']['key']
download_path = '/tmp/{}{}'.format(uuid.uuid4(), key)
s3_client.download_file(bucket, key, download_path)
其中,事件指的是从 Lambda 函数传入的 JSON 文件,它表示触发事件已发生。由于我们的触发事件是定时事件,因此JSON 文件中不包含关于该特定事件和存储桶的任何信息,我们可以排除该事件,从而创建一个通常用于打开指定存储桶和键的函数。
try:
data = s3.get_object(Bucket=bucket_name, Key=metadata)
json_data = json.loads(data['Body'].read().decode('utf-8'))
except Exception as e:
print(e)
raise e
2) 提取元数据并将其放入一个字典中,以文件名作为键,元数据作为值。我们可以将其放入一个字典中defaultdict,因为这些字典默认是有序的(从 3.6 版本开始,所有字典都将是有序的,但为了保险起见,我们这里仍然使用有序字典)。
indexed_json = defaultdict()
for value in json_data:
artist = value['artistName']
title = value['title']
year = value['year']
values = [artist, title, year]
# return only image name at end of URL
find_index = value['image'].rfind('/')
img_suffix = value['image'][find_index + 1:]
img_link = img_suffix
try:
indexed_json[img_link].append(values)
except KeyError:
indexed_json[img_link] = (values)
(顺便提一下,我之前不知道的一个很棒的 Python 字符串实用工具是 rsplit,它对文件名解析真的很有帮助
[ http://python-reference.readthedocs.io/en/latest/docs/str/rsplit.html ]。)
3)随机选择一个文件名发布到推特(single_image_metadata = random.choice(list(indexed_json.items())))
4)发布图片及相关元数据
目前有一些用于 Twitter 的 Python 库。我一开始用的是 Tweepy,但很遗憾,我发现它已经停止维护了。 (谢谢Timo的提醒。 )
所以我改用了Twython,虽然它稍微复杂一些,但功能是最新的。
最终执行推文发送操作的那段代码如下:
twitter = Twython(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_SECRET)
try:
tmp_dir = tempfile.gettempdir()
#clears out lambda dir from previous attempt, in case testing lambdas keeps previous lambda state
call('rm -rf /tmp/*', shell=True)
path = os.path.join(tmp_dir, url)
print(path)
s3_resource.Bucket(bucket_name).download_file(url, path)
print("file moved to /tmp")
print(os.listdir(tmp_dir))
with open(path, 'rb') as img:
print("Path", path)
twit_resp = twitter.upload_media(media=img)
twitter.update_status(status="\"%s\"\n%s, %s" % (title, painter, year), media_ids=twit_resp['media_id'])
except TwythonError as e:
print(e)
这样做是为了利用 Lambda 函数的临时空间:
今天才知道 AWS Lambda 函数有微型文件系统,可以用作临时存储(stackoverflow.com/questions/3564…)。2018年1月3日 晚上9:03
将文件从 S3 拉取到 Lambda 的/tmp/文件夹中,并按文件名将其与元数据进行匹配,此时元数据采用键值格式。
该twitter.upload_media方法上传图像并获取媒体 ID,然后将该媒体 ID 传递给该update_status方法twit_resp['media_id']。
就这样,图片和文字都已发布。
开发:Lambda 函数的调度
第二部分是配置函数,使其按计划运行。Lambda 函数可以通过两种方式触发:
- 发生的事件
- 定时日程表。
事件可以是任何操作,例如文件进入 S3 存储桶,或者轮询 Kinesis 流。
计划事件可以通过 cron 或固定频率来编写。我一开始编写的是cron 规则,但由于我的机器人没有任何特殊要求,只需要每六小时发布一次内容,所以固定频率就足以满足我的需求了:
最后,我需要将 Lambda 函数打包以便分发。Lambda 函数运行在 Linux 机器上,而 Linux 机器预装的 Python 库并不多(除了 boto3,这是我之前使用的 Amazon Python 客户端库,它将 Lambda 函数连接到 AWS 生态系统的其他部分,以及 json)。
我的脚本中导入了很多库。其中,Twython 是一个外部库,需要与 lambda 函数一起打包并上传。
from twython import Twython, TwythonError
部署:机器人推文!
于是我按照这些说明打包了 Lambda 函数,第一次是手动打包的,方法是将一个 zip 文件上传到 Lambda 控制台。
就这样!我的两个一次性脚本都准备好了,我的机器人也启动并运行了。
接下来去哪儿?
关于苏联艺术机器人,我还有很多想做的事情。
最重要的第一步是调整代码,确保每幅画每周重复出现不超过一次。这个频率对于Twitter粉丝来说似乎比较合适,不会让他们感到厌烦。
与此同时,我想专注于测试和维护。
测试和维护
我第一次完整地执行整个流程时,是从我在 PyCharm 中创建并在GitHub上进行版本控制的本地 Python 项目开始的。
因此,当我修改流程的任何部分时,我的执行流程如下:
- 在本地运行 Wikiart 下载功能
- 使用以下命令“在本地”测试 lambda 函数
python-lambda-local - 将 Lambda 文件压缩并上传到 Lambda。
- Lambda 代码中出现错误
- 拉上 lambda 的拉链,然后再次运行。
对我来说,这并不是理想的工作流程,因为我不想每次都手动重新上传 Lambda 函数,所以我决定使用 Travis CI,它与 GitHub 的集成非常好。问题在于,它涉及很多设置:虚拟环境、同步 AWS 凭证、设置允许 Travis 访问 Lambda 函数的 IAM 角色和配置文件、设置用于测试 Travis 集成的 Twitter 和 AWS 测试环境等等。
目前,该机器人已在生产环境中运行,在它运行正常的情况下,我将继续在我的开发分支中自动化部署的更多部分。(这篇文章对压缩 Lambda 函数特别有帮助,我的部署脚本在这里。)
完成这两项工作后,我想:
1)重构 lambda 代码,使其能够利用pathlib操作系统而非其他方式,从而使我的代码标准化(这应该只是一个很小的改动)。
2)寻找更多画作。WikiArt 很棒,但社会主义现实主义类别下只有大约 500 幅画作。我想找到更多拥有高质量元数据和大量艺术作品的资源。然后,我想……
3)创建一个前端,任何人都可以上传社会主义现实主义作品,供机器人发布到推特上。这可能比定制爬虫程序更容易,而且可以让我实现众包数据。在这个过程中,我需要一种方法来筛选内容,然后再将其上传到我的最终 S3 存储桶。
由此可得:
4)检查现有图库,确保所有图片都相关且为SWF格式。看看是否可以通过编程方式实现。
和:
5)机器学习和深度学习的潜在应用:寻找一种分类器来过滤掉包含裸露/敏感内容的艺术作品,并弄清楚如何界定“敏感”的含义。可以使用AWS Rekognition,或者构建我自己的卷积神经网络 (CNN)。
其他机器学习应用机会:
-
将#devart标签与绘画内容结合使用,看看机器人能否根据绘画内容为画作生成有趣的标题。
-
从不同流派的艺术作品中提取色彩,并观察不同流派和年代的色彩差异。
结论
软件开发可能是一个漫长而疲惫的过程,涉及众多环节和决策,但如果将项目分解成一个个易于处理的小模块,并持续推进,就能避免被整个任务压垮,从而变得轻松有趣得多。当然,另一方面,开发过程本身也必须充满乐趣,这样才能让你在经历种种挑战后,最终收获一个令人满意的成品。
文章来源:https://dev.to/vboykis/building-a-twitter-art-bot-with-python-aws-and-art--74p






