PyCon China 官网去哪里了?

看到 PyCon China 2020 要开始筹备了,我想着赶紧把去年参与筹备的总结文章写完,再不写就过期了,然后就发现官网打不开了。不仅为了国内访问更快部署的 pyconchina.org.cn 打不开,连最正式的 cn.pycon.org 也打不开。前者就我有限几次访问来看,先是显示备案报错,接着是 SSL 证书过期,现在直接没有任何响应了。而后者此前只是图片加载慢一点,还是能正常访问的,现在无论直连还是绕路都没用。

没有博客就算了,官网打不开都不修了吗?作为一个技术大会,只在微信公众号和新浪微博发布动态太不正式了,而且也没有同步到 Twitter 和 Facebook。要是我还在筹备组的话……

我还是先自己做个存档吧 :P

https://greyli.com/archives/pyconchina

11/2 更新:cn.pycon.org 现在显示 Azure 的 404 页面了,前两天是长时间无响应。

11/7 更新:cn.pycon.org 恢复了。

2021 年更新:

今年 Frost Ming 帮忙做了一个漂亮又像样的官方网站,同时恢复了往年的站点历史。

2022/7/8 更新:

网站又访问不了了。似乎每年只有大会前后短暂的时间才能访问。

2020 暑假总结

最后一次写暑假总结是在 2016 年,之后因为不再上学,也就没有了暑假这个概念。今年夏天回家呆了一个月,算是自己给自己放的暑假。

农村像是冰箱下面的冷藏室,时间在这里不会产生多大作用。也正是这样,你一下子就能看出有哪些新变化。首先是家里的狗生了七只小狗,因为胎盘没及时去掉,死了三只。然后家里的蚂蚁似乎变多了,在院子里扔一小块饼干或西瓜,过一会就会看到密密麻麻的蚂蚁趴在上面(拔菜园里的花生时发现三个蚂蚁窝!)。除此之外,一切几乎都是老样子:狗总是热得吐舌头。西瓜甜不甜要看运气,但价格大都不到城里的一半。附近的工厂有时会在早晨制造噪音,半夜偶尔还会听到邻居用「古法洗衣」时棍子敲打衣服的声音*。

夏天全家人一起吹空调吃西瓜看电视是很幸福的事情,我试图想把我和瑶瑶都很喜欢的《请回答 1988》推荐给大家,但也许是每一集太长,或者是语言和文化差异的缘故,大家并没有表现出很大的兴趣。不过我们还策划了一次去水上乐园的短途旅行,这一次大家都玩得很开心。

暑假像是一年的短暂中断,一个可以喘口气的中场休息。上学的时候暑假让我从无聊痛苦的学校生活里抽离,而现在则可以临时放下各种喜欢和不喜欢的工作。我用这段时间清理掉了上半年积攒下来的一堆便签和纸片,整理了电脑上的各种笔记、杂乱文件和标签页。修正了年目标,因为一年过去大半,所以有些不切实际的目标自然就变得非常明显,现在只好先放弃掉。剩下的几乎就都是娱乐时间了。

《皇室战争》玩到了大师联赛,然后就有点厌倦,估计下半年都不会想再玩(不过无限圣水模式实在太容易沉迷了)。因为想找一个联机游戏和李家玩,又安装回了 Steam,找到大学时买的《兽人必须死 2》。结果他沉迷在 Epic 免费领的 《GTA 5》,我就只好自己玩。接着大学时的进度通了关,然后玩了很久的无尽模式(Endless)。像我这种喜欢整理和规划的人大概都会喜欢这种塔防游戏——可以把所有空间完美的利用好,天花板、墙和地面上每一个防具都摆放的整齐对称。但是无尽模式实在太消耗精力,每一局都会让眼睛和右手累的不行,最后止步于 44 波的最高成绩(放弃的另一个原因是解锁所有武器和技能太耗费时间)。玩的时候有时会想自己面对的敌人不过是每一波都增加了几个数字的难度变量,有一种被代码控制和愚弄的意味……

也不是完全没有成果。花了一些时间来更新 Bootstrap-Flask,发布了 1.5 版本,接下来一段时间可以把它放一边了。给 Flask 书的示例程序 SayHello 加了一个验证码功能。从去年开始就有人在这里灌垃圾留言,直到现在才下决心清理和阻止(一共删掉了 9083 条)。说是验证码,其实只是加了一个验证输入值是否是「宝塔镇河妖」的文本输入框……不过在垃圾留言程序弄明白规则之前应该都没问题。最后还把大部分 Python 的 Web API 相关工具库的文档都看了一遍。

暑假里大部分时间都被随心所欲的花掉了,但我并不把这看作浪费,如果暑假时间也要全用来学习和工作,那生活就太无聊了。这样想的话,在小时候的暑假,写作业是浪费,玩游戏、在门前发呆和出去玩就不是。现在的暑假呢,为别人工作是浪费,做自己喜欢的事情就不是。


*比起在南京时楼上孩子练习长跑和玩积木的声音、楼下孩子半夜的哭闹和夫妻吵架的声音,家里的这两种倒更容易忍受,至少你知道什么时间能结束。

知乎——没有知识存在的荒原

最近打开知乎的链接经常看到 404 页面:

想象一个这样的场景:一个不了解 404 含义的用户,访问某个知乎的网址,结果看到提示「你似乎来到了没有知识存在的荒原」。咦,原来知乎是没有知识存在的荒原?想想就很有意思。以前没有细想它的文案,现在发现这其实是一个预言:知识要么都被你的审查官删掉了,要么就被混进越来越多的垃圾信息里,这样下去知乎恐怕真的要变成知识的荒原了。

审查会造成信息的一元化,最终造成观点的一面倒,而知乎的审查会加强这种效果。知乎上有很多价值判断的问题(如何看待 XXX?)。正常来说,一件事情如果没法用常识大致区分对错,既然需要拿出来讨论,那么必然有人支持也有人反对。在所有回答里,如果持反对和批评态度的答案大都被知乎删除,那么后来的读者便会认为大多数人的观点都偏向支持,从而进一步认为支持才是正确的观点(如果没有独立思考和判断的习惯的话),于是也下意识的选择支持。再加上如果写答案的人都趋于使用简单粗暴的语言进行攻击和贴标签,读者也会被鼓动起来,变成不会思考只会攻击对立观点的傀儡。

