python常見題型

 

 

語言特性

1. 談談對 Python 和其他語言的區別

答:Python 是一門語法簡潔優美,功能強大無比,應用領域非常廣泛,具有強大完備的第三方庫,他是一門強類型的可移植、可擴展,可嵌入的解釋型編程語言,屬於動態語言。

拿 C 語言和 Python 比: Python 的第三方類庫比較齊全並且使用簡潔,很少代碼就能實現一些功能,如果用 C 去實現相同的功能可能就比較複雜。但是對於速度來說 Python 的運行速度相較於 C 就比較慢了。所以有利的同時也有弊端,畢竟我們的學習成本降低了。

2. 簡述解釋型和編譯型編程語言

答:解釋型語言是在運行程序的時候才翻譯,每執行一次,要翻譯一次,效率較低。 編譯型就是直接編譯成機型可以執行的,只翻譯一次,所以效率相對來說較高。

3. Python 的解釋器種類以及相關特點?

答:

  • CPython c 語言開發的,使用最廣的解釋器
  • IPython 基於 cPython 之上的一個交互式計時器,交互方式增強功能和 cPython 一樣
  • PyPy 目標是執行效率,採用 JIT 技術。對 Python 代碼進行動態編譯,提高執行效率
  • JPython 運行在 Java 上的解釋器,直接把 Python 代碼編譯成 Java 字節碼執行
  • IronPython 運行在微軟 .NET 平臺上的解釋器,把 Python 編譯成 . NET 的字節碼。

4. Python3 和 Python2 的區別?

答: 這裏例舉 5 條

  1. print 在 Python3 中是函數必須加括號,Python2 中 print 爲 class。
  2. Python2 中使用 xrange,Python3 使用 range。
  3. Python2 中默認的字符串類型默認是 ASCII,Python3 中默認的字符串類型是 Unicode。
  4. Python2 中/的結果是整型,Python3 中是浮點類型。
  5. Python2 中聲明元類:_metaclass_ = MetaClass,Python3 中聲明元類:class newclass(metaclass=MetaClass):pass。

5. Python3 和 Python2 中 int 和 long 區別?

答:Python2 有 int 和 long 類型。int 類型最大值不能超過 sys.maxint,而且這個最大值是平臺相關的。可以通過在數字的末尾附上一個L來定義長整型,顯然,它比 int 類型表示的數字範圍更大。在 Python3 裏,只有一種整數類型 int,大多數情況下,和 Python2中的長整型類似。

6. xrange 和 range 的區別?

答:xrange 是在 Python2 中的用法,Python3 中只有 range xrange 用法與 range 完全相同,所不同的是生成的不是一個 list 對象,而是一個生成器。

編碼規範

7. 什麼是 PEP8?

答:PEP8 通常會聽別人提到,但是具體的指什麼內容呢,簡單介紹下。 《Python Enhancement Proposal #8》(8 號 Python 增強提案)又叫 PEP8,他針對的 Python 代碼格式而編訂的風格指南。

8. 瞭解 Python 之禪麼?

答:通過 import this 語句可以獲取其具體的內容。它告訴大家如何寫出高效整潔的代碼。

9. 瞭解 DocStrings 麼?

答:DocStrings 文檔字符串是一個重要工具,用於解釋文檔程序,幫助你的程序文檔更加簡單易懂。主要是解釋代碼作用的。

10. 瞭解類型註解麼?

答:PEP 484 引入了類型提示,這使得可以對 Python 代碼進行靜態類型檢查。 在使用 Ide 的時候可以獲取到參數的類型,更方便傳入參數。使用格式如下

def foo(num: int) -> None:
    print(f"接收到的數字是:{num}")

介紹下這個簡單例子,我們可以在函數的參數部分使用參數名+:+類型,來指定參數可以接受的類型,這裏的話就是 num 參數爲 int 類型,然後後面->接的是返回值的類型。這裏返回值爲 None,然後通過 fstring 格式化字符串輸出傳入的數字。

11. 例舉你知道 Python 對象的命名規範,例如方法或者類等

答:

類:總是使用首字母大寫單詞串,如 MyClass。內部類可以使用額外的前導下劃線。 變量:小寫,由下劃線連接各個單詞。方法名類似 常量:常量名所有字母大寫 等

12. Python 中的註釋有幾種?

答:總體來說分爲兩種,單行註釋和多行註釋。

  1. 單行註釋在行首是 #
  2. 多行註釋可以使用三個單引號或三個雙引號,包括要註釋的內容。

13. 如何優雅的給一個函數加註釋?

答:可以使用 docstring 配合類型註解

14. 如何給變量加註釋?

答:可以通過變量名:類型的方式如下

a: str = "this is string type"

15. Python 代碼縮進中是否支持 Tab 鍵和空格混用。

答:不允許 tab 鍵和空格鍵混用,這種現象在使用 sublime 的時候尤爲明顯。

一般推薦使用 4 個空格替代 tab 鍵。

16. 是否可以在一句 import 中導入多個庫?

答:可以是可以,但是不推薦。因爲一次導入多個模塊可讀性不是很好,所以一行導入一個模塊會比較好。同樣的儘量少用 from modulename import *,因爲判斷某個函數或者屬性的來源有些困難,不方便調試,可讀性也降低了。

17. 在給 Py 文件命名的時候需要注意什麼?

答:給文件命名的時候不要和標準庫庫的一些模塊重複,比如 abc。 另外要名字要有意義,不建議數字開頭或者中文命名。

18. 例舉幾個規範 Python 代碼風格的工具

答:pylint 和 flake8

數據類型-字符串

19. 列舉 Python 中的基本數據類型?

答: Python3 中有六個標準的數據類型:字符串(String)、數字(Digit)、列表(List)、元組(Tuple)、集合(Sets)、字典(Dictionary)。

20. 如何區別可變數據類型和不可變數據類型

答: 從對象內存地址方向來說

  1. 可變數據類型:在內存地址不變的情況下,值可改變(列表和字典是可變類型,但是字典中的 key 值必須是不可變類型)
  2. 不可變數據類型:內存改變,值也跟着改變。(數字,字符串,布爾類型,都是不可變類型)可以通過 id() 方法進行內存地址的檢測。

21. 將"hello world"轉換爲首字母大寫"Hello World"

答: 這個得看清題目是要求兩個單詞首字母都要大寫,如果只是第一個單詞首字母大小的話,只使用 capitalize 即可,但是這裏是兩個單詞,所以用下面的方法。

arr = "hello world".split(" ")
new_str = f"{arr[0].capitalize()} {arr[1].capitalize()}"
print(new_str)

後來評論中有朋友提到了下面的方法,這裏感謝這位朋友提醒。方案如下

