當前位置:
首頁 > 知識 > Python爬蟲抓取純靜態網站及其資源

Python爬蟲抓取純靜態網站及其資源

點擊上方「

Python開發

」,選擇「置頂公眾號」


關鍵時刻,第一時間送達!






作者: Mask


來自:https://segmentfault.com/a/1190000015880780


Python開發整理髮布,轉載請聯繫作者獲得授權




遇到的需求



前段時間需要快速做個靜態展示頁面,要求是響應式和較美觀。由於時間較短,自己動手寫的話也有點麻煩,所以就打算上網找現成的。




中途找到了幾個頁面發現不錯,然後就開始思考怎麼把頁面給下載下來。



由於之前還沒有了解過爬蟲,自然也就沒有想到可以用爬蟲來抓取網頁內容。所以我採取的辦法是:




  1. 打開chrome的控制台,進入Application選項



  2. 找到Frames選項,找到html文件,再右鍵Save As...



  3. 手動創建本地的js/css/images目錄



  4. 依次打開Frames選項下的Images/Scripts/Stylesheets,一個文件就要右鍵Save As...




這個辦法是我當時能想到的最好辦法了。不過這種人為的辦法有以下缺點:




  1. 手工操作,麻煩費時



  2. 一不小心就忘記保存哪個文件



  3. 難以處理路徑之間的關係,比如一張圖片a.jpg, 它在html中的引用方式是images/banner/a.jpg,這樣我們以後還要手動去解決路徑依賴關係




然後剛好前段時間接觸了一點python,想到可以寫個python爬蟲來幫我自動抓取靜態網站。於是就馬上動手,參考相關資料等等。




下面跟大家詳細分享一下寫爬蟲抓取靜態網站的全過程。



前置知識儲備


在下面的代碼實踐中,用到了python知識、正則表達式等等,

核心技術是正則表達式


我們來一一了解一下。


Python基礎知識


如果你之前有過其他語言的學習經歷,相信你可以很快上手python這門語言。具體學習可以上查看python官方文檔或者其他教程。


爬蟲的概念


爬蟲,按照我的理解,其實是一段自動執行的計算機程序,在web領域中,它存在的前提是模擬用戶在瀏覽器中的行為。


它的原理就是模擬用戶訪問web網頁,獲取網頁內容,然後分析網頁內容,找出我們感興趣的部分,並且最後處理數據。


流程圖是:



現在流行的爬蟲主流實現形式有以下幾種:




  1. 自己抓取網頁內容,然後自己實現分析過程



  2. 用別人寫好的爬蟲框架,比如Scrapy




正則表達式




概念


正則表達式是由一系列元字元和普通字元組成的字元串,它的作用是根據一定的規則來匹配文本,最終可以對文本做出一系列的處理。


元字元是正則表達式中的保留字元,它有特殊的匹配規則,比如*代表匹配0到無窮多次,普通字元就是普通的abcd等等。


比如在前端中,常見的一個操作就是判斷用戶的輸入是否為空,這時候我們可以先通過正則表達式來進行匹配,先過濾掉用戶輸入的兩邊空白值,具體實現如下:

function

trim

(value)

{
   

return

value.replace(

/^s+|s+$/g

,

""

)
}

// 輸出 => "Python爬蟲"


trim(

" Python爬蟲 "

);

下面我們一起來具體了解一下正則表達式中的元字元。

正則表達式中的元字元


在上面,我們說過元字元是正則表達式中的保留字元,它有特殊的匹配規則,所以我們首先要了解經常出現的元字元。


匹配單個字元的元字元





  • .代表匹配一個任意字元,除了(換行符),比如可以匹配任意的字母數字等等



  • [...]表示字元組,裡面可以有任意字元,它只會匹配當中的任意一個,比如[abc]可以匹配abc,這裡值得注意的是,字元組裡面的元字元有時候會被當成是普通字元,比如[-*?]等等,它代表的僅僅是-*?,而不是-代表區間*代表0到無窮次匹配?代表0或1次匹配



  • [^...][...]的含義相反,它的意思是匹配一個不屬於[...]裡面的字元,而不是不匹配[...]裡面的字元,這兩種說法雖然細微但是有很大差別,前者規定一定要匹配一個字元,這個切記。