当然,知乎的审查手段很多,比如现在用知乎的搜索框搜「李文亮」,只会看到几条知乎官方账号发布的致敬啦、悼念啦、怀念啦之类的官媒模板新闻,看不到一个问题,连相关用户也被一并屏蔽了(你要祈祷不会有和你同名的人成为国家主席或是变成「争议人物」)。大多相关的问题也被删的七七八八,而你通过各种犄角旮旯找到的问题里,回答以及回答的评论也都是过滤后的结果。再加上作者在答题之前以及在问题被建议修改后的自我审查。这种审查达到了极致。

在其他网站,比如豆瓣和新浪微博,也存在这种一体化的审查机制,但是对待一个广播、一篇日记或是一条微博提出的事件,用户的评论也许是反对或支持,也许是用户对事件本身的补充,也许只是无聊的灌水。因为评论很少有长篇论述,即使被审查筛选,你也不会把 100 条评论看得和 100 个回答一般重要。在知乎上,对于一个「如何看待」类问题,所有的回答天然都默认作为一种价值判断和立场选择。长篇大论看似有理有据但也许充满了逻辑漏洞和陷阱,但这种回答却让你下意识的认为其专业而正确,而问题下的「支持」和「反对」按钮更是加重了回答的价值判断意味。

这种特性也许会让知乎变成一个「超级高效的观念、立场和价值观塑造工具」,每一个没法独立思考的人,扔给他一个「如何看待」类问题,只要从上往下把回答刷上一遍,就完成了一个价值观改造流程。在正常情况下,这种工具有一定的益处,它可以让一个人看到更多元化的观点,甚至接受不同的观点。但是一旦搭配上同样高效的审查和专业的写手,知乎问题就变成了强有力的思想塑造工具,除了政府想要拥有它,其他想要塑造公众形象的明星公关公司和大企业也会非常喜欢(还可以搭配付费热搜调节一起使用)。

一个人、一件事的对错并不容易界定,非黑即白并不是唯一答案。我们要谨慎对待知乎上的价值判断类问题。

另一方面,理性的观点探讨、不同意见的互相碰撞正在从互联网上消失,不仅仅是因为承载这种信息的环境和平台也正在消失(即《中文互联网中“讨论”的消亡》中提到的「公共讨论空间消失」),除了审查的影响,这些理性的声音自身也逐渐被大量无意义的噪音和攻击冲淡,或是因此而退却和沉默。而事实上这不仅仅是知乎的问题,而是娱乐至上背景下整个互联网环境的问题。国内因为言论审查和无良大公司的存在,所以情况更加糟糕。西方世界的大部分人也许沉浸在赫胥黎的预言里声色犬马,而中国人则沉睡在赫胥黎和奥威尔的双重预言编织的梦魇里难以醒来。

如何抵抗这种变化?建立你自己的网站吧。一方面可以尽可能自由的发出自己的声音。另一方面,不用把自己的数据全部交到别人的手里,要不然一不留神知识变荒原,你写的文章和回答也就成了沙子和尘土。

超链接跳转费

昨天有人给《Flask 入门教程》书稿源码提交了一个 PR,主要的修改是在书中的链接前后添加了空格。比如:

  • 首先要注册一个 GitHub 账户,点击访问注册页面根据指示完成注册流程。登录备用。

改成了:

  • 首先要注册一个 GitHub 账户,点击访问 注册页面根据指示完成注册流程。登录备用。

我一直对这个排版风格持观望态度。如果合并的话,那就要在以后的书稿更新甚至所有网络写作中保持这个习惯,再三考虑之下,我还是决定暂不这样做。最主要的考虑是添加空格会影响阅读体验,好像本来平淡无奇的超链接突然自带聚光灯。一来在观感上突兀,二来会影响阅读的流畅感(大多数情况下链接文字本身也是句子的一部分)。

不过话说回来,超链接也该被拎出来亮一亮了,因为超链接在国内的网络环境里一直处于被打压和控制的状态:微信公众号不允许插入超链接,新浪微博从下个月起要停止把 URL 解析成超链接(只为白名单内的政府、公司等网站提供链接渲染服务),而知乎也曾悄悄停止把用户主页一句话介绍里的 URL 解析成超链接。在网络世界里,超链接是再平常不过的东西。但这些公司却把互联网的基础设施垄断、控制起来进行牟利、打压竞争对手以及用来把用户尽可能的圈在平台内。

我在想,新浪微博之类的平台在以后也许可以尝试收超链接费或是超链接跳转费(比如推出一个超链接套餐,100 块钱可以发 50 个链接),然后再按照点击量收取超链接点击费或是超链接流量费。听起来就像是政府要征收空气税和重力税……

除了不被渲染为超链接,有时在手机上访问超链接也要跨过千难万险。如果你在微信上打开一个超链接,你首先要跟 GFW 打声招呼,接着或许还会被网络运营商搜身,最后才会经过微信的审查。如果微信发现你要打开的是淘宝之类竞争对手的网址,或是它「自己认为」危险的不适合访问的网址,你就只能通过长按复制网址的方式用浏览器打开。如果你很不幸安装的是国产浏览器的话,那恭喜你还要再面临一层国产浏览器厂商的审查。

想让你的网站被渲染成超链接并且可以被正常访问?好,那就申报,登记,填表,备案吧。

也许未来会有很多人忘记为什么网页上一段文字里某些文字会是蓝色和下划线样式。

被抖音、标语和宣传画占领的城市

在上下班高峰的公交车和地铁上,我才开始深刻的体会到「数字劳工」这个词的含义:大家辛辛苦苦上了一天班,结果下班了还要继续努力娱乐来训练大公司的推荐算法,观看他们的广告,认真在下班时间也给其他人创造财富。

今天下午去某地办理文件,去的时候还好,回来时刚好赶上了下班高峰,几乎站了一路。前面的大爷先是打开了微信,和几乎所有人一样,满屏乱七八糟的群聊(感谢张小龙的克制),先是看了微信群里关于龙卷风的视频,跳转到微信看一看,下滑了几条洪水的视频,接着开始翻订阅号里的文章列表,几乎每篇文章标题里都有至少一个感叹号(「最新!」、「震撼!」、「愤怒!」、「重磅!」),然后打开了拼多多,划了几条商品推荐,签到领积分,最后又回到微信,如此循环。右前方的女生看了一会儿抖音,然后切换到微信,接着关掉了屏幕。三分钟左右,又打开手机,进入微信,然后又打开抖音,这次开始专注起来。左边的人、右边的右边的人、右边的右边的前面的人,或许还有其他半车人都在看抖音(抖音俨然成为了一种新兴宗教)。