"hello world".title()

非常簡單一句話搞定。

22. 如何檢測字符串中只含有數字?

答:可以通過 isdigit 方法,例子如下

s1 = "12223".isdigit()
print(s1)

s2 = "12223a".isdigit()
print(s2)

#結果如下:
#True
#False

23. 將字符串"ilovechina"進行反轉

答:

s1 = "ilovechina"[::-1]
print(s1)

24. Python 中的字符串格式化方式你知道哪些?

答:%s,format,fstring(Python3.6 開始才支持,現在推薦的寫法)

25. 有一個字符串開頭和末尾都有空格,比如“ adabdw ”,要求寫一個函數把這個字符串的前後空格都去掉。

答:因爲題目要是寫一個函數所以我們不能直接使用 strip,不過我們可以把它封裝到函數啊

def strip_function(s1):
    return s1.strip()

s1 = " adabdw "
print(strip_function(s1))

26. 獲取字符串”123456“最後的兩個字符。

答:切片使用的考察,最後兩個即開始索引是 -2,代碼如下

a = "123456"
print(a[-2::])

27. 一個編碼爲 GBK 的字符串 S,要將其轉成 UTF-8 編碼的字符串,應如何操作?

答:

a= "S".encode("gbk").decode("utf-8",'ignore')
print(a)

28. (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()))

29. (1) 怎樣將字符串轉換爲小寫。 (2) 單引號、雙引號、三引號的區別?

答: (1) 使用字符串的 lower() 方法。

(2)單獨使用單引號和雙引號沒什麼區別,但是如果引號裏面還需要使用引號的時候,就需要這兩個配合使用了,然後說三引號,同樣的三引號也分爲三單引號和三雙引號,兩個都可以聲名長的字符串時候使用,如果使用 docstring 就需要使用三雙引號。

數據類型 - 列表

30. 已知 AList = [1,2,3,1,2],對 AList 列表元素去重,寫出具體過程。

答:

list(set(AList))

31. 如何實現 "1,2,3" 變成 ["1","2","3"]

答:

s = "1,2,3"
print(s.split(","))

32. 給定兩個 list,A 和 B,找出相同元素和不同元素

答:

A、B 中相同元素:print(set(A)&set(B)) 
A、B 中不同元素:print(set(A)^set(B))

33. [[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)

34. 合併列表 [1,5,7,9] 和 [2,2,6,8]

答:使用 extend 和 + 都可以。

a = [1,5,7,9]
b = [2,2,6,8]
a.extend(b)
print(a)

35. 如何打亂一個列表的元素?

答:

import random

a = [1, 2, 3, 4, 5]
random.shuffle(a)
print(a)

數據類型 - 字典

36. 字典操作中 del 和 pop 有什麼區別

答:del 可以根據索引(元素所在位置)來刪除的,沒有返回值。 pop 可以根據索引彈出一個值,然後可以接收它的返回值。

37. 按照字典的內的年齡排序

d1 = [
    {'name':'alice', 'age':38},
    {'name':'bob', 'age':18},
    {'name':'Carl', 'age':28},
]

答:

d1 = [
    {'name': 'alice', 'age': 38},
    {'name': 'bob', 'age': 18},
    {'name': 'Carl', 'age': 28},
]

print(sorted(d1, key=lambda x:x["age"]))

38. 請合併下面兩個字典 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})

39. 如何使用生成式的方式生成一個字典,寫一段功能代碼。

答:

# 需求 3: 把字典的 key 和 value 值調換;
d = {'a':'1', 'b':'2'}

print({v:k for k,v in d.items()})

40. 如何把元組 ("a","b") 和元組 (1,2),變爲字典 {"a":1,"b":2}

答 zip 的使用,但是最後記得把 zip 對象再轉換爲字典。

a = ("a", "b")
b = (1, 2)
print(dict(zip(a, b)))

數據類型 - 綜合

41. 下列字典對象鍵類型不正確的是?

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 對象,所以不能用列表做爲字典的鍵。

42. 如何交換字典 {"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()))

43. Python 裏面如何實現 tuple 和 list 的轉換?

答: Python 中的類型轉換,一般通過類型強轉即可完成 tuple 轉 list 是 list() 方法 list 轉 tuple 使用 tuple() 方法

44. 我們知道對於列表可以使用切片操作進行部分元素的選擇,那麼如何對生成器類型的對象實現相同的功能呢?

答: 這個題目考察了 Python 標準庫的 itertools 模快的掌握情況,該模塊提供了操作生成器的一些方法。 對於生成器類型我們使用 islice 方法來實現切片的功能。例子如下

from itertools import islice
gen = iter(range(10)) #iter()函數用來生成迭代器
#第一個參數是迭代器,第二個參數起始索引,第三個參數結束索引,不支持負數索引
for i in islice(gen,0,4): 
    print(i)

45. 請將 [i for i in range(3)] 改成生成器

答:通過把列表生產式的中括號,改爲小括號我們就實現了生產器的功能即,

(i for i in range(3))

46. a="hello" 和 b="你好" 編碼成 bytes 類型

答: 這個題目一共三種方式,第一種是在字符串的前面加一個 b,第二種可以使用 bytes 方法,第三種使用字符串 encode 方法。具體代碼如下,abc 代表三種情況

a = b"hello"
b = bytes("你好", "utf-8")
c = "你好".encode("utf-8")
print(a, b, c)

47. 下面的代碼輸出結果是什麼?

a = (1,2,3,[4,5,6,7],8)
a[2] = 2

答: 我們知道元組裏的元素是不能改變的所以這個題目的答案是出現異常。

48. 下面的代碼輸出的結果是什麼?

a = (1,2,3,[4,5,6,7],8)
a[3][0] = 2

答:前面我說了元組的裏元素是不能改變的,這句話嚴格來說是不準確的,如果元組裏面元素本身就是可變類型,比如列表,那麼在操作這個元素裏的對象時,其內存地址也是不變的。a[3] 對應的元素是列表,然後對列表第一個元素賦值,所以最後的結果是: (1,2,3,[2,5,6,7],8)

操作類題目

49. Python 交換兩個變量的值

答:在 Python 中交換兩個對象的值通過下面的方式即可

a , b = b ,a 

但是需要強調的是這並不是元組解包,通過 dis 模塊可以發現,這是交換操作的字節碼是 ROT_TWO,意思是在棧的頂端做兩個值的互換操作。

50. 在讀文件操作的時候會使用 read、readline 或者 readlines,簡述它們各自的作用

答:.read() 每次讀取整個文件,它通常用於將文件內容放到一個字符串變量中。如果希望一行一行的輸出那麼就可以使用 readline(),該方法會把文件的內容加載到內存,所以對於對於大文件的讀取操作來說非常的消耗內存資源,此時就可以通過 readlines 方法,將文件的句柄生成一個生產器,然後去讀就可以了。

51. 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))