例子:[^123]可以匹配4/5/6等等,但是不匹配1/2/3


提供計數功能的元字元





  • *代表匹配0次到無窮次,可以不匹配任何字元



  • +代表匹配1次到無窮次,至少匹配1次



  • ?代表匹配0次或1次



  • {min, max}代表匹配min次到max次,如a{3, 5}表示a至少匹配3-5次




提供位置的元字元





  • ^代表匹配字元串開頭,如^a表示a要出現在字元串開頭,bcd則不匹配



  • $代表匹配字元串結尾, 如A$表示A要出現在字元串結尾,ABAB則不匹配




其他元字元





  • |代表一個範圍,可以匹配任意的子表達式,比如abc|def可以匹配abc或者def,不匹配abd



  • (...)代表分組,它的作用有界定子表達式的範圍和與提供功能的元字元相結合,比如(abc|def)+代表可以匹配1次或1次以上的abc或者defdef,如abcabcabc,def



  • i代表反向引用,i可以為1/2/3等整數,它的含義是指向上一個()裡面匹配的內容。比如匹配(abc)+(12)*,如果匹配成功的話,的內容是abc,的內容是12或者空。反向引用通常用在匹配""或者""




環視




我理解的環視是界定當前匹配子表達式的左邊文本和右邊文本出現的情況,環視本身不會佔據匹配的字元,它是當前子表達式的匹配規則但是本身不算進匹配文本。而我們上面說的元字元都代表一定的規則和佔據一定的字元。




環視可分為四種:肯定順序環視、否定順序環視、肯定逆序環視和否定逆序環視。它們的工作流程如下:




  • 肯定順序環視:先找到環視中的文本在右側出現的初始位置,然後從匹配到的右側文本的最左的位置開始匹配字元



  • 否定順序環視:先找到環視中的文本在右側沒有出現的初始位置,然後從匹配到的右側文本的最左的位置開始匹配字元



  • 肯定逆序環視:先找到環視中的文本在左側出現的初始位置,然後從匹配到的左側文本的最右的位置開始匹配字元



  • 否定逆序環視:先找到環視中的文本在左側沒有出現的初始位置,然後從匹配到的左側文本的最右的位置開始匹配字元




肯定順序環視


肯定順序環視匹配成功的條件是當前的子表達式能夠匹配右側文本,它的寫法是(?=...),...代表要環視的內容。比如正則表達式(?=hello)he的意思是匹配包含hello的文本,它只匹配位置,不匹配具體字元,匹配到位置之後,才真正匹配要佔用的字元是he,所以後面可以具體匹配llo等。


對於(?=hello)he而言,hello world可以匹配成功,而hell world則匹配失敗。具體代碼如下:

import

re

reg1 =

r"(?=hello)he"

print(re.search(reg1,

"hello world"

))
print(re.search(reg1,

"hell world hello"

))
print(re.search(reg1,

"hell world"

))

# 輸出結果


<_sre.SRE_Match object; span=(

0

,

2

), match=

"he"

>
<_sre.SRE_Match object; span=(

11

,

13

), match=

"he"

>

None




否定順序環視


否定順序環視匹配成功的條件是當前的子表達式不能匹配右側文本,它的寫法是(?!...),...代表要環視的內容,還是上面的例子,比如正則表達式(?!hello)he的意思是匹配不是hello的文本,找到位置,然後匹配he。


例子如下:

import

re

reg2 =

r"(?!hello)he"

print(re.search(reg2,

"hello world"

))
print(re.search(reg2,

"hell world hello"

))
print(re.search(reg2,

"hell world"

))

# 輸出結果


None


<_sre.SRE_Match object; span=(

0

,

2

), match=

