本文轉載自測試人社區(ceshiren.com),原文鏈接:https://ceshiren.com/tag/精華帖
語言特性
- 談談對Python和其他語言的區別
答:Python是一門語法簡潔優美, 功能強大無比, 應用領域非常廣泛, 具有強大完備的第三方庫,它是一門強類型的可移植、可擴展、可嵌入的解釋型編程語言,屬於動態語言。
拿C語言和Python比:Python的第三方類庫比較齊全並且使用簡潔, 很少代碼就能實現一些功能,如果用C去實現相同的功能可能就比較復雜。
但是對於速度來說Python的運行速度相較於C就比較慢了。所以有利的同時也有弊端,畢竟我們的學習成本降低了。
- 簡述解釋型和編譯型編程語言
答:解釋型語言是在運行程序的時候才翻譯,每執行一次,要翻譯一次,效率較低。 編譯型就是直接編譯成機型可以執行的,只翻譯一次,所以效率相對來說較高。
- Python的解釋器種類以及相關特點?
答:CPythonc語言開發的,使用最廣的解釋器IPython基於cPython之上的一個交互式計時器,交互方式增強功能和cPython一樣PyPy目標是執行效率,采用JIT技術。
對Python代碼進行動態編譯,提高執行效率JPython運行在Java上的解釋器,直接把Python代碼編譯成Java字節碼執行IronPython運行在微軟.NET平台上的解釋器,把Python編譯成.NET的字節碼。
- Python3和Python2的區別?
答: 這里例舉5條print在Python3中是函數必須加括號,Python2中print為class。Python2中使用xrange,Python3使用range。
Python2中默認的字符串類型默認是ASCII,Python3中默認的字符串類型是Unicode。Python2中 /(除) 的結果是整型,Python3中是浮點類型。
Python2中聲明元類:metaclass = MetaClass, Python3中聲明元類:class newclass(metaclass=MetaClass):pass。
- Python3和Python2中int和long區別?
答:Python2有int和long類型。int類型最大值不能超過sys.maxint,而且這個最大值是平台相關的。可以通過在數字的末尾附上一個L來定義長整型,顯然,它比int類型表示的數字范圍更大。
在Python3里,只有一種整數類型int,大多數情況下,和Python2中的長整型類似。
- xrange和range的區別?
答:xrange是在Python2中的用法,Python3中只有rangexrange用法與range完全相同,所不同的是生成的不是一個list對象,而是一個生成器。
編碼規范
- 什么是PEP8?
答:PEP8通常會聽別人提到,但是具體的指什么內容呢,簡單介紹下。 《PythonEnhancementProposal # 8》(8 號 Python 增強提案)又叫 PEP8,
他針對的 Python 代碼格式而編訂的風格指南。
- 了解Python之禪么?
答:通過import this語句可以獲取其具體的內容。它告訴大家如何寫出高效整潔的代碼。
- 了解DocStrings么?
答:DocStrings文檔字符串是一個重要工具,用於解釋文檔程序,幫助你的程序文檔更加簡單易懂。主要是解釋代碼作用的。
- 了解類型注解么?
答:PEP484引入了類型提示,這使得可以對Python代碼進行靜態類型檢查。 在使用Ide的時候可以獲取到參數的類型,更方便傳入參數。使用格式如下
def foo(num:int) -> None:
print(f"接收到的數字是:{num}")
介紹下這個簡單例子,我們可以在函數的參數部分使用參數名 +:+類型,來指定參數可以接受的類型,這里的話就是num參數為int類型,然后后面->接的是返回值的類型。
這里返回值為None,然后通過fstring格式化字符串輸出傳入的數字。
- 例舉你知道Python對象的命名規范,例如方法或者類等
答:類:總是使用首字母大寫單詞串,如MyClass。內部類可以使用額外的前導下划線。
變量:小寫,由下划線連接各個單詞。方法名類似
常量:常量名所有字母大寫等
- Python中的注釋有幾種?
答:總體來說分為兩種,單行注釋和多行注釋。單行注釋在行首是# 。多行注釋可以使用三個單引號或三個雙引號,包括要注釋的內容。
- 如何優雅的給一個函數加注釋?
答:可以使用docstring配合類型注解
- 如何給變量加注釋?
答:可以通過變量名:類型的方式如下
a: str = "this is string type"
- Python代碼縮進中是否支持Tab鍵和空格混用。
答:不允許tab鍵和空格鍵混用,這種現象在使用sublime的時候尤為明顯。一般推薦使用4個空格替代tab鍵。
- 是否可以在一句import 中導入多個庫?
答:可以是可以,但是不推薦。因為一次導入多個模塊可讀性不是很好,所以一行導入一個模塊會比較好。同樣的盡量少用
from modulename import *,因為判斷某個函數或者屬性的來源有些困難,不方便調試,可讀性也降低了。
- 在給Py文件命名的時候需要注意什么?
答:給文件命名的時候不要和標准庫庫的一些模塊重復,比如abc。 另外要名字要有意義,不建議數字開頭或者中文命名。
- 例舉幾個規范Python代碼風格的工具
答:pylint和flake8
數據類型 - 字符串
- 列舉Python中的基本數據類型?
答: Python3中有六個標准的數據類型:字符串(String)、數字(Digit)、列表(List)、元組(Tuple)、集合(Sets)、字典(Dictionary)。
- 如何區別可變數據類型和不可變數據類型
答: 從對象內存地址方向來說
可變數據類型:在內存地址不變的情況下,值可改變(列表和字典是可變類型,但是字典中的key值必須是不可變類型)
不可變數據類型:內存改變,值也跟着改變。(數字,字符串,布爾類型,都是不可變類型)可以通過id()方法進行內存地址的檢測。
- 將"hello world"轉換為首字母大寫"Hello World"
答: 這個得看清題目是要求兩個單詞首字母都要大寫,如果只是第一個單詞首字母大小的話,只使用capitalize即可,但是這里是兩個單詞,所以用下面的方法。
arr = "hello world".split(" ")
new_str = f"{arr[0].capitalize()} {arr[1].capitalize()}"
print(new_str)
后來評論中有朋友提到了下面的方法,這里感謝這位朋友提醒。方案如下
"hello world".title()
非常簡單一句話搞定。
- 如何檢測字符串中只含有數字?
答:可以通過isdigit方法,例子如下
s1 = "12223".isdigit()
print(s1)
s2 = "12223a".isdigit()
print(s2)
# 結果如下:
# True
# False
- 將字符串"ilovechina"進行反轉
答:
s1 = "ilovechina"[::-1]
print(s1)
- Python中的字符串格式化方式你知道哪些?
答: % s,format,fstring(Python3.6開始才支持,現在推薦的寫法)
- 有一個字符串開頭和末尾都有空格,比如“ adabdw ”,要求寫一個函數把這個字符串的前后空格都去掉。
答:因為題目要是寫一個函數所以我們不能直接使用strip,不過我們可以把它封裝到函數啊
def strip_function(s1):
return s1.strip()
s1 = " adabdw "
print(strip_function(s1))
- 獲取字符串”123456“最后的兩個字符。
答:切片使用的考察,最后兩個即開始索引是 - 2,代碼如下
a = "123456"
print(a[-2::])
- 一個編碼為GBK的字符串S,要將其轉成UTF - 8編碼的字符串,應如何操作?
答:
a = "S".encode("gbk").decode("utf-8", 'ignore')
print(a)
- (1)s = "info:xiaoZhang 33 shandong",用正則切分字符串輸出['info', 'xiaoZhang', '33', 'shandong']。(2)a = "你好 中國 ",去除多余空格只留一個空格。
答:(1)我們需要根據冒號或者空格切分
import re
s = "info:xiaoZhang 33 shandong"
res = re.split(r":| ", s)
print(res)
(2)s = "你好 中國 "
print(" ".join(s.split()))
- (1)怎樣將字符串轉換為小寫。 (2)單引號、雙引號、三引號的區別?
答:(1)使用字符串的lower()方法。
(2)單獨使用單引號和雙引號沒什么區別,但是如果引號里面還需要使用引號的時候,就需要這兩個配合使用了,
然后說三引號,同樣的三引號也分為三單引號和三雙引號,兩個都可以聲名長的字符串時候使用,如果使用docstring就需要使用三雙引號。
數據類型 - 列表
- 已知AList = [1, 2, 3, 1, 2],對AList列表元素去重,寫出具體過程。
答:list(set(AList))
- 如何實現"1,2,3"變成["1", "2", "3"]
答:
s = "1,2,3"
print(s.split(","))
- 給定兩個list,A和B,找出相同元素和不同元素
答:
A、B中相同元素:
print(set(A) & set(B))
A、B中不同元素:
print(set(A) ^ set(B))
- [[1, 2], [3, 4], [5, 6]]一行代碼展開該列表,得出[1, 2, 3, 4, 5, 6]
答:
l = [[1, 2], [3, 4], [5, 6]]
x = [j for i in l for j in i]
print(x)
- 合並列表[1, 5, 7, 9]和[2, 2, 6, 8]
答:使用extend和 + 都可以。
a = [1, 5, 7, 9]
b = [2, 2, 6, 8]
a.extend(b)
print(a)
- 如何打亂一個列表的元素?
答:
import random
a = [1, 2, 3, 4, 5]
random.shuffle(a)
print(a)
數據類型 - 字典
- 字典操作中del 和pop有什么區別
答:del 可以根據索引(元素所在位置)來刪除的,沒有返回值。 pop可以根據索引彈出一個值,然后可以接收它的返回值。
- 按照字典的內的年齡排序
d1 = [{'name':'alice', 'age':38},
{'name':'bob', 'age':18},
{'name':'Carl', 'age':28},
]
答:
print(sorted(d1, key=lambda x:x["age"]))
- 請合並下面兩個字典a = {"A":1, "B":2}, b = {"C":3, "D":4}
答: 合並字典方法很多,可以使用
a.update(b)
或者下面字典解包的方式
a = {"A":1, "B":2}
b = {"C":3, "D":4}
print({**a, **b})
- 如何使用生成式的方式生成一個字典,寫一段功能代碼。
答:# 需求 3: 把字典的 key 和 value 值調換;
d = {'a':'1', 'b':'2'}
print({v:k for k, v in d.items()})
- 如何把元組("a", "b")和元組(1, 2),變為字典{"a":1, "b":2}
答: zip的使用,但是最后記得把zip對象再轉換為字典。
a = ("a", "b")
b = (1, 2)
print(dict(zip(a, b)))
數據類型 - 綜合
- 下列字典對象鍵類型不正確的是?
A:{1:0, 2:0, 3:0}
B:{"a":0, "b":0, "c":0}
C:{(1, 2):0, (2, 3):0}
D:{[1, 2]:0, [2, 3]:0}
答:D因為只有可hash的對象才能做字典的鍵,列表是可變類型不是可hash對象,所以不能用列表做為字典的鍵。
- 如何交換字典{"A":1, "B":2}的鍵和值
答:
s = {"A":1, "B":2}
# 方法一:
dict_new = {value: key for key,value in s.items()}
# 方法二:
new_s = dict(zip(s.values(),s.keys()))
- Python里面如何實現tuple和list的轉換?
答: Python中的類型轉換,一般通過類型強轉即可完成
tuple轉list是list()方法
list轉tuple使用tuple()方法
- 我們知道對於列表可以使用切片操作進行部分元素的選擇,那么如何對生成器類型的對象實現相同的功能呢?
答:這個題目考察了Python標准庫的itertools模快的掌握情況,該模塊提供了操作生成器的一些方法。 對於生成器類型我們使用islice方法來實現切片的功能。例子如下
from itertools import islice
gen = iter(range(10)) # iter()函數用來生成迭代器
# 第一個參數是迭代器,第二個參數起始索引,第三個參數結束索引,不支持負數索引
for i in islice(gen, 0, 4):
print(i)
- 請將[i for i in range(3)] 改成生成器
答:通過把列表生產式的中括號,改為小括號我們就實現了生產器的功能即,
(i for i in range(3))
- a = "hello"和b = "你好"編碼成bytes類型
答: 這個題目一共三種方式,第一種是在字符串的前面加一個b,第二種可以使用bytes方法,第三種使用字符串encode方法。具體代碼如下,
abc代表三種情況
a = b"hello"
b = bytes("你好", "utf-8")
c = "你好".encode("utf-8")
print(a, b, c)
- 下面的代碼輸出結果是什么?
a = (1, 2, 3, [4, 5, 6, 7], 8)
a[2] = 2
答: 我們知道元組里的元素是不能改變的所以這個題目的答案是出現異常。
- 下面的代碼輸出的結果是什么?
a = (1, 2, 3, [4, 5, 6, 7], 8)
a[3][0] = 2
答:前面我說了元組的里元素是不能改變的,這句話嚴格來說是不准確的,如果元組里面元素本身就是可變類型,比如列表,那么在操作這個元素里的對象時,其內存地址也是不變的。
a[3]對應的元素是列表,然后對列表第一個元素賦值,所以最后的結果是: (1, 2, 3, [2, 5, 6, 7], 8)
操作類題目
- Python交換兩個變量的值
答:在Python中交換兩個對象的值通過下面的方式即可
a, b = b, a
但是需要強調的是這並不是元組解包,通過dis模塊可以發現,這是交換操作的字節碼是ROT_TWO,意思是在棧的頂端做兩個值的互換操作。
- 在讀文件操作的時候會使用read、readline或者readlines,簡述它們各自的作用
答:read()每次讀取整個文件,它通常用於將文件內容放到一個字符串變量中。如果希望一行一行的輸出那么就可以使用readline(),
該方法會把文件的內容加載到內存,所以對於對於大文件的讀取操作來說非常的消耗內存資源,此時就可以通過readlines方法,將文件的句柄生成一個生產器,然后去讀就可以了。
- json序列化時,可以處理的數據類型有哪些?如何定制支持datetime類型?
答: 可以處理的數據類型是str、int、list、tuple、dict、bool、None, 因為datetime類不支持json序列化,所以我們對它進行拓展。
# 自定義時間序列化
import json
from datetime import datetime, date
# JSONEncoder 不知道怎么去把這個數據轉換成 json 字符串的時候
# ,它就會去調 default()函數,所以都是重寫這個函數來處理它本身不支持的數據類型,
# default()函數默#認是直接拋異常的。
class DateToJson(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime):
return obj.strftime('%Y-%m-%d %H:%M:%S')
elif isinstance(obj, date):
return obj.strftime('%Y-%m-%d')
else :
return json.JSONEncoder.default(self, obj)
d = {'name': 'cxa', 'data': datetime.now()}
print(json.dumps(d, cls=DateToJson))
- json序列化時,默認遇到中文會轉換成unicode,如果想要保留中文怎么辦?
答:可以通過json.dumps的ensure_ascii參數解決,代碼示例如下:
import json
a = json.dumps({"name":"張三"}, ensure_ascii = False)
print(a)
- 有兩個磁盤文件A和B,各存放一行字母,要求把這兩個文件中的信息合並(按字母順序排列),輸出到一個新文件C中。
答:
# 文件 A.txt 內容為 ASDCF
# 文件 B.txt 內容為 EFGGTG
with open("A.txt") as f1:
f1_txt = f1.readline()
with open("B.txt") as f2:
f2_txt = f2.readline()
f3_txt = f1_txt + f2_txt
f3_list = sorted(f3_txt)
with open("C.txt", "a+") as f:
f.write("".join(f3_list))
# 輸出的文件C的內容為ACDEFFGGGST
- 如果當前的日期為20190530,要求寫一個函數輸出N天后的日期,(比如 N 為 2,則輸出 20190601)。
答:這個題目考察的是datetime里的timedelta方法的使用,參數可選、默認值都為0:
datetime.timedelta(days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0)
通過這個參數可以指定不同的日期類型進行加減操作,這里我們需要改的是days,代碼如下
import datetime
def datetime_operate(n:int):
now = datetime.datetime.now() # 獲取當前時間
_new_date = now + datetime.timedelta(days=n) # 獲取指定天數后的新日期
new_date = _new_date.strftime("%Y%m%d") # 轉換為指定的輸出格式
return new_date
if __name__ == '__main__':
print(datetime_operate(4))
- 寫一個函數,接收整數參數n,返回一個函數,函數的功能是把函數的參數和n相乘並把結果返回。
答:這個題目考查了閉包的使用代碼示例如下,返回函數之類型是函數對象。
def mul_operate(num):
def g(val):
return num * val
return g
m = mul_operate(8)
print(m(5))
- 下面代碼會存在什么問題,如何改進?
def strappend(num):
str = 'first'
for i in range(num):
str += str(i)
return str
答: 首先不應該使用Python的內置類似str作為變量名這里我把它改為了s, 另外在Python, str是個不可變對象,每次迭代都會生成新的存儲空間,
num越大,創建的str對象就會越多,內存消耗越大。使用yield 改成生成器即可, 還有一點就是命名規范的位置,函數名改為_分割比較好,完整的代碼如下:
def str_append(num):
s = 'first'
for i in range(num):
s += str(i)
yield s
if __name__ == '__main__':
for i in str_append(3):
print(i)
- 一行代碼輸出1 - 100之間的所有偶數。
答:可以通過列表生成式,然后使用與操作如果如1與之后結果為0則表明為偶數,等於1則為奇數。
# 方法1
print([i for i in range(1, 101) if i & 0x1 == 0])
# 方法2:測試發現方法二效率更高
print(list(range(2, 101, 2)))
- with 語句的作用,寫一段代碼?
with 語句適用於對資源進行訪問的場合,確保不管使用過程中是否發生異常都會執行必要的“清理”操作,釋放資源,比如文件使用后自動關閉、線程中鎖的自動獲取和釋放等。
其他的內容看下面我之前寫的代碼。
# 一般訪問文件資源時我們會這樣處理:
f = open(
'c:\test.txt', 'r')
data = f.read()
f.close()
# 這樣寫沒有錯,但是容易犯兩個毛病:
# 1. 如果在讀寫時出現異常而忘了異常處理。
# 2. 忘了關閉文件句柄
# 以下的加強版本的寫法:
f = open('c:\test.txt', 'r')
try:
data = f.read()
finally:
f.close()
# 以上的寫法就可以避免因讀取文件時異常的發生而沒有關閉問題的處理了。代碼長了一些。
# 但使用 with 有更優雅的寫法:
with open(r'c:\test.txt', 'r') as f:
data = f.read()
# with 的實現
class Test:
def __enter__(self):
print('__enter__() is call!')
return self
def dosomething(self):
print('dosomethong!')
def __exit__(self, exc_type, exc_value, traceback):
print('__exit__() is call!')
print(f'type:{exc_type}')
print(f'value:{exc_value}')
print(f'trace:{traceback}')
print('__exit()__ is call!')
with Test() as sample:
pass
# 當對象被實例化時,就會主動調用__enter__()方法,任務執行完成后就會調用__exit__()方法,
# 另外,注意到,__exit__()方法是帶有三個參數的(exc_type, exc_value, traceback),
# 依據上面的官方說明:如果上下文運行時沒有異常發生,那么三個參數都將置為 None,
# 這里三個參數由於沒有發生異常,的確是置為了 None, 與預期一致.
# 修改后不出異常了
class Test:
def __enter__(self):
print('__enter__() is call!')
return self
def dosomething(self):
x = 1 / 0
print('dosomethong!')
def __exit__(self, exc_type, exc_value, traceback):
print('__exit__() is call!')
print(f'type:{exc_type}')
print(f'value:{exc_value}')
print(f'trace:{traceback}')
print('__exit()__ is call!')
return True
with Test() as sample:
pass
- Python字典和json字符串相互轉化方法
答:在Python中使用dumps方法將dict對象轉為Json對象,使用loads方法可以將Json對象轉為dict對象。
dic = {'a': 123, 'b': "456", 'c': "liming"}
json_str = json.dumps(dic)
print(json_str)
dic2 = json.loads(json_str)
print(dic2)
# '{"a": 123, "b": "456", "c": "liming"}'
# {'a': 123, 'b': '456', 'c': 'liming'}
我們再來看一個特殊的例子
import json
dic = {'a': 123, 'b': "456", 'c': "liming"}
dic_str = json.loads(str(dic).replace("'", "\""))
print(dic_str)
下面我解釋下上面代碼是測試什么:
首先json.loads(jsonstr)這里面的參數只能是jsonstr格式的字符串.當我們使用str將字典dic轉化為字符串以后,
得到的結果為: "{'a': 123, 'b': '456', 'c': 'liming'}"。如果直接使用json.loads(str(dic))你會發現出現錯誤,
原因就是,單引號的字符串不符合Json的標准格式所以再次使用了replace("'", """)。得到字典
其實這個例子主要目的是告訴大家Json的標准格式是不支持單引號型字符串的,否則會出現以下錯誤。
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column2(char1)
- 請寫一個Python邏輯,計算一個文件中的大寫字母數量
答:
with open('A.txt') as fs:
count = 0
for i in fs.read():
if i.isupper():
count += 1
print(count)
- 請寫一段Python連接Mongo數據庫,然后的查詢代碼。
答:
# -*- coding: utf-8 -*-
import pymongo
db_configs = {
'type': 'mongo',
'host': '地址',
'port': '端口',
'user': 'spider_data',
'passwd': '密碼',
'db_name': 'spider_data'
}
class Mongo():
def __init__(self, db=db_configs["db_name"], username=db_configs["user"],
password=db_configs["passwd"]):
self.client = pymongo.MongoClient(f'mongodb://{db_configs["host"]}:db_configs["port"]')
self.username = username
self.password = password
if self.username and self.password:
self.db1 = self.client[db].authenticate(self.username, self.password)
self.db1 = self.client[db]
def find_data(self):
# 獲取狀態為0的數據
data = self.db1.test.find({"status": 0})
gen = (item for item in data)
return gen
if __name__ == '__main__':
m = Mongo()
print(m.find_data())
- 說一說Redis的基本類型
答: Redis支持五種數據類型: string(字符串) 、 hash(哈希)、list(列表) 、 set(集合) 及zset(sortedset: 有序集合)。
- 請寫一段Python連接Redis數據庫的代碼。
答:
from redis import StrictRedis, ConnectionPool
redis_url = "redis://:xxxx@112.27.10.168:6379/15"
pool = ConnectionPool.from_url(redis_url, decode_responses=True)
r = StrictRedis(connection_pool=pool)
- 請寫一段Python連接Mysql數據庫的代碼。
答:
conn = pymysql.connect(host='localhost',
port=3306, user='root',
passwd='1234', db='user', charset='utf8mb4') # 聲明mysql連接對象
cursor = conn.cursor(cursor=pymysql.cursors.DictCursor) # 查詢結果以字典的形式
cursor.execute('sql語句字符串') # 執行sql語句
conn.close() # 關閉鏈接
- 了解Redis的事務么
答: 簡單理解,可以認為redis事務是一些列redis命令的集合,並且有如下兩個特點:
1.事務是一個單獨的隔離操作:事務中的所有命令都會序列化、按順序地執行。事務在執行的過程中,不會被其他客戶端發送來的命令請求所打斷。
2.事務是一個原子操作:事務中的命令要么全部被執行,要么全部都不執行。 一般來說,事務有四個性質稱為ACID,分別是原子性,一致性,隔離性和持久性。一個事務從開始到執行會經歷以下三個階段:
開始事務\命令入隊\執行事務
代碼示例:
import redis
import sys
def run():
try:
conn = redis.StrictRedis('192.168.80.41')
# Python中redis事務是通過pipeline的封裝實現的
pipe = conn.pipeline()
pipe.sadd('s001', 'a')
sys.exit()
# 在事務還沒有提交前退出,所以事務不會被執行。
pipe.sadd('s001', 'b')
pipe.execute()
pass
except Exception as err:
print(err)
pass
if __name__ == "__main__":
run()
- 了解數據庫的三范式么?
答: 經過研究和對使用中問題的總結,對於設計數據庫提出了一些規范,這些規范被稱為范式一般需要遵守下面3范式即可: \
- 第一范式(1NF):強調的是列的原子性,即列不能夠再分成其他幾列。
- 第二范式(2NF):首先是1NF,另外包含兩部分內容,一是表必須有一個主鍵;二是沒有包含在主鍵中的列必須完全依賴於主鍵,而不能只依賴於主鍵的一部分。
- 第三范式(3NF):首先是2NF,另外非主鍵列必須直接依賴於主鍵,不能存在傳遞依賴。即不能存在:非主鍵列A依賴於非主鍵列B,非主鍵列B依賴於主鍵的情況。
- 了解分布式鎖么
答: 分布式鎖是控制分布式系統之間的同步訪問共享資源的一種方式。 對於分布式鎖的目標,我們必須首先明確三點:
- 任何一個時間點必須只能夠有一個客戶端擁有鎖。
- 不能夠有死鎖,也就是最終客戶端都能夠獲得鎖,盡管可能會經歷失敗。
- 錯誤容忍性要好,只要有大部分的Redis實例存活,客戶端就應該能夠獲得鎖。
分布式鎖的條件
- 互斥性:分布式鎖需要保證在不同節點的不同線程的互斥
- 可重入性:同一個節點上的同一個線程如果獲取了鎖之后,能夠再次獲取這個鎖。
- 鎖超時:支持超時釋放鎖,防止死鎖
- 高效,高可用:加鎖和解鎖需要高效,同時也需要保證高可用防止分布式鎖失效,可以增加降級。
- 支持阻塞和非阻塞:可以實現超時獲取失敗,tryLock(longtimeOut) 支持公平鎖和非公平鎖
分布式鎖的實現方案
- 1、數據庫實現(樂觀鎖)
- 2、基於zookeeper的實現
- 3、基於Redis的實現(推薦)
- 用Python實現一個Reids的分布式鎖的功能
答: REDIS分布式鎖實現的方式:SETNX + GETSET, NX是NoteXists的縮寫,如SETNX命令就應該理解為:SET if NoteXists。 多個進程執行以下Redis命令:
SETNX
lock.foo < current
Unix
time + lock
timeout + 1 >
如果SETNX返回1,說明該進程獲得鎖,SETNX將鍵lock.foo的值設置為鎖的超時時間(當前時間 + 鎖的有效時間)。
如果SETNX返回0,說明其他進程已經獲得了鎖,進程不能進入臨界區。進程可以在一個循環中不斷地嘗試SETNX操作,以獲得鎖。
import time
import redis
from conf.config import REDIS_HOST, REDIS_PORT, REDIS_PASSWORD
class RedisLock:
def __init__(self):
self.conn = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=1)
self._lock = 0
self.lock_key = ""
@staticmethod
def my_float(timestamp):
"""
Args:
timestamp:
Returns:
float或者0
如果取出的是None,說明原本鎖並沒人用,getset已經寫入,返回0,可以繼續操作。
"""
if timestamp:
return float(timestamp)
else:
# 防止取出的值為None,轉換float報錯
return 0
@staticmethod
def get_lock(cls, key, timeout=10):
cls.lock_key = f"{key}_dynamic_lock"
while cls._lock != 1:
timestamp = time.time() + timeout + 1
cls._lock = cls.conn.setnx(cls.lock_key, timestamp)
# if 條件中,可能在運行到or之后被釋放,也可能在and之后被釋放
# 將導致 get到一個None,float失敗。
if cls._lock == 1 or (
time.time() > cls.my_float(cls.conn.get(cls.lock_key)) and
time.time() > cls.my_float(cls.conn.getset(cls.lock_key, timestamp))):
break
else:
time.sleep(0.3)
@staticmethod
def release(cls):
if cls.conn.get(cls.lock_key) and time.time() < cls.conn.get(cls.lock_key):
cls.conn.delete(cls.lock_key)
def redis_lock_deco(cls):
def _deco(func):
def __deco(*args, **kwargs):
cls.get_lock(cls, args[1])
try:
return func(*args, **kwargs)
finally:
cls.release(cls)
return __deco
return _deco
@redis_lock_deco(RedisLock())
def my_func():
print("myfunc() called.")
time.sleep(20)
if __name__ == "__main__":
my_func()
- 寫一段Python使用mongo數據庫創建索引的代碼
答:
# -*- coding: utf-8 -*-
# @Time : 2018/12/28 10:01 AM
import pymongo
db_configs = {
'type': 'mongo',
'host': '地址',
'port': '端口',
'user': 'spider_data',
'passwd': '密碼',
'db_name': 'spider_data'
}
class Mongo():
def __init__(self, db=db_configs["db_name"], username=db_configs["user"],
password=db_configs["passwd"]):
self.client = pymongo.MongoClient(f'mongodb://{db_configs["host"]}:{db_configs["port"]}')
self.username = username
self.password = password
if self.username and self.password:
self.db1 = self.client[db].authenticate(self.username, self.password)
self.db1 = self.client[db]
def add_index(self):
# 通過create_index添加索引
self.db1.test.create_index([('name', pymongo.ASCENDING)], unique=True)
def get_index(self, ):
# 查看索引列表
indexlist = self.db1.test.list_indexes()
for index in indexlist:
print(index)
if __name__ == '__main__':
m = Mongo()
m.add_index()
print(m.get_index())
高級特性
- 函數裝飾器有什么作用?請列舉說明?
答: 裝飾器就是一個函數,它可以在不需要做任何代碼變動的前提下給一個函數增加額外功能,啟動裝飾的效果。 它經常用於有切面需求的場景,
比如:插入日志、性能測試、事務處理、緩存、權限校驗等場景。 下面是一個日志功能的裝飾器
from functools import wraps
def log(label):
def decorate(func):
@wraps(func)
def _wrap(*args, **kwargs):
try:
func(*args, **kwargs)
print("name", func.__name__)
except Exception as e:
print(e.args)
return _wrap
return decorate
@log("info")
def foo(a, b, c):
print(a + b + c)
print("in foo")
decorate=decorate(foo)
if __name__ == '__main__':
foo(1, 2, 3)
decorate()
- Python垃圾回收機制?
答:Python不像C++,Java等語言一樣,他們可以不用事先聲明變量類型而直接對變量進行賦值。對Python語言來講,對象的類型和內存都是在運行時確定的。
這也是為什么我們稱Python語言為動態類型的原因。主要體現在下面三個方法:
- 1.引用計數機制
- 2.標記 - 清除
- 3.分代回收
- 魔法函數_call_怎么使用?
答: _call_可以把類實例當做函數調用。 使用示例如下
class Bar:
def __call__(self, *args, **kwargs):
print('in call')
if __name__ == '__main__':
b = Bar()
b()
- 如何判斷一個對象是函數還是方法?
答:看代碼已經結果就懂了
from types import MethodType, FunctionType
class Bar:
def foo(self):
pass
def foo2():
pass
def run():
print("foo 是函數", isinstance(Bar().foo, FunctionType))
print("foo 是方法", isinstance(Bar().foo, MethodType))
print("foo2 是函數", isinstance(foo2, FunctionType))
print("foo2 是方法", isinstance(foo2, MethodType))
if __name__ == '__main__':
run()
'''
foo是函數,False
foo是方法,True
foo2是函數,True
foo2是方法,False
'''
- @ classmethod和 @ staticmethod用法和區別
答:相同之處:@staticmethod和 @classmethod都可以直接類名.方法名()來調用,不用在示例化一個類。
@classmethod:我們要寫一個只在類中運行而不在實例中運行的方法。如果我們想讓方法不在實例中運行,可以這么做:
def iget_no_of_instance(ins_obj):
return ins_obj.__class__.no_inst
class Kls(object):
no_inst = 0
def __init__(self):
Kls.no_inst = Kls.no_inst + 1
ik1 = Kls()
ik2 = Kls()
print(iget_no_of_instance(ik1))
@staticmethod:經常有一些跟類有關系的功能但在運行時又不需要實例和類參與的情況下需要用到靜態方法IND = 'ON'
class Kls(object):
def __init__(self, data):
self.data = data
@staticmethod
def check_ind():
return (IND == 'ON')
def do_reset(self):
if self.check_ind():
print('Reset done for:', self.data)
def set_db(self):
if self.check_ind():
self.db = 'New db connection'
print('DB connection made for: ', self.data)
ik1 = Kls(12)
ik1.do_reset()
ik1.set_db()
- Python中的接口如何實現?
答: 接口提取了一群類共同的函數,可以把接口當做一個函數的集合,然后讓子類去實現接口中的函數。但
是在Python中根本就沒有一個叫做interface的關鍵字,如果非要去模仿接口的概念,可以使用抽象類來實現。抽象類是一個特殊的類,它的特殊之處在於只能被繼承,不能被實例化。
使用abc模塊來實現抽象類。
- Python中的反射了解么?
答:Python的反射機制設定較為簡單,一共有四個關鍵函數分別是getattr、hasattr、setattr、delattr。
- metaclass作用?以及應用場景?
答: metaclass即元類,metaclass是類似創建類的模板,所有的類都是通過他來create的(調用new),這使得你可以自由的控制創建類的那個過程,實現你所需要的功能。
我們可以使用元類創建單例模式和實現ORM模式。
- hasattr()、getattr()、setattr()的用法
答:這三個方法屬於Python的反射機制里面的,
hasattr可以判斷一個對象是否含有某個屬性,
getattr可以充當get獲取對象屬性的作用,
而setattr可以充當person.name = "liming"的賦值操作。代碼示例如下:
class Person():
def __init__(self):
self.name = "liming"
self.age = 12
def show(self):
print(self.name)
print(self.age)
def set_name(self):
setattr(Person, "sex", "男")
def get_name(self):
print(getattr(self, "name"))
print(getattr(self, "age"))
print(getattr(self, "sex"))
def run():
if hasattr(Person, "show"):
print("判斷 Person 類是否含有 show 方法")
Person().set_name()
Person().get_name()
if __name__ == '__main__':
run()
- 請列舉你知道的Python的魔法方法及用途。
答:
- 1.
__init__
:類的初始化方法。它獲取任何傳給構造器的參數(比如我們調用x = SomeClass(10, ‘foo’)
,__init__
就會接到參數10和 ‘foo’ 。
__init__
在Python的類定義中用的最多。 - 2.
__new__
:__new__
是對象實例化時第一個調用的方法,它只取下cls參數,並把其他參數傳給__init__
。
__new__
很少使用,但是也有它適合的場景,尤其是當類繼承自一個像元組或者字符串這樣不經常改變的類型的時候. - 3.
__del__
:__new__
和__init__
是對象的構造器,__del__
是對象的銷毀器。它並非實現了語句del x(因此該語句不等同於x.del())。
而是定義了當對象被垃圾回收時的行為。當對象需要在銷毀時做一些處理的時候這個方法很有用,比如socket對象、文件對象。
但是需要注意的是,當Python解釋器退出但對象仍然存活的時候,__del__
並不會執行。 所以養成一個手工清理的好習慣是很重要的,比如及時關閉連接。
- 如何知道一個Python對象的類型?
答:可以通過type方法
- Python的傳參是傳值還是傳址?
答:Python中的傳參即不是傳值也不是傳地址,傳的是對象的引用。
- Python中的元類(metaclass)使用舉例
答:可以使用元類實現一個單例模式,代碼如下:
class Singleton(type):
def __init__(self, *args, **kwargs):
print("in __init__")
self.__instance = None
super(Singleton, self).__init__(*args, **kwargs)
def __call__(self, *args, **kwargs):
print("in __call__")
if self.__instance is None:
self.__instance = super(Singleton, self).__call__(*args, **kwargs)
return self.__instance
class Foo(metaclass=Singleton):
pass # 在代碼執行到這里的時候,元類中的__new__方法和__init__方法其實已經被執行了,而不是在 Foo 實例化的時候執行。且僅會執行一次。
foo1 = Foo()
foo2 = Foo()
print(foo1 is foo2)
- 簡述any()和all()方法
答:
any(x):判斷x對象是否為空對象,如果都為空、0、false,則返回false,如果不都為空、0、false,則返回true。
all(x):如果all(x)參數x對象的所有元素不為0、''、False或者x為空對象,則返回True,否則返回False。
- filter方法求出列表所有奇數並構造新列表,a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
答:
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(list(filter(lambda x: x % 2 == 1, a)))
其實現在不推薦使用filter, map等方法了,一般列表生成式就可以搞定了。
- 什么是猴子補丁?
答: 猴子補丁(monkeypatching):在運行時動態修改模塊、類或函數,通常是添加功能或修正缺陷。猴子補丁在代碼運行時內存中)發揮作用,不會修改源碼,因此只對當前運行的程序實例有效。
因為猴子補丁破壞了封裝,而且容易導致程序與補丁代碼的實現細節緊密耦合,所以被視為臨時的變通方案,不是集成代碼的推薦方式。大概是下面這樣的一個效果
def post():
print("this is post")
print("想不到吧")
class Http():
@classmethod
def get(self):
print("this is get")
def main():
Http.get = post # 動態的修改了 get 原因的功能,
if __name__ == '__main__':
main()
Http.get()
- 在Python中是如何管理內存的?
答:
- 垃圾回收:Python不像C + +,Java等語言一樣,他們可以不用事先聲明變量類型而直接對變量進行賦值。對Python語言來講,對象的類型和內存都是在運行時確定的。這也是為什么我們稱Python語言為動態類型的原因(這里我們把動態類型可以簡單的歸結為對變量內存地址的分配是在運行時自動判斷變量類型並對變量進行賦值。
- 引用計數:Python采用了類似Windows內核對象一樣的方式來對內存進行管理。每一個對象,都維護這一個對指向該對對象的引用的計數。當變量被綁定在一個對象上的時候,該變量的引用計數就是1,(還有另外一些情況也會導致變量引用計數的增加),系統會自動維護這些標簽,並定時掃描,當某標簽的引用計數變為0的時候,該對就會被回收。
- 內存池機制:Python的內存機制以金字塔行,1、2層主要有操作系統進行操作;第0層是C中的malloc,free等內存分配和釋放函數進行操作;
第1層和第2層是內存池,有Python的接口函數PyMem_Malloc函數實現,當對象小於256K時有該層直接分配內存;
第3層是最上層,也就是我們對Python對象的直接操作在C中如果頻繁的調用malloc與free時,是會產生性能問題的。再加上頻繁的分配與釋放小塊的內存會產生內存碎片。
Python在這里主要干的工作有:如果請求分配的內存在1~256字節之間就使用自己的內存管理系統,否則直接使用malloc。
這里還是會調用malloc分配內存,但每次會分配一塊大小為256k的大塊內存。經由內存池登記的內存到最后還是會回收到內存池,並不會調用C的free釋放掉以便下次使用。
對於簡單的Python對象,例如數值、字符串,元組(tuple不允許被更改)采用的是復制的方式(深拷貝?),也就是說當將另一個變量B賦值給變量A時,雖然A和B的內存空間仍然相同,但當A的值發生變化時,會重新給A分配空間,A和B的地址變得不再相同。
- 當退出Python時是否釋放所有內存分配?
答:不是的,循環引用其他對象或引用自全局命名空間的對象的模塊,在Python退出時並非完全釋放。另外,也不會釋放c庫保留的內存部分
正則表達式
- (1)使用正則表達式匹配出 < html > < h1\ > www.baidu.com < / h1 > < / html > 中的地址,(2)a = "張明 98 分",用re.sub,將98替換為100
答:第一問答案
import re
source = "<html><h1>www.baidu.com</h1></html>"
pat = re.compile("<html><h1>(.*?)</h1></html>")
print(pat.findall(source)[0])
第二問答案
import re
s = "張明 98 分"
print(re.sub(r"\d+", "100", s))
- **正則表達式匹配中(. )和(. ?)匹配區別?
答:(.)為貪婪模式極可能多的匹配內容, (. ?)為非貪婪模式又叫懶惰模式,一般匹配到結果就好,匹配字符的少為主,示例代碼如下
import re
s = "<html><div>文本 1</div><div>文本 2</div></html>"
pat1 = re.compile(r"\<div>(.*?)\</div>")
print(pat1.findall(s))
pat2 = re.compile(r"\<div>(.*)\</div>")
print(pat2.findall(s))
# 輸出
# ['文本 1', '文本 2']
# ['文本 1</div><div>文本 2']
- 寫一段匹配郵箱的正則表達式
答:關於郵箱的匹配這個還真的是一個永恆的話題。
電子郵件地址有統一的標准格式:用戶名 @ 服務器域名。用戶名表示郵件信箱、注冊名或信件接收者的用戶標識,@符號后是你使用的郵件服務器的域名。
@可以讀成“at”,也就是“在”的意思。整個電子郵件地址可理解為網絡中某台服務器上的某個用戶的地址。用戶名,可以自己選擇。
由字母a~z(不區分大小寫)、數字0~9、點、減號或下划線組成;只能以數字或字母開頭和結尾。與你使用的網站有關,代表郵箱服務商。例如網易的有@163.com;新浪有@vip.sina.com等。
網上看到了各種各樣的版本,都不確定用哪個,於是自己簡單的總結了一個。大家有更好的歡迎留言。
r"^[a-zA-Z0-9]+[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
下面解釋上面的表達式,首先強調一點關於\w的含義,\w匹配英文字母和俄語字母或數字或下划線或漢字。
注意 ^ []
和[ ^]
的區別,[]
表示字符集合,^ []
表示以[]
內的任意字符集開始,[ ^]
表示。
^ [a - zA - Z0 - 9] +
:這里注意 ^ []
和[ ^]
的, 第一個 ^ 表示已什么開頭,第二個[]
的 ^ 表示不等於[]內。所以這段表示以英文字母和數字開頭,后面緊跟的 +,限定其個數 >= 1個。
[a - zA - Z0 - 9. + -] +
:表示匹配英文字母和數字開頭以及. + -, 的任意一個字符,並限定其個數 >= 1個。為了考慮 @ 前面可能出現. + -(但是不在開頭出現)。@就是郵箱必備符號了@
[a - zA - Z0 - 9 -] +\.
:前面的不用說了,后面的.表示.轉義了, 也是必備符號。
[a - zA - Z0 - 9 -.]+
:$符表示以什么結束, 這里表示以英文字和數字或 - .1個或多個結尾。
來個例子驗證一波:
import re
plt = re.compile(r"^[a-zA-Z0-9]+[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
b = plt.findall('adas+fefe.we@qq.com.cn')
print(b)
網上找了個驗證郵件地址的通用正則表達式(符合RFC5322標准)
(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])
其他內容
- 解釋一下Python中pass語句的作用?
答:pass實際上就是一個占位符,在寫一個函數但是不確定里面寫啥的時候,這個時候可以使用pass。示例如下
def foo():
pass
- 簡述你對input()函數的理解
答:在Python3中input函數可以接收用戶輸入的字符串。然后根據程序的需要轉換成所需格式即可。
- Python中的 is 和 ==
答:先說 == 它的作用是判斷兩個對象的值是否相同,然后說 is。 is 表示的誰是誰,這也就意味着對象完全相等。我們知道一個對象有各自的內存地址和對應的值,當內存地址和值都相同的時候使用 is 可以得到結果
True。另外需要注意的下面兩點特殊的情況。這些變量很可能在許多程序中使用。 通過池化這些對象,Python可以防止對一致使用的對象進行內存分配調用。
1.介於數字 - 5和256之間的整數
2.字符串僅包含字母、數字或下划線
- Python中的作用域
答:Python中,一個變量的作用域總是由在代碼中被賦值的地方所決定當Python遇到一個變量的話它會按照這的順序進行搜索本地作用域(Local) - -->當前作用域被嵌入的本地作用域(Enclosinglocals)
--->全局 / 模塊作用域(Global) - -->內置作用域(Built - in)
- 三元運算寫法和應用場景?
答:Python中的三元運算又稱三目運算,是對簡單的條件語句的簡寫。 是一種比較Pythonic的學法,形式為:val = 1 if 條件成立 else 2
代碼示例如下:
a = 2
b = 5
# 普通寫法
if a > b:
val = True
else :
val = False
# 改為三元運算符后
val = a if a > b else b
print(val) # 5
- 了解enumerate么?
答:enumerate
可以在迭代一個對象的時候,同時獲取當前對象的索引和值。 代碼示例如下
from string import ascii_lowercase
s = ascii_lowercase
for index, value in enumerate(s):
print(index, value)
- 列舉5個Python中的標准模塊
答: pathlib:路徑操作模塊,比os模塊拼接方便。 urllib:網絡請求模塊,包括對url的結構解析。 asyncio: Python的異步庫,基於事件循環的協程模塊。
re:正則表達式模塊。 itertools:提供了操作生成器的一些模塊。
- 如何在函數中設置一個全局變量
答:# 通過使用 global 對全局變量進行修改。
n = 0
def foo():
global n
n = 100
foo()
print(n)
x = 0
- pathlib的用法舉例
答:pathlib是面向對象的文件系統路徑,可以對文件以及文件的其他屬性進行操作。比較喜歡的一點是路徑拼接符"/"的使用。
from pathlib import Path, PurePath
p = Path('.')
# 列出子目錄
for x in p.iterdir():
print(f"path:{x},is_dir:{x.is_dir()}")
# 路徑拼接
>>> p = PurePath('/etc')
>>> p / 'init.d' / 'apache2'
PurePosixPath('/etc/init.d/apache2')
>>> q = PurePath('bin')
>>> '/usr' / q
PurePosixPath('/usr/bin')
- Python中的異常處理,寫一個簡單的應用場景
答: 比如在計算除法中出現為0的情況出現異常
try:
1 / 0
except ZeroDivisionError as e:
print(e.args)
- Python中遞歸的最大次數,那如何突破呢?
答:Python有遞歸次數限制,默認最大次數為1000。通過下面的代碼可以突破這個限制
import sys
sys.setrecursionlimit(1500) # set the maximum depth as 1500
另外需要注意的是sys.setrecursionlimit()
只是修改解釋器在解釋時允許的最大遞歸次數,此外,限制最大遞歸次數的還和操作系統有關。
- 什么是面向對象的mro
答:Python是支持面向對象編程的,同時也是支持多重繼承的。一般我們通過調用類對象的mro()方法獲取其繼承關系。
- isinstance作用以及應用場景?
答:isinstance是判斷一個對象是否為另一個對象的子類的,例如我們知道在Python3中bool類型其實是int的子類,所以我們可以對其檢測。
print(isinstance(True, int))
- 什么是斷言?應用場景?
答:在Python中是斷言語句assert 實現此功能,一般在表達式為True的情況下,程序才能通過。
# assert()方法,斷言成功,則程序繼續執行,斷言失敗,則程序報錯
# 斷言能夠幫助別人或未來的你理解代碼,
# 找出程序中邏輯不對的地方。一方面,
# 斷言會提醒你某個對象應該處於何種狀態,
# 另一方面,如果某個時候斷言為假,
# 會拋出 AssertionError 異常,很有可能終止程序。
def foo(a):
assert a == 2, Exception("不等於 2")
print("ok", a)
if __name__ == '__main__':
foo(1)
- lambda 表達式格式以及應用場景?
答:lambda 表達式其實就是一個匿名函數, 在函數編程中經常作為參數使用。 例子如下
a = [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
a_1 = list(map(lambda x:x[0], a))
- 新式類和舊式類的區別
答:Python2.x中默認都是經典類,只有顯式繼承了object才是新式類,Python3.x中默認都是新式類,經典類被移除,不必顯式的繼承object。 新式類都從object繼承,經典類不需要。
新式類的MRO(methodresolutionorder基類搜索順序)算法采用C3算法廣度優先搜索,而舊式類的MRO算法是采用深度優先搜索。新式類相同父類只執行一次構造函數,經典類重復執行多次。
- dir()是干什么用的?
答:當在使用某一個對象不知道有哪些屬性或者方法可以使用時,此時可以通過dir()方法進行查看。
- 一個包里有三個模塊,demo1.py、demo2.py、demo3.py,但使用
from tools import *
導入模塊時,如何保證只有demo1、demo3被導入了。
答: 增加_init_.py文件,並在文件中增加:all = ['demo1', 'demo3']
- 列舉5個Python中的異常類型以及其含義
答:
- AttributeError對象沒有這個屬性
- NotImplementedError尚未實現的方法
- StopIteration迭代器沒有更多的值
- TypeError對類型無效的操作
- IndentationError縮進錯誤
- copy和deepcopy的區別是什么?
答: copy.copy()淺拷貝,只拷貝父對象,不會拷貝對象的內部的子對象。 copy.deepcopy()深拷貝,拷貝對象及其子對象。
- 代碼中經常遇到的 * args, ** kwargs含義及用法。
答: 在函數定義中使用 * args和 ** kwargs傳遞可變長參數。 *args用來將參數打包成tuple給函數體調用。 ** kwargs打包關鍵字參數成dict給函數體調用。
- Python中會有函數或成員變量包含單下划線前綴和結尾,和雙下划線前綴結尾,區別是什么?
答: "單下划線"開始的成員變量叫做保護變量,意思是只有類對象和子類對象自己能訪問到這些變量; "雙下划線"開始的是私有成員,意思是只有類對象自己能訪問,連子類對象也不能訪問到這個數據。
以單下划線開頭(_foo)的代表不能直接訪問的類屬性,需通過類提供的接口進行訪問,不能用“from xxx import *”而導入;以雙下划線開頭的(_foo)代表類的私有成員;
以雙下划線開頭和結尾的(foo)代表Python里特殊方法專用的標識,如__init()代表類的構造函數。
- w、a +、wb文件寫入模式的區別
答: w表示寫模式支持寫入字符串,如果文件存在則覆蓋。 a + 和w的功能類型不過如果文件存在的話內容不會覆蓋而是追加。 wb是寫入二進制字節類型的數據。
- 舉例sort和sorted的區別
答: 相同之處sort和sorted都可以對列表元素排序,
sort()與sorted()的不同在於,sort是在原位重新排列列表,而sorted()是產生一個新的列表。 sort是應用在list上的方法,sorted可以對所有可迭代的對象進行排序操作。
list的sort方法返回的是對已經存在的列表進行操作,而內建函數sorted方法返回的是一個新的list,而不是在原來的基礎上進行的操作。
- 什么是負索引?
答:負索引一般表示的是從后面取元素。
- pprint模塊是干什么的?
答:pprint是print函數的美化版,可以通過import pprint導入。示例如下
import pprint
pprint.pprint("this is pprint")
- 解釋一下Python中的賦值運算符
答:通過下面的代碼列舉出所有的賦值運算符
a = 7
a += 1
print(a)
a -= 1
print(a)
a *= 2
print(a)
a /= 2
print(a)
a **= 2
print(a)
a //= 3
print(a)
a %= 4
print(a)
- 解釋一下Python中的邏輯運算符
答:Python中有三個邏輯運算符: and 、 or 、not
print(False and True) # False
print(7 < 7 or True) # True
print(not 2 == 2) # False
- 講講Python中的位運算符
答:按位運算符是把數字看作二進制來進行計算的。Python中的按位運算法則如下:
下表中變量a為60,b為13,二進制格式如下:
a = 0011
1100
b = 0000
1101
-----------------
a & b = 0000
1100
a | b = 0011
1101
a ^ b = 0011
0001
~a = 1100
0011
enter
image
description
here
- 在Python中如何使用多進制數字?
答: 我們在Python中,除十進制外還可以使用二進制、八進制和十六進制,二進制數字由0和1組成,我們使用0b或0B前綴表示二進制數print(int(0b1010)) # 10
使用bin()函數將一個數字轉換為它的二進制形式print(bin(0xf)) # 0b1111
八進制數由數字0 - 7組成,用前綴0o或0O表示8進制數print(oct(8)) # 0o10
十六進數由數字0 - 15組成,用前綴0x或者0X表示16進制數print(hex(16)) # 0x10,print(hex(15)) # 0xf
- 怎樣聲明多個變量並賦值?
答:Python是支持多個變量賦值的,代碼示例如下
# 對變量 a,b,c 聲明並賦值
a, b, c = 1, 2, 3
算法和數據結構
- 已知:
AList = [1, 2, 3]
BSet = {1, 2, 3}
(1)從AList和BSet中查找4,最壞時間復雜度哪個大?
(2)從AList和BSet中插入4,最壞時間復雜度哪個大?
答: (1)
對於查找,列表和集合的最壞時間復雜度都是O(n),所以一樣的。 (2)列表操作插入的最壞時間復雜度為o(n), 集合為o(1),所以Alist大。 set是哈希表所以操作的復雜度基本上都是o(1)。
- 用Python實現一個二分查找的函數
答:
def binary_search(arr, target):
n = len(arr)
left = 0
right = n - 1
while left <= right :
mid = (left + right) // 2
if arr[mid] < target:
left = mid + 1
elif arr[mid] > target:
right = mid - 1
else :
print(f"index:{mid},value:{arr[mid]}")
return True
return False
if __name__ == '__main__':
l = [1, 3, 4, 5, 6, 7, 8]
binary_search(l, 8)
- Python單例模式的實現方法
答:實現單例模式的方法有多種,之前再說元類的時候用call方法實現了一個單例模式,另外Python的模塊就是一個天然的單例模式,這里我們使用new關鍵字來實現一個單例模式。
"""
通過 new 函數實現簡單的單例模式。
"""
class Book:
def __new__(cls, title):
if not hasattr(cls, "_ins"):
cls._ins = super().__new__(cls)
print('in __new__')
return cls._ins
def __init__(self, title):
print('in __init__')
super().__init__()
self.title = title
if __name__ == '__main__':
b = Book('The Spider Book')
b2 = Book('The Flask Book')
print(id(b))
print(id(b2))
print(b.title)
print(b2.title)
- 使用Python實現一個斐波那契數列
答: 斐波那契數列:數列從第3項開始,每一項都等於前兩項之和。
def fibonacci(num):
a, b = 0, 1
l = [a, b]
for i in range(num):
a, b = b, a + b
l.append(b)
return l
if __name__ == '__main__':
print(fibonacci(10))
- 找出列表中的重復數字
答:
"""
從頭掃到尾,只要當前元素值與下標不同,就做一次判斷,numbers[i]與 numbers[numbers[i]],
相等就認為找到了重復元素,返回 true,否則就交換兩者,繼續循環。直到最后還沒找到認為沒找到重復元素。
"""
# -*- coding:utf-8 -*-
class Solution:
def duplicate(self, numbers):
if numbers is None or len(numbers) <= 1:
return False
use_set = set()
duplication = {}
for index, value in enumerate(numbers):
if value not in use_set:
use_set.add(value)
else:
duplication[index] = value
return duplication
if __name__ == '__main__':
s = Solution()
d = s.duplicate([1, 2, -3, 4, 4, 95, 95, 5, 2, 2, -3, 7, 7, 5])
print(d)
- 找出列表中的單個數字
答:
def find_single(l :list):
result = 0
for v in l:
result ^= v
if result == 0:
print("沒有落單元素")
else :
print("落單元素", result)
if __name__ == '__main__':
l = [1, 2, 3, 4, 5, 6, 2, 3, 4, 5, 6]
find_single(l)
- 寫一個冒泡排序
答:
def bubble_sort(arr):
n = len(arr)
for i in range(n - 1):
for j in range(n - i - 1):.
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
if __name__ == '__main__':
l = [1, 2, 3, 4, 5, 55, 6, 3, 4, 5, 6]
bubble_sort(l)
print(l)
- 寫一個快速排序
答:
def quick_sort(arr, first, last):
if first >= last:
return
mid_value = arr[first]
low = first
high = last
while low < high:
while low < high and arr[high] >= mid_value:
high -= 1 # 游標左移
arr[low] = arr[high]
while low < high and arr[low] < mid_value:
low += 1
arr[high] = arr[low]
arr[low] = mid_value
quick_sort(arr, first, low - 1)
quick_sort(arr, low + 1, last)
if __name__ == '__main__':
l = [1, 2, 3, 4, 5, 55, 6, 3, 4, 5, 6]
quick_sort(l, 0, len(l) - 1)
print(l)
- 寫一個拓撲排序
答:
"""
對應於該圖的拓撲排序。每一個有向無環圖都至少存在一種拓撲排序。
"""
import pysnooper
from typing import Mapping
@pysnooper.snoop()
def topological_sort(graph:Mapping):
# in_degrees = {'a': 0, 'b': 0, 'c': 0, 'd': 0, 'e': 0, 'f': 0}
in_degrees = dict((u, 0) for u in graph)
for u in graph:
for v in graph[u]: # 根據鍵找出值也就是下級節點
in_degrees[v] += 1 # 對獲取到的下級節點的入度加 1
# 循環結束之后的結果: {'a': 0, 'b': 1, 'c': 1, 'd': 2, 'e': 1, 'f': 4}
Q = [u for u in graph if in_degrees[u] == 0] # 入度為 0 的節點
in_degrees_zero = []
while Q:
u = Q.pop() # 默認從最后一個移除
in_degrees_zero.append(u) # 存儲入度為 0 的節點
for v in graph[u]:
in_degrees[v] -= 1 # 刪除入度為 0 的節點,以及移除其指向
if in_degrees[v] == 0:
Q.append(v)
return in_degrees_zero
if __name__ == '__main__':
# 用字典的鍵值表示圖的節點之間的關系,鍵當前節點。值是后續節點。
graph_dict = {
'a': 'bf', # 表示 a 指向 b 和 f
'b': 'cdf',
'c': 'd',
'd': 'ef',
'e': 'f',
'f': ''}
t = topological_sort(graph_dict)
print(t)
- Python實現一個二進制計算
答:
"""
二進制加法
"""
def binary_add(a:str, b: str):
return bin(int(a, 2) + int(b, 2))[2:]
if __name__ == '__main__':
num1 = input("輸入第一個數,二進制格式:\n")
num2 = input("輸入第二個數,二進制格式:\n")
print(binary_add(num1, num2))
- 有一組“+”和“-”符號,要求將“+”排到左邊,“-”排到右邊,寫出具體的實現方法。
答:
"""
有一組“+”和“-”符號,要求將“+”排到左邊,“-”排到右邊,寫出具體的實現方法。
如果讓+等於 0,-等於 1 不就是排序了么。
"""
from collections import deque
from timeit import Timer
s = "++++++----+++----"
# 方法一
def func1():
new_s = s.replace("+", "0").replace("-", "1")
result = "".join(sorted(new_s)).replace("0", "+").replace("1", "-")
return result
# 方法二
def func2():
q = deque()
left = q.appendleft
right = q.append
for i in s:
if i == "+":
left("+")
elif i == "-":
right("-")
# 方法三
def func3():
data = list(s)
start_index = 0
end_index = 0
count = len(s)
while start_index + end_index < count:
if data[start_index] == '-':
data[start_index], data[count - end_index - 1] = data[count - end_index - 1], data[start_index]
end_index += 1
else :
start_index += 1
return "".join(data)
if __name__ == '__main__':
timer1 = Timer("func1()", "from __main__ import func1")
print("func1", timer1.timeit(1000000))
timer2 = Timer("func2()", "from __main__ import func2")
print("func2", timer2.timeit(1000000))
timer3 = Timer("func3()", "from __main__ import func3")
print("func3", timer3.timeit(1000000))
# 1000000 測試結果
# func1 1.39003764
# func2 1.593012875
# func3 3.3487415590000005
# func1 的方式最優,其次是 func2
- 單鏈表反轉
答:
"""單鏈表反轉"""
class Node:
def __init__(self, val=None):
self.val = val
self.next = None
class SingleLinkList:
def __init__(self, head=None):
"""鏈表的頭部"""
self._head = head
def add(self, val:int):
"""
給鏈表添加元素
:param val: 傳過來的數字
:return:
"""
# 創建一個節點
node = Node(val)
if self._head is None:
self._head = node
else :
cur = self._head
while cur.next is not None:
cur = cur.next # 移動游標
cur.next = node # 如果 next 后面沒了證明以及到最后一個節點了
def traversal(self):
if self._head is None:
return
else :
cur = self._head
while cur is not None:
print(cur.val)
cur = cur.next
def size(self):
"""
獲取鏈表的大小
:return:
"""
count = 0
if self._head is None:
return count
else :
cur = self._head
while cur is not None:
count += 1
cur = cur.next
return count
def reverse(self):
"""單鏈表反轉思路:
讓 cur.next 先斷開即指向 none,指向設定 pre 游標指向斷開的元素,然后cur.next 指向斷開的元素,再把開始 self._head 再最后一個元素的時候.
:return:
"""
if self._head is None or self.size() == 1:
return
else :
pre = None
cur = self._head
while cur is not None:
post = cur.next
cur.next = pre
pre = cur
cur = post
self._head = pre # 逆向后的頭節點
if __name__ == '__main__':
single_link = SingleLinkList()
single_link.add(3)
single_link.add(5)
single_link.add(6)
single_link.add(7)
single_link.add(8)
print("對鏈表進行遍歷")
single_link.traversal()
print(f"size:{single_link.size()}")
print("對鏈表進行逆向操作之后")
single_link.reverse()
single_link.traversal()
- 交叉鏈表求交點
答:
# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def getIntersectionNode(self, headA, headB):
"""
:tye head1, head1: ListNode
:rtye: ListNode
"""
if headA is not None and headB is not None:
cur1, cur2 = headA, headB
while cur1 != cur2:
cur1 = cur1.next if cur1 is not None else headA
cur2 = cur2.next if cur2 is not None else headB
return cur1
cur1、cur2,2個指針的初始位置是鏈表headA、headB頭結點,cur1、cur2兩個指針一直往后遍歷。 直到cur1指針走到鏈表的末尾,然后cur1指向headB;
直到cur2指針走到鏈表的末尾,然后cur2指向headA; 然后再繼續遍歷; 每次cur1、cur2指向None,則將cur1、cur2分別指向headB、headA。
循環的次數越多,cur1、cur2的距離越接近,直到cur1等於cur2。則是兩個鏈表的相交點。
- 用隊列實現棧ww
答: 下面代碼分別使用1個隊列和2個隊列實現了棧。
from queue import Queue
# 使用 2 個隊列實現
class MyStack:
def __init__(self):
"""
Initialize your data structure here.
"""
# q1 作為進棧出棧,q2 作為中轉站
self.q1 = Queue()
self.q2 = Queue()
def push(self, x):
"""
Push element x onto stack.
:type x: int
:rtype: void
"""
self.q1.put(x)
def pop(self):
"""
Removes the element on top of the stack and returns that element.
:rtype: int
"""
while self.q1.qsize() > 1:
self.q2.put(self.q1.get()) # 將 q1 中除尾元素外的所有元素轉到 q2 中
if self.q1.qsize() == 1:
res = self.q1.get() # 彈出 q1 的最后一個元素
self.q1, self.q2 = self.q2, self.q1 # 交換 q1,q2
return res
def top(self):
"""
Get the top element.
:rtype: int
"""
while self.q1.qsize() > 1:
self.q2.put(self.q1.get()) # 將 q1 中除尾元素外的所有元素轉到 q2 中
if self.q1.qsize() == 1:
res = self.q1.get() # 彈出 q1 的最后一個元素
self.q2.put(res) # 與 pop 唯一不同的是需要將 q1 最后一個元素保存到 q2 中
self.q1, self.q2 = self.q2, self.q1 # 交換 q1,q2
return res
def empty(self):
"""
Returns whether the stack is empty.
:rtype: bool
"""
return not bool(self.q1.qsize() + self.q2.qsize()) # 為空返回 True,不為空返回 False
# 使用 1 個隊列實現
class MyStack2(object):
def __init__(self):
"""
Initialize your data structure here.
"""
self.sq1 = Queue()
def push(self, x):
"""
Push element x onto stack.
:type x: int
:rtype: void
"""
self.sq1.put(x)
def pop(self):
"""
Removes the element on top of the stack and returns that element.
:rtype: int
"""
count = self.sq1.qsize()
if count == 0:
return False
while count > 1:
x = self.sq1.get()
self.sq1.put(x)
count -= 1
return self.sq1.get()
def top(self):
"""
Get the top element.
:rtype: int
"""
count = self.sq1.qsize()
if count == 0:
return False
while count:
x = self.sq1.get()
self.sq1.put(x)
count -= 1
return x
def empty(self):
"""
Returns whether the stack is empty.
:rtype: bool
"""
return self.sq1.empty()
if __name__ == '__main__':
obj = MyStack2()
obj.push(1)
obj.push(3)
obj.push(4)
print(obj.pop())
print(obj.pop())
print(obj.pop())
print(obj.empty())
- 找出數據流的中位數
答:對於一個升序排序的數組,中位數為左半部分的最大值,右半部分的最小值,而左右兩部分可以是無需的,只要保證左半部分的數均小於右半部分即可。因此,左右兩半部分分別可用最大堆、最小堆實現。
如果有奇數個數,則中位數放在左半部分;如果有偶數個數,則取左半部分的最大值、右邊部分的最小值之平均值。
分兩種情況討論: 當目前有偶數個數字時,數字先插入最小堆,然后選擇最小堆的最小值插入最大堆(第一個數字插入左半部分的最小堆)。
當目前有奇數個數字時,數字先插入最大堆,然后選擇最大堆的最大值插入最小堆。 最大堆:根結點的鍵值是所有堆結點鍵值中最大者,且每個結點的值都比其孩子的值大。 最小堆:根結點的鍵值是所有堆結點鍵值中最小者,且每個結點的值都比其孩子的值小。
# -*- coding:utf-8 -*-
from heapq import *
class Solution:
def __init__(self):
self.maxheap = []
self.minheap = []
def Insert(self, num):
if (len(self.maxheap) + len(self.minheap)) & 0x1: # 總數為奇數插入最大堆
if len(self.minheap) > 0:
if num > self.minheap[0]: # 大於最小堆里的元素
heappush(self.minheap, num) # 新數據插入最小堆
heappush(self.maxheap, -self.minheap[0]) # 最小堆中的最小插入最大堆
heappop(self.minheap)
else :
heappush(self.maxheap, -num)
else :
heappush(self.maxheap, -num)
else : # 總數為偶數 插入最小堆
if len(self.maxheap) > 0: # 小於最大堆里的元素
if num < -self.maxheap[0]:
heappush(self.maxheap, -num) # 新數據插入最大堆
heappush(self.minheap, -self.maxheap[0]) # 最大堆中的最大元素插入最小堆
heappop(self.maxheap)
else :
heappush(self.minheap, num)
else :
heappush(self.minheap, num)
def GetMedian(self, n=None):
if (len(self.maxheap) + len(self.minheap)) & 0x1:
mid = self.minheap[0]
else :
mid = (self.minheap[0] - self.maxheap[0]) / 2.0
return mid
if __name__ == '__main__':
s = Solution()
s.Insert(1)
s.Insert(2)
s.Insert(3)
s.Insert(4)
print(s.GetMedian())
- 二叉搜索樹中第K小的元素
答: ??二叉搜索樹(BinarySearchTree),又名二叉排序樹(BinarySortTree)。 ? 二叉搜索樹是具有有以下性質的二叉樹:?
若左子樹不為空,則左子樹上所有節點的值均小於或等於它的根節點的值。
若右子樹不為空,則右子樹上所有節點的值均大於或等於它的根節點的值。
左、右子樹也分別為二叉搜索樹。二叉搜索樹按照中序遍歷的順序打印出來正好就是排序好的順序。所以對其遍歷一個節點就進行計數,計數達到k的時候就結束。
class TreeNode:
def __init__(self, x):
self.val = x
self.left = None
self.right = None
class Solution:
count = 0
nodeVal = 0
def kthSmallest(self, root, k):
"""
:type root: TreeNode
:type k: int
:rtype: int
"""
self.dfs(root, k)
return self.nodeVal
def dfs(self, node, k):
if node != None:
self.dfs(node.left, k)
self.count = self.count + 1
if self.count == k:
self.nodeVal = node.val
# 將該節點的左右子樹置為 None,來結束遞歸,減少時間復雜度
node.left = None
node.right = None
self.dfs(node.right, k)
爬蟲相關
- 在requests模塊中,requests.content和requests.text什么區別
答: requests.content獲取的是字節,requests.text獲取的是文本內容。
- 簡要寫一下lxml模塊的使用方法框架
答:
from lxml import html
source = '''
< div class ="nam" > < span > 中國 < / span > < / div >
'''
root = html.fromstring(source)
_content = root.xpath("string(//div[@class='nam'])")
if _content and isinstance(_content, list):
content = _content[0]
elif isinstance(_content, str):
content = _content
print(content)
- 說一說scrapy的工作流程
答:首先還是先看張圖enter image description here 以www.baidu.com為例: 首先需要知道的事各個模塊之間調用都是通過引擎進行的。
spider把百度需要下載的第一個url:www.baidu.com交給引擎。引擎把url交給調度器排序入隊處理。調度器把處理好的request返回給引擎。
通過引擎調動下載器,按照下載中間件的設置下載這個request。下載器下載完畢結果返回給引擎(如果失敗:不好意思,這個request下載失敗,然后引擎告訴調度器,這個
request下載失敗了,你記錄一下,我們待會兒再下載。)引擎調度spider,把按照Spider中間件處理過了的請求,交給spider處理。spider把處理好的url和item傳給引擎。
引擎根據不同的類型調度不同的模塊,調度ItemPipeline處理item。把url交給調度器。 然后從第4步開始循環,直到獲取到你需要的信息,注意!只有當調度器中不存在任何
request了,整個程序才會停止。
- scrapy的去重原理
答:scrapy本身自帶一個去重中間件,scrapy源碼中可以找到一個dupefilters.py去重器。里面有個方法叫做request_seen,它在scheduler(發起請求的第一時間)
的時候被調用。它代碼里面調用了request_fingerprint方法(就是給request生成一個指紋)。就是給每一個傳遞過來的url生成一個固定長度的唯一的哈希值。但是這種量級千萬到億的級別內存是可以應付的。
- scrapy中間件有幾種類,你用過哪些中間件
答: scrapy的中間件理論上有三種(SchdulerMiddleware, SpiderMiddleware, DownloaderMiddleware)。
在應用上一般有以下兩種爬蟲中間件SpiderMiddleware:主要功能是在爬蟲運行過程中進行一些處理。
下載器中間件DownloaderMiddleware:這個中間件可以實現修改User - Agent等headers信息,處理重定向,設置代理,失敗重試,設置cookies等功能。
- 你寫爬蟲的時候都遇到過什么?反爬蟲措施,你是怎么解決的?
答:Headers: 從用戶的headers進行反爬是最常見的反爬蟲策略。Headers是一種區分瀏覽器行為和機器行為中最簡單的方法,還有一些網站會對Referer (上級鏈接)
進行檢測(機器行為不太可能通過鏈接跳轉實現)從而實現爬蟲。 相應的解決措施:通過審查元素或者開發者工具獲取相應的headers然后把相應的headers傳輸給Python的requests,
這樣就能很好地繞過。
IP限制一些網站會根據你的IP地址訪問的頻率,次數進行反爬。也就是說如果你用單一的IP地址訪問頻率過高,那么服務器會在短時間內禁止這個IP訪問。
解決措施:構造自己的IP代理池,然后每次訪問時隨機選擇代理(但一些IP地址不是非常穩定,需要經常檢查更新)。
UA限制UA是用戶訪問網站時候的瀏覽器標識,其反爬機制與ip限制類似。
解決措施:使用隨機UA驗證碼反爬蟲或者模擬登陸
驗證碼:這個辦法也是相當古老並且相當的有效果,如果一個爬蟲要解釋一個驗證碼中的內容,這在以前通過簡單的圖像識別是可以完成的,但是就現在來講,驗證碼的干擾線,
噪點都很多,甚至還出現了人類都難以認識的驗證碼。
相應的解決措施:驗證碼識別的基本方法:截圖,二值化、中值濾波去噪、分割、緊縮重排(讓高矮統一)、字庫特征匹配識別。(Python的PIL庫或者其他),復雜的情況需求接入打碼平台。
Ajax動態加載網頁:不希望被爬蟲拿到的數據使用Ajax動態加載,這樣就為爬蟲造成了絕大的麻煩,如果一個爬蟲不具備js引擎,或者具備js引擎,但是沒有處理js返回的方案,
或者是具備了js引擎,但是沒辦法讓站點顯示啟用腳本設置。基於這些情況,ajax動態加載反制爬蟲還是相當有效的。Ajax動態加載的工作原理是:從網頁的url加載網頁的源代碼之后,
會在瀏覽器里執行JavaScript程序。這些程序會加載出更多的內容,並把這些內容傳輸到網頁中。這就是為什么有些網頁直接爬它的URL時卻沒有數據的原因。
處理方法:找對應的ajax接口,一般數據返回類型為json。
cookie限制:一次打開網頁會生成一個隨機cookie,如果再次打開網頁這個cookie不存在,那么再次設置,第三次打開仍然不存在,這就非常有可能是爬蟲在工作了。
解決措施:在headers掛上相應的cookie或者根據其方法進行構造(例如從中選取幾個字母進行構造)。如果過於復雜,可以考慮使用selenium模塊(可以完全模擬瀏覽器行為)。
- 為什么會用到代理?
答:如果使用同一個ip去不斷的訪問的網站的話, 會很容易被封ip,嚴重的永久封禁,導致當前的訪問不了該網站。不只是通過程序,通過瀏覽器也無法訪問。
- 代理失效了怎么處理?
答:一般通過大家代理池來實現代理切換等操作,來實現時時使用新的代理ip,來避免代理失效的問題。
- 列出你知道header的內容以及信息
答:
User - Agent:User - Agent的內容包含發出請求的用戶信息。
Accept:指定客戶端能夠接收的內容類型。
Accept - Encoding:指定瀏覽器可以支持的web服務器返回內容壓縮編碼類型。
Accept - Language:瀏覽器可接受的語言。 Connection:表示是否需要持久連接。(HTTP1.1默認進行持久連接)。
Content - Length:請求的內容長度。
If - Modified - Since:如果請求的部分在指定時間之后被修改則請求成功,未被修改則返回304代碼。
Referer:先前網頁的地址,當前請求網頁緊隨其后,即來路。
- 說一說打開瀏覽器訪問www.baidu.com獲取到結果,整個流程。
答: 瀏覽器向DNS服務器發送baidu.com域名解析請求。 DNS服務器返回解析后的ip給客戶端瀏覽器,瀏覽器向該ip發送頁面請求。 DNS服務器接收到請求后,查詢該頁面,
並將頁面發送給客戶端瀏覽器。 客戶端瀏覽器接收到頁面后,解析頁面中的引用,並再次向服務器發送引用資源請求。 服務器接收到資源請求后,查找並返回資源給客戶端。
客戶端瀏覽器接收到資源后,渲染,輸出頁面展現給用戶。
- 爬取速度過快出現了驗證碼怎么處理
答:一般在爬取過程中出現了驗證碼根據不同的情況,處理不一樣。 如果在一開始訪問就有驗證碼, 那么就想辦法繞開驗證碼, 比如通過wap端或者app去發現其他接口等,
如果不行就得破解驗證碼了,復雜驗證碼就需要接入第三方打碼平台了。 如果開始的時候沒有驗證碼,爬了一段時間才出現驗證碼,這個情況就要考慮更換代理ip了。
可能因為同一個訪問頻率高導致的。
- scrapy和scrapy - redis有什么區別?為什么選擇redis數據庫?
答: scrapy是一個Python爬蟲框架,爬取效率極高,具有高度定制性,但是不支持分布式。而scrapy - redis一套基於redis數據庫、運行在scrapy框架之上的組件,可以讓
scrapy支持分布式策略,Slaver端共享Master端redis數據庫里的item隊列、請求隊列和請求指紋集合。
為什么選擇redis數據庫,因為redis支持主從同步,而且數據都是緩存在內存中的,所以基於redis的分布式爬蟲,對請求和數據的高頻讀取效率非常高。
- 分布式爬蟲主要解決什么問題
答:使用分布式主要目的就是為了給爬蟲加速。解決了單個ip的限制,寬帶的影響,以及CPU的使用情況和io等一系列操作
- 寫爬蟲是用多進程好?還是多線程好? 為什么?
答: 多線程,因為爬蟲是對網絡操作屬於io密集型操作適合使用多線程或者協程。
- 解析網頁的解析器使用最多的是哪幾個
答:lxml,pyquery,bs4等
- 需要登錄的網頁,如何解決同時限制ip,cookie, session(其中有一些是動態生成的)在不使用動態爬取的情況下?
答: 解決限制IP可以搭建代理IP地址池、adsl撥號使用等。不適用動態爬取的情況下可以使用反編譯JS文件獲取相應的文件,或者換用其他平台(比如手機端)看看是否可以獲取相應的
json文件,一般要學會習慣性的先找需要爬取網站的h5端頁面,看看有沒有提供接口,進而簡化操作。
- 驗證碼的解決?
答: 圖形驗證碼:干擾、雜色不是特別多的圖片可以使用開源庫Tesseract進行識別,太過復雜的需要借助第三方打碼平台。
點擊和拖動滑塊驗證碼可以借助selenium、無圖形界面瀏覽器(chromedirver或者phantomjs)和pillow包來模擬人的點擊和滑動操作,pillow可以根據色差識別需要滑動的位置。
- 使用最多的數據庫(mysql,mongodb,redis等),對他的理解?
答:
MySQL數據庫:開源免費的關系型數據庫,需要實現創建數據庫、數據表和表的字段,表與表之間可以進行關聯(一對多、多對多),是持久化存儲。
mongodb數據庫:是非關系型數據庫,數據庫的三元素是,數據庫、集合、文檔,可以進行持久化存儲,也可作為內存數據庫,存儲數據不需要事先設定格式,數據以鍵值對的形式存儲。
redis數據庫:非關系型數據庫,使用前可以不用設置格式,以鍵值對的方式保存,文件格式相對自由,主要用與緩存數據庫,也可以進行持久化存儲。
網絡編程
- TCP和UDP的區別?
答: UDP是面向無連接的通訊協議,UDP數據包括目的端口號和源端口號信息。
優點:UDP速度快、操作簡單、要求系統資源較少,由於通訊不需要連接,可以實現廣播發送。
缺點:UDP傳送數據前並不與對方建立連接,對接收到的數據也不發送確認信號,發送端不知道數據是否會正確接收,也不重復發送,不可靠。
TCP是面向連接的通訊協議,通過三次握手建立連接,通訊完成時四次揮手。
優點:TCP在數據傳遞時,有確認、窗口、重傳、阻塞等控制機制,能保證數據正確性,較為可靠。
缺點:TCP相對於UDP速度慢一點,要求系統資源較多。
- 簡要介紹三次握手和四次揮手
答: 三次握手:第一次握手:主機A發送同步報文段(SYN)請求建立連接。 第二次握手:主機B聽到連接請求,就將該連接放入內核等待隊列當中,並向主機A發送針對SYN的確認ACK,
同時主機B也發送自己的請求建立連接(SYN)。 第三次握手:主機A針對主機B的SYN的確認應答ACK。
四次揮手:第一次揮手:當主機A發送數據完畢后,發送FIN結束報文段。 第二次揮手:主機B收到FIN報文段后,向主機A發送一個確認序號ACK(為了防止在這段時間內,對方重傳FIN報文段)。
第三次揮手:主機B准備關閉連接,向主機A發送一個FIN結束報文段。 第四次揮手:主機A收到FIN結束報文段后,進入TIME_WAIT狀態。並向主機B發送一個ACK表示連接徹底釋放。
除此之外經常看的問題還有,為什么2、3次揮手不能合在一次揮手中? 那是因為此時A雖然不再發送數據了,但是還可以接收數據,B可能還有數據要發送給A,所以兩次揮手不能合並為一次。
- 什么是粘包? socket中造成粘包的原因是什么? 哪些情況會發生粘包現象?
答:TCP是流式協議,只有字節流,流是沒有邊界的,根部就不存在粘包一說,一般粘包都是業務上沒處理好造成的。但是在描述這個現象的時候,可能還得說粘包。
TCP粘包通俗來講,就是發送方發送的多個數據包,到接收方后粘連在一起,導致數據包不能完整的體現發送的數據。導致TCP粘包的原因,可能是發送方的原因,也有可能是接受方的原因。
發送方由於TCP需要盡可能高效和可靠,所以TCP協議默認采用Nagle算法,以合並相連的小數據包,再一次性發送,以達到提升網絡傳輸效率的目的。
但是接收方並不知曉發送方合並數據包,而且數據包的合並在TCP協議中是沒有分界線的,所以這就會導致接收方不能還原其本來的數據包。
接收方TCP是基於“流”的。網絡傳輸數據的速度可能會快過接收方處理數據的速度,這時候就會導致,接收方在讀取緩沖區時,緩沖區存在多個數據包。
在TCP協議中接收方是一次讀取緩沖區中的所有內容,所以不能反映原本的數據信息。
一般的解決方案大概下面幾種:
發送定長包。如果每個消息的大小都是一樣的,那么在接收對等方只要累計接收數據,直到數據等於一個定長的數值就將它作為一個消息。
包尾加上\r\n標記。FTP協議正是這么做的。但問題在於如果數據正文中也含有\r\n,則會誤判為消息的邊界。包頭加上包體長度。包頭是定長的4個字節,說明了包體的長度。
接收對等方先接收包體長度,依據包體長度來接收包體。
並發
- 舉例說明concurrent.future的中線程池的用法
答:
from concurrent.futures import ThreadPoolExecutor
import requests
URLS = ['http://www.163.com', 'https://www.baidu.com/', 'https://github.com/']
def load_url(url):
req = requests.get(url, timeout=60)
print(f'{url} page is {len(req.content))} bytes')
with ThreadPoolExecutor(max_workers=3) as pool:
pool.map(load_url, URLS)
print('主線程結束')
- 說一說多線程,多進程和協程的區別。
答:
概念:
進程:進程是具有一定獨立功能的程序關於某個數據集合上的一次運行活動,進程是系統進行資源分配和調度的一個獨立單位。每個進程都有自己的獨立內存空間,
不同進程通過進程間通信來通信。由於進程比較重量,占據獨立的內存,所以上下文進程間的切換開銷(棧、寄存器、虛擬內存、文件句柄等)比較大,但相對比較穩定安全。
線程:線程是進程的一個實體, 是 CPU 調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位.線程自己基本上不擁有系統資源,
只擁有一點在運行中必不可少的資源(如程序計數器, 一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源。
線程間通信主要通過共享內存,上下文切換很快,資源開銷較少,但相比進程不夠穩定容易丟失數據。
協程:協程是一種用戶態的輕量級線程,協程的調度完全由用戶控制。協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,
在切回來的時候,恢復先前保存的寄存器上下文和棧,直接操作棧則基本沒有內核切換的開銷,可以不加鎖的訪問全局變量,所以上下文的切換非常快。
區別:
進程與線程比較: 線程是指進程內的一個執行單元, 也是進程內的可調度實體。
線程與進程的區別:
- 地址空間:線程是進程內的一個執行單元,進程內至少有一個線程,它們共享進程的地址空間,
而進程有自己獨立的地址空間 - 資源擁有:進程是資源分配和擁有的單位, 同一個進程內的線程共享進程的資源
- 線程是處理器調度的基本單位, 但進程不是
- 二者均可並發執行
- 每個獨立的線程有一個程序運行的入口、順序執行序列和程序的出口,但是線程不能夠獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制
協程與線程進行比較: - 一個線程可以多個協程,一個進程也可以單獨擁有多個協程,這樣 Python 中則能使用多核 CPU。
- 線程進程都是同步機制,而協程則是異步
- 協程能保留上一次調用時的狀態,每次過程重入時,就相當於進入上一次調用的狀態
- 簡述 GIL
答: GIL:全局解釋器鎖。每個線程在執行的過程都需要先獲取 GIL,保證同一時刻只有一個線程可以執行代碼。
線程釋放 GIL 鎖的情況:在 IO 操作等可能會引起阻塞的 systemcall 之前, 可以暫時釋放 GIL,但在執行完畢后, 必須重新獲取 GIL,
Python3.x 使用計時器(執行時間達到閾值后,當前線程釋放 GIL)或 Python2.x,tickets 計數達到 100 。
Python 使用多進程是可以利用多核的 CPU 資源的。多線程爬取比單線程性能有提升,因為遇到 IO 阻塞會自動釋放 GIL 鎖。
- 進程之間如何通信
答: 可以通過隊列的形式,示例如下
from multiprocessing import Queue, Process
import time, random
# 要寫入的數據
list1 =["java", "Python", "JavaScript"]
def write(queue):
"""
向隊列中添加數據
:param queue:
:return:
"""
for value in list1:
print(f"正在向隊列中添加數據-->{value}")
# put_nowait 不會等待隊列有空閑位置再放入數據,如果數據放入不成功就直接崩潰,比如數據滿了。put 的話就會一直等待
queue.put_nowait(value)
time.sleep(random.random())
def read(queue):
while True:
# 判斷隊列是否為空
if not queue.empty():
# get_nowait 隊列為空,取值的時候不等待,但是取不到值那么直接崩潰了
value = queue.get_nowait()
print(f'從隊列中取到的數據為-->{value}')
time.sleep(random.random())
else :
break
if __name__ == '__main__':
# 父進程創建出隊列,通過參數的形式傳遞給子進程
# queue = Queue(2)
queue = Queue()
# 創建兩個進程 一個寫數據 一個讀數據
write_data = Process(target=write, args=(queue,))
read_data = Process(target=read, args=(queue,))
# 啟動進程 寫入數據
write_data.start()
# 使用 join 等待寫數據結束
write_data.join()
# 啟動進程 讀取數據
print('*' * 20)
read_data.start()
# 使用 join 等待讀數據結束
read_data.join()
print('所有的數據都寫入並讀取完成。。。')
- IO多路復用的作用?
答: 阻塞I / O只能阻塞一個I / O操作,而I / O復用模型能夠阻塞多個I / O操作,所以才叫做多路復用。
I / O多路復用是用於提升效率,單個進程可以同時監聽多個網絡連接IO。 在IO密集型的系統中, 相對於線程切換的開銷問題,IO多路復用可以極大的提升系統效率。
- select、poll、epoll模型的區別?
答: select,poll,epoll都是IO多路復用的機制。I / O多路復用就通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),
能夠通知程序進行相應的讀寫操作。
select模型: select目前幾乎在所有的平台上支持,其良好跨平台支持也是它的一個優點。select的一個缺點在於單個進程能夠監視的文件描述符的數量存在最大限制,在
Linux上一般為1024,可以通過修改宏定義甚至重新編譯內核的方式提升這一限制,但是這樣也會造成效率的降低。
poll模型: poll和select的實現非常類似,本質上的區別就是存放fd集合的數據結構不一樣。select在一個進程內可以維持最多1024個連接,poll在此基礎上做了加強,
可以維持任意數量的連接。但select和poll方式有一個很大的問題就是,我們不難看出來select是通過輪詢的方式來查找是否可讀或者可寫,打個比方,如果同時有
100萬個連接都沒有斷開,而只有一個客戶端發送了數據,所以這里它還是需要循環這么多次,造成資源浪費。所以后來出現了epoll系統調用。
epoll模型: epoll是select和poll的增強版,epoll同poll一樣,文件描述符數量無限制。但是也並不是所有情況下epoll都比select / poll
好,比如在如下場景:在大多數客戶端都很活躍的情況下,系統會把所有的回調函數都喚醒,所以會導致負載較高。既然要處理這么多的連接,那倒不如select遍歷簡單有效。
- 什么是並發和並行?
答:“並行是指同一時刻同時做多件事情,而並發是指同一時間間隔內做多件事情”。
並發與並行是兩個既相似而又不相同的概念:並發性,又稱共行性,是指能處理多個同時性活動的能力;並行是指同時發生的兩個並發事件,具有並發的含義,而並發則不一定並行,
也亦是說並發事件之間不一定要同一時刻發生。並發的實質是一個物理CPU(也可以多個物理CPU) 在若干道程序之間多路復用,並發性是對有限物理資源強制行使多用戶共享以提高效率。
並行性指兩個或兩個以上事件或活動在同一時刻發生。在多道程序環境下,並行性使多個程序同一時刻可在不同CPU上同時執行。並行,是每個CPU運行一個程序。
- 一個線程1讓線程2去調用一個函數怎么實現
答:
import threading
def func1(t2):
print('正在執行函數func1')
t2.start()
def func2():
print('正在執行函數func2')
if __name__ == '__main__':
t2 = threading.Thread(target=func2)
t1 = threading.Thread(target=func1, args=(t2,))
t1.start()
- 解釋什么是異步非阻塞?
答: 異步與同步相對,當一個異步過程調用發出后,調用者在沒有得到結果之前,就可以繼續執行后續操作。當這個調用完成后,一般通過狀態、通知和回調來通知調用者。
對於異步調用,調用的返回並不受調用者控制。
非阻塞是這樣定義的,當線程遇到I / O操作時,不會以阻塞的方式等待I / O操作的完成或數據的返回,而只是將I / O請求發送給操作系統,繼續執行下一條語句。當操作系統完成
I / O操作時,以事件的形式通知執行I / O操作的線程,線程會在特定時候處理這個事件。簡答理解就是如果程序不會卡住,可以繼續執行,就是說非阻塞的。
- threading.local的作用?
答: threading.local()這個方法是用來保存一個全局變量,但是這個全局變量只有在當前線程才能訪問,如果你在開發多線程應用的時候,
需要每個線程保存一個單獨的數據供當前線程操作,可以考慮使用這個方法,簡單有效。代碼示例
import threading
import time
a = threading.local() # 全局對象
def worker():
a.x = 0
for i in range(200):
time.sleep(0.01)
a.x += 1
print(threading.current_thread(), a.x)
for i in range(20):
threading.Thread(target=worker).start()
Git面試題
- 說說你知道的git命令
答: git init:該命令將創建一個名為.git的子目錄, 這個子目錄含有你初始化的Git倉庫中所有的必須文件, 這些文件是Git倉庫的骨干
git clone url:將服務器代碼下載到本地
git pull:將服務器的代碼拉到本地進行同步,如果本地有修改會產生沖突。
git push:提交本地修改的代碼到服務器
git checkout - b branch:創建並切換分支
git status:查看修改狀態
git add 文件名:提交到暫存區
git commit - m "提交內容":輸入提交的注釋內容
git log:查看提交的日志情況
- git如何查看某次提交修改的內容
答:我們首先可以git log顯示歷史的提交列表之后我們用git show便可以顯示某次提交的修改內容同樣git show filename可以顯示某次提交的某個內容的修改信息。
更多技術文章分享及測試資料點此獲取