Python参数传递,既不是传值也不是传引用

面试的时候,有没有被问到Python传参是传引用还是传值这种问题?有没有听到过Python传参既不是传值也不是传引用这种说法?一个小小的参数默认值也可能让代码出现难以查找的bug?

如果你也遇到过上面的问题,不妨我们来探究下Python函数传递的种种。

万物皆对象

Python中有一个非常重要的概念——万物皆对象,无论是一个数字、字符串,还是数组、字典,在Python中都会以一个对象的形式存在。

1
a = 123

对于上面这行代码,在Python看来就是创建一个PyObject对象,值为123,然后定义一个指针a,a指向这个PyObject对象。

可变对象和不可变对象

Python中的对象分为两种类型,可变对象和不可变对象,不可变对象指tuple、str、int等类型的对象,可变对象指的是dict、list、自定义对象等类型的对象,我们用一段代码说明他们的区别。

1
2
3
4
5
6
7
8
9
a = [1, 2, 3]
print(id(a)) # 2587116690248
a += [4]
print(id(a)) # 2587116690248

b = 1
print(id(b)) # 2006430784
b += 1
print(id(b)) # 2006430816

上面代码中我们分别定义了一个可变对象和一个不可变对象,并且对他们进行修改,打印修改前后的对象标识可以发现,对可变对象进行修改,变量对其引用不会发生变化,对不可变对象进行修改,变量引用发生了变化。

可变对象的引用

上图是一个可变对象,当修改对象时,例如删除数组中的一个元素,实际上把其中一个元素从对象中移除,对象本身的标识是不发生变化的。

不可变对象的引用

改变一个不可变对象时,例如给一个int型加2,语法上看上去是直接修改了i这个对象,但是如前面所说,i只是一个指向对象73的一个变量,Python会将这个变量指向的对象加2后,生成一个新的对象,然后再让i指向这个新的对象。

参数传递时的表现

了解了对象的原理后,我们就可以来尝试理解一下参数传递时他们的不同表现了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a = [1, 2, 3]
print(id(a)) # 1437494204232
def mutable(a):
print(id(a)) # 1437494204232
a += [4]
print(id(a)) # 1437494204232
mutable(a)

b = 1
print(id(b)) # 2006430784
def immutable(b):
print(id(b)) # 2006430784
b += 1
print(id(b)) # 2006430816
immutable(b)

通过上面的代码可以看出,修改传进的可变参数时,会对外部对象产生影响,修改不可变参数时则不会影响。

概括地说,Python参数传递时,既不是传对象也不是传引用,之所以会有上述的区别,跟Python的对象机制有关,参数传递只是给对象绑定了一个新的变量(实际上是传递C中的指针)。

参数传递时的坑

理解了参数传递的逻辑,我们需要注意一下这种逻辑可能引发的问题。

1
2
3
4
5
6
7
def test(b=[]):
b += [1]
print(b)

test() # [1]
test() # [1, 1]
test() # [1, 1, 1]

上面的代码的输出,按照可变对象传参的逻辑,应该每次调用都输出[1]才对,而实际输出看上去好像默认参数好像只生效了一次。原因在于Python的函数也是对象(万物皆对象),这个对象只初始化一次,加上参数又是不可变对象,所以每次调用实际上都修改的是一个对象。

解决这个问题,推荐再参数传递可变对象时,默认值设置为None,在函数内部对None进行判断后再赋予默认值。

1
2
3
4
5
6
7
8
def test(b=None):
b = b or []
b += [1]
print(b)

test() # [1]
test() # [1]
test() # [1]

再看一段代码。

1
2
3
4
5
6
i = 1
def test(a=i):
print(a)

i = 2
test() # 1

由于参数默认值是在函数定义时而不是函数执行时确定的,所以这段代码test方法的参数默认值时1而不是2。

关注Python私房菜

实战 | 用aiohttp和uvloop实现一个高性能爬虫

asyncio于Python3.4引入标准库,增加了对异步I/O的支持,asyncio基于事件循环,可以轻松实现异步I/O操作。接下来,我们用基于asyncio的库实现一个高性能爬虫。

准备工作

Earth View from Google Earth是一款Chrome插件,会在打开新标签页时自动加载一张来自Google Earth的背景图片。

Earth View from Google Earth

使用Chrome开发者工具观察插件的网络请求,我们发现插件会请求一个地址如https://www.gstatic.com/prettyearth/assets/data/v2/1234.json的JSON文件,文件中包含了经过Base64的图片内容,观察发现,图片的ID范围大致在1000-8000之间,我们的爬虫就要来爬取这些精美的背景图片。

