如何成为一名爬虫工程师

程序员有时候很难和外行人讲明白自己的工作是什么,甚至有些时候,跟同行的人讲清楚“你是干什么的”也很困难。比如我自己,就对Daivd在搞的语义网一头雾水。所以我打算写一篇博客,讲一下“爬虫工程师”的工作内容是什么,需要掌握哪些技能,难点和好玩的地方等等,讲到哪里算哪里吧。

一、爬虫工程师是干嘛的?

1.主要工作内容?

互联网是由一个一个的超链接组成的,从一个网页的链接可以跳到另一个网页,在新的网页里,又有很多链接。理论上讲,从任何一个网页开始,不断点开链接、链接的网页的链接,就可以走遍整个互联网!这个过程是不是像蜘蛛沿着网一样爬?这也是“爬虫”名字的由来。

作为爬虫工程师,就是要写出一些能够沿着网爬的”蜘蛛“程序,保存下来获得的信息。一般来说,需要爬出来的信息都是结构化的,如果不是结构化的,那么也就没什么意义了(百分之八十的数据是非结构化的)。爬虫的规模可达可小,小到可以爬取豆瓣的top 250电影,定时爬取一个星期的天气预报等。大到可以爬取整个互联网的网页(例如google)。下面这些,我认为都可以叫做爬虫:

  1. 爬知乎的作者和回答
  2. 爬百度网盘的资源,存到数据库中(当然,只是保存资源的链接和标题),然后制作一个网盘的搜索引擎
  3. 同上,种子网站的搜索引擎也是这样的

到这里,我们知道爬虫的任务是获取数据。现在比较流行大数据,从互联网方面讲,数据可以分成两种,一种是用户产生的(UGC),第二种就是通过一些手段获得的,通常就是爬虫。爬虫又不仅仅局限于从网页中获得数据,也可以从app抓包等。简而言之,就是聚合数据并让他们结构化。那么,哪些工作需要爬虫呢?

2.爬虫能做什么?

典型的数据聚合类的网站都需要爬虫。比如Google搜索引擎。Google能在几毫秒之内提供给你包含某些关键字的页面,肯定不是实时给你去找网页的,而是提前抓好,保存在他们自己的数据库里(那他们的数据库得多大呀)。所以种子搜索引擎,网盘搜索引擎,Resillio key引擎等都是用爬虫实现抓好数据放在数据库里的。

另外有一些提供信息对比的网站,比如比价类的网站,就是通过爬虫抓取不同购物网站商品的价格,然后将各个购物网站的价格展示在网站上。购物网站的价格时时都在变,但是比价网站抓到的数据不会删除,所以可以提供价格走势,这是购物网站不会提供的信息。

除此之外,个人还可以用爬虫做一些好玩的事情。比如我们想看大量的图片,可以写一个爬虫批量下载下来,不必一个一个点击保存,还要忍受网站的广告了;比如我们想备份自己的资料,例如保存下来我们在豆瓣发布过的所有的广播,可以使用爬虫将自己发布的内容全部抓下来,这样即使一些网站没有提供备份服务,我们也可以自己丰衣足食。

二、爬虫工程师需要掌握哪些技能?

我见过这样的说法:“爬虫是低级、重复性很多的工作,没有发展前途”。这是误解。首先,对于程序员来说基本上不存在重复性的工作,任何重复劳动都可以通过程序自动解决。例如博主之前要抓十几个相似度很高但是html结构不太一样的网站,我就写了一个简单的代码生成器,从爬虫代码到单元测试代码都可以自动生成,只要对应html结构稍微修改一下就行了。所以我认为,重复性的劳动在编程方面来说基本上是不存在的,如果你认为自己做的工作是重复性的,说明你比较勤快,不愿意去偷懒。而我还认为,勤快的程序员不是好程序员。下面我根据自己这段时间的工作经历,讲一讲爬虫需要哪些相关的技能。

1.基本的编码基础(至少一门编程语言)

这个对于任何编程工作来说都是必须的。基础的数据结构你得会吧。数据名字和值得对应(字典),对一些url进行处理(列表)等等。事实上,掌握的越牢固越好,爬虫并不是一个简单的工作,也并不比其他工作对编程语言的要求更高。熟悉你用的编程语言,熟悉相关的框架和库永远是百益无害。

我主要用Python,用Java写爬虫的也有,理论上讲任何语言都可以写爬虫的,不过最好选择一门相关的库多,开发迅速的语言。用C语言写肯定是自找苦吃了。

2.任务队列