"he"

>
<_sre.SRE_Match object; span=(

0

,

2

), match=

"he"

>

肯定逆序環視


肯定逆序環視匹配成功的條件是當前的子表達式能夠匹配左側文本,它的寫法是(?<=...),...代表要環視的內容,比如正則表達式(?<=hello)-python的意思是匹配包含-python的子表達式,並且它的左側必須出現hello,hello只匹配位置,不匹配具體字元,真正佔用的字元是後面的-python。


例子如下:

import

re

reg3 =

r"(?<=hello)-python"


print(re.search(reg3,

"hello-python"

))
print(re.search(reg3,

"hell-python hello-python"

))
print(re.search(reg3,

"hell-python"

))

# 輸出結果


<_sre.SRE_Match object; span=(

5

,

12

), match=

"-python"

>
<_sre.SRE_Match object; span=(

17

,

24

), match=

"-python"

>

None


否定逆序環視


否定逆序環視匹配成功的條件是當前的子表達式不能匹配左側文本,它的寫法是(?<!...),...代表要環視的內容,比如正則表達式(?<!hello)-python的意思是匹配包含-python的子表達式,並且它的左側必須不能出現hello。


例子如下:

import

re

reg3 =

r"(?<=hello)-python"


print(re.search(reg3,

"hello-python"

))
print(re.search(reg3,

"hell-python hello-python"

))
print(re.search(reg3,

"hell-python"

))

# 輸出結果


<_sre.SRE_Match object; span=(

5

,

12

), match=

"-python"

>
<_sre.SRE_Match object; span=(

17

,

24

), match=

"-python"

>

None


環視在對字元串插入某些字元很有效,你可以利用它來匹配位置,然後插入對應的字元,而不需要對原來的文本進行替換。


捕獲分組


在正則表達式中,分組可以幫助我們提取出想要的特定信息。


指明分組很簡單,只需要在想捕獲的表達式中兩端加上()就可以了。在python中,我們可以用re.search(reg, xx).groups()來獲取到所有的分組。
默認的()中都指明了一個分組,分組序號為i,i從1開始,分別用re.search(reg, xx).group(i)來獲取。


如果不想捕獲分組可以使用(?:...)來指明。


具體例子如下:

import

re

reg7 =

r"hello,([a-zA-Z0-9]+)"


print(re.search(reg7,

"hello,world"

).groups())
print(re.search(reg7,

"hello,world"

).group(

1

))
print(re.search(reg7,

"hello,python"

).groups())
print(re.search(reg7,

"hello,python"

).group(

1

))

# 輸出結果


(

"world"

,)
world
(

"python"

,)
python



貪婪匹配


貪婪匹配是指正則表達式儘可能匹配多的字元,也就是趨於最大長度匹配。


正則表達式默認是貪婪模式。


例子如下:

import

re

reg5 =

r"hello.*world"


print(re.search(reg5,

"hello world,hello python,hello world,hello javascript"

))

# 輸出結果


<_sre.SRE_Match object; span=(

0

,

36

), match=

"hello world,hello python,hello world"

>

由上可以看到它匹配的是hello world,hello python,hello world而不是剛開始的hello world。那如果我們只是想匹配剛開始的hello world,這時候我們可以利用正則表達式的非貪婪模式。


非貪婪匹配正好與貪婪匹配相反,它是指儘可能匹配少的字元,只要匹配到了就結束。要使用貪婪模式,僅需要在量詞後面加上一個問號(?)就可以。


還是剛剛那個例子:

import

re

reg5 =

r"hello.*world"


reg6 =

r"hello.*?world"


print(re.search(reg5,

"hello world,hello python,hello world,hello javascript"

))
print(re.search(reg6,

"hello world,hello python,hello world,hello javascript"

))

# 輸出結果


<_sre.SRE_Match object; span=(

0

,

36

), match=

"hello world,hello python,hello world"

>
<_sre.SRE_Match object; span=(

0

,

11

), match=

"hello world"

>

由上可以看到這是我們剛剛想要匹配的效果。


進入開發


有了上面的基礎知識,我們就可以進入開發環節了。


我們想實現的最終效果


本次我們的最終目的是寫一個簡單的python爬蟲,這個爬蟲能夠下載一個靜態網頁,並且在保持網頁引用資源的相對路徑下下載它的靜態資源(如js/css/images)。測試網站為http://www.peersafe.cn/index.html,效果圖如下:



開發流程


我們的總體思路是先獲取到網頁的內容,然後利用正則表達式來提取我們想要的資源鏈接,最後就是下載資源。


獲取網頁內容


我們選用python3自帶的urllib.http來發出http請求,或者你可以採用第三方請求庫requests


獲取內容的部分代碼如下:

url =

"http://www.peersafe.cn/index.html"

# 讀取網頁內容


webPage = urllib.request.urlopen(url)
data = webPage.read()
content = data.decode(

"UTF-8"

)
print(

"> 網站內容抓取完畢,內容長度:"

, len(content))

獲取到內容之後,我們需要把它保存下來,也就是寫到本地磁碟上。我們定義一個SAVE_PATH路徑,代表專門放置爬蟲下載的文件。

# python-spider-downloads是我們要放置的目錄


# 這裡推薦使用os模塊來獲取當前的目錄或者拼接路徑


# 不推薦直接使用"F://xxx" + "//python-spider-downloads"等方式

SAVE_PATH = os.path.join(os.path.abspath(

"."

),

"python-spider-downloads"

)

接下來就是為這個站點創建一個單獨的文件夾了。這個站點文件夾的格式是xxxx-xx-xx-domain,比如2018-08-03-www.peersafe.cn。在此之前,我們需要寫一個函數來提取出一個url鏈接的域名、相對路徑、請求文件名和請求參數等等,這個在後續在根據資源文件的引用方式創建相對應的文件夾時也會用到。


比如輸入http://www.peersafe.cn/index.html,那麼將會輸出:

{"baseUrl": "http://www.peersafe.cn", "fullPath": "http://www.peersafe.cn/", "protocol": "http://", "domain": "www.peersafe.cn", "path": "/", "fileName": "index.html", "ext": "html", "params": ""}

部分代碼如下:

REG_URL =

r"^(https?://|//)?((?:[a-zA-Z0-9-_]+.)+(?:[a-zA-Z0-9-_:]+))((?:/[-_.a-zA-Z0-9]*?)*)((?<=/)[-a-zA-Z0-9]+(?:.([a-zA-Z0-9]+))+)?((?:?[a-zA-Z0-9%&=]*)*)$"

regUrl = re.compile(REG_URL)

# ...

"""
解析URL地址
"""


def

parseUrl

(url)

:


   

if

not

url:
       

return

   res = regUrl.search(url)
   

# 在這裡,我們把192.168.1.109:8080的形式也解析成域名domain,實際過程中www.baidu.com等才是域名,192.168.1.109隻是IP地址


   

# ("http://", "192.168.1.109:8080", "/abc/images/111/", "index.html", "html", "?a=1&b=2")


   

if

res

is

not

None

:
       path = res.group(

3

)
       fullPath = res.group(

1

) + res.group(

2

) + res.group(

3

)

       

if

not

path.endswith(

"/"

):
           path = path +

"/"


           fullPath = fullPath +

"/"


       

return

dict(
           baseUrl=res.group(

1

) + res.group(

2

),
           fullPath=fullPath,
           protocol=res.group(

1

),
           domain=res.group(

2

),
           path=path,
           fileName=res.group(

4

),
           ext=res.group(

5

),
           params=res.group(

6

)
       )

"""
解析路徑

eg:
   basePath => F:Programspythonpython-spider-downloads
   resourcePath => /a/b/c/ or a/b/c

   return => F:Programspythonpython-spider-downloadsac
"""


def

resolvePath

(basePath, resourcePath)

:


   