实现主要逻辑

由于爬取目标是JSON文件,爬虫的主要逻辑就变成了爬取JSON–>提取图片–>保存图片

requests是一个常用的http请求库,但是由于requests的请求都是同步的,我们使用aiohttp这个异步http请求库来代替。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
async def fetch_image_by_id(item_id):
url = f'https://www.gstatic.com/prettyearth/assets/data/v2/{item_id}.json'
# 由于URL是https的,所以选择不验证SSL
async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
async with session.get(url) as response:
# 获取后需要将JSON字符串转为对象
try:
json_obj = json.loads(await response.text())
except json.decoder.JSONDecodeError as e:
print(f'Download failed - {item_id}.jpg')
return
# 获取JSON中的图片内容字段,经过Base64解码成二进制内容
image_str = json_obj['dataUri'].replace('data:image/jpeg;base64,', '')
image_data = base64.b64decode(image_str)
save_folder = dir_path = os.path.dirname(
os.path.realpath(__file__)) + '/google_earth/'
with open(f'{save_folder}{item_id}.jpg', 'wb') as f:
f.write(image_data)
print(f'Download complete - {item_id}.jpg')

aiohttp基于asyncio,所以在调用时需要使用async/await语法糖,可以看到,由于aiohttp中提供了一个ClientSession上下文,代码中使用了async with的语法糖。

加入并行逻辑

上面的代码是抓取单张图片的逻辑,批量抓取图片,需要再嵌套一层方法:

1
2
3
4
5
6
7
async def fetch_all_images():
# 使用Semaphore限制最大并发数
sem = asyncio.Semaphore(10)
ids = [id for id in range(1000, 8000)]
for current_id in ids:
async with sem:
await fetch_image_by_id(current_id)

接下来,将这个方法加入到asyncio的事件循环中。

1
2
3
event_loop = asyncio.get_event_loop()
future = asyncio.ensure_future(fetch_all_images())
results = event_loop.run_until_complete(future)

使用uvloop加速

uvloop基于libuv,libuv是一个使用C语言实现的高性能异步I/O库,uvloop用来代替asyncio默认事件循环,可以进一步加快异步I/O操作的速度。

uvloop的使用非常简单,只要在获取事件循环前,调用如下方法,将asyncio的事件循环策略设置为uvloop的事件循环策略。

1
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())

使用上面的代码,我们可以快速将大约1500张的图片爬取下来。

爬取下来的Google Earth图片

性能对比

为了验证aiohttp和uvloop的性能,笔者使用requests+concurrent库实现了一个多进程版的爬虫,分别爬取20个id,消耗的时间如图。

可以看到,耗时相差了大概7倍,aiohttp+uvloop的组合在爬虫这种I/O密集型的场景下,可以说具有压倒性优势。相信在不远的将来,基于asyncio的库会将无数爬虫工程师从加班中拯救出来。

扫码关注Python私房菜

拯救加班的你

与面试官谈笑风生 | Python面向对象之访问控制

Python从设计之初就是一门面向对象的语言,面向对象思想的第一个要素就是封装。所谓封装,通俗的讲就是类中的属性和方法,分为公有和私有,公有可以被外界访问,私有不能被外界访问,这就是封装中最关键的概念——访问控制。

面向对象编程

访问控制有三种级别:私有、受保护、公有

私有(Private):只有类自身可以访问
受保护(Protected):只有类自身和子类可以访问
公有(Public):任何类都可以访问

由于Python不像Java,有访问控制符(private / public / protected),所以Python的访问控制也是容易被应聘者忽视和搞错的。

公有(Public)

在Python的类中,默认情况下定义的属性都是公有的。

1
2
3
4
5
6
7
8
9
10
class Foo(object):
bar = 123

def __init__(self, bob):
self.bob = bob

print(Foo.bar) # 123

foo = Foo(456)
print(foo.bob) # 456

上面类Foo中的bar属性就是类属性,__init__方法中定义的bob是实例属性,barbob都是公有的属性,外部可以访问,分别print类中的bar和实例中的bob,输出了对应的值。

受保护(Protected)

在Python中定义一个受保护的属性,只需要在其名字前加一个下划线_,我们将Foo方法中的bobbar改为_bob_bar,他们就变成了受保护的属性了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Foo(object):
_bar = 123

def __init__(self, bob):
self._bob = bob


class Son(Foo):

def print_bob(self):
print(self._bob)

@classmethod
def print_bar(cls):
print(cls._bar)