抖音、快手们以及各种抖音快手化的程序们争相接过了电视的接力棒,以一种更高效的方式来对人类进行信息投喂(他们称之为算法推荐)。我像躲避电视一样躲避这类被动灌输的信息流,我知道自己一样抵挡不住,所以只在偶尔出门的时候和公交地铁邻座的陌生人(偷偷)瞟两眼,不过大部分的抖音视频和配音都让人反胃。

我有一次下载了抖音想看看在经过筛选后会留下哪些视频。在筛掉一堆无聊的视频后(常常伴随着无聊的配音和字幕),剩下的都是晚霞和云、海浪、滑板、街道、动画电影。到最后这些重复的内容让我有些厌倦,而且不停的打断让我感到烦躁。相比于看一堆十几秒的夕阳、风景和精彩电影画面剪辑。我更希望看看真的夕阳,看一部完整的电影,亲自去外面走一走。

车里无聊,车外也一样。看窗外的店铺招牌和政府宣传画大多时候是进行审丑训练,字体和排版大都千篇一律,命名、措辞也都无聊透顶。最难看的总要数各种政府机构做的宣传画,第二名是那种政府给整条街布置的统一招牌背景板。作为城市景观破坏之王的横幅更不用说,要么是红底白字,要么是红底黄字,仿佛中国的布料工厂只生产红布。而政治宣传画大都是一种面孔:红黄白三色混合、群山、太阳、天安门、长城、国旗、白鸽、红色绸缎,几乎 80% 的宣传画都离不开这些元素。除了空洞的政治宣传画,其他的非政治宣传画也大都花里胡哨,字体混乱,设计水平低劣。或许政府宣传机构的大部分设计者从来就没有进行过任何现代的审美和设计训练,只要会用 PPT 贴艺术字就能评优秀职工。

南京图书馆的宣传画

南京图书馆右侧的宣传画,左侧对称的是「社会主义核心价值观」

城市里到处都是这些视线污染源。来源五花八门:文明办、宣教局、市委宣传部、街道办事处,每个政府机构都想给城市景观插一脚,可是你好歹要有基本的文化、审美和设计修养。而且那么密集的宣传画完全就是一种破坏和滥用:仿佛只要哪里有几米空地,几面白墙,不立一块「讲文明,树新风」和「自由民主文明和谐」的牌子就难受。拜托,眼睛也要喘口气的好吧!再美的城市景观也抵不过这些宣传画的破坏。相比之下,我更情愿看商业广告和「乱涂乱画」,所以政府反倒不如把这些宣传位卖给商业公司们,政府里的设计专家们还是看看抖音刷刷朋友圈就好。

退一步说,他们真的认为把丑的要命的宣传画和低幼、空洞或谄媚的标语堆满城市就会让市民变得更文明更有教养?可是这些设计者似乎除了文明和谐之类的共产话语,就只知道三字经和二十四孝图。不知道中国还有多少地方没有这些空洞、不知所谓、丑陋的标语和宣传画?这些密集重复的东西浪费了多少资源?为什么这些东西可以随意的破坏城市景观,侵占人们的视线和思想?

我一直在想,如果可能的话,我提议每个城市增设一个市设计局(反正那么多各种办局处,多这一个也没关系),专管整个城市的标语和宣传画等具象的景观设计。先花一个月清理掉整个城市所有的劣质宣传画和不必要的标语,拆掉所有的统一招牌背景板,然后出台「XX 市城市景观设计风格指南和相关规定」。限制每个政府机构可以张贴的标语/宣传画数量、位置、措辞和设计,每个政府宣传机构在张贴标语和宣传画之前都要经过市设计办的审核和批准。这样一来,我们才能有一个真正干净的城市,我也才能没有障碍的爱这个城市。

看过美好之后的审丑训练并不完全没好处,但如果我有孩子的话,我不想让我的孩子在还没有了解到美和文明之前就读着那样的标语和宣传画、看着陌生人外放的抖音长大。

FlaskCon 2020 参会指南

FlaskCon 是由社区举办的免费线上 Flask 会议,欢迎 Flask 的使用者和爱好者参加。这是一份非官方参会指南,希望可以帮到你。

会议时间

基于演讲者优先的考虑(因为听众可以看录播),为了让演讲者不用半夜爬起来演讲,所以有了现在的日程时间:

  • 7 月 4 号:晚上 10 点到第二天凌晨 2 点 20
  • 7 月 5 号:下午 5 点到晚上 9 点 20

议题

一共收到 44 个议题申请,按照两天的时间安排,最终只保留了 18 个。大致分为下面几类:

我们是怎么用 Flask 的?

这里的「我们」包括 Google Cloud、FEC(美国联邦选举委员会)、ScholarPack……

  • How Google Cloud uses Flask(Dustin Ingram)07/04 10:00 PM
  • How the FEC uses Flask to increase transparency in US elections(Laura Beaufort)07/04 11:00 PM
  • Lessons Learned Building Microservices with Flask(Joe Coburn)07/05 02:00 AM
  • OpenPatch an Example for Building Microservices with Flask(Mike Barkmin)07/05 06:00 PM

Flask 相关特性介绍

介绍 Flask 的灵活性、工厂模式、请求上下文、异步 Flask 实现——Quart……

  • The Application Factory Architecture(Bruno Rocha)07/04 10:30 PM
  • Demystifying Flask’s Application and Request Contexts with pytest(Patrick Kennedy)07/04 11:30 PM
  • Flask’s flexibility for the win(Brett Kromkamp)07/05 06:30 PM
  • Async Flask, an introduction to Quart(Philip Jones) 07/05 09:00 PM

用 Flask 开发 Web API

  • Flask-AppBuilder new REST API, and Superset MVC to SPA revamp(Daniel Gaspar)07/05 07:30 PM
  • Using Flask-RESTy to build flexible REST APIs, fast!(Jimmy Jia)07/05 01:30 AM
  • Securing Flask Web Applications with JWT and CSRF tokens(Solomon Esenyi)07/05 01:00 AM

先介绍这么多,完整的议题时间表各位可以在官方网站上的 Schedule 部分看到,议题信息里的时间即北京时间(GMT+8)。

前一天在晚上 11 点还会有两个小采访,不过看起来没什么意思。

直播和录播

