2020年9月12日星期六

57 《零基礎入門學習Python》筆記 第057講:論一隻爬蟲的自我修養5:正則表達式

《零基礎入門學習Python》筆記    第057講:論一隻爬蟲的自我修養5:正則表達式


如果你在課後有勤加練習,那麼你對於字符串的查找應該是已經深惡痛絕了,你發現下載一個網頁是很容易的,但是要在網頁中查找到你需要的內容,那就是困難的,你發現字符串查找並沒有你想像的那麼簡單,並不是說直接使用find 方法找到匹配字符串的位置就可以了。
我們來舉個例子,學習了前面幾節課你應該已經嘗試過寫一個腳本來自動獲取最新的代理ip 地址,但是呢,你肯定會遇到困難,我現在來重現一下大家會遇到的困難。
大家肯定會先踩點,在  https://www.xicidaili.com/wt  網點審查元素,找一下代理ip前後有什麼標籤,例如:
ip 為61.135.217.7 前後的標籤為td ,但是呢,別的地方也會有td,但是裡麵包括的不是ip 地址,你可能會費了九牛二虎之力,先找table,再找tbody,再找td,終於找到了ip 地址的唯一特性,找到了一個ip 地址,但是這樣寫不僅麻煩,而且不具有通用性,你在這個網站可行,在另外一個網站就不可行了,而且,萬一站長哪天心血來潮,改了一下網頁,那你更是心塞啊。
這時候你就會琢磨,可不可以按照我們需要的內容特徵來進行自動查找呢?也就是說,如果我要找一個ip 地址,那ip 地址的特徵是什麼呢?
就是由4 段數字組成,每段數字的範圍是0-255,分別是由3個點號隔開,這就是ip 地址的特徵嘛。根據這個特徵,它去網頁裡面查找。
很抱歉,字符串所附帶的方法你無法做到。
但是呢,我們遇到的問題,計算機老前輩們也早就已經想到了,並且已經幫我們設計出了非常優秀的解決方案,就是我們今天要講的正則表達式。
今天,我們將來學習使用正則表達式來匹配ip 地址。
關於正則表達式有一個非常經典的美式笑話:
有些人面臨一個問題的時候會想:“我知道,可以使用正則表達式來解決這個問題。”於是,現在他就有兩個問題了。
沒錯,正則表達式的確是很難學,但卻非常有用。在編寫字符串網頁或程序的時候,經常會有查找某些符合複雜規則字符串的需求,例如,我們需要查找的ip 地址的規則。那麼,使用 Pythob 自帶的字符串方法,你一定會惱羞成怒,那麼這時候,如果你懂得正則表達式,你會發現,這真是靈丹妙藥啊。
因為正則表達式就是為了描述這些複雜規則的工具。正則表達式本身就是用於描述這些規則的。不同的語言均有使用正則表達式的方法,但各不相同,Python 是使用re 模塊來實現的,因為這一部分比較難,所以我們邊舉例子邊講解。
  1. >>> import re
  2. >>> re.search(r'Python', 'I love Python')
  3. <_sre.SRE_Match object; span=(7, 13), match='Python'>
  4. >>> re.search(r'Python', 'I love Python').span()
  5. (7, 13)

re.search方法

re.search 掃描整個字符串並返回正則表達式模式第一次成功匹配的位置。

函數語法:

re.search(pattern, string, flags=0)

函數參數說明:
參數描述
pattern匹配的正則表達式
string要匹配的字符串。
flags標誌位,用於控制正則表達式的匹配方式,如:是否區分大小寫,多行匹配等等。
匹配成功re.search方法返回一個匹配的對象,否則返回None。
有人就會認為,你說了這麼多,和find() 方法也沒有什麼區別啊,使用find() 方法也可以實現上面的功能啊。如下:
  1. >>> ("I love Python".find('Python'), "I love Python".find('Python') + len('Python'))
  2. (7, 13)
那我們來一個find() 方法沒辦法實現的:
大家都知道通配符(就是我們實際中經常使用的* 和?這一類可以表示任何字符的符號),例如我們想找到word 類型的文件的時候,我們就會搜索*.docx。
正則表達式也有所謂的通配符,這裡是使用點號(.),它可以匹配除了換行符以外的任何字符。
  1. >>> re.search(r'.', 'I love Python')
  2. <_sre.SRE_Match object; span=(0, 1), match='I'>
這裡它就匹配到了字符串中的'I'。
  1. >>> re.search(r'Pytho.', 'I love Python')
  2. <_sre.SRE_Match object; span=(7, 13), match='Python'>
這裡點號(.)就匹配到了'c',然後正則表達式就匹配到了'Python'。
看了上面的例子,會思考的小伙伴們就有了一個問題了,既然點號(.)可以匹配任何字符,那我如果想要匹配點號(.)字符本身,那你要怎麼辦?
正如Python 的字符串規則一樣,想要消除一個字符串的特殊功能,就在前面加上反斜杠(\),如:
  1. >>> re.search(r'\.', 'I love Python.com')
  2. <_sre.SRE_Match object; span=(13, 14), match='.'>
這裡'\.'匹配的就是點號(.)本身了,這時候,點號不代表任何其他字符,它只代表點號,前面的反斜杠已經將其解譯了。
也就是說,在正則表達式中,反斜杠同樣具有剝奪元字符的特殊功能的能力,什麼是元字符,就是這個字符它本身代表著其他含義、有特殊功能的字符,例如點號(.)。
同樣呢,反斜杠還可以使得普通的字符具有特殊能力,例如我們想要匹配數字,我們可以使用'\d' 來匹配任何數字,如:
  1. >>> re.search(r'\d', 'I love Python35.com')
  2. <_sre.SRE_Match object; span=(13, 14), match='3'>
  1. >>> re.search(r'\d\d', 'I love Python35.com')
  2. <_sre.SRE_Match object; span=(13, 15), match='35'>
OK,我們結合以上兩點知識,就可以匹配一個ip 地址大概會這麼寫:
  1. >>> re.search(r'\d\d\d\.\d\d\d\.\d\d\d\.\d\d\d', '192.168.111.123')
  2. <_sre.SRE_Match object; span=(0, 15), match='192.168.111.123'>
大家會看到,匹配成功了,但是我們這麼寫是有問題的。首先,\d 表示匹配的數字是0~9,但是ip 地址的約定範圍每組數字的範圍是0~255,那你這裡\d\d\d 最大匹配數字是999 ,而ip 地址的最大範圍是255;然後,你這裡要求ip 地址每組必須是三位數字,但實際上有些ip 地址中的某組數字只有1 位或者2 位,像這種情況,我們就匹配不了了。
那我們怎麼解決呢?
為了表示一個字符串的範圍,我們可以創建一個叫做字符類的東西,使用中括號[]來創建一個字符類,字符類的含義就是你只要匹配字符類中的一個字符,那麼就算匹配,舉例:
我們想要匹配元音字母(aeiou),我們就可以這樣寫:
  1. >>> re.search(r'[aeiou]', 'I love Python')
  2. <_sre.SRE_Match object; span=(3, 4), match='o'>
我們匹配到了'o',那大家可能會有疑惑了,為什麼沒有匹配大寫字母'I' 呢,這是因為正則表達式是默認開啟大小字母敏感模式的,所以大寫'I' 和小寫'i'會區分開來。解決的方案有兩種,一種是關閉大小寫敏感模式(後邊進行講解),另一種是修改我們的字符類[aeiou]為[aeiouAEIOU]。
你還可以在字符類中使用橫桿'-' 表示一個範圍,例如:
  1. >>> re.search(r'[a-z]', 'I love Python')
  2. <_sre.SRE_Match object; span=(2, 3), match='l'>
  1. >>> re.search(r'[0-9]', 'I love 123 Python')
  2. <_sre.SRE_Match object; span=(7, 8), match='1'>
  1. >>> re.search(r'[2-9]', 'I love 123 Python')
  2. <_sre.SRE_Match object; span=(8, 9), match='2'>
數字範圍的問題我們解決了,那接下來我們處理的第二個問題就是匹配個數的問題:
限定重複匹配的次數,我們可以使用大括號來解決,舉例:
  1. >>> re.search(r'ab{3}c', 'abbbc')
  2. <_sre.SRE_Match object; span=(0, 5), match='abbbc'>
因為這個大括號裡面的數字表示前面要匹配的字符要重複多少次。上面的{3} 就表示前面的b 匹配時要重複3次,即匹配abbbc。
  1. >>> re.search(r'ab{3}c', 'abbbbc') #这样子就匹配不了,返回 None
  2. >>>
大括號裡還可以給出重複匹配次數的範圍,例如{a, b} 表示匹配a 到b 次。
  1. >>> re.search(r'ab{3,10}c', 'abbbbbbbc')
  2. <_sre.SRE_Match object; span=(0, 9), match='abbbbbbbc'>
接下來我們來考慮一下,如何使用正則表達式來匹配0~255 ?
有些人想都不用想,就寫給你看:
  1. >>> re.search(r'[0-255]', '188')
  2. <_sre.SRE_Match object; span=(0, 1), match='1'>
本來我們想匹配188 ,結果只是匹配到了1。
還有的同學會這樣寫:
  1. >>> re.search(r'[0-2][0-5][0-5]', '188')
  2. >>>
結果匹配不到。
跟你想像的不一樣啊,要記住,正則表達式匹配的是字符串,所以呢,數字對於字符來說,只有0~9,例如188,就是由1、8、8這三個字符來組成的,並沒有說百十千這些單位。所以呢,[0-255]這個字符類(其中0-2,就是0 1 2,後面兩個5重複了)表示的是[0125]這四個數字中的某一個,所以re.search(r '[0-255]', '188')就只匹配到了一個1。
所以呢,要匹配0~255 這個範圍裡的數字,我們使用正則表達式應該這麼寫:
  1. >>> re.search('[01]\d\d|2[0-4]\d|25[0-5]', '188')
  2. <_sre.SRE_Match object; span=(0, 3), match='188'>
這裡寫的匹配的正則表達式就是 [01]\d\d或者2[0-4]\d或者25[0-5],這其中任何一個成立都是ok的。這裡的“或”和C語言中的“或”是一樣的。
但是上面的寫法還是存在問題,要求匹配的數字必須是3 位的,例如:
  1. >>> re.search('[01]\d\d|2[0-4]\d|25[0-5]', '8')
  2. >>>
  3. >>> re.search('[01]\d\d|2[0-4]\d|25[0-5]', '18')
  4. >>>
可以像下面這樣改寫,讓前面的兩位可以重複0 次(因為默認是重複1次嘛):
  1. >>> re.search('[01]{0,1}\d{0,1}\d|2[0-4]\d|25[0-5]', '8')
  2. <_sre.SRE_Match object; span=(0, 1), match='8'>
  3. >>> re.search('[01]{0,1}\d{0,1}\d|2[0-4]\d|25[0-5]', '80')
  4. <_sre.SRE_Match object; span=(0, 2), match='80'>
  5. >>> re.search('[01]{0,1}\d{0,1}\d|2[0-4]\d|25[0-5]', '118')
  6. <_sre.SRE_Match object; span=(0, 3), match='118'>
按照這樣的話,我們就可以來匹配一個ip 地址啦:
  1. >>> re.search('(([01]{0,1}\d{0,1}\d|2[0-4]\d|25[0-5])\.){3}[01]{0,1}\d{0,1}\d|2[0-4]\d|25[0-5]', '192.168.42.1')
  2. <_sre.SRE_Match object; span=(0, 12), match='192.168.42.1'>
上面的小括號的意思就是分組,首先([01]{0,1}\d{0,1}\d|2[0-4]\d|25[0-5]) 作為一個組,然後加上點號,(([01]{0,1}\d{0,1}\d|2[0-4]\d|25[0-5])\.) 作為新的組,然後這個組重複3次,然後再加一組數字。這樣就完成了對ip 地址的匹配啦。
現在應該可以充分理解,“當你發現一個問題可以使用正則表達式來解決的時候,於是你就會有兩個問題。”這句話的含義了。
但是大家也充分理解到掌握正則表達式的重要性,因為我們這裡主要是講python爬蟲,所以並沒有花太多時間來講正則表達式的隱藏技能,大家可以看一下Python正則表達式更深層次的知識:->  Python3如何優雅地使用正則表達式

0 留言:

發佈留言