Son.print_bar() # 123

son = Son(456)
son.print_bob() # 456

定义一个类Son继承自Foo,由于受保护的对象只能在类的内部和子类中被访问,不能直接调用print(Son._bar)print(son._bob)来输出这两个属性的值,所以定义了print_barprint_bob方法,实现在子类中输出,这段代码也正常的输出了_bar_bob的值。

接下来,试着反向验证一下,在类的外部,能不能访问其属性,将上面代码的输出部分修改如下:

1
2
3
4
print(Son._bar)  # 123

son = Son(456)
print(son._bob) # 456

(假装)惊讶的发现,竟然没有报错,也输出了正确的值。

Python中用加下划线来定义受保护变量,是一种约定的规范,而不是语言层面真的实现了访问控制,所以,我们定义的保护变量,依然可以在外部被访问到(这是个feature,不是bug)。

私有(private)

Python定义私有属性,需要在属性名前加两个下划线__,把上面的代码修改一下,运行一下会发现下面的代码中的任何一个print都会报错的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Foo(object):
__bar = 123

def __init__(self, bob):
self.__bob = bob


class Son(Foo):

def print_bob(self):
print(self.__bob) # Error

@classmethod
def print_bar(cls):
print(cls.__bar) # Error


print(Son.__bar) # Error

son = Son(456)
print(son._bob) # Error

深入一下——私有属性真的就访问不到了吗?

要了解私有属性是否真的访问不到,需要从Python是如何实现私有属性入手。CPython中,会把双下划线的属性变为_ClassName__PropertyName的形式,用代码演示一下:

1
2
3
4
5
class Foo(object):
__bar = 123


print(Foo._Foo__bar) # 123

运行一下可以知道,正常输出了__bar的值,但是不推荐这样去访问私有属性,因为不同的Python解释器对于私有属性的处理不一样。

特例

使用双下划线定义私有属性,有一种特殊情况,当属性后也有两个下划线的时候,这个属性会被Python解释器当做魔术方法,从而不做私有处理。

1
2
3
4
5
class Foo(object):
__bar__ = 123


print(Foo.__bar__) # 123

上面代码输出了123,证明Python解释器并没有把__bar__当做私有属性。当定义私有属性时,需要注意名字最后最多只能有一个下划线。

另一个特例

假如定义的属性名就叫__呢?不妨直接试一下:

1
2
3
4
5
class Foo(object):
__ = 123


print(Foo.__) # 123

可以发现名字叫__的属性也不会被认为是私有属性,名字是多个下划线的属性也不是私有属性(比如_______)。

函数的访问控制

前面主要介绍了属性的访问控制,在Python中函数是一等公民,所谓一等公民,就是函数可以像变量一样使用,所以函数的访问控制和属性一样,一样应用上面的规则。

关注Python私房菜

只发优质Python文章的公众号

用Python批量提取Win10锁屏壁纸

使用Win10的朋友会发现,每次开机锁屏界面都会有不一样的漂亮图片,这些图片通常选自优秀的摄影作品,十分精美。

但是由于系统会自动更换这些图片,所以就算再好看的图片,也许下次开机之后就被替换掉了。

借助Python,我们可以用简单的几行代码,批量提取这些精美的锁屏图片。把喜欢的图片设置成桌面背景,就不用担心被替换掉啦。

提取原理

Win10系统会自动下载最新的锁屏壁纸,并将他们保存在一个系统文件夹中,路径是C:\Users\[用户名]\AppData\Local\Packages\Microsoft.Windows.ContentDeliveryManager_cw5n1h2txyewy\LocalState\Assets

随机命名的锁屏图片

直接打开这个文件夹,里面会有随机命名的多个文件,每一个文件就是一张图片。但是由于文件没有扩展名,所以并不能预览。为了不搞坏系统文件,并且把这些文件变成可以预览的格式,我们用Python把这些文件复制出来,加上JPG作为扩展名。

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import os, shutil
from datetime import datetime


# 把这个文件所在目录wallpapers文件夹作为保存图片的目录
save_folder = dir_path = os.path.dirname(
os.path.realpath(__file__)) + '\wallpapers'
# 动态获取系统存放锁屏图片的位置
wallpaper_folder = os.getenv('LOCALAPPDATA') + (
'\Packages\Microsoft.Windows.ContentDeliveryManager_cw5n1h2txyewy'
'\LocalState\Assets')
# 列出所有的文件
wallpapers = os.listdir(wallpaper_folder)
for wallpaper in wallpapers:
wallpaper_path = os.path.join(wallpaper_folder, wallpaper)
# 小于150kb的不是锁屏图片
if (os.path.getsize(wallpaper_path) / 1024) < 150:
continue
wallpaper_name = wallpaper + '.jpg'
save_path = os.path.join(save_folder, wallpaper_name)
shutil.copyfile(wallpaper_path, save_path)
print('Save wallpaper ' + save_path)