会议会通过在 FlaskCon 的 YouTube 频道直播,录制的视频也会放到这里。如果你没法访问 YouTube,不用担心,墙外开花墙内也能闻得到,因为 PyCon China 的 bilibili 账号也会同时进行转播,后续视频也会放到这里。英语听不懂的话,YouTube 可以自动生成字幕,也可以实时翻译,如果你没法访问 YouTube,那你就要自己想办法了,试试重启路由器吧。

如何参加?

嗯……因为是免费活动,看直播就算是参加了。如果你想获得更多的临场感和参与感,可以加入 Pallets 的官方 Discord 服务器,在 #flaskcon 频道(会议期间开放)里参与讨论,向演讲者提问(每个演讲有 5 分钟的 QA 环节),记得用英语。另外也可以在这个非官方 Python Discord 服务器或是 Reddit 上的 Flask 节点参与讨论。如果你访问不了 Discord 和 Reddit,或者英语不好的话,天无绝人之路,你还可以在 PyCon China 的 bilibili 直播间刷礼物和发弹幕 :)

Update 2020/7/4 看起来 PyCon China 并没有转播 :/

Update 2020/7/7 PyCon China 没有转播是因为一个乌龙事件「They missed the date due to an error in our meeting notes」,最终改为 7/14 转播,并且(据说)会添加中文翻译字幕。

比修 Typo 还简单的开源贡献方式

最近给 WTForms 提交了一个 PR,这个 PR 向源码、测试和本地化文件里添加了 537 个句号。听起来似乎很奇怪,为什么 WTForms 会需要增加 537 个句号?别着急,下面会慢慢解释(事实上我只完成了一部分的工作,还有大概 500 个句号需要添加)。我发现我似乎很喜欢提交这种 PR,这类开源贡献没有太多技术含量(基本就是体力活),但是能有效提高项目的整体完美度,让用户获得更一致和舒服的体验。下面是一些可以归到这一类开源贡献的 PR。

1. 给 WTForms 添加 537 个句号

https://github.com/wtforms/wtforms/pull/620

WTForms 从添加 CSRF 保护功能作为分界,之前定义的验证错误消息都包含结尾的句号,之后的错误消息都漏掉了句号。这会导致错误消息不一致(想象同一个表单显示两个输入框的错误消息,一个有句号,一个没有句号)。这个 PR 补齐了源码、测试、本地化文本(POT 和部分 PO 文件)中错误消息的句号,包括 506 个英文句号和 31 个中文句号。

本地化文件在翻译时大都按照源文本决定是否添加句号,所以也存在错误消息不一致的问题。因为精力有限,在 32 个本地化文件里,我只更新了简体中文、繁体中文、日文、德文和俄文。大概还有二十几个本地化文件需要进行确认和更新,如果感兴趣的话,你可以考虑去做这件事。

2. 给 Flask 添加 96 个美元符号

https://github.com/pallets/flask/pull/2877

Flask 文档、和各类 README.md 文件里对于命令行命令的标识很混乱,有时没有命令提示符,有时用「>」,有时用「$」,有时用「#」。这个 PR 统一了所有命令行命令,统一添加美元符号作为提示符,仅在需要明确区分 Windows 命令的几处使用「>」。

3. 给 Flask 更换 13 个 URL

https://github.com/pallets/flask/pull/3427

Flask 去年陆续把文档迁移到了 palletsprojects.com 域名下, 访问旧的 pocoo.org 会进行跳转,这个 PR 更新了所有文档和源码里的旧 URL。

4. 给 PyCon ChinaFlaskCon 压缩 119 张图片