当爬虫任务很大的时候,写一个程序跑下来是不合适的:

  1. 如果中间遇到错误停掉,重头再来?这不科学
  2. 我怎么知道程序在哪里失败了?任务和任务之间不应该相互影响
  3. 如果我有两台机器怎么分工?

所以我们需要一种任务队列,它的作用是:讲计划抓取的网页都放到任务队列里面去。然后worker从队列中拿出来一个一个执行,如果一个失败,记录一下,然后执行下一个。这样,worker就可以一个接一个地执行下去。也增加了扩展性,几亿个任务放在队列里也没问题,有需要可以增加worker,就像多一双亏筷子吃饭一样。

常用的任务队列有kafkabeanstalkdcelery等。

3.数据库

这个不用讲了,数据保存肯定要会数据库的。不过有时候一些小数据也可以保存成json或者csv等。我有时想抓一些图片就直接按照文件夹保存文件。

推荐使用NoSQL的数据库,比如mongodb,因为爬虫抓到的数据一般是都字段-值得对应,有些字段有的网站有有的网站没有,mongo在这方面比较灵活,况且爬虫爬到的数据关系非常非常弱,很少会用到表与表的关系。

4.HTTP知识

HTTP知识是必备技能。因为要爬的是网页,所以必须要了解网页啊。

首先html文档的解析方法要懂,比如子节点父节点,属性这些。我们看到的网页是五彩斑斓的,只不过是被浏览器处理了而已,原始的网页是由很多标签组成的。处理最好使用html的解析器,如果自己用正则匹配的话坑会很多。我个人非常喜欢xpath,跨语言,表达比价好,但是也有缺点,正则、逻辑判断有点别扭。

HTTP协议要理解。HTTP协议本身是无状态的,那么“登录”是怎么实现的?这就要求去了解一下session和cookies了。GET方法和POST方法的区别(事实上除了字面意思不一样没有任何区别)。

浏览器要熟练。爬虫的过程其实是模拟人类去浏览器数据的过程。所以浏览器是怎么访问一个网站的,你要学会去观察,怎么观察呢?Developer Tools!Chrome的Developer Tools提供了访问网站的一切信息。从traffic可以看到所有发出去的请求。copy as curl功能可以给你生成和浏览器请求完全一致的curl请求!我写一个爬虫的一般流程是这样的,先用浏览器访问,然后copy as curl看看有哪些header,cookies,然后用代码模拟出来这个请求,最后处理请求的结果保存下来。

5.运维

这个话题要说的有很多,实际工作中运维和开发的时间差不多甚至更多一些。维护已经在工作的爬虫是一个繁重的工作。随着工作时间增加,一般我们都会学着让写出来的爬虫更好维护一些。比如爬虫的日志系统,数据量的统计等。将爬虫工程师和运维分开也不太合理,因为如果一个爬虫不工作了,那原因可能是要抓的网页更新了结构,也有可能出现在系统上,也有可能是当初开发爬虫的时候没发现反扒策略,上线之后出问题了,也可能是对方网站发现了你是爬虫把你封杀了,所以一般来说开发爬虫要兼顾运维。

所以爬虫的运维我可以提供下面几个思路:

首先,从数据增量监控。定向爬虫(指的是只针对一个网站的爬虫)比较容易,一段时间之后对一些网站的数据增量会有一个大体的了解。经常看看这些数据的增加趋势是否是正常就可以了(Grafana)。非定向爬虫的数据增量不是很稳定,一般看机器的网络状况,网站的更新情况等(这方面我的经验不多)。

然后看爬虫执行的成功情况。在上面提到了用任务队列控制爬虫工作,这样解耦可以带来很多好处,其中一个就是可以就是可以对一次爬虫执行进行日志。可以在每次爬虫任务执行的时候,将执行的时间、状态、目标url、异常等放入一个日志系统(比如kibana),然后通过一个可视化的手段可以清晰地看到爬虫的失败率。

爬虫抛出的Exception。几乎所有的项目都会用到错误日志收集(Sentry),这里需要注意的一点是,忽略正常的异常(比如Connection错误,锁冲突等),否则的话你会被这些错误淹没。

三、爬虫与反爬

这同样是很深的一个话题,就像攻击武器与防御武器一样,双方总是在不断升级。常见的反爬措施(我遇到过的)有下面几种:

1.访问频率

很好理解,如果访问太频繁网站可能针对你的ip封锁一段时间,这和防DDoS的原理一样。对于爬虫来说,碰到这样的限制一下任务的频率就可以了,可以尽量让爬虫想人类一样访问网页(比如随机sleep一段时间,如果每隔3s访问一次网站很显然不是正常人的行为)。

2.登录限制

也比较常见。不过公开信息的网站一般不会有这个限制,这样让用户也麻烦了。其实反爬措施都或多或少的影响真实用户,反爬越严格,误杀用户的可能性也越高。对爬虫来说,登录同样可以通过模拟登录的方式解决,加个cookie就行了(话又说回来,网络的原理很重要)。

3.通过Header封杀

一般浏览器访问网站会有header,比如Safari或者Chrome等等,还有操作系统信息。如果使用程序访问并不会有这样的header。破解也很简单,访问的时候加上header就行。

4.JavaScript脚本动态获取网站数据

有一些网站(尤其是单页面网站)的内容并不是通过服务器直接返回的,而是服务器只返回一个客户端JavaScript程序,然后JavaScript获取内容。更高级的是,JavaScript在本地计算一个token,然后拿这个token来进行AJAX获取内容。而本地的JavaScript又是经过代码混淆和加密的,这样我们做爬虫的通过看源代码几乎不可能模拟出来这个请求(主要是token不可能破解),但是我们可以从另一个角度:headless的浏览器,也就是我们直接运行这个客户端程序,这可以100%地模拟真实用户!

5.验证码

这几乎是终极武器了,验证码是专门用来区分人和计算机的手段。对于反爬方来说,这种方式对真实用户和搜索引擎(其实可以通过记录搜索引擎爬虫的ip来区别对待,可以解决)的危害比较大,相信读者都有输入验证码的痛苦经历。但这种方法也并不是无敌的!通过现在很火的机器学习可以轻松的识别大部分的验证码!Google的reCAPTCHA是一种非常高级的验证码,但是听过通过模拟浏览器也是可以破解的。

6.ip限制

网站可能将识别的ip永久封杀,这种方式需要的人力比较大,而且误伤用户的代价也很高。但是破解办法却非常简单。目前代理池几乎是搞爬虫的标配了,甚至还有很多高匿代理等好用的东西。所以这基本上只能杀杀小爬虫。

7.网站内容反爬

有一些网站将网站内容用只有人类可以接收的形式来呈现(其实反爬就是区别对待人类和机器嘛)。比如将内容用图片的形式显示。但是近几年来人类和机器的差别越来越小,图片可以用OCR准确率非常高地去识别。

反爬总结

爬虫和反爬是典型的攻防双方的互相升级。但是我认为,这种升级不像军事,军事是无尽头的,但是爬虫和反爬是有尽头的。

爬虫的尽头就是浏览器,一旦使用浏览器,程序完全可以模拟真实用户发出请求,缺点是就是消耗资源,因为需要新开一个进程,解析DOM,运行客户端JavaScript代码。(chrome的node api在github开源仅仅两天,就拿到8k个star)

反爬的尽头就是像Google这种超级厉害的验证码,毕竟验证码的根本目的就是识别人类和机器的。

我正好有一个反爬做的非常好的例子。Google Arts Project项目是一个汇聚世界名画的艺术长廊,我比较喜欢里面的一些画,所以想下载一些(当然这是不对的),然后发现这个网站反爬做的相当好(因为版权属于收藏作品的博物馆,所以Google Arts Project肯定不会提供下载),要下载几乎是不可能的。我有点不服,开始用各种手段试图下载原图。尝试了一番,发现这个网站block掉了鼠标右键功能、审查元素发现图片并不是一个常规的图片、追踪网络包发现原图竟然不是一次网络请求拿到的,而是分成了好几次请求base64编码的字符流每次请求图片的一部分,然后在客户端组装起来图片!当然在客户端的代码也是经过加密和混淆的!这完全可以作为反爬的教科书了,既没有误伤用户,又让爬虫无法下手。我目前有一些想法:通过浏览器抓下来一张图片你的各个部分,然后通过计算机视觉把这个图片组装起来,有空的话会去实现以下看看。

图片每次只请求部分

四、职业道德

成规模的爬虫一般都会使用集群,一般的小网站服务器规模可能不如爬虫集群的规模大。所以很多时候我们最好对要爬的网站限制一下频率。否则这些爬虫就相当于DoS攻击集群了!一般的网站都会有robots.txt可以参考。


好了,上面就是我的人生经验,做爬虫时间也不长,也没去搜集很多相关的资料,纯属个人整理,所以肯定有不全的地方。

总结来说,写爬虫需要经验积累,需要灵活的思路。比如说我之前就遇到过网站,需要验证码验证拿到一个token,可是通过看网络请求发现这个token长得很像一个时间戳,然后本地自己生成一个时间戳发现也是能用的!于是就这样绕过了验证码。所以多多积累和尝试,可以偷不少懒,嘿嘿。

另外爬虫也不是和我之前想的那样是一个枯燥无味的工作,比如我就发现了不少很垃圾很搞笑的网站,乐趣也蛮多的。学到的东西也不少。万变不离其宗嘛。

 

西红柿鸡蛋面加狮子头

欣 下午12:56
我 下午12:58
看起来真好吃
欣 下午1:00
多少钱
我 下午1:02
15
欣 下午1:02
7
我 下午1:03
哇塞
欣 下午1:03
番茄鸡蛋面5
欣 下午1:03
狮子头面6.5
欣 下午1:03
另加番茄鸡蛋2
欣 下午1:04
我点了番茄鸡蛋面5
欣 下午1:04
问加狮子头多少钱
我 下午1:04
这不是13.5?
欣 下午1:04
答2
欣 下午1:04
于是我加了狮子头
欣 下午1:04
花了5+2
我 下午1:04
哦我明白了
我 下午1:04
真厉害
欣 下午1:04
我觉得师傅是个傻子
我 下午1:04
研究生就是厉害
我 下午1:05
狮子头3.5
欣 下午1:05
同样的我说狮子头面加番茄鸡蛋就是8.5
我 下午1:05
哈哈哈
 

调教Mac外接显示器(开启HiDPI)

我的Mac外接的Dell U2515H显示器,总觉得很模糊,用的是2048*1152分辨率。觉得Mac只对外接显示器开开了这么几个分辨率很奇怪。

后来发现,原来按住option键点击“缩放”,可以开启隐藏的分辨率。

于是选择了1080P用了一段时间,还挺满意的。直到……某天看到了同事的屏幕,明明是和我一样的显示器,怎么会好这么多!

原来,还有个叫做 HiDPI 的东西。

简单来说,开启 HiDPI 和不开启的情况下,外接显示器截图如下(效果可以代表像素点数)。

不开启 HiDPI 效果
开启 HiDPI 效果

简直是两个世界!

Mac HiDPI 开启方法

1. 关闭系统保护

具体操作是,重启电脑并按住Command+R,进入恢复模式,选择系统语言(仅仅会影响此次保护模式会话),然后选择工具(在最上面的bar)>终端。输入csrutil disable; reboot。更多参考这里

2. 覆盖显示器配置文件

发现一个很有用的项目,点击这里。可以生成显示器 HiDPI 的设置文件。如果你和我一样用的显示器型号是DELL U2515H,可以直接下载DisplayProductID-*.plist文件然后执行sudo cp ~/Downloads/DisplayProductID-d06e.plist /System/Library/Displays/Contents/Resources/Overrides/DisplayVendorID-10ac/DisplayProductID-d06e(注意,如果你没有按照第一步关闭系统保护,那么即使加了sudo也会得到Operation Permitted),重启搞定。如果不是,就要参考这个页面自己config一下显示器的配置文件。

OS X Catalina 用户需要看下文末的文件系统读写权限问题。

3. 设置显示器的分辨率

推荐使用RDM项目管理显示器的分辨率,可以从这里下载编译好的安装包。(2023年更新:macOS 13.4.1 以及以上,不需要 RDM 软件了,见文末)

RDM界面

推荐的分辨率是1920*1080.

关于第二步和第三步,不想这么麻烦可以直接使用SwitchResX这个软件,但是第一步是必须有的。虽然这个软件是收费的,但是听说可以无限试用。

另外,不知道是不是我的错觉,觉得开启之后显示器变得卡了一些。不过显示效果提升了一个档次。

可能遇到的问题

被其他配置文件覆盖。

升级了macOS High Sierra 10.13系统之后,HiDPI 效果没有了。重复操作一次之后也无效。于是怀疑我自定义的显示配置文件被覆盖了。删除其他配置文件,只保留一个之后,成功找回原来的分辨率 1920*1080(HiDPI)。

2019年11月06日更新:OS X Catalina 权限问题

在上面 “2. 覆盖显示器配置文件” 中,如果你的系统是 Catalina ,那么你执行 csrutil disable 也无法用 sudo 覆盖文件。会提示:Read-only file system。

原因如下:

解决方法是:执行 sudo mount -uw / 再执行上述的 cp 就可以了。解决方法来源。

2023年11月11日更新:macOS 13.4.1 以及以上,不需要 RDM 软件了

系统已经支持直接设置了。

第一步,在设置里面打开 Show resolutions as list:Settings > Display > Advanced > Show resolutions as list > Done. 如下图。

show resources as list

然后在设置里面就可以看到所有的分辨率设置选项了。跟 RDM 软件看到的一致。

更多的分辨率选择。

方法来源。

 

奇葩网站吐槽第二弹

很久之前写过一篇《吐槽一些神奇的政府网站》,但是工作中(目前的工作是爬虫)碰到的奇葩网站远远不止这些,后来在那篇文章陆续更新了一些。但是考虑到修改文章不会发送rss feed,而且文章也被更新的越来越长,所以这里拆出来第二弹再发一次吧。而且我相信之后我会碰到更新这种奇葩网站的……估计不知道要写到第几弹。以下按照更新顺序贴出来,文章就不需要承上启下的润色了:


20170616更新:日了狗了,今天又碰到一个神奇的网站,比如某个详情页面如下:

本来是没有什么奇怪的,但是我把url拼起来之后一直是404,然后发现,如果把中文的部分encode两次,就和目标url一样了……日……


20170619更新:日了狗,今天写一个xpath怎么写都不对,看了看发出去的请求,好嘛,都到站外去了。找了半天,终于找到了罪魁祸首。我使用一个标签的id定位的,结果发现,相同id的html标签在这个页面下竟然有五个(没想到吧……像我这么屌的还有四个……)


170627更新:笑死我了,这个网站每次请求都会从相应收到一段cookies,然后就加到请求上去。不是修改,而是一直加一直加,访问几个页面之后,页面就会显示400Bad Request,cookies too big,哈哈哈哈。之前也碰到一个,是post请求不断增加字段,重复的字段越来越多,请求越来越大的…… 这,这应该叫做“饼干泄露”(内存泄露)吧,哈哈


170717更新:在post请求中传SQL语句的……


170802更新:今天要抓一个发布开庭公告的网站,打开一看,很整齐!很规则!很开心有没有!

一看源代码,人都傻了……


170802更新:日了够了,PM给我一个url,结果我发现已经打不开了,但是从主页是可以点进去的。研究了一下,发现这个url里面包含一个session id……

http://www.gzthfy.gov.cn/pa2/wel_3g.seam;jsessionid=04FF3DF586DE3D26555A210796A93B30.nod3?ggbh=91fa6f637dd947819df4c4b45878b514&cid=134752


170802更新:此站通过发送一个AJAX获取下一页的数据,我使用Python模拟发出一样的AJAX,企图在url或者post请求中找到与页码相关的信息改一下。发现此站翻任何一页发送的请求都是一样的!

一毛一样!Form一样、url一样,甚至连他喵的cookie都是一样!

观察一番,发现页码信息是记录在session里面的!也就是说,form里面的乱七八糟的数据(并不知道有什么用)只表示两种信息:往上翻,往下翻。当前你的位置,记录在服务器上(如此反人类,请问管理员你怎么把第二页的url发给你的上司呢?)

还有这种操作。

 

理解Python的UnboundLocalError(Python的作用域)

今天写代码碰到一个百思不得解为什么会出错的代码,简化如下:

意图很明显,首先我定义了一个全局的x,在函数中,如果有特殊需要,就重新重新赋值一下x,否则就使用全局的x。

可以这段代码在运行的时候抛出这个Error:

UnboundLocalError: local variable ‘a’ referenced before assignment

研究了一番,觉得挺有意思的。而且这是一个比较常见的问题,在Stack Overflow的Python tag下面基本上是个周经问题。


出现赋值就是局部变量!

基本的原理很简单,在Python FAQ中提到了:

在Python中,如果变量仅仅是被引用而没有被赋值过,那么默认被视作全局变量。如果一个变量在函数中被赋值过,那么就被视作局部变量。

Effective Python也提到过:

Python是这样处理赋值操作的:如果变量在当前的作用域已经定义过,那么就会变成赋值操作。否则的话会在当前的作用域定义一个新的变量。(Assigning a value to a variable works differently. If the variable is already defined in the current scope, then it will just take on the new value. If the variable doesn’t exist in the current scope, then Python treats the assignment as a variable definition. The scope of the newly defined variable is the function that contains the assignment.)

重点强调一下,这里的被赋值过,指的是在函数体内任何地方被赋值过。无论是否会被执行到(比如在if语句中),甚至是变量引用之后再赋值(参考下面的代码),都被作为“被赋值过”,都变成了局部变量。

其实到这里这个问题的答案已经出来了,只要是在函数体内被赋值过,那么变量就是local的,任何赋值之前的操作都会出现一个RuntimeError。下面会深入解释一下。

赋值操作的编译过程(原理)

Python文档中有关赋值语句提到:

Assignment of an object to a single target is recursively defined as follows. If the target is an identifier (name):

  • If the name does not occur in a global statement in the current code block: the name is bound to the object in the current local namespace.
  • Otherwise: the name is bound to the object in the current global namespace.

就是说,如果赋值操作的变量没有用global声明,那么就将这个name绑定到局部名字空间,否则就绑定到全局名字空间。

我们可以使用symtable这个lib验证一下:

可以看到,x变量确实被绑定到了局部。使用dis库可以看到编译的代码:

其中,LOAD_FAST是从local的stack中读取变量名(LOAD_FAST对之后字节码的优化很重要)。由此可以看到,的确是在局部变量找x没有找到(前面并没有STORE_FAST操作),引发了UnboundLocalError。

所以我的理解是:Python编译建立抽象语法树的时候,根据语法书建立符号表,从语法书的函数体内决定符号是local的还是global(是否出现assignment语句),然后在编译其他语句生成字节码。

那么既然这样,为什么要等到运行的时候才报错,而不是编译的时候就报错呢?

参考下面的代码:

如果something_true(),x的赋值就会执行,那么代码不会抛异常。但是编译器并不会知道这个赋值语句会不会执行。换句话说,函数体内出现了赋值语句,但是Python编译过程无法得知赋值语句会不会执行到的。所以只要出现了赋值语句,就将变量视为局部。至于会不会出现未赋值就使用(UnboundLocalError),就运行看看了。

 Python为什么要这样处理?

这并不是缺陷,而是一个设计选择。

Python不要求声明变量,但是假设函数体内定义的变量都是局部变量。这比JavaScript好多了,JavaScript也不要求声明变量,但是如果不是var声明的变量,可能在不知情的情况下修改了全局变量。《Fluent Python》7.4

(PS:ES6的 let 也有了类似的机制,叫做“temporal dead zone”,参考

这应该很好理解,试想一下,如果在函数中引用了一个函数内不存在的变量,后面又进行了赋值。而Python将这个变量当做全局变量,那么可能隐式地给你覆盖了全局变量。这如果是debug起来肯定是个噩梦。

这种设计选择正是提现了Python的设计哲学:“Explicit is better than implicit.”

解决方法

前面已经提到了,显示地指定使用global就可以,这样即使出现赋值,也不会产生作为local的变量,而是去改变global的变量。

但是依然存在一个问题:

external的x既不是local,也不是global。这种情况应该使用Python3的nonlocal。这样Python不会在当前的作用域找x,会去上一层找。

可惜Python2不支持nonlocal,但是我们可以使用“闭包”来解决。其实思想就是,如果我们无法改变不可变的对象,就将这个对象变成可以改变的对象。

如上代码,x不是一个不可改变的int,而是一个可变的list对象。这样x[0] += 1就会变成一个赋值操作,而不会申请新的变量。

 

2018年8月30日更新:最近读《代码之髓》这本书,对 Python 的作用域以及它的行为有了新的理解。Python 是静态作用域的,而且变量无须声明,赋值即声明。像 Perl,JavaScript 这样的需要是需要声明的,比如带上 var 就是局部变量,否则就是全局变量。Python 这种赋值即声明的方式,好处就是我们在写的时候很爽,一般都符合我们的直觉。缺点就是在嵌套函数内部如果想要赋值,那么依据“赋值即声明”,我们就会创建新的变量,而不会去修改外部函数的变量。

与之类似的语言是 Ruby,在 Ruby 中同样“赋值即声明”,不过行为却与 Python 恰恰相反。

在 Ruby 中,如果嵌套方法,外部方法的变量在内部方法中依然视作外部方法的变量;如果在内部方法创建变量,那么只会存在于内部方法中,不会影响外部方法。通俗一点,如果内部方法对一个变量 a 赋值的话,如果外部方法有 a ,那么外部方法的 a 的值会被修改;否则,会在内部方法创建一个 a,内部方法结束之后,a 就不存在了。一下代码为例:

 

参考资料

  1. Understanding UnboundLocalError in Python
  2. Effective Python:Item 15: Know How Closures Interact with Variable Scope