首先确定系统存放锁屏图片的文件夹位置,由于文件夹位于用户的个人文件夹内,每个用户的用户名是不一样的,所以我们需要通过系统的LOCALAPPDATA变量动态的获取路径。代码会把提取出来的图片保存在wallpapers文件夹下,所以代码文件所在的目录没有wallpapers文件夹,需要手工创建一个。

在代码文件旁新建一个wallpapers文件夹

执行上面这段Python代码,再打开wallpapers文件夹,就可以看到提取出的锁屏图片了。

关注Python私房菜

原创Python实战文章

15行Python代码,帮你理解令牌桶算法

在网络中传输数据时,为了防止网络拥塞,需限制流出网络的流量,使流量以比较均匀的速度向外发送,令牌桶算法就实现了这个功能,可控制发送到网络上数据的数目,并允许突发数据的发送。

什么是令牌

从名字上看令牌桶,大概就是一个装有令牌的桶吧,那么什么是令牌呢?

紫薇格格拿的令箭,可以发号施令,令行禁止。在计算机的世界中,令牌也有令行禁止的意思,有令牌,则相当于得到了进行操作的授权,没有令牌,就什么都不能做。

用令牌实现限速器

我们用1块令牌来代表发送1字节数据的资格,假设我们源源不断的发放令牌给程序,程序就有资格源源不断的发送数据,当我们不发放令牌给程序,程序就相当于被限流,无法发送数据了。接下来我们说说限速器,所谓限速器,就是让程序在单位时间内,最多只能发送一定大小的数据。假设在1秒发放10块令牌,那么程序发送数据的速度就会被限制在10bytes/s。如果1秒内有大于10bytes的数据需要发送,就会因为没有令牌而被丢弃。

改进限速器——加个桶

我们实现的限速器,速度是恒定的,但是在实际的应用中,往往会有突发的传输需求(需要更快速的发送,但是不会持续太久,也不会引起网络拥塞),这种数据碰上我们的限速器,就因为拿不到令牌而无法发送。

对限速器进行一下改动,依然1秒产生10块令牌,但是我们把产生出来的令牌先放到一个桶里,当程序需要发送的时候,从桶里取令牌,不需要的时候,令牌就会在桶里沉淀下来,假设桶里沉淀了10块令牌,程序最多就可以在1秒内发送20bytes的数据,满足了突发数据传输的要求,并且由于桶里的令牌被用完,下一秒最多依然只能发10bytes的数据,不会因为持续发送大量数据,对网络造成压力。

15行Python代码实践令牌桶

令牌桶需要以一定的速度生成令牌放入桶中,当程序要发送数据时,再从桶中取出令牌。这里似乎有点问题,如果我们使用一个死循环,来不停地发放令牌,程序就被阻塞住了,有没有更好的办法?

我们可以在取令牌的时候,用现在的时间减去上次取令牌的时间,乘以令牌的发放速度,计算出桶里可以取的令牌数量(当然不能超过桶的大小),从而避免循环发放的逻辑。

接下来看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import time


class TokenBucket(object):

# rate是令牌发放速度,capacity是桶的大小
def __init__(self, rate, capacity):
self._rate = rate
self._capacity = capacity
self._current_amount = 0
self._last_consume_time = int(time.time())

# token_amount是发送数据需要的令牌数
def consume(self, token_amount):
increment = (int(time.time()) - self._last_consume_time) * self._rate # 计算从上次发送到这次发送,新发放的令牌数量
self._current_amount = min(
increment + self._current_amount, self._capacity) # 令牌数量不能超过桶的容量
if token_amount > self._current_amount: # 如果没有足够的令牌,则不能发送数据
return False
self._last_consume_time = int(time.time())
self._current_amount -= token_amount
return True

扫码关注Python私房菜

最爱你的人,会让你不费脑细胞的理解区块链原理

区块链是一个近期非常火的概念,随便走进一个写字楼的电梯,都会听到有人谈论区块链,或者炒币: ) 希望通过这篇文章,能让你对区块链的概念有一个整体的认识,在理解概念后,下一篇文章将用大约300行Python代码,实现一个区块链网络。

区块 和 链

所谓区块,就是一个块咯(要不然还是什么?),把这些块一个一个连在一起,像链条一样,就称为区块链(别急着打我,先往下看)。

区块和区块链

这是一些链条,链条的价值更多的体现在锁楼下小电动车的时候,不过我们再仔细看一下这个链条是不是有点像那什么?⛓

脱氧核糖核酸

很聪明,高中生物老师在冲你微笑!这就是传说中的脱氧核糖核酸(DNA),DNA也是一种链式结构,携带了遗传信息。区块链中的“区块”,就好比DNA分子中的脱氧核糖核苷酸(我也不知道自己在说什么),而区块链中的“链”,就好比DNA分子的链式结构。

DNA和链条的价值差异,关键在于DNA携带了大量遗传信息,而链条什么都没有携带。对于区块链来说,携带信息也是它的一个重要特点(不携带信息连锁电动车的作用都没有)。

当区块链携带的是账务信息的时候,它就变成了一个特别厉害的东西——账本(我们给他起了个00后的名字——比特币)。

Peer-to-Peer

为了显得专业一些,我们祭出比特币的创造者,中本聪的论文《Bitcoin: A Peer-to-Peer Electronic Cash System》,从这篇论文的题目我们可以看到,区块链是基于Peer-to-Peer的,下面就来了解一下什么是Peer-to-Peer。

Peer-to-Peer,简称P2P(不是互联网借贷的那个P2P),它是一种点对点网络,看图(我也不知道该怎么讲)。

P2P网络

图中每一个方脑袋就是一个Peer(节点),注意一下,这些方脑袋有一个共同点,他们都一毛一样(不仅长得一样,每一个脑袋都跟其它脑袋相连)。这也是P2P网络的最大特点——去中心化,P2P网络中不存在中心节点,所有节点都是平等的,任何一个节点,想跟谁说话就跟谁说话,并且谁(节点)都不能说了算。

共识机制

你和好基友老王,还有你们共同喜欢的一个姑娘阿圆(对,就是圆滚滚的圆),组成了一个三节点的P2P网络,根据P2P网络定义你们三个人谁都不能说了算。这一天,阿圆生日,你和老王同时给阿圆送了一个大蛋糕,那么问题来了,阿圆先吃哪个呢?

既然没有人能说了算,也不能让你和老王决斗(计算机这么做恐怕人类会毁灭),那么就需要采用一个文明的办法决定——商量。既然商量,就要有规矩,这个规矩叫共识机制

Proof-of-work

区块链共识机制有很多种,Proof-of-work(POW,工作量证明)是其中一种,所谓工作量证明,有点像是比武招亲。你和老王对阿圆都很好,阿圆也很难抉择到底先吃谁的蛋糕,于是她请了两位武力相当的武林高手,让你和老王分别与高手过招,谁赢了高手,就先吃谁的蛋糕,如果你们都赢了高手,那么谁先赢算数。

把这些捏在一起

了解了POW、共识机制、P2P、区块、链的概念,我们就可以把他们拼在一起,看看会发生什么了。

区块链诞生了!

我们把P2P网络中的每一个节点,赋予一条链,这样网络中所有的链都是平等的了,接下来在其中一条链上增加一个带有信息的区块,P2P网络会将这个区块同步到所有的链上,也就是这条信息会被存储在所有节点。

把上面的一段话缩成一句(会显得比较厉害):

区块链是用分布式数据库识别、传播和记载信息的智能化对等网络, 也称为价值互联网。

看到这里是不是对区块链略知一二了?

关注Python私房菜

下一篇文章会用300行Python代码实现一个区块链,不要错过哟

面试不再怕,20行Python代码帮你搞懂LRU算法

LRU算法在后端工程师面试中,是一个比较常出现的题目,这篇文章带大家一起,理解LRU算法,并最终用Python轻松实现一个基于LRU算法的缓存。

缓存是什么

先看一张图,当我们访问网页,浏览器会给服务器发请求,服务器会经过一系列的运算,把页面返回给浏览器。

当有多个浏览器同时访问的时候,就会在短时间内发起多个请求,而服务器对每一个请求都要进行一系列相同的操作。重复工作不仅浪费资源,还可能导致响应速度变慢。

而缓存则可以把服务器返回的页面保存下来,当有其他的浏览器再访问时候,就不必劳服务器大驾,直接由缓存返回页面。为了保证响应速度,缓存通常是基于比较昂贵的硬件,比如RAM,这就决定了我们很难用大量的缓存把所有的页面都存下来,当恰好没有缓存浏览器请求的页面时,依然需要请求服务器。由于缓存容量有限,而数据量无限(互联网每天新产生的页面数无法估计),就需要把好刚用在刀刃上,缓存那些最有用的信息。

LRU是什么

LRU是一种缓存淘汰算法(在OS中也叫内存换页算法),由于缓存空间是有限的,所以要淘汰缓存中不常用的数据,留下常用的数据,达到缓存效率的最大化。LRU就是这样一种决定“淘汰谁留下谁”的算法,LRU是Least recently used的缩写,从字面意思“最近最少使用”,我们就可以理解LRU的淘汰规则。

LRU的淘汰逻辑

我们用一张图来描述LRU的淘汰逻辑,图中的缓存是一个列表结构,上面是头结点下面是尾节点,缓存容量为8(8个小格子):

  • 有新数据(意味着数据之前没有被缓存过)时,加入到列表头
  • 缓存到达最大容量时,需要淘汰数据多出来的数据,此时淘汰列表尾部的数据
  • 当缓存中有数据被命中,则将数据移动到列表头部(相当于新加入缓存)

按上面的逻辑我们可以看到,一个数据如果经常被访问就会不断地被移动到列表头部,不会被淘汰出缓存,而越不经常访问的数据,越容易被挤出缓存。

20行Python代码实践LRU

接下来我们用Python来实现一个采用LRU算法的缓存。

从前面的文章中我们可以知道,缓存简化下来就两个功能,一个是往里装数据(缓存数据),一个是往外吐数据(命中缓存),所以我们的缓存对外只需要put和get两个接口就可以了。

按照前面的示意图,缓存内部我们只需要有一个列表(list)就可以实现LRU逻辑,不过用列表虽然能实现逻辑,但是在判断是否命中缓存时,速度可能非常慢(列表需要遍历才能知道数据有没有在里面)。在Python中,我们可以用基于hash的结构,比如字典(dict)或集合(set),来快速判断数据是否存在,解决列表实现的性能问题。但是字典和集合又是没有顺序的,如果能有一种既能排序,又是基于hash存储的数据结构,就好了。

在Python的collections包中,已经内置了这种实用的结构OrderedDict,OrderedDict是dict的子类,但是存储在内部的元素是有序的(列表的特点)。

解决了数据结构的问题,我们可以直接上手写逻辑了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class LRUCache:

def __init__(self, capacity):
self.capacity = capacity
self.queue = collections.OrderedDict()

def get(self, key):
if key not in self.queue:
return -1 // 要找的数据不在缓存中返回-1
value = self.queue.pop(key) // 将命中缓存的数据移除
self.queue[key] = value // 将命中缓存的数据重新添加到头部
return self.queue[key]


def put(self, key, value):
if key in self.queue: // 如果已经在缓存中,则先移除老的数据
self.queue.pop(key)
elif len(self.queue.items()) == self.capacity:
self.queue.popitem(last=False) // 如果不在缓存中并且到达最大容量,则把最后的数据淘汰
self.queue[key] = value // 将新数据添加到头部

下次面试在遇到LRU的题目,是不是就胸有成竹了?

扫码关注Python私房菜

60行Python代码,实现多线程PDF转Word

工作中经常会遇到需要提取PDF文件中文字的情况,一个PDF还好,复制粘贴一下也花不了太多时间,如果需要把大量PDF转为Word,怎么办呢?

今天教大家用60行代码实现,多线程批量PDF转Word。没兴趣看具体过程可以直接拉到最后,有代码。

分解任务

把PDF转为Word,分几步?两步,第一步读取PDF文件,第二步写入Word文件。

是的,就是这么简单,借助Python第三方包,可以轻松实现上面两个过程,我们要用到pdfminer3k和python-docx这两个包。

读取PDF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pdfminer.pdfinterp import PDFResourceManager
from pdfminer.pdfinterp import process_pdf
from pdfminer.converter import TextConverter
from pdfminer.layout import LAParams


resource_manager = PDFResourceManager()
return_str = StringIO()
lap_params = LAParams()

device = TextConverter(resource_manager, return_str, laparams=lap_params)
process_pdf(resource_manager, device, file) // file是使用open方法打开的PDF文件句柄
device.close()

// 此处content就是转换为文字的PDF内容
content = return_str.getvalue()

content变量存储的就是我们从PDF文件中读取出的文字内容,可以看到,使用pdfminer3k可以轻松完成这个任务。接下来我们需要把文字内容写入成一个word文件。

写入Word

1
2
3
4
5
6
7
8
9
10
11
12
from docx import Document


doc = Document()
for line in content.split('\n'):
paragraph = doc.add_paragraph()
paragraph.add_run(remove_control_characters(line))
doc.save(file_path)
content是我们前面读取出的文字内容,由于是讲整个PDF读成一个字符串,所以需要使用split方法将每一行分隔开,然后按行写入word,否则所有的文字会在同一行。同时这段代码使用了一个remove_control_characters函数,这个函数是需要自己实现的,目的是移除控制字符(换行符、制表符、转义符等),因为python-docx是不支持控制字符写入的。
def remove_control_characters(content):
mpa = dict.fromkeys(range(32))
return content.translate(mpa)

控制字符就是ASCII码在32以下的,所以我们使用str的translate方法,把32以下的字符移除就可以。

用是能用,但是太慢了!

如果我们用上面代码去转换100个PDF文件,就会发现速度慢到难以接受,每个PDF都需要花很长时间才能转换好,怎么办?别急,接下来我们引入多线程,同时转换多个PDF,可以有效加快转换速度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import os
from concurrent.futures import ProcessPoolExecutor


with ProcessPoolExecutor(max_workers=int(config['max_worker'])) as executor:
for file in os.listdir(config['pdf_folder']):
extension_name = os.path.splitext(file)[1]
if extension_name != '.pdf':
continue
file_name = os.path.splitext(file)[0]
pdf_file = config['pdf_folder'] + '/' + file
word_file = config['word_folder'] + '/' + file_name + '.docx'
print('正在处理: ', file)
result = executor.submit(pdf_to_word, pdf_file, word_file)
tasks.append(result)
while True:
exit_flag = True
for task in tasks:
if not task.done():
exit_flag = False
if exit_flag:
print('完成')
exit(0)

代码中config是包含存储PDF文件夹地址和word文件夹地址的字典,使用Python标准库中的concurrent包,实现多进程,pdf_to_word方法是对上面读取PDF和写入word逻辑的封装。后面的while循环是查询任务是否进行完成。

效果

到这里,我们已经实现了多线程批量转换PDF为word文档。拿谋篇著名文章来试验一下,效果如图(左侧是转换后的word,右侧是PDF):

不想写代码?

本文介绍的所有代码,已经打包成了一个独立可运行的项目,存放在github,如果不想自己写代码,可以直接clone或下载github项目运行。项目地址如下(记得点star):

https://github.com/simpleapples/pdf2word

欢迎关注Python私房菜

「贪吃蛇大作战」的刷分尝试

昨天同学推荐了一个最近比较火的游戏「贪吃蛇大作战」,今天已经到了AppStore总榜第二的位置。这两天一有空就会掏出手机玩几盘,不过无尽模式玩到3000分左右就遇到了瓶颈,于是怀着单纯的目的想研究下这个游戏的接口…

图1

使用Charles抓取请求

既然是研究接口,第一步就是看看接口的URL和参数返回都是什么了,请出神器Charles。

图2

通过Charles可以清楚地看到,游戏提交分数的接口和参数,至于返回,貌似没什么用。接下来仔细研究下请求,有几个参数能比较容易的判断他们的含义和作用。

1
2
3
4
5
6
7
8
9
10
11
12
device_id: 设备ID
game_mode: 1是无尽模式 2是限时模式
kill: 击杀次数
length: 蛇的长度
market: 我抓的是iOS设备发的请求,所以这里是apple
platform: 抓到的请求里是1,还不太明白具体的意思
push_channel: 也是1,不明白意思
push_id: 固定值111111111222222223333333344444444,后面逆向apk也证明了这一点
sid: 每次登录都会更新
snake_sign: 签名
uid: 用户ID
version: 我使用的版本是固定值2.1

通过逆向APK获取签名算法

明确了参数的意思,接下来就可以伪造请求了,不过在伪造请求之前,还有一个参数比较麻烦,那就是snake_sign,请求签名,从图中snake_sign的内容看,似乎有点像是一个base64过的东西,不过base64decode发现,根本不可读。与其猜签名算法,不如考虑一下其他办法。

「贪吃蛇大作战」还有安卓版本,并且引导下载的页面提供了apk,于是下载apk,使用jadx-gui进行逆向,使用jadx-gui打开apk后,代码一览无余。

通过查看代码,可以清楚的看到snake_sign字段的签名逻辑,首先将请求中所有参数按ASCII码顺序排列拼接成字符串,然后在字符串前添加POST& + [URL Path],添加后使用Key进行SHA1加密,key也可以在代码中获取到。对加密生成的结果,需要做一次base64,才是最终snake_sign字段的结果。