除此之外,勉强能沾上边的还有给 PyCon ChinaFlaskCon 的网站分别压缩了 105 和 14 张图片。因为缺乏规范,有些技术大会的网站图片在上传之前没有经过压缩和裁剪,这会让页面加载变得非常慢。这两个 PR(PyConChina #3FlaskCon #7)分别让两个网站的图片总大小从 25M 和 4M 降低到 5M 和 967KB。

给文档修 typo 是很常见的开源贡献类型,这也是很多人一开始参与开源的方式。有人甚至会走火入魔最后变成专业的「开源 typo 修复专家」,不停的用英文语法检查工具去检查每一个流行的开源项目文档……相比之下,上面这一类 PR 要比修 typo 更简单(在智力上),有时也更有价值。

我发现相对于技术实现,我其实更关注 API 设计和用户体验,总想要尽可能的追求设计的一致和美观。这也导致我会在写作和编程时花费大量时间在命名、文件组织、措辞、章节安排、排版、文案和彩蛋这些事情上。这大概就是我编程水平进步缓慢、写作速度缓慢的原因。

P.S. 文中的数字都是估算,大概会有 1~5 左右的偏差。精确的数字是为了让措辞看起来更一致和美观瞎编的。

知乎专栏降级

2016 年 11 月创建了一个知乎专栏来写 Flask 相关的内容,到现在关注人数慢慢涨到了 10000。

blank

前段时间偶然看了下专栏,发现整个页面有很大的变化,原有的这些东西不见了:

    • 专栏头像
    • 专栏短介绍(被替换为关于页面内的长介绍)
    • 关于页面
    • 置顶文章
    • 关注人数
    • 专栏设置页面
    • 文章条目的作者和发布时间

页面上部去掉了很多信息,同时空间被压缩的很小,导致整体样式变得很难看。专栏介绍自动替换为长介绍加剧了这种情况,尝试修改介绍,发现字数限制和之前相比变得更严格了——只允许 25 个字符,这样原有的短介绍也没法使用,只好留空。

同时还发生了另外一个奇怪的变化:知乎账号的关注人数突然多出来 8000 左右。

查了一下,才发现原来这是知乎的一次专栏升级,而上面的这些变化属于这一条升级举措:

专栏升级后,过去喜欢和认可专栏的知友,将直接关注创作者本人帐号。对于升级完成后的专栏,「关注专栏」这一功能将会下线,专栏会完全属于创作者个人。

这也就意味着:

  • 不考虑专栏关注者意愿,强制专栏关注者关注作者。
  • 不考虑专栏作者意愿,直接去除关注等功能。
  • 要关注一个专栏就要关注这个作者,接收所有和专栏不相关的动态。

在我看来,这是知乎专栏的一次降级。

这让在互联网上想只接收自己关注的资讯变得更难了一点。

国内技术书盗版现状

我在 18 年出版了一本技术书,快两年过去了,和盗版打了很多交道。这篇文章从一个技术书作者的视角以 2018~2020 作为切片来记录一下国内技术书的盗版现状。

盗版的起源

我的书第一次上架到电子书平台是在 18 年 8 月 24 号,纸质书正式开始发售是在同年 9 月 13 号。仅仅不到一个月的时间,在 10 月初,淘宝上就开始出现了大量盗版影印书(大约有十几个,价格多在 ¥30 左右)。紧接着,在 11 月初,CSDN 上出现很多盗版电子书。同一时间,多个盗版电子书网站开始有了这本书,基本占领了百度和 Google 的前几页搜索结果。

SEO 赢家

从纸书发售的两个月后开始,一直到现在,不管在哪个搜索引擎上,盗版文件的链接都是 SEO 赢家。以 Google 为例,直接搜索书名「Flask Web开发实战:入门、进阶与原理解析」,前几页的盗版链接比例如下(四月末数据):

  • 第一页:4/10
  • 第二页:7/10
  • 第三页:5/10

单是直接搜索书名就已经得到这么高的盗版比例,更别说加上 PDF 之类的盗版关联词。

下面是在 Google 上的第一页搜索结果截图:

Google 搜索结果

书刚出版那段时间,盗版泛滥,但我能做的事情并不多。算是作为恶作剧,我在自己的博客上也放了一个盗版电子书下载页面(上面搜索结果里的第五位),复制了某个盗版电子书网站的标题和内容,特意用了中文 URL 和盗版关键词。本来是打算在页面结尾放下载链接的地方讽刺几句,嘲弄一下点进来想下载盗版电子书的笨蛋。但是又担心激怒了某个家伙,然后被到处写差评。而且这样也太没有气度,所以只是把下载链接指向了这本书的主页。

我的博客上的文章点击基本都是来自搜索引擎的自然流量,18 年 12 月 19 日发布的一篇无聊的技术文章到现在有 262 个点击,而第二天发布的这个盗版电子书下载文章有 2673 个点击:

盗版文章点击量

 

淘宝和闲鱼

淘宝是最早出现盗版影印书的地方,闲鱼其次。18 年集中处理了一批淘宝影印书,中间又零散处理一些,最近试着再次用关键词搜索,发现盗版影印书又开始卷土重来了。这是在淘宝使用关键词「Flask Web开发实战:入门、进阶与原理解析」得到的第一页结果(2020/5/23 数据),48 个商品里有 16 个是盗版影印书,刚好占据三分之一:

淘宝搜索结果大多盗版影印书都会打着二手的旗号卖盗版。以第一页里看起来最像是真二手的这家店为例(它甚至在商品介绍图片里清晰的注明「拒绝盗版」、「8 新正版二手」):

淘宝影印书

当你进一步询问,几乎总会得到这样的回答:

盗版影印书聊天对话

淘宝网站和闲鱼上自带的举报功能是没用的,即使商家有极其明显的售假描述,比如「PDF」、「影印」:

盗版商品描述

但是举报之后得到的举报结果总是固定不变的「从信息层面暂未能核实到售假信息」:

淘宝举报处理结果

对于淘宝和闲鱼的盗版书,更有效的方式是在阿里巴巴集团知识版权保护平台(https://ipp.alibabagroup.com/)提交投诉,不过每一件商品的投诉都要经历与客服/商家对话确认、生成阿里旺旺举证号、填写投诉单这样一个漫长的过程。慢慢地,我开始直接在对方承认售假后要求对方下架。大多数商家都是愿意合作的(偶尔有些商家在下架一段时间后会偷偷再次上架,这时再花时间在淘宝 IPP 平台提交投诉),所以那段时间用这种方式处理了一大批盗版影印书:

盗版影印书合影

虽然淘宝上的盗版影印书现在又大批量复活了,但我已经没有新书发售时那样的动力和愤慨心情去处理它们。

百度网盘

大部分独立站点售卖的盗版电子书都是放到百度网盘上的,这些独立站点的站长有些联系不上,有些拒绝删除,所以更直接的处理方式是在百度网盘上举报。

和淘宝类似,无论你提交多少次,百度网盘文件页面的举报功能是没用的。你需要在百度版权中心(http://copyright.baidu.com/)进行投诉,前前后后一共处理了 19 个盗版文件:

百度网盘投诉记录

但是这样做只对那些比较懒的盗版商人有用,因为勤劳的盗版商人总是会不停地更换掉失效的网盘链接,甚至每次都换一个相关性弱一点的名字,试图绕过百度网盘盗版审核的判断标准。我曾经连续四次跟踪举报某篇文章里的盗版网盘链接,对方把网盘的文件名从「《Flask Web开发实战入门进阶与原理解析》PDF+配套源代码+资料+参考」一直缩减到「Python Web 开发」。

这些电子书文件有很多版本,有一些是资源售卖者制作的不完整文件,最后一页会放一个购买方式,比如微信公众号或 QQ 号,还有一些会在最后放一个资源售卖网站的链接,你需要付费获取用来提取文件的密钥。他们还会像模像样的放一个这样的「版权声明」:

盗版电子书的版权声明

还有一些就是热心读者或者说乐于分享的读者自发分享出来的完整电子书文件,其中最完整的是一份 312M 的压缩包文件,其中包含了:

  • 一份从亚马逊 Kindle 客户端破解的 PDF 文件
  • 一份纸质书扫描版的 PDF 文件
  • 从 GitHub 下载的前六章源码
  • 一份包含对方盗版电子书资源网站信息的 TXT 文件

阿里云对象存储服务器

影响最大,最难处理的是阿里云服务器上的一个盗版文件。有人在阿里云的对象存储服务器上放了一个完整的 PDF 文件,而且这个文件在 Google 的搜索结果中排名非常靠前,这也就意味着任何人只要用 Google 搜索关键词,都可以直接点击链接在浏览器打开这个文件并下载。

在我没处理掉这个文件之前,它长期占据搜索引擎结果第一名的位置(使用次要关键词也会排在第三名):

盗版 PDF 的 Google 搜索排名

阿里云对象存储(OSS)是阿里云提供的存储服务,这个 PDF 文件存储在深圳区的阿里云服务器上,域名也是阿里云的域名(oss-cn-shenzhen.aliyuncs.com)。2020 年 3 月 2 号我在阿里云举报中心提交了一个举报,流程很麻烦,除了要提供身份证扫描件和著作权证明外,还要填一个列举各类证据和信息的承诺书,但是处理结果却很简单:

阿里云举报结果

先让我提供版权证明和列举侵权证据,最后却告诉我「除非接到法院判决或行政裁决,阿里云无权对侵权文件进行处理」。更好笑的是,建议我「直接通知该网站经营者或相关域名持有人并与之协商」,而这个盗版文件的「网站经营者和相关域名持有人」正是阿里云本身。

虽然声称「我方无权作出任何处置动作」,但是其 OSS 的服务条款里却清楚的注明了「阿里云有权采取相应的处理措施」:

「3.1.3.5.不利用阿里云提供的资源和服务上传(Upload)、下载(download)、储存、发布如下信息或者内容,不为他人发布该等信息提供任何便利(包括但不限于设置URL、BANNER链接等):

……

3.1.3.5.7.侵害他人合法权益的信息和/或其他有损于社会秩序、社会治安、公共道德的信息或内容;

……

3.1.3.11.如阿里云发现您违反上述条款的约定,有权根据情况采取相应的处理措施,包括但不限于立即终止服务、中止服务或删除相应信息等。如果第三方机构或个人对您提出质疑或投诉,阿里云将通知您,您有责任在规定时间内进行说明并出具证明材料,如您未能提供相反证据或您逾期未能反馈的,阿里云将采取包括但不限于立即终止服务、中止服务或删除相应信息等处理措施。因您未及时更新联系方式或联系方式不正确而致使未能联系到您的,亦视为您逾期未能反馈。」

我没法接受让一个盗版 PDF 文件直接出现在书名搜索结果的第一名。4 月 24 号,我在新闻出版广电总局的盗版举报平台(http://www.sapprft.gov.cn/sapprft/channels/6979.shtml)提交了对阿里云的举报。我想要是这也没用,那就只能起诉了。

结果某天正在写这篇文章的时候(这篇文章一直断断续续写了很久),多番尝试之后,搜索到了这个 OSS 仓库(Bucket)的拥有者放到 GitHub 上的相关代码,翻了下 commit 历史,竟然发现了被意外提交到 Git 仓库的 Access key ID 和密钥(用来登录阿里云 OSS 的认证信息):

GitHub 代码历史

所以,我就自己用这些认证信息登录阿里云 OSS 删掉了对方存储的盗版文件:

删除阿里云 OSS 文件

加上一个网络教程的 MP3 和 HTML 文件,这位同学一共存储了 400 多个盗版文件。我不是狂热的正版卫士,所以没有做出更大的破坏,只是删除了我的书。

也许更合理的方式是发电子邮件(他的 GitHub 资料上放了 Email 地址)请求他删除,但是既然他收到阿里云(如果阿里云的确通知了)的删除请求,但依然无所作为,而且这个文件困扰了我很长时间,自己删掉会让我觉得有报复的平衡……事后我发了邮件跟他说了这件事,结果对方很诚恳的道歉,倒让我因为「自己动手」有点不好意思。

技术博客和电子书网站

CSDN 的下载板块是盗版电子书的聚集地,还好我的书刚开始在 CSDN 传播的时候,出版社和 CSDN 协商删除了大部分文件,所以我不用跟 CSDN 有太多纠缠。

CSDN 下载页面

售卖电子书文件的人大都会在CSDN、博客园和 51CTO 上面发文章分享网盘链接,因为这三个站点的链接在搜索引擎上的排名都很靠前。51 CTO 和博客园上的文章都没有举报按钮,网站底部也没有投诉通道,所以你需要发邮件给网站方请求删除相关文章。

而对于各类专业的电子书资源网站,如果网站上有联系方式的话,那你可以直接联系站长,要求他们删除。如果站长什么也没留,那就没办法了。

有一个盗版网站的站长,我假装买电子书加上他的 QQ 后,他发给我一个包含大量技术书列表的 Excel 文件,意思是任我挑选。我问他你这样随便卖盗版电子书,假如大多数人都来买盗版,那作者拿不到钱不就没有人愿意写书了?

他告诉我说,他其实是从亚马逊买了正版电子书,破解以后便宜卖给别人,其实就相当于大家一起凑钱买了书(众筹?),这样大家都可以看到书。那一刻,我感觉对方心里有火也有光,仿佛面前是一个向无数编程学习者打开技术世界大门的武林前辈,一个带领大家走向共产新世界的先进同志。

多搜索几页,你会发现,这样热心伟大、一心只为人类进步,一本书只卖一块五的人,有很多:

盗版电子书网站

IM 群组和微信公众号

IM 群组尤其是 QQ 群也是盗版电子书的重灾区,技术相关的 QQ 群非常多,几乎每一个群里都有大量盗版电子书文件,有些还按照语言和框架整理的非常整齐:

QQ 群的盗版文件

这里的处理难题是,你没有时间和精力去申请加入每一个群,然后定期挨个检查有没有你的盗版书被上传。

去年年初,我也创建了一个 Flask 技术讨论 QQ 群,有趣的是,竟然会有人在群里分享我的书的电子书文件:

HelloFlask 群聊盗版电子书

微信群因为没有群文件功能,分享出来的盗版书影响相对要小一点。但还有很多技术相关的微信公众号靠分享盗版电子书来引流,每一个这类公众号都有不少关注者。

这种现状还会持续多久?

体面的生活里不应该存在盗版,何况是技术书这种存在大量正版购买渠道、基本没有封禁和删减的东西。但你很难让大多数人在匿名的网络空间里也保持现实中的道德感,而且还有很多人并不认为盗版有什么不对。

盗版读者越多,网络上的盗版资源商、淘宝影印书就会越来越多。搜索盗版关键词的人越多,盗版网站就会越来越多,排名也就越来越靠前。你关注分享盗版电子书的微信公众号,那靠分享盗版电子书吸引关注者的微信公众号就会越来越多。

与此同时,出版社和作者的收入就会越来越少,用心写书和出书的作者和出版社也会越来越少,有能力写出好书的人也就会更加犹豫要不要写书。无论盗版用户怎么辩驳, 盗版都是一件损害创作者和出版方利益的事情,最终也会影响整个技术写作环境和每一个人的阅读体验和选择。

百度知道盗版分享

看着这些礼貌的、充满对知识真诚的渴望和期盼的留言,我在想这种盗版现状还会持续多久?

我想不出答案。

P.S. 没有动物在本文写作过程中受到伤害,但是有 3 本淘宝影印盗版书、5 个百度网盘盗版电子书文件、一份阿里云 OSS 深圳服务器里的盗版文件,一篇博客园分享盗版的文章永远的离开了这个世界。

P.P.S 如果你没法访问 Google,请检查网线有没有正确连接,或打电话询问运营商,确认接入的是互联网而不是光明网。

相关链接:


Update 2020/7/30

QQ 上有一个群成员跟我私聊,说他在网上找到我的 Flask 教程,但是不知道解压密码,想问我密码是多少。我很疑惑,不知道他到底在说什么,所以让他发链接给我看看,结果他发过来一个盗版资源售卖网站的链接,链接解析信息写着:「本资料为最新整理的《Flask Web 开发实战》PDF+源代码,用于学习,内容丰富!」……

Update 2020/8/17

有读者在知乎上发现了别人新分享的盗版 PDF 分享文章,在 QQ 群里提醒我。我还没开始处理,已经有几个群友在文章下发评论要求删除,第二天作者自己删除了文章。

Update 2020/11/5

又在淘宝上发现大概二十多本影印书,价格大都在 25 左右。好奇下载了拼多多,搜索结果里 90% 都是盗版影印书。

欢迎参加 FlaskCon 2020!

FlaskCon 是一个社区举办的 Flask 大会,将会在 7 月 4 号举行(为期两天)。按照官方的介绍,它:

  • 100% Remote
  • 100% Free
  • 100% Community-driven

和 DjangoCon 类似,FlaskCon 会专注于 Flask 相关话题,包括 Flask 扩展介绍、开发经验、最佳实践、类似框架的对比等等。

如果你想分享 Flask 开发相关的经验,介绍你对 Flask 的有趣应用,或者是有任何和 Flask 相关而且你很想拿出来聊一聊的话题,欢迎报名演讲。演讲有 20 分钟和 40 分钟两种类型,需要使用英语。

你可能会觉得大会的 Logo 不是很好看……不要担心,这些设计还在慢慢改进中,如果你有任何和大会网站、议程设置、设计相关的建议,可以发邮件到 flaskcon@gmail.com,或是在 Discord 频道和 Reddit 主题帖里反馈。

P.S. 如果你想帮忙改进大会网站,最直接的方式是自己动手修改源码

相关链接:

一个困扰我两年的 Flask「Bug」

TL;DR 版本:

如果你的程序存储在单脚本里,比如 app.py,那么 .flaskenv 和 .env 应该放在程序脚本的同级目录:

myproject/
    - app.py
    - templates
    - static
    - .flaskenv
    - .env

如果你的程序存储在程序包里,那么 .flaskenv 和 .env 应该放在程序包的同级目录:

myproject/
    - app/
        - templates
        - static
        - __init__.py
        - views.py
    - .flaskenv
    - .env

如果你把 .flaskenv 和 .env 放到了再往上一层(myproject 再往上),或是把你的程序包或程序脚本放到了一个子目录(比如 /myproject/myapp),那么执行 flask run 就会出错。


严格来说算不上 bug,而是一个很容易导致出错的设计。具体行为是,如果你安装了 python-dotenv,同时在 Flask 程序的上层目录创建了 .env 或 .flaskenv 文件,那么你将没法成功执行 flask run 等命令,因为这会导致 Flask 没法正确找到对应的 Flask 程序实例。

这个问题从 Flask 开始引入 CLI 机制开始就存在了,困扰了我两年。18 年偶然在用户根目录创建了一个 .env 文件,发现 Flask 程序没法运行了,当时遇到的各种 bug 太多,没仔细考虑这两者之间的关联。后来经过几次测试,才确定下来是上层目录的 .env 和 .flaskenv 文件导致,但是一时找不到原因,就暂时放下了。直到 19 年 11 月,花了几个小时排查,还是没找到原因。

中间花了很长时间来追踪 Windows 特定的 Flask 程序无法启动的 bug(TypeError: environment can only contain strings),实在是怕了。因为 Flask 的 CLI 涉及太多东西,有时你要钻进 python-dotenv(#101) 和 Werkzeug(#1320) 才能找到问题的原因。

但是问题不解决的话,你永远睡不好觉。《Flask Web 开发实战》第一部分的示例程序都放在了一个程序仓库,而且都放在了子目录,这意味着如果读者错误的在仓库根目录创建 .env 和 .flaskenv 文件的话,就会导致子目录下的六个示例程序没法运行。前后大概收到 6 个相关的读者反馈,虽然后续在网站上添加了提醒,在重印的书里介绍创建 .env/.flaskenv 文件的地方追加提醒,但这终究没有真正解决问题,而且总会有人可以完美的错过所有提示。

如果每个人都可以在书上标记出错位置并共享,那么这一页应该会有很多红色小叉号(参考《超级马里奥制造》,也许未来某个电子书平台会做出来这个功能 :P)。

前几天在这个 Issue 的提醒下,又花了两个小时排查,这次终于找到原因。加上写 PR(#3560)和 Issue(#3561),前后两年一共花了 8 个小时,这个问题终于有了着落。没意外的话,预计会在下一个版本的 Flask 中更新。下面是具体原因。

本来以为这个问题和 python-dotenv 或 Werkzeug 相关,没想到只是 Flask 本身代码的问题,我太笨了,这个问题本可以早一点解决。

按照预定的行为,当安装了 python-dotenv,Flask 会自动加载 .env 和 .flaskenv 里的环境变量。python-dotenv 在搜索存储环境变量的文件时,会从当前目录开始向上搜索,如果找到就返回对应的文件路径。但是这时 Flask 如果发现 .env 或 .flaskenv 的所在目录不是当前目录,就会把当前工作目录切换到 .env 和 .flaskenv 所在的目录(相关源码)。而如果你的程序模块或程序包不是和 .env/.flaskenv 同级目录的话,就会导致找不到程序实例。

使用下面的步骤可以重现:

$ git clone https://github.com/greyli/flask-env-test
$ cd flask-env-test
$ pip install -r requirements.txt  # or just pip install flask[dotenv]
$ cd hello
$ flask run

示例项目的文件结构如下:

- flask-env-test
    - .env
    - hello
        - app.py

像示例程序这样把程序存储在 app.py 文件中时,运行 flask run 你会看到下面的报错:

$ flask run
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
Usage: flask run [OPTIONS]

Error: Could not locate a Flask application. You did not provide the "FLASK_APP" environment variable, and a "wsgi.py" or "app.py" module was not found in the current directory.

如果你使用 FLASK_APP 指定了程序的导入路径,那么错误大概会是这样:

$ flask run
 * Serving Flask app "myapp"
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
Usage: flask run [OPTIONS]

Error: Could not import "myapp".

相关链接

安装 Python 依赖出现 MarkupSafe ImportError … Feature 报错的解决方法

这个报错在 3 月 8 号 setuptools 发布新版本之后出现,通常会在安装 Python 依赖时触发。

报错信息

使用 pip 安装依赖时的报错如下:

Collecting markupsafe==1.0
  Downloading https://.../MarkupSafe-1.0.tar.gz (14 kB)
    ERROR: Command errored out with exit status 1:
     command: '...\python.exe' -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'...\\pip-install-bsormril\\markupsafe\\setup.py'"'"'; __file__='"'"'...\\pip-install-bsormril\\markupsafe\\setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' egg_info --egg-base '...\pip-install-bsormril\markupsafe\pip-egg-info'
         cwd: ...\pip-install-bsormril\markupsafe\
    Complete output (5 lines):
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "...\pip-install-bsormril\markupsafe\setup.py", line 6, in <module>
        from setuptools import setup, Extension, Feature
    ImportError: cannot import name 'Feature'
    ----------------------------------------
ERROR: Command errored out with exit status 1: python setup.py egg_info Check the logs for full command output.

使用 Pipenv 安装依赖时的报错如下:

An error occurred while installing markupsafe==1.0 --hash=sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665! Will try again.
Installing initially failed dependencies…
[pipenv.exceptions.InstallError]:   File "...\Python\Python36\site-packages\pipenv\core.py", line 1874, in do_install
[pipenv.exceptions.InstallError]:       keep_outdated=keep_outdated
[pipenv.exceptions.InstallError]:   File "...Python\Python36\site-packages\pipenv\core.py", line 1253, in do_init
[pipenv.exceptions.InstallError]:       pypi_mirror=pypi_mirror,
[pipenv.exceptions.InstallError]:   File "...\Python\Python36\site-packages\pipenv\core.py", line 859, in do_install_dependencies
[pipenv.exceptions.InstallError]:       retry_list, procs, failed_deps_queue, requirements_dir, **install_kwargs
[pipenv.exceptions.InstallError]:   File "...\Python\Python36\site-packages\pipenv\core.py", line 763, in batch_install
[pipenv.exceptions.InstallError]:       _cleanup_procs(procs, not blocking, failed_deps_queue, retry=retry)
[pipenv.exceptions.InstallError]:   File "...\Python\Python36\site-packages\pipenv\core.py", line 681, in _cleanup_procs
[pipenv.exceptions.InstallError]:       raise exceptions.InstallError(c.dep.name, extra=err_lines)
[pipenv.exceptions.InstallError]: ['Looking in indexes: https://.../pypi/simple', 'Collecting markupsafe==1.0', '  Using cached https://.../MarkupSafe-1.0.tar.gz (14 kB)']
[pipenv.exceptions.InstallError]: ['ERROR: Command errored out with exit status 1:', '
command: \'...\\.virtualenvs\\helloflask-evdb6idn\\scripts\\python.exe\' -c \'import sys, setuptools, tokenize; sys.argv[0] = \'"\'"\'...\pip-install-pkgojp4t\\\\markupsafe\\\\setup.py\'"\'"\'; 
__file__=\'"\'"\'...\\\\pip-install-pkgojp4t\\\\markupsafe\\\\setup.py\'"\'"\';f=getattr(tokenize, \'"\'"\'open\'"\'"\', open)(__file__);code=f.read().replace(\'"\'"\'\\r\\n\'"\'"\', \'"\'"\'\\n\'"\'"\');f.close();exec(compile(code, __file__, \'"\'"\'exec\'"\'"\'))\' egg_info --egg-base \'...\\pip-install-pkgojp4t\\markupsafe\\pip-egg-info\'', '
cwd: ...\\pip-install-pkgojp4t\\markupsafe\\', '    Complete output (5 lines):', '    
Traceback (most recent call last):', '      
File "<string>", line 1, in <module>', ' 
File "...\\pip-install-pkgojp4t\\markupsafe\\setup.py", line 6, in <module>', '
from setuptools import setup, Extension, Feature', "
ImportError: cannot import name 'Feature'", '----------------------------------------',
'ERROR: Command errored out with exit status 1: python setup.py egg_info Check the logs for full command output.']
ERROR: ERROR: Package installation failed...

其他工具输出类似,主要异常信息是 MarkupSafe setup.py: ImportError: cannot import name Feature。通常会在安装 Flask 项目的依赖时发生,因为 MarkupSafe 是 Flask 的依赖之一。

原因和解决方法

出现这个报错的原因是因为 Python 打包工具 setuptools 在 46.0.0 版本删掉了弃用的 Feature,而 MarkupSafe 刚好在 setup.py 文件里导入了这个类,所以会出现报错。

解决方法很多,最直接的是更新 MarkupSafe 到最新版本(1.1.1),新版本去掉了对 Feature 类的导入。如果使用 requirements.txt 存储依赖列表,那就把 MarkupSafe 的版本号改成 1.1.1(找到 MarkupSafe 开头那一行,替换版本号):

MarkupSafe==1.1.1

然后重新执行:

$ pip install -r requirements.txt

对于 Pipenv,可以直接执行:

$ pipenv install markupsafe==1.1.1

如果你是《Flask Web 开发实战》的读者,正在为第一部分的示例程序安装依赖,那你还需要执行下面的命令固定 sendgrid-python 的版本(它在新版本添加了不向后兼容的 API 变动):

$ pipenv install sendgrid==5.3.0

《Flask Web 开发实战》读者备注

如果你在 2020 年 3 月 8 号到 4 月 5 号之间买了《Flask Web 开发实战》,把示例程序克隆到了本地,然后尝试运行 pipenv install 或 pip install -r requirements.txt 命令来安装依赖,那很大概率你会遇到这个问题。

除了使用上面的方法之外,你还可以通过更新本地代码来解决。我最近给所有示例程序的依赖文件做了一次更新,除了书里涉及的 API 产生变动的依赖,其他依赖都已经更新到最新版本。

你可以使用下面的命令来更新你在本地的程序仓库(注意这会重置你对源码进行的修改):

$ git fetch --all
$ git fetch --tags
$ git reset --hard origin/master

然后重新执行一次 pipenv install 或 pip install -r requirements.txt 即可解决这个问题。如果遇到其他虚拟环境和依赖安装的问题可以参考这篇文章解决。

相关链接: