谢乾坤 | Kingname

给时光以生命。

在我以前的一篇文章:一日一技:如何正确移除Selenium中window.navigator.webdriver的值,我讲到了如何在Selenium启动的Chrome中,通过设置启动参数隐藏window.navigator.webdriver,驳斥了网上垃圾文章中流传的使用JavaScript注入的弊端。

由于Selenium启动的Chrome中,有几十个特征可以被识别,所以在爬虫界已经没有以前那么受欢迎了。模拟浏览器的新秀Puppeteer异军突起,逐渐受到了爬虫界的关注。Puppeteer需要使用JavaScript来控制,如果你是用Python,那么就需要使用Pyppeteer.

如果你使用模拟浏览器爬淘宝,你会发现,无论怎么修改参数,Selenium总是可以立刻被识别。但是如果你使用了本文的方法,用Pyppeteer抓取淘宝,你就会发现另外一个广阔的天地。

今天,我们来讲讲如何在Pyppeteer中隐藏window.navigator.webdriver

阅读全文 »

知乎用户@Manjusaka 在阅读了我的文章《Python正则表达式,请不要再用re.compile了!!!》以后,写了一篇驳文《驳 <Python正则表达式,请不要再用re.compile了!!!>》

今天,我在这里回应一下这篇驳文。首先标题里面,我用的是,意为回复,而不是继续驳斥@Manjusaka的文章。因为没有什么好驳斥的,他的观点没有什么问题。

首先说明,我自己在公司的代码里面,也会使用re.compile。但是,我现在仍然坚持我的观点,让看这篇文章的人,不要用re.compile

阅读全文 »

当我们使用Python从MongoDB里面读取数据时,可能会这样写代码:

1
2
3
4
5
6
import pymongo

handler = pymongo.MongoClient().db.col

for row in handler.find():
parse_data(row)

短短4行代码,读取MongoDB里面的每一行数据,然后传入parse_data做处理。处理完成以后再读取下一行。逻辑清晰而简单,能有什么问题?只要parse_data(row)不报错,这一段代码就完美无缺。

但事实并非这样。

阅读全文 »

看过《Python爬虫开发 从入门到实战》的同学,应该对multiprocessing这个模块比较熟悉,在书上我使用这个模块通过几行代码实现了一个简单的多线程爬虫:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests
from multiprocessing.dummy import Pool

def get(url):
print(requests.get(url).text, '\n')

url_list = [
'http://exercise.kingname.info/exercise_middleware_ip/1',
'http://exercise.kingname.info/exercise_middleware_ip/2',
'http://exercise.kingname.info/exercise_middleware_ip/3',
'http://exercise.kingname.info/exercise_middleware_ip/4'
]

pool = Pool(3)
result = pool.map(get, url_list)

运行效果如下图所示:

阅读全文 »

少数派(https://sspai.com/)是一个以生产力、效率工具为主要内容的数字消费指南平台。由于网站需要盈利,所以他们在网站上有所克制地增加了一些付费内容。如下图所示。

这种付费文章点进去以后,只能看到前几段,然后就需要购买会员才能正常阅读。

作为一个网站需要盈利,因此开设付费内容无可厚非。奈何少数派的付费内容对我毫无吸引力,因此我不希望每次都在首页上看到这些付费文章。

从网页上删除这些付费内容的原理非常简单,在网页上右键,点击“检查”,打开Chrome的开发者工具,如下图所示。

点击箭头所指向的图标,如下图所示:

然后在网页上任意选中一个付费内容,此时开发者工具里面将会自动变成下图所示的样子:

其中方框框住的这个img标签对应了付费内容这个小图标,因此我们可以使用这个标签来进行定位。

现在视线在HTML代码区域往上走,可以看到dl标签

每一个文章块就对应一个dl标签。如果我们在上面右键删除这个标签,就会发现对应的付费条目不见了,如下图所示:

但这种做法每次只能删除一条付费内容,有没有办法把所有付费内容全部删除呢?答案就是使用JavaScript:

1
2
3
4
5
6
7
8
9
var pay_tag_list = document.getElementsByClassName('series-logo')

while(pay_tag_list.length > 0){
pay_tag = pay_tag_list[0]
dl_tag = pay_tag.parentNode.parentNode.parentNode
div_tag = dl_tag.parentNode
div_tag.removeChild(dl_tag)
pay_tag_list = document.getElementsByClassName('series-logo')
}

复制上面这一段代码到开发者工具的Console标签页并粘贴,敲下回车键,付费内容就消失了,如下图所示。

在Python 3.5(含)以前,字典是不能保证顺序的,键值对A先插入字典,键值对B后插入字典,但是当你打印字典的Keys列表时,你会发现B可能在A的前面。

但是从Python 3.6开始,字典是变成有顺序的了。你先插入键值对A,后插入键值对B,那么当你打印Keys列表的时候,你就会发现B在A的后面。

不仅如此,从Python 3.6开始,下面的三种遍历操作,效率要高于Python 3.5之前:

1
2
3
4
5
for key in 字典

for value in 字典.values()

for key, value in 字典.items()

从Python 3.6开始,字典占用内存空间的大小,视字典里面键值对的个数,只有原来的30%~95%。

Python 3.6到底对字典做了什么优化呢?为了说明这个问题,我们需要先来说一说,在Python 3.5(含)之前,字典的底层原理。

阅读全文 »

如果你在Google或者百度或者某些技术社区上面搜索uwsgi + Flask,你会发现大量的文章,是教你如何使用uwsgi + flask + Nginx搭建网站。如下图所示:

怪现状

而且这些文章,全部都像是约定俗成一样,一定会首先用命令行启动uwsgi,测试uwsgi与Flask运行是否正常,然后写uwsgi的配置文件。然后使用Unix 套接字沟通uwsgi与Nginx。所以uwsgi的配置文件里面一定会写成类似于下面这样:

1
socket = /xxx/yyy/zzz.sock

Nginx的配置一定有类似于下面这一段:

1
2
3
4
location / {
include uwsgi_params;
uwsgi_pass unix:///xxx/yyy/zzz.sock;
}

他们为什么要这样写?因为他们看的别的博客上就是这样写的!他们知其然,但是不知其所以然。

阅读全文 »

在看Kenneth Reitz大神的Records项目时,注意到在Readme中,读取数据有三种写法:

Values can be accessed many ways: row.user_email, row[‘user_email’], or row[3]

假设一个数据表如下所示:

username active name user_email timezone
model-t True Henry Ford model-t@gmail.com 2016-02-06 22:28:23.894202

那么,当你想读取user_email这一列的时候,除了可以使用row.user_emailrow['user_email']以外,由于user_email在第3列(username是第0列),所以还可以使用row[3]来读取。

今天我们就来研究一下,他是如何实现这个功能的。

假设我们现在有一个类:

1
2
3
4
5
6
7
class People:
def __init__(self, name, age):
self.name = name
self.age = age

def walk(self):
print('i am walk')

基于这个类初始化一个对象kingname:

1
2
kingname = People('kingname', 26)
print(kingname.age)

运行效果如下图所示:

可以看到,我们已经实现了类似于records项目中的row.user_email的写法。

但是当我们想像字典一样取读取的时候,就会报错,如下图所示。

此时,为了让一个对象可以像字典一样被读取,我们需要实现它的__getitem__方法:

1
2
3
4
5
6
7
8
9
10
class People:
def __init__(self, name, age):
self.info = {'name': name,
'age': age}

def walk(self):
print('i am walk')

def __getitem__(self, key):
return self.info.get(key, '不存在')

此时就可以像字典一样去读取了,运行效果如下图所示:

但新的问题又来了,不能直接读取使用kinganme.age读取数据了,这样写会导致报错,如下图所示:

为了解决这个问题,我们再来实现这个类的__getattr__方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
class People:
def __init__(self, name, age):
self.info = {'name': name,
'age': age}

def walk(self):
print('i am walk')

def __getitem__(self, key):
return self.info.get(key, '不存在')

def __getattr__(self, key):
return self.info.get(key, '不存在')

运行效果如下图所示:

那么如何实现records里面的row[3]这种写法呢?这就需要先解释一下,records这个库是用来做什么的,以及它的数据是如何存放的。

K大写的records这个库,是用于来更加方便地读写SQL数据库,避免繁琐地写各种SQL语句。

这个库的源代码只有一个文件:源代码

我们今天要研究的这个写法,在Record这个类里面。这个类用来保存MySQL中的一行数据。Record这个类包含两个属性,分别为_keys_values,其中_keys用于记录所有的字段名,_values用于记录一行的所有值。字段名和值是按顺序一一对应的。

例如_keys里面下标为3表示字段名user_email,那么_values里面下标为3的数据就是user_email的值。

正式由于这样一个一一对应的关系,所以row[3]才能实现row['user_email']row.user_email相同的效果。

回到我们的People类,为了实现相同的目的,我们再次修改代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class People:
def __init__(self, name, age):
self._keys = ['name', 'age']
self._values = [name, age]

def walk(self):
print('i am walk')

def __getitem__(self, key):
if isinstance(key, int):
if 0 <= key < len(self._keys):
return self._values[key]
return '索引超出边界'

index = self._keys.index(key)
if index == -1:
return 'Key不存在'
return self._values[index]

def __getattr__(self, key):
return self.__getitem__(key)

运行效果如下图所示:

需要说明的是,无论是使用kingname['key']还是kingname[1],他们都会进入到__getitem__方法中去。

我这里给出的例子相较于records项目的代码做了简化,不过关键的部分都已经囊括了进来。

最后,推荐有兴趣的同学通读records这个项目的源代码,你将会从K大的代码中学到非常多的东西。

如果大家在网上搜索Python 正则表达式,你将会看到大量的垃圾文章会这样写代码:

1
2
3
4
5
import re

pattern = re.compile('正则表达式')
text = '一段字符串'
result = pattern.findall(text)

这些文章的作者,可能是被其他语言的坏习惯影响了,也可能是被其他垃圾文章误导了,不假思索拿来就用。

在Python里面,真的不需要使用re.compile!

为了证明这一点,我们来看Python的源代码。

在PyCharm里面输入:

1
2
3
import re

re.search

然后Windows用户按住键盘上的Ctrl键,鼠标左键点击search,Mac用户按住键盘上的Command键,鼠标左键点击search,PyCharm会自动跳转到Python的re模块。在这里,你会看到我们常用的正则表达式方法,无论是findall还是search还是sub还是match,全部都是这样写的:

1
_compile(pattern, flag).对应的方法(string)

例如:

1
2
3
4
5
6
7
8
9
def findall(pattern, string, flags=0):
"""Return a list of all non-overlapping matches in the string.

If one or more capturing groups are present in the pattern, return
a list of groups; this will be a list of tuples if the pattern
has more than one group.

Empty matches are included in the result."""
return _compile(pattern, flags).findall(string)

如下图所示:

然后我们再来看compile

1
2
3
def compile(pattern, flags=0):
"Compile a regular expression pattern, returning a Pattern object."
return _compile(pattern, flags)

如下图所示:

看出问题来了吗?

我们常用的正则表达式方法,都已经自带了compile了!

根本没有必要多此一举先re.compile再调用正则表达式方法。

此时,可能会有人反驳:

如果我有一百万条字符串,使用某一个正则表达式去匹配,那么我可以这样写代码:

1
2
3
4
texts = [包含一百万个字符串的列表]
pattern = re.compile('正则表达式')
for text in texts:
pattern.search(text)

这个时候,re.compile只执行了1次,而如果你像下面这样写代码:

1
2
3
texts = [包含一百万个字符串的列表]
for text in texts:
re.search('正则表达式', text)

相当于你在底层对同一个正则表达式执行了100万次re.compile

Talk is cheap, show me the code.

我们来看源代码,正则表达式re.compile调用的是_compile,我们就去看_compile的源代码,如下图所示:

红框中的代码,说明了_compile自带缓存。它会自动储存最多512条由type(pattern), pattern, flags)组成的Key,只要是同一个正则表达式,同一个flag,那么调用两次_compile时,第二次会直接读取缓存。

综上所述,请你不要再手动调用re.compile了,这是从其他语言(对的,我说的就是Java)带过来的陋习。

我们知道,写Python代码的时候,如果一个包(package)里面的一个模块要导入另一个模块,那么我们可以使用相对导入:

假设当前代码结构如下图所示:

其中test_1是一个包,在util.py里面想导入同一个包里面的read.py中的read函数,那么代码可以写为:

1
2
3
4
5
from .read import read


def util():
read()

其中的.read表示当前包目录下的read.py文件。此时read.py文件中的内容如下:

1
2
def read():
print('阅读文件')

通过包外面的main.py运行代码,运行效果如下图所示:

现在,我们增加一个数据文件,data.txt,它的内容如下图所示:

并且想通过read.py去读取这个数据文件并打印出来。

修改read.py,试图使用相对路径来打开这个文件:

1
2
3
4
def read():
with open('./data.txt', encoding='utf-8') as f:
text = f.read()
print(text)

运行代码发现报错:

这个原因很简单,就是如果数据文件的地址写为:./data.txt,那么Python就会从当前工作区文件夹里面寻找data.txt。由于我们运行的是main.py,那么当前工作区就是main.py所在的文件夹,而不是test_1文件夹。所以就会出现找不到文件的情况。

为了解决这个问题,我们有三种解决方式。

  • 使用绝对路径