1
POST&top_list_v2/update_score&device_id=XXX&game_mode=1&kill=1&length=35&market=apple&platform=1&push_channel=1&push_id=111111111222222223333333344444444&sid=XXX&uid=XXX&version=2.1

图3

关于「贪吃蛇大作战」的一些猜测

签名方法已经获取到,本以为可以安心刷分了,但是只成功刷了一次4000分160击杀。猜测可能进入排行版需要人工审核,一个理由是刷完后过了几分钟才显示到排行版,另一个理由是,刷完4000分169击杀后,很快刷了一个高的离谱的分,但是并没有显示在排行榜,后面无论刷多少分都再也没有显示出来过,怀疑可能被人工屏蔽。

另外整个游戏和API的交互只有登录、获取用户信息、获取排行版最高分、提交分数等几种,所以游戏也不是真正的实时。

图4

在阿里云CentOS7中配置基于Nginx+Supervisor+Gunicorn的Flask项目

需要在阿里云的CentOS7中搭建Flask应用的生产环境,记录一下。

配置Centos7

root登录后,首先新建一个普通用户并设置密码

1
2
adduser user
passwd user

接下来将用户的公钥复制到~/.ssh中,命名为authorised_keys,修改/etc/ssh/sshd_config禁用ssh中的root登录,修改默认ssh端口,并使用证书登陆,修改如下内容

1
2
3
Port 65535
PasswordAuthentication no
PermitRootLogin no

配置完成后重启ssh服务

1
systemctl restart sshd.service

CentOS7中用firewalld替换了iptables,需要手动将80端口和修改后的ssh端口添加到firewalld中

1
2
3
firewalld --add-port 80/tcp --permanent
firewalld --add-port 65535/tcp --permanent
firewalld --reload

配置Nginx

yum中可以直接安装nginx

1
yum install nginx

安装好后在/etc/nginx/default.d中添加location的配置,并指向8001端口,以后Flask会监听8001端口

1
2
3
location / {
proxy_pass http://127.0.0.1:8001
}

配置好后重新载入nginx配置

1
systemctl reload nginx.service

安装Python

CentOS自带Python2.7,如果使用Python3,需要单独安装。

1
2
3
4
5
6
wget https://www.python.org/ftp/python/3.4.3/Python-3.4.3.tgz
tar xf Python-3.4.3.tgz
cd Python-3.4.3
./configure --prefix=/usr/local --enable-shared
make
make altinstall

接下来在项目中搭建虚拟环境,Python2虚拟环境使用virtualenv安装(使用pip install virtualenv命令安装),Python3环境使用pyvenv安装(Python3自带pyvenv),以Python3环境为例,在Web项目中,建立虚拟环境文件夹venv

1
pyvenv venv

接下来在项目路径下启用虚拟环境

1
source venv/bin/active

退出虚拟环境使用Ctrl+C或deactive命令

安装配置Gunicorn

Gunicorn使用pip install gunicorn安装,注意需要在虚拟环境中使用pip安装,这样才对应虚拟环境中的Python版本。安装好后,新建一个Gunicorn的配置文件,比如deploy_config.py,加入内容如下

1
2
3
4
5
6
7
8
import os
bind='127.0.0.1:8001' #绑定的端口
workers=4 #worker数量
backlog=2048
debug=True
proc_name='gunicorn.pid'
pidfile='/var/log/gunicorn/debug.log'
loglevel='debug'

保存文件后在虚拟环境中使用Gunicorn尝试启动

1
gunicorn -c deploy_config.py myapp:app

myapp是入口Python文件名,app是函数名。如果输出worker相关信息,表明启动成功。

安装配置Supervisor

yum可以直接安装Supervisor,需要注意的是Supervisor只支持Python2,所以不要在虚拟环境中使用pip安装supervisor。

1
yum install supervisor

安装后,在/etc/supervisord.d中建立配置文件xxx.ini,内容如下

1
2
3
4
5
6
7
[program:xxx]
command=/var/proj/xxx/venv/bin/python /usr/bin/gunicorn -c /var/proj/xxx/deploy_config.py myapp:app
autorstart=true
directory=/var/proj/xxx
autorestart=true
startsecs=10
startretries=20

xxx是项目名称,注意command中最好都写全路径,以区别系统环境和项目虚拟环境。完成后启动supervisord,使配置生效

1
supervisord -c /etc/supervisord.conf