52. json 序列化時,默認遇到中文會轉換成 unicode,如果想要保留中文怎麼辦?

答:可以通過 json.dumps 的 ensure_ascii 參數解決,代碼示例如下:

import json
a=json.dumps({"name":"張三"},ensure_ascii=False)
print(a)

53. 有兩個磁盤文件 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

54. 如果當前的日期爲 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))

55. 寫一個函數,接收整數參數 n,返回一個函數,函數的功能是把函數的參數和 n 相乘並把結果返回。

答:這個題目考查了閉包的使用代碼示例如下,返回函數之類型是函數對象。

def mul_operate(num):
    def g(val):
        return num * val

    return g


m = mul_operate(8)
print(m(5))

56. 下面代碼會存在什麼問題,如何改進?

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)

57. 一行代碼輸出 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)))

58. 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:

59. Python 字典和 json 字符串相互轉化方法

答:

在 Python 中使用 dumps 方法 將 dict 對象轉爲 Json 對象,使用 loads 方法可以將 Json 對象轉爲 dict 對象。

dic = {'a': 123, 'b': "456", 'c': "liming"}
json_str = json.dumps(dic)
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 column 2 (char 1)

60. 請寫一個 Python 邏輯,計算一個文件中的大寫字母數量

答:

with open('A.txt') as fs:
    count = 0
    for i in fs.read():
        if i.isupper():
            count += 1
print(count)

61. 請寫一段 Python連接Mongo數據庫,然後的查詢代碼。

答:

# -*- coding: utf-8 -*-
# @Author : 陳祥安
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())

62.說一說Redis的基本類型

答: Redis 支持五種數據類型: string(字符串) 、 hash(哈希)、list(列表) 、 set(集合) 及 zset(sorted set: 有序集合)。

63. 請寫一段 Python連接Redis數據庫的代碼。

答:

from redis import StrictRedis, ConnectionPool
redis_url="redis://:[email protected]:6379/15"
pool = ConnectionPool.from_url(redis_url, decode_responses=True)
r= StrictRedis(connection_pool=pool)

64. 請寫一段 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()#關閉鏈接

65.瞭解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()

66.瞭解數據庫的三範式麼?

答: 經過研究和對使用中問題的總結,對於設計數據庫提出了一些規範,這些規範被稱爲範式 一般需要遵守下面3範式即可: 第一範式(1NF):強調的是列的原子性,即列不能夠再分成其他幾列。 第二範式(2NF):首先是 1NF,另外包含兩部分內容,一是表必須有一個主鍵;二是沒有包含在主鍵中的列必須完全依賴於主鍵,而不能只依賴於主鍵的一部分。 第三範式(3NF):首先是 2NF,另外非主鍵列必須直接依賴於主鍵,不能存在傳遞依賴。即不能存在:非主鍵列 A 依賴於非主鍵列 B,非主鍵列 B 依賴於主鍵的情況。

67.瞭解分佈式鎖麼

答: 分佈式鎖是控制分佈式系統之間的同步訪問共享資源的一種方式。 對於分佈式鎖的目標,我們必須首先明確三點:

  • 任何一個時間點必須只能夠有一個客戶端擁有鎖。
  • 不能夠有死鎖,也就是最終客戶端都能夠獲得鎖,儘管可能會經歷失敗。
  • 錯誤容忍性要好,只要有大部分的Redis實例存活,客戶端就應該能夠獲得鎖。 分佈式鎖的條件 互斥性:分佈式鎖需要保證在不同節點的不同線程的互斥 可重入性:同一個節點上的同一個線程如果獲取了鎖之後,能夠再次獲取這個鎖。 鎖超時:支持超時釋放鎖,防止死鎖 高效,高可用:加鎖和解鎖需要高效,同時也需要保證高可用防止分佈式鎖失效,可以增加降級。 支持阻塞和非阻塞:可以實現超時獲取失敗,tryLock(long timeOut) 支持公平鎖和非公平鎖

分佈式鎖的實現方案 1、數據庫實現(樂觀鎖) 2、基於zookeeper的實現 3、基於Redis的實現(推薦)

68.用 Python 實現一個 Reids 的分佈式鎖的功能

答:REDIS分佈式鎖實現的方式:SETNX + GETSET,NX是Not eXists的縮寫,如SETNX命令就應該理解爲:SET if Not eXists。 多個進程執行以下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()

69.寫一段 Python 使用 mongo 數據庫創建索引的代碼:

答:

# -*- coding: utf-8 -*-
# @Time : 2018/12/28 10:01 AM
# @Author : cxa
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())

高級特性

70. 函數裝飾器有什麼作用?請列舉說明?

答: 裝飾器就是一個函數,它可以在不需要做任何代碼變動的前提下給一個函數增加額外功能,啓動裝飾的效果。 它經常用於有切面需求的場景,比如:插入日誌、性能測試、事務處理、緩存、權限校驗等場景。 下面是一個日誌功能的裝飾器

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()

71. Python 垃圾回收機制?

答:Python 不像 C++,Java 等語言一樣,他們可以不用事先聲明變量類型而直接對變量進行賦值。對 Python 語言來講,對象的類型和內存都是在運行時確定的。這也是爲什麼我們稱 Python 語言爲動態類型的原因。

主要體現在下面三個方法:

1.引用計數機制 2.標記-清除 3.分代回收

72. 魔法函數 _call_怎麼使用?

答: _call_ 可以把類實例當做函數調用。 使用示例如下

class Bar:
    def __call__(self, *args, **kwargs):
        print('in call')


if __name__ == '__main__':
    b = Bar()
    b()

73. 如何判斷一個對象是函數還是方法?

答:看代碼已經結果就懂了

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

74. @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()

75. Python 中的接口如何實現?

答: 接口提取了一羣類共同的函數,可以把接口當做一個函數的集合,然後讓子類去實現接口中的函數。但是在 Python 中根本就沒有一個叫做 interface 的關鍵字,如果非要去模仿接口的概念,可以使用抽象類來實現。抽象類是一個特殊的類,它的特殊之處在於只能被繼承,不能被實例化。使用 abc 模塊來實現抽象類。

76. Python 中的反射了解麼?

答:Python 的反射機制設定較爲簡單,一共有四個關鍵函數分別是 getattr、hasattr、setattr、delattr。

77. metaclass 作用?以及應用場景?

答: metaclass 即元類,metaclass 是類似創建類的模板,所有的類都是通過他來 create 的(調用new),這使得你可以自由的控制創建類的那個過程,實現你所需要的功能。 我們可以使用元類創建單例模式和實現 ORM 模式。

78. 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()

79. 請列舉你知道的 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__並不會 執行。 所以養成一個手工清理的好習慣是很重要的,比如及時關閉連接。

80. 如何知道一個 Python 對象的類型?

答:可以通過 type 方法

81. Python 的傳參是傳值還是傳址?

答:Python 中的傳參即不是傳值也不是傳地址,傳的是對象的引用。

82. 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)

83. 簡述 any() 和 all() 方法

答: any(x):判斷 x 對象是否爲空對象,如果都爲空、0、false,則返回 false,如果不都爲空、0、false,則返回 true。 all(x):如果 all(x) 參數 x 對象的所有元素不爲 0、''、False 或者 x 爲空對象,則返回 True,否則返回 False。

84. 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 等方法了,一般列表生成式就可以搞定了。

85. 什麼是猴子補丁?

答: 猴子補丁(monkey patching):在運行時動態修改模塊、類或函數,通常是添加功能或修正缺陷。猴子補丁在代碼運行時內存中)發揮作用,不會修改源碼,因此只對當前運行的程序實例有效。因爲猴子補丁破壞了封裝,而且容易導致程序與補丁代碼的實現細節緊密耦合,所以被視爲臨時的變通方案,不是集成代碼的推薦方式。大概是下面這樣的一個效果

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() 

86. 在 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 的地址變得不再相同。

87. 當退出 Python 時是否釋放所有內存分配?

答:不是的,循環引用其他對象或引用自全局命名空間的對象的模塊,在 Python 退出時並非完全釋放。

另外,也不會釋放 c 庫保留的內存部分

正則表達式

88. (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))

89. 正則表達式匹配中(.*)和(.*?)匹配區別?

答:(.*) 爲貪婪模式極可能多的匹配內容 ,(.*?) 爲非貪婪模式又叫懶惰模式,一般匹配到結果就好,匹配字符的少爲主,示例代碼如下

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']

90. 寫一段匹配郵箱的正則表達式

答:關於郵箱的匹配這個還真的是一個永恆的話題。

電子郵件地址有統一的標準格式:用戶名@服務器域名。用戶名錶示郵件信箱、註冊名或信件接收者的用戶標識,@符號後是你使用的郵件服務器的域名。@可以讀成“at”,也就是“在”的意思。整個電子郵件地址可理解爲網絡中某臺服務器上的某個用戶的地址。

  1. 用戶名,可以自己選擇。由字母 a~z(不區分大小寫)、數字 0~9、點、減號或下劃線組成;只能以數字或字母開頭和結尾。
  2. 與你使用的網站有關,代表郵箱服務商。例如網易的有@163.com 新浪有@vip.sina.com 等。

網上看到了各種各樣的版本,都不確定用哪個,於是自己簡單的總結了一個。大家有更好的歡迎留言。

r"^[a-zA-Z0-9]+[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"

下面解釋上面的表達式

  1. 首先強調一點關於\w 的含義,\w 匹配英文字母和俄語字母或數字或下劃線或漢字。
  2. 注意^[]和[^]的區別,[]表示字符集合,^[]表示已[]內的任意字符集開始,[^]表示。
  3. ^[a-zA-Z0-9]+:這裏注意^[]和[^]的,第一個^表示已什麼開頭,第二個[]的^表示不等於[]內。所以這段表示以英文字母和數字開頭,後面緊跟的+,限定其個數>=1 個。
  4. [a-zA-Z0-9.+-]+:表示匹配英文字母和數字開頭以及.+-, 的任意一個字符,並限定其個數>=1 個。爲了考慮@前面可能出現.+-(但是不在開頭出現)。
  5. @就是郵箱必備符號了
  6. @[a-zA-Z0-9-]+.:前面的不用說了,後面的.表示.轉義了,也是必備符號。
  7. [ 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('[email protected]')
print(b)

網上找了個驗證郵件地址的通用正則表達式(符合 RFC 5322 標準)

(?:[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])+)\])

其他內容

91. 解釋一下 Python 中 pass 語句的作用?

答:pass 實際上就是一個佔位符,在寫一個函數但是不確定裏面寫啥的時候,這個時候可以使用 pass。示例如下

def foo():
 pass

92. 簡述你對 input()函數的理解

答:在 Python3 中 input 函數可以接收用戶輸入的字符串。

然後根據程序的需要轉換成所需格式即可。

93. Python 中的 is 和==

答:先說==它的作用是判斷兩個對象的值是否相同,然後說 is。is 表示的誰是誰,這也就意味着對象完全相等。我們知道一個對象有各自的內存地址和對應的值,當內存地址和值都相同的時候使用 is 可以得到結果 True。另外需要注意的下面兩點特殊的情況。

這些變量很可能在許多程序中使用。 通過池化這些對象,Python 可以防止對一致使用的對象進行內存分配調用。

1.介於數字-5 和 256 之間的整數
2.字符串僅包含字母、數字或下劃線

94. Python 中的作用域

答:

Python 中,一個變量的作用域總是由在代碼中被賦值的地方所決定

當 Python 遇到一個變量的話它會按照這的順序進行搜索

本地作用域(Local)--->當前作用域被嵌入的本地作用域(Enclosing locals)--->全局/模塊作用域(Global)--->內置作用域(Built-in)

95. 三元運算寫法和應用場景?

答: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

96. 瞭解 enumerate 麼?

答:enumerate 可以在迭代一個對象的時候,同時獲取當前對象的索引和值。 代碼示例如下

from string import ascii_lowercase

s = ascii_lowercase

for index, value in enumerate(s):
    print(index, value)

97. 列舉 5 個 Python 中的標準模塊

答: pathlib:路徑操作模塊,比 os 模塊拼接方便。 urllib:網絡請求模塊,包括對 url 的結構解析。 asyncio: Python 的異步庫,基於事件循環的協程模塊。 re:正則表達式模塊。 itertools:提供了操作生成器的一些模塊。

98. 如何在函數中設置一個全局變量

答:

# 通過使用 global 對全局變量進行修改。
n = 0
def foo():
    global n
    n = 100
foo()
print(n)
x = 0

之前我在視頻教程中對這塊做了個講解,具體點擊下方鏈接 https://www.bilibili.com/video/av50865713

99. pathlib 的用法舉例

答:pathlib 可以對文件以及文件的其他屬性進行操作。比較喜歡的一點是路徑拼接符"/"的使用,之前在公衆號中寫過 pathlib 一些其他的用法這裏就不一一例舉了。

100. Python 中的異常處理,寫一個簡單的應用場景

答: 比如在計算除法中出現爲 0 的情況出現異常

try:
    1 / 0
except ZeroDivisionError as e:
    print(e.args)

101. Python 中遞歸的最大次數,那如何突破呢?

答:Python 有遞歸次數限制,默認最大次數爲 1000。通過下面的代碼可以突破這個限制

import sys
sys.setrecursionlimit(1500) # set the maximum depth as 1500

另外需要注意的是 sys.setrecursionlimit() 只是修改解釋器在解釋時允許的最大遞歸次數,此外,限制最大遞歸次數的還和操作系統有關。

102. 什麼是面向對象的 mro

答:Python 是支持面向對象編程的,同時也是支持多重繼承的。一般我們通過調用類對象的 mro()方法獲取其繼承關係。

103. isinstance 作用以及應用場景?

答:isinstance 是判斷一個對象是否爲另一個對象的子類的,例如我們知道在 Python3 中 bool 類型其實是 int 的子類,所以我們可以對其檢測。

print(isinstance(True,int))

104. 什麼是斷言?應用場景?

答:在 Python 中是斷言語句 assert 實現此功能,一般在表達式爲 True 的情況下,程序才能通過。

#author:陳祥安
#公衆號:Python 學習開發

#assert()方法,斷言成功,則程序繼續執行,斷言失敗,則程序報錯
# 斷言能夠幫助別人或未來的你理解代碼,
# 找出程序中邏輯不對的地方。一方面,
# 斷言會提醒你某個對象應該處於何種狀態,
# 另一方面,如果某個時候斷言爲假,
# 會拋出 AssertionError 異常,很有可能終止程序。

def foo(a):
    assert a==2,Exception("不等於 2")
    print("ok",a)

if __name__ == '__main__':
    foo(1)

105. lambda 表達式格式以及應用場景?

答:lambda 表達式其實就是一個匿名函數,在函數編程中經常作爲參數使用。 例子如下

a = [('a',1),('b',2),('c',3),('d',4)]
a_1 = list(map(lambda x:x[0],a))

106. 新式類和舊式類的區別

答:Python 2.x 中默認都是經典類,只有顯式繼承了 object 纔是新式類,Python 3.x 中默認都是新式類,經典類被移除,不必顯式的繼承 object。 新式類都從 object 繼承,經典類不需要。 新式類的 MRO(method resolution order 基類搜索順序)算法採用 C3 算法廣度優先搜索,而舊式類的 MRO 算法是採用深度優先搜索。 新式類相同父類只執行一次構造函數,經典類重複執行多次。

107. dir()是幹什麼用的?

答:當在使用某一個對象不知道有哪些屬性或者方法可以使用時,此時可以通過 dir() 方法進行查看。

108. 一個包裏有三個模塊,demo1.py、demo2.py、demo3.py,但使用 from tools import *導入模塊時,如何保證只有 demo1、demo3 被導入了。

答: 增加_init_.py 文件,並在文件中增加:

__all__ = ['demo1','demo3']

109. 列舉 5 個 Python 中的異常類型以及其含義

答:

AttributeError 對象沒有這個屬性

NotImplementedError 尚未實現的方法

StopIteration 迭代器沒有更多的值

TypeError 對類型無效的操作

IndentationError 縮進錯誤

110. copy 和 deepcopy 的區別是什麼?

答: copy.copy()淺拷貝,只拷貝父對象,不會拷貝對象的內部的子對象。 copy.deepcopy()深拷貝,拷貝對象及其子對象。

111. 代碼中經常遇到的*args, **kwargs 含義及用法。

答: 在函數定義中使用 *args 和**kwargs 傳遞可變長參數。 *args 用來將參數打包成 tuple 給函數體調用。 **kwargs 打包關鍵字參數成 dict 給函數體調用。

112. Python 中會有函數或成員變量包含單下劃線前綴和結尾,和雙下劃線前綴結尾,區別是什麼?

答: "單下劃線" 開始的成員變量叫做保護變量,意思是隻有類對象和子類對象自己能訪問到這些變量; "雙下劃線" 開始的是私有成員,意思是隻有類對象自己能訪問,連子類對象也不能訪問到這個數據。

以單下劃線開頭(_foo)的代表不能直接訪問的類屬性,需通過類提供的接口進行訪問,不能用“from xxx import *”而導入;以雙下劃線開頭的(__foo)代表類的私有成員;

以雙下劃線開頭和結尾的(_foo)代表 Python 裏特殊方法專用的標識,如 _init()代表類的構造函數。

代碼示例

class Person:
    """docstring for ClassName"""
    def __init__(self):
       self.__age = 12
       self._sex = 12
    def _sex(self):
        return "男"
    def set_age(self,age):
        self.__age = age

    def get_age(self):
        return self.__age   

if __name__ == '__main__':
    p=Person()
    print(p._sex)
    #print(p.__age)
    #Python 自動將__age 解釋成 _Person__age,於是我們用 _Person__age 訪問,這次成功。
    print(p._Person__age)

113. w、a+、wb 文件寫入模式的區別

答: w 表示寫模式支持寫入字符串,如果文件存在則覆蓋。 a+ 和 w 的功能類型不過如果文件存在的話內容不會覆蓋而是追加。 wb 是寫入二進制字節類型的數據。

114. 舉例 sort 和 sorted 的區別

答: 相同之處 sort 和 sorted 都可以對列表元素排序,sort() 與 sorted() 的不同在於,sort 是在原位重新排列列表,而 sorted() 是產生一個新的列表。 sort 是應用在 list 上的方法,sorted 可以對所有可迭代的對象進行排序操作。

list 的 sort 方法返回的是對已經存在的列表進行操作,而內建函數 sorted 方法返回的是一個新的 list,而不是在原來的基礎上進行的操作。

115. 什麼是負索引?

答:負索引一般表示的是從後面取元素。

116. pprint 模塊是幹什麼的?

答:pprint 是 print 函數的美化版,可以通過 import pprint 導入。示例如下

import pprint
pprint.pprint("this is pprint")

117. 解釋一下 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)

118. 解釋一下 Python 中的邏輯運算符

答:Python 中有三個邏輯運算符:and、or、not

print(False and True) #False
print(7<7 or True) #True
print(not 2==2) #False

119. 講講 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

120. 在 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

121. 怎樣聲明多個變量並賦值?

答:Python 是支持多個變量賦值的,代碼示例如下

#對變量 a,b,c 聲明並賦值
a,b,c = 1,2,3

算法和數據結構

122. 已知:

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)。

123. 用 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)   

124. 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)

125. 使用 Python 實現一個斐波那契數列

答: 斐波那契數列:數列從第 3 項開始,每一項都等於前兩項之和。