1
2
3
4
def read():
with open('/Users/kingname/Project/DataFileExample/test_1/data.txt', encoding='utf-8') as f:
text = f.read()
print(text)

运行效果如下图所示:

  • 先获取read.py文件的绝对路径,再拼接出数据文件的绝对路径:
1
2
3
4
5
6
7
8
9
10
import os


def read():
basepath = os.path.abspath(__file__)
folder = os.path.dirname(basepath)
data_path = os.path.join(folder, 'data.txt')
with open(data_path, encoding='utf-8') as f:
text = f.read()
print(text)

运行效果如下图所示:

  • 使用pkgutil库
1
2
3
4
5
6
7
import pkgutil


def read():
data_bytes = pkgutil.get_data(__package__, 'data.txt')
data_str = data_bytes.decode()
print(data_str)

运行效果如下图所示:

pkgutil是Python自带的用于包管理相关操作的库,pkgutil能根据包名找到包里面的数据文件,然后读取为bytes型的数据。如果数据文件内容是字符串,那么直接decode()以后就是正文内容了。

为什么pkgutil读取的数据文件是bytes型的内容而不直接是字符串类型?
这是因为并不是所有数据文件都是字符串,如果某些数据文件是二进制文件或者图片,那么以字符串方式打开就会导致报错。所以为了通用,pkgutil会以bytes型方式读入数据,这相当于open函数的“rb”读取方式。

使用pkgutil还有一个好处,就是只要知道包名就可以找到对应包下面的数据文件,数据文件并不一定要在当前包里面。

例如修改代码结构如下图所示:

另一个包test_2里面有一个数据文件data2.txt。此时如果要在teat_1包的read.py中读取data2.txt中的内容,那么只需要修改pkgutil.get_data的第一个参数为test_2和数据文件的名字即可,运行效果如下图所示:

而前两种方法都不如pkgutil简单。

所以使用pkgutil可以大大简化读取包里面的数据文件的代码。

0%