# 解析資源路徑


   res = resourcePath.split(

"/"

)
   

# 去掉空目錄 /a/b/c/ => [a, b, c]


   dirList = list(filter(

lambda

x: x, res))

   

# 目錄不為空


   

if

dirList:
       

# 拼接出絕對路徑


       resourcePath = reduce(

lambda

x, y: os.path.join(x, y), dirList)
       dirStr = os.path.join(basePath, resourcePath)
   

else

:
       dirStr = basePath

   

return

dirStr

上面的正則表達式REG_URL有點長,這個正則表達式能解析目前我遇到的各種url形式,如果有不能解析的,你可以自行補充,我測試過的url列表可以去我的github中查看。


首先一個最複雜的url鏈接(比如"http://192.168.1.109:8080/abc/images/111/index.html?a=1&b=2")來說,我們想分別提取出http://192.168.1.109:8080/abc/images/111/index.html?a=1&b=2。提取出/abc/images/111/的目的是為以後創建目錄做準備,index.html是寫入網頁內容的名字。


有需要的可以深入研究一下REG_URL的寫法,如果有更好的或者看不懂的,我們可以一起探討。


有了parseUrl函數之後,我們就可以把剛剛獲取網頁內容和寫入文件聯繫起來了,代碼如下:

# 首先創建這個站點的文件夾


urlDict = parseUrl(url)
print(

"分析的域名:"

, urlDict)
domain = urlDict[

"domain"

]

filePath = time.strftime(

"%Y-%m-%d"

, time.localtime()) +

"-"

+ domain

# 如果是192.168.1.1:8000等形式,變成192.168.1.1-8000,:不可以出現在文件名中


filePath = re.sub(

r":"

,

"-"

, filePath)
SAVE_PATH = os.path.join(SAVE_PATH, filePath)

# 讀取網頁內容


webPage = urllib.request.urlopen(url)
data = webPage.read()
content = data.decode(

"UTF-8"

)
print(

"> 網站內容抓取完畢,內容長度:"

, len(content))

# 把網站的內容寫下來


pageName =

""


if

urlDict[

"fileName"

]

is

None

:
   pageName =

"index.html"


else

:
   pageName = urlDict[

"fileName"

]

pageIndexDir = resolvePath(SAVE_PATH, urlDict[

"path"

])

if

not

os.path.exists(pageIndexDir):
   os.makedirs(pageIndexDir)

pageIndexPath = os.path.join(pageIndexDir, pageName)
print(

"主頁的地址:"

, pageIndexPath)
f = open(pageIndexPath,

"wb"

)
f.write(data)
f.close()

提取有用的資源鏈接


我們想要的資源是圖片資源,js文件、css文件和字體文件。如果我們要對網頁內容一一進行解析,利用分組,來捕獲出我們想要的鏈接形式,比如images/1.pngscripts/lib/jquery.min.js


代碼如下:

REG_RESOURCE_TYPE =

r"(?:href|src|data-original|src)=[""](.+?.(?:js|css|jpg|jpeg|png|gif|svg|ico|ttf|woff2))[a-zA-Z0-9?=.]*[""]"

# re.S代表開啟多行匹配模式


regResouce = re.compile(REG_RESOURCE_TYPE, re.S)

# ...

# 解析網頁內容,獲取有效的鏈接


# content是上一步讀取到的網頁內容


contentList = re.split(

r"s+"

, content)
resourceList = []

for

line

in

contentList:
   resList = regResouce.findall(line)
   

if

resList

is

not

None

:
       resourceList = resourceList + resList

下載資源


在解析出資源鏈接後,我們要針對每一個資源鏈接進行檢查,把它變成符合http請求的url格式,比如把images/1.png加上http頭和剛剛的domain,也就是http://domain/images/1.png


下面是對資源鏈接進行處理的代碼:

# ./static/js/index.js


# /static/js/index.js


# static/js/index.js


# //abc.cc/static/js


# http://www.baidu/com/static/index.js