def fibonacci(num):
    """
    獲取指定位數的列表
    :param num:
    :return:
    """
    a, b = 0, 1
    l = []
    for i in range(num):
        a, b = b, a + b
        l.append(b)
    return l


if __name__ == '__main__':
    print(fibonacci(10))

126. 找出列表中的重複數字

答:

"""
從頭掃到尾,只要當前元素值與下標不同,就做一次判斷,numbers[i]與 numbers[numbers[i]],
相等就認爲找到了重複元素,返回 true,否則就交換兩者,繼續循環。直到最後還沒找到認爲沒找到重複元素。
"""


# -*- coding:utf-8 -*-
class Solution:
    def duplicate(self, numbers):
        """

        :param numbers:
        :return:
        """
        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)

127. 找出列表中的單個數字

答:

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)        

128. 寫一個冒泡排序

答:

"""
冒泡排序
"""
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)

129. 寫一個快速排序

答:

"""
快速排序
"""


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)

130. 寫一個拓撲排序

答:

"""
拓撲排序
對應於該圖的拓撲排序。每一個有向無環圖都至少存在一種拓撲排序。

"""
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)

131. 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))

132. 有一組“+”和“-”符號,要求將“+”排到左邊,“-”排到右邊,寫出具體的實現方法。

答:

"""
有一組“+”和“-”符號,要求將“+”排到左邊,“-”排到右邊,寫出具體的實現方法。

如果讓+等於 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

133. 單鏈表反轉

答:

"""
單鏈表反轉
"""


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()

134. 交叉鏈表求交點

答:

# 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。則是兩個鏈表的相交點。

135. 用隊列實現棧

答: 下面代碼分別使用 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())

136. 找出數據流的中位數

答:對於一個升序排序的數組,中位數爲左半部分的最大值,右半部分的最小值,而左右兩部分可以是無需的,只要保證左半部分的數均小於右半部分即可。因此,左右兩半部分分別可用最大堆、最小堆實現。

如果有奇數個數,則中位數放在左半部分;如果有偶數個數,則取左半部分的最大值、右邊部分的最小值之平均值。

分兩種情況討論: 當目前有偶數個數字時,數字先插入最小堆,然後選擇最小堆的最小值插入最大堆(第一個數字插入左半部分的最小堆)。

當目前有奇數個數字時,數字先插入最大堆,然後選擇最大堆的最大值插入最小堆。 最大堆:根結點的鍵值是所有堆結點鍵值中最大者,且每個結點的值都比其孩子的值大。 最小堆:根結點的鍵值是所有堆結點鍵值中最小者,且每個結點的值都比其孩子的值小。

# -*- 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())

137. 二叉搜索樹中第 K 小的元素

答:   二叉搜索樹(Binary Search Tree),又名二叉排序樹(Binary Sort Tree)。   二叉搜索樹是具有有以下性質的二叉樹: 

  1. 若左子樹不爲空,則左子樹上所有節點的值均小於或等於它的根節點的值。
  2. 若右子樹不爲空,則右子樹上所有節點的值均大於或等於它的根節點的值。
  3. 左、右子樹也分別爲二叉搜索樹。

二叉搜索樹按照中序遍歷的順序打印出來正好就是排序好的順序。所以對其遍歷一個節點就進行計數,計數達到 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)

爬蟲相關

138. 在 requests 模塊中,requests.content 和 requests.text 什麼區別

答: requests.content 獲取的是字節,requests.text 獲取的是文本內容。

139. 簡要寫一下 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)

140. 說一說 scrapy 的工作流程

答:

首先還是先看張圖

enter image description here

已 www.baidu.com 爲例: 首先需要知道的事各個模塊之間調用都是通過引擎進行的。

  1. spider 把百度需要下載的第一個 url:www.baidu.com 交給引擎。
  2. 引擎把 url 交給調度器排序入隊處理。
  3. 調度器把處理好的 request 返回給引擎。
  4. 通過引擎調動下載器,按照下載中間件的設置下載這個 request。
  5. 下載器下載完畢結果返回給引擎(如果失敗:不好意思,這個 request 下載失敗,然後引擎告訴調度器,這個 request 下載失敗了,你記錄一下,我們待會兒再下載。)
  6. 引擎調度 spider,把按照 Spider 中間件處理過了的請求,交給 spider 處理。
  7. spider 把處理好的 url 和 item 傳給引擎。
  8. 引擎根據不同的類型調度不同的模塊,調度 Item Pipeline 處理 item。
  9. 把 url 交給調度器。 然後從第 4 步開始循環,直到獲取到你需要的信息,

注意!只有當調度器中不存在任何 request 了,整個程序纔會停止。

141. scrapy 的去重原理

答:scrapy 本身自帶一個去重中間件,scrapy 源碼中可以找到一個 dupefilters.py 去重器。裏面有個方法叫做 request_seen,它在 scheduler(發起請求的第一時間)的時候被調用。它代碼裏面調用了 request_fingerprint 方法(就是給 request 生成一個指紋)。

就是給每一個傳遞過來的 url 生成一個固定長度的唯一的哈希值。但是這種量級千萬到億的級別內存是可以應付的。

142. scrapy 中間件有幾種類,你用過哪些中間件

答: scrapy 的中間件理論上有三種(Schduler Middleware,Spider Middleware,Downloader Middleware)。在應用上一般有以下兩種

  1. 爬蟲中間件 Spider Middleware:主要功能是在爬蟲運行過程中進行一些處理。
  2. 下載器中間件 Downloader Middleware:這個中間件可以實現修改 User-Agent 等 headers 信息,處理重定向,設置代理,失敗重試,設置 cookies 等功能。

143. 你寫爬蟲的時候都遇到過什麼?反爬蟲措施,你是怎麼解決的?

答:

  • 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 模塊(可以完全模擬瀏覽器行爲)。

144. 爲什麼會用到代理?

答:如果使用同一個 ip 去不斷的訪問的網站的話,會很容易被封 ip,嚴重的永久封禁,導致當前的訪問不了該網站。不只是通過程序,通過瀏覽器也無法訪問。

145. 代理失效了怎麼處理?

答:一般通過大家代理池來實現代理切換等操作,來實現時時使用新的代理 ip,來避免代理失效的問題。

146. 列出你知道 header 的內容以及信息

答: User-Agent:User-Agent 的內容包含發出請求的用戶信息。 Accept:指定客戶端能夠接收的內容類型。 Accept-Encoding:指定瀏覽器可以支持的 web 服務器返回內容壓縮編碼類型。 Accept-Language:瀏覽器可接受的語言。 Connection:表示是否需要持久連接。(HTTP 1.1 默認進行持久連接)。 Content-Length:請求的內容長度。 If-Modified-Since:如果請求的部分在指定時間之後被修改則請求成功,未被修改則返回 304 代碼。 Referer:先前網頁的地址,當前請求網頁緊隨其後,即來路。

147. 說一說打開瀏覽器訪問 www.baidu.com 獲取到結果,整個流程。

答: 瀏覽器向 DNS 服務器發送 baidu.com 域名解析請求。 DNS 服務器返回解析後的 ip 給客戶端瀏覽器,瀏覽器想該 ip 發送頁面請求。 DNS 服務器接收到請求後,查詢該頁面,並將頁面發送給客戶端瀏覽器。 客戶端瀏覽器接收到頁面後,解析頁面中的引用,並再次向服務器發送引用資源請求。 服務器接收到資源請求後,查找並返回資源給客戶端。 客戶端瀏覽器接收到資源後,渲染,輸出頁面展現給用戶。

148. 爬取速度過快出現了驗證碼怎麼處理

答:一般在爬取過程中出現了驗證碼根據不同的情況,處理不一樣。 如果在一開始訪問就有驗證碼,那麼就想辦法繞開驗證碼,比如通過 wap 端或者 app 去發現其他接口等,如果不行就得破解驗證碼了,複雜驗證碼就需要接入第三方打碼平臺了。 如果開始的時候沒有驗證碼,爬了一段時間纔出現驗證碼,這個情況就要考慮更換代理 ip 了。 可能因爲同一個訪問頻率高導致的。

149. scrapy 和 scrapy-redis 有什麼區別?爲什麼選擇 redis 數據庫?

答: scrapy 是一個 Python 爬蟲框架,爬取效率極高,具有高度定製性,但是不支持分佈式。而 scrapy-redis 一套基於 redis 數據庫、運行在 scrapy 框架之上的組件,可以讓 scrapy 支持分佈式策略,Slaver 端共享 Master 端 redis 數據庫裏的 item 隊列、請求隊列和請求指紋集合。

爲什麼選擇 redis 數據庫,因爲 redis 支持主從同步,而且數據都是緩存在內存中的,所以基於 redis 的分佈式爬蟲,對請求和數據的高頻讀取效率非常高。

150. 分佈式爬蟲主要解決什麼問題

答:使用分佈式主要目的就是爲了給爬蟲加速。解決了單個 ip 的限制,寬帶的影響,以及 CPU 的使用情況和 io 等一系列操作

151. 寫爬蟲是用多進程好?還是多線程好? 爲什麼?

答: 多線程,因爲爬蟲是對網絡操作屬於 io 密集型操作適合使用多線程或者協程。

152. 解析網頁的解析器使用最多的是哪幾個

答:lxml,pyquery

153. 需要登錄的網頁,如何解決同時限制 ip,cookie,session(其中有一些是動態生成的)在不使用動態爬取的情況下?

答: 解決限制 IP 可以搭建代理 IP 地址池、adsl 撥號使用等。

不適用動態爬取的情況下可以使用反編譯 JS 文件獲取相應的文件,或者換用其他平臺(比如手機端)看看是否可以獲取相應的 json 文件,一般要學會習慣性的先找需要爬取網站的 h5 端頁面,看看有沒有提供接口,進而簡化操作。

154. 驗證碼的解決?

答: 圖形驗證碼:干擾、雜色不是特別多的圖片可以使用開源庫 Tesseract 進行識別,太過複雜的需要藉助第三方打碼平臺。 點擊和拖動滑塊驗證碼可以藉助 selenium、無圖形界面瀏覽器(chromedirver 或者 phantomjs)和 pillow 包來模擬人的點擊和滑動操作,pillow 可以根據色差識別需要滑動的位置。

155. 使用最多的數據庫(mysql,mongodb,redis 等),對他的理解?

答: MySQL 數據庫:開源免費的關係型數據庫,需要實現創建數據庫、數據表和表的字段,表與表之間可以進行關聯(一對多、多對多),是持久化存儲。

mongodb 數據庫:是非關係型數據庫,數據庫的三元素是,數據庫、集合、文檔,可以進行持久化存儲,也可作爲內存數據庫,存儲數據不需要事先設定格式,數據以鍵值對的形式存儲。

redis 數據庫:非關係型數據庫,使用前可以不用設置格式,以鍵值對的方式保存,文件格式相對自由,主要用與緩存數據庫,也可以進行持久化存儲。

網絡編程

156. TCP 和 UDP 的區別?

答: UDP 是面向無連接的通訊協議,UDP 數據包括目的端口號和源端口號信息。

優點:UDP 速度快、操作簡單、要求系統資源較少,由於通訊不需要連接,可以實現廣播發送。

缺點:UDP 傳送數據前並不與對方建立連接,對接收到的數據也不發送確認信號,發送端不知道數據是否會正確接收,也不重複發送,不可靠。

TCP 是面向連接的通訊協議,通過三次握手建立連接,通訊完成時四次揮手。

優點:TCP 在數據傳遞時,有確認、窗口、重傳、阻塞等控制機制,能保證數據正確性,較爲可靠。

缺點:TCP 相對於 UDP 速度慢一點,要求系統資源較多。

157. 簡要介紹三次握手和四次揮手

答: 三次握手 第一次握手:主機 A 發送同步報文段(SYN)請求建立連接。 第二次握手:主機 B 聽到連接請求,就將該連接放入內核等待隊列當中,並向主機 A 發送針對 SYN 的確認 ACK,同時主機 B 也發送自己的請求建立連接(SYN)。 第三次握手:主機 A 針對主機 BSYN 的確認應答 ACK。

四次揮手 第一次揮手:當主機 A 發送數據完畢後,發送 FIN 結束報文段。 第二次揮手:主機 B 收到 FIN 報文段後,向主機 A 發送一個確認序號 ACK(爲了防止在這段時間內,對方重傳 FIN 報文段)。 第三次揮手:主機 B 準備關閉連接,向主機 A 發送一個 FIN 結束報文段。 第四次揮手:主機 A 收到 FIN 結束報文段後,進入 TIME_WAIT 狀態。並向主機 B 發送一個 ACK 表示連接徹底釋放。

除此之外經常看的問題還有,爲什麼 2、3 次揮手不能合在一次揮手中? 那是因爲此時 A 雖然不再發送數據了,但是還可以接收數據,B 可能還有數據要發送給 A,所以兩次揮手不能合併爲一次。

158. 什麼是粘包? socket 中造成粘包的原因是什麼? 哪些情況會發生粘包現象?

答:TCP 是流式協議,只有字節流,流是沒有邊界的,根部就不存在粘包一說,一般粘包都是業務上沒處理好造成的。

但是在描述這個現象的時候,可能還得說粘包。TCP 粘包通俗來講,就是發送方發送的多個數據包,到接收方後粘連在一起,導致數據包不能完整的體現發送的數據。

導致 TCP 粘包的原因,可能是發送方的原因,也有可能是接受方的原因。

發送方 由於 TCP 需要儘可能高效和可靠,所以 TCP 協議默認採用 Nagle 算法,以合併相連的小數據包,再一次性發送,以達到提升網絡傳輸效率的目的。但是接收方並不知曉發送方合併數據包,而且數據包的合併在 TCP 協議中是沒有分界線的,所以這就會導致接收方不能還原其本來的數據包。

接收方 TCP 是基於“流”的。網絡傳輸數據的速度可能會快過接收方處理數據的速度,這時候就會導致,接收方在讀取緩衝區時,緩衝區存在多個數據包。在 TCP 協議中接收方是一次讀取緩衝區中的所有內容,所以不能反映原本的數據信息。

一般的解決方案大概下面幾種:

  1. 發送定長包。如果每個消息的大小都是一樣的,那麼在接收對等方只要累計接收數據,直到數據等於一個定長的數值就將它作爲一個消息。
  2. 包尾加上\r\n 標記。FTP 協議正是這麼做的。但問題在於如果數據正文中也含有\r\n,則會誤判爲消息的邊界。
  3. 包頭加上包體長度。包頭是定長的 4 個字節,說明了包體的長度。接收對等方先接收包體長度,依據包體長度來接收包體。

併發

159. 舉例說明 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('主線程結束')

160. 說一說多線程,多進程和協程的區別。

答: 概念:

進程:

進程是具有一定獨立功能的程序關於某個數據集合上的一次運行活動,
進程是系統進行資源分配和調度的一個獨立單位。每個進程都有自己的獨立內存空間,
不同進程通過進程間通信來通信。由於進程比較重量,佔據獨立的內存,
所以上下文進程間的切換開銷(棧、寄存器、虛擬內存、文件句柄等)比較大,但相對比較穩定安全。

線程:

線程是進程的一個實體,是 CPU 調度和分派的基本單位,
它是比進程更小的能獨立運行的基本單位.
線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),
但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源。
線程間通信主要通過共享內存,上下文切換很快,資源開銷較少,但相比進程不夠穩定容易丟失數據。

協程:

協程是一種用戶態的輕量級線程,協程的調度完全由用戶控制。
協程擁有自己的寄存器上下文和棧。
協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧,
直接操作棧則基本沒有內核切換的開銷,可以不加鎖的訪問全局變量,所以上下文的切換非常快。

區別: 進程與線程比較: 線程是指進程內的一個執行單元,也是進程內的可調度實體。線程與進程的區別:

1) 地址空間:線程是進程內的一個執行單元,進程內至少有一個線程,它們共享進程的地址空間,
而進程有自己獨立的地址空間
2) 資源擁有:進程是資源分配和擁有的單位,同一個進程內的線程共享進程的資源
3) 線程是處理器調度的基本單位,但進程不是
4) 二者均可併發執行
5) 每個獨立的線程有一個程序運行的入口、順序執行序列和程序的出口,
但是線程不能夠獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制

協程與線程進行比較:

1) 一個線程可以多個協程,一個進程也可以單獨擁有多個協程,這樣 Python 中則能使用多核 CPU。
2) 線程進程都是同步機制,而協程則是異步
3) 協程能保留上一次調用時的狀態,每次過程重入時,就相當於進入上一次調用的狀態

161. 簡述 GIL

答: GIL:全局解釋器鎖。每個線程在執行的過程都需要先獲取 GIL,保證同一時刻只有一個線程可以執行代碼。

線程釋放 GIL 鎖的情況:在 IO 操作等可能會引起阻塞的 systemcall 之前,可以暫時釋放 GIL,但在執行完畢後, 必須重新獲取 GIL,Python3.x 使用計時器(執行時間達到閾值後,當前線程釋放 GIL)或 Python2.x,tickets 計數達到 100 。

Python 使用多進程是可以利用多核的 CPU 資源的。

多線程爬取比單線程性能有提升,因爲遇到 IO 阻塞會自動釋放 GIL 鎖。

162. 進程之間如何通信

答: 可以通過隊列的形式,示例如下

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('所有的數據都寫入並讀取完成。。。')

163. IO 多路複用的作用?

答: 阻塞 I/O 只能阻塞一個 I/O 操作,而 I/O 複用模型能夠阻塞多個 I/O 操作,所以才叫做多路複用。

I/O 多路複用是用於提升效率,單個進程可以同時監聽多個網絡連接 IO。 在 IO 密集型的系統中, 相對於線程切換的開銷問題,IO 多路複用可以極大的提升系統效率。

164. 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 遍歷簡單有效。

165. 什麼是併發和並行?

答:“並行是指同一時刻同時做多件事情,而併發是指同一時間間隔內做多件事情”。

併發與並行是兩個既相似而又不相同的概念:併發性,又稱共行性,是指能處理多個同時性活動的能力;並行是指同時發生的兩個併發事件,具有併發的含義,而併發則不一定並行,也亦是說併發事件之間不一定要同一時刻發生。

併發的實質是一個物理 CPU(也可以多個物理 CPU) 在若干道程序之間多路複用,併發性是對有限物理資源強制行使多用戶共享以提高效率。 並行性指兩個或兩個以上事件或活動在同一時刻發生。在多道程序環境下,並行性使多個程序同一時刻可在不同 CPU 上同時執行。

並行,是每個 CPU 運行一個程序。

166. 一個線程 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()

167. 解釋什麼是異步非阻塞?

答: 異步 異步與同步相對,當一個異步過程調用發出後,調用者在沒有得到結果之前,就可以繼續執行後續操作。當這個調用完成後,一般通過狀態、通知和回調來通知調用者。對於異步調用,調用的返回並不受調用者控制。

非阻塞 非阻塞是這樣定義的,當線程遇到 I/O 操作時,不會以阻塞的方式等待 I/O 操作的完成或數據的返回,而只是將 I/O 請求發送給操作系統,繼續執行下一條語句。當操作系統完成 I/O 操作時,以事件的形式通知執行 I/O 操作的線程,線程會在特定時候處理這個事件。簡答理解就是如果程序不會卡住,可以繼續執行,就是說非阻塞的。

168. 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 

169. 說說你知道的 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:查看提交的日誌情況

170. git 如何查看某次提交修改的內容

答:我們首先可以 git log 顯示歷史的提交列表 之後我們用 git show 便可以顯示某次提交的修改內容 同樣 git show filename 可以顯示某次提交的某個內容的修改信息。


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章