if

resourceUrl.startswith(

"./"

):
   resourceUrl = urlDict[

"fullPath"

] + resourceUrl[

1

:]

elif

resourceUrl.startswith(

"//"

):
   resourceUrl =

"https:"

+ resourceUrl

elif

resourceUrl.startswith(

"/"

):
   resourceUrl = urlDict[

"baseUrl"

] + resourceUrl

elif

resourceUrl.startswith(

"http"

)

or

resourceUrl.startswith(

"https"

):
   

# 不處理,這是我們想要的url格式


   

pass


elif

not

(resourceUrl.startswith(

"http"

)

or

resourceUrl.startswith(

"https"

)):
   

# static/js/index.js這種情況


   resourceUrl = urlDict[

"fullPath"

] + resourceUrl

else

:
   print(

"> 未知resource url: %s"

% resourceUrl)

接著就是對每個規範的資源鏈接進行解析(parseUrl),提取出它要存放的目錄和文件名等等,然後創建對應的目錄。


在這裡,我也處理了引用的其他網站的資源。

# 解析文件,查看文件路徑


resourceUrlDict = parseUrl(resourceUrl)

if

resourceUrlDict

is

None

:
   print(

"> 解析文件出錯:%s"

% resourceUrl)
   

continue

resourceDomain = resourceUrlDict[

"domain"

]
resourcePath = resourceUrlDict[

"path"

]
resourceName = resourceUrlDict[

"fileName"

]

if

resourceDomain != domain:
   print(

"> 該資源不是本網站的,也下載:"

, resourceDomain)
   

# 如果下載的話,根目錄就要變了


   

# 再創建一個目錄,用於保存其他地方的資源


   resourceDomain =  re.sub(

r":"

,

"-"

, resourceDomain)
   savePath = os.path.join(SAVE_PATH, resourceDomain)
   

if

not

os.path.exists(SAVE_PATH):
       print(

"> 目標目錄不存在,創建:"

, savePath)
       os.makedirs(savePath)
   

# continue


else

:
   savePath = SAVE_PATH

# 解析資源路徑


dirStr = resolvePath(savePath, resourcePath)

if

not

os.path.exists(dirStr):
   print(

"> 目標目錄不存在,創建:"

, dirStr)
   os.makedirs(dirStr)

# 寫入文件


downloadFile(resourceUrl, os.path.join(dirStr, resourceName))

下載的函數downloadFile的代碼是:

"""
下載文件
"""


def

downloadFile

(srcPath, distPath)

:


   

global

downloadedList

   

if

distPath

in

downloadedList:
       

return


   

try

:
       response = urllib.request.urlopen(srcPath)
       

if

response

is

None

or

response.status !=

200

:
           

return

print(

"> 請求異常:"

, srcPath)
       data = response.read()

       f = open(distPath,

"wb"

)
       f.write(data)
       f.close()

       downloadedList.append(distPath)
       

# print(">>>: " + srcPath + ":下載成功")

   

except

Exception

as

e:
       print(

"報錯了:"

, e)

以上就是我們的開發全過程。


知識總結




本次開發用到的技術






  1. 利用urllib.http來發網路請求



  2. 利用正則表達式來解析資源鏈接



  3. 利用os系統模塊來處理文件路徑問題




心得體會


這篇文章也算是我這段時間學習python的一個實踐總結,順便記錄下正則表達式的知識。同時我也希望能夠幫助到那些想學習正則表達式和爬蟲的小夥伴。


該python爬蟲的源代碼已經放在github上(https://github.com/qzcmask/python-codes/blob/master/static-resource-spider.py),有興趣的小夥伴可以上去看看,滿意的可以順便給個Star,感謝支持。


【點擊成為Java大神】

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Python開發 的精彩文章:

在機房,某python大佬當年用這個病毒毀了老師的多年積累的資源!
python爬蟲實戰,干翻一個網站,爬取資源鏈接並用多線程下載!

TAG:Python開發 |