Python爬蟲-抓取網頁數據並解析,寫入本地文件


  之前沒學過Python,最近因一些個人需求,需要寫個小爬蟲,於是就搜羅了一批資料,看了一些別人寫的代碼,現在記錄一下學習時爬過的坑。

  如果您是從沒有接觸過Python的新手,又想迅速用Python寫出一個爬蟲,那么這篇文章比較適合你。

  首先,我通過:

  https://mp.weixin.qq.com/s/ET9HP2n3905PxBy4ZLmZNw

找到了一份參考資料,它實現的功能是:爬取當當網Top 500本五星好評書籍

  源代碼可以在Github上找到:

  https://github.com/wistbean/learn_python3_spider/blob/master/dangdang_top_500.py

然而,當我運行這段代碼時,發現CPU幾乎滿負荷運行了,卻根本沒有輸出。

現在我們來分析一下其源代碼,並將之修復。

  先給出有問題的源碼:

 

 1 import requests
 2 import re
 3 import json
 4 
 5 
 6 def request_dandan(url):
 7     try:
 8         response = requests.get(url)
 9         if response.status_code == 200:
10             return response.text
11     except requests.RequestException:
12         return None
13 
14 
15 def parse_result(html):
16     pattern = re.compile(
17         '<li>.*?list_num.*?(\d+).</div>.*?<img src="(.*?)".*?class="name".*?title="(.*?)">.*?class="star">.*?class="tuijian">(.*?)</span>.*?class="publisher_info">.*?target="_blank">(.*?)</a>.*?class="biaosheng">.*?<span>(.*?)</span></div>.*?<p><span\sclass="price_n">¥(.*?)</span>.*?</li>',
18         re.S)
19     items = re.findall(pattern, html)
20 
21     for item in items:
22         yield {
23             'range': item[0],
24             'iamge': item[1],
25             'title': item[2],
26             'recommend': item[3],
27             'author': item[4],
28             'times': item[5],
29             'price': item[6]
30         }
31 
32 
33 def write_item_to_file(item):
34     print('開始寫入數據 ====> ' + str(item))
35     with open('book.txt', 'a', encoding='UTF-8') as f:
36         f.write(json.dumps(item, ensure_ascii=False) + '\n')
37 
38 
39 def main(page):
40     url = 'http://bang.dangdang.com/books/fivestars/01.00.00.00.00.00-recent30-0-0-1-' + str(page)
41     html = request_dandan(url)
42     items = parse_result(html)  # 解析過濾我們想要的信息
43     for item in items:
44         write_item_to_file(item)
45 
46 
47 if __name__ == "__main__":
48     for i in range(1, 26):
49         main(i)

 

  是不是有點亂?別急,我們來一步步分析。(如果您不想看大段的分析,可以直接跳到最后,在那里我會給出修改后的,帶有完整注釋的代碼)

  首先,Python程序中的代碼是一行行順序執行的,前面都是函數定義,因此直接先運行第47-49行的代碼:

if __name__ == "__main__":
    for i in range(1, 26):
        main(i)

 

  看樣子這里是在調用main函數(定義在第39行),那么__name__是什么呢?

  __name__是系統內置變量,當直接運行包含main函數的程序時,__name__的值為"__main__",因此main函數會被執行,而當包含main函數程序作為module被import時,__name__的值為對應的module名字,此時main函數不會被執行。

  為了加深理解,可以閱讀這篇文章,講得非常清楚:

  https://www.cnblogs.com/keguo/p/9760361.html

我們的程序里是直接運行包含main函數的程序的,因此__name__的值就是__main__。   

 

還有個小細節需要注意一下:

  像Lua這種語言,函數在結束之前會有end作為函數結束標記,包括if,for這種語句,都會有相應的end標記。但Python中是沒有的,Python中是用對應的縮進來表示各個作用域的,我們把第47-49行的代碼稍微改一下來進一步說明:

  新建個Python文件,直接輸入:

if __name__ == "__main__":
    for i in range(1,5):
        print("內層")
    print("外層")

  此時for語句比if語句縮進更多,因此位於if的作用域內,同理,print("內層")語句位於for語句的作用域內,因此會打印5次,print("外層")已經不在for語句的作用域內,而在if語句的作用域內,因此只打印1次,運行結果如下:

    

  那么47-49行做的就是循環調用25次main函數(range左閉右開),為什么是25次呢?因為要爬取的當當網好評榜一頁有20本圖書數據,要爬500本我們需要發送25次數據請求。

  我們看一下main函數(39-44行)做了什么:

  首先進行了url的拼接,每次調用時傳入不同的page,分別對應第1-25頁數據。隨后調用request_dandan發送數據請求,看一下request_dandan(第6-12行)做了什么:

  這里調用了requests模塊向服務器發送get請求,因此要在程序開頭導入requests模塊(第1行),get請求去指定的url獲取網頁數據,隨后對響應碼作了判斷,200代表獲取成功,成功就返回獲取的響應數據。要注意的一點是,這里get請求是同步請求,意思是發送請求后程序會阻塞在原地,直到收到服務器的響應后繼續執行下一行代碼。

  接下來main函數要調用parse_result(第15到30行)對獲取到的html文本進行解析,提取其中與圖書有關的信息,在分析這段代碼之前,我們需要先了解下返回的html文件的格式:

我們可以在chrome瀏覽器中的開發者工具里,查看對應請求網頁響應的html格式,以我的為例: 

 以第一本書“有話說出來”為例,用Command+F(Mac下)快速翻找一下與要爬取的圖書有關的信息:

   每一本書的信息格式是這樣的:

<li>
    <div class="list_num red">1.</div>   
    <div class="pic"><a href="http://product.dangdang.com/25345988.html" target="_blank"><img src="http://img3m8.ddimg.cn/8/26/25345988-1_l_1.jpg" alt="有話說出來!(徹底顛覆社會人脈的固有方式,社交電池幫你搞定社交。社交恐懼症患者必須擁有的一本實用社交指南,初入大學和職場的必備“攻略”,拿起這本書,你也是“魏瓔珞”)纖閱出品"  title="有話說出來!(徹底顛覆社會人脈的固有方式,社交電池幫你搞定社交。社交恐懼症患者必須擁有的一本實用社交指南,初入大學和職場的必備“攻略”,拿起這本書,你也是“魏瓔珞”)纖閱出品"/></a></div>    
    <div class="name"><a href="http://product.dangdang.com/25345988.html" target="_blank" title="有話說出來!(徹底顛覆社會人脈的固有方式,社交電池幫你搞定社交。社交恐懼症患者必須擁有的一本實用社交指南,初入大學和職場的必備“攻略”,拿起這本書,你也是“魏瓔珞”)纖閱出品">有話說出來!(徹底顛覆社會人脈的固有方式,社交電池幫你搞定社<span class='dot'>...</span></a></div>    
    <div class="star"><span class="level"><span style="width: 100%;"></span></span><a href="http://product.dangdang.com/25345988.html?point=comment_point" target="_blank">17757條評論</a><span class="tuijian">100%推薦</span></div>    
    <div class="publisher_info">【美】<a href="http://search.dangdang.com/?key=帕特里克·金" title="【美】帕特里克·金 著,張捷/李旭陽 譯" target="_blank">帕特里克·金</a> 著,<a href="http://search.dangdang.com/?key=張捷" title="【美】帕特里克·金 著,張捷/李旭陽 譯" target="_blank">張捷</a>/<a href="http://search.dangdang.com/?key=李旭陽" title="【美】帕特里克·金 著,張捷/李旭陽 譯" target="_blank">李旭陽</a> 譯</div>    
    <div class="publisher_info"><span>2018-08-01</span>&nbsp;<a href="http://search.dangdang.com/?key=天津人民出版社" target="_blank">天津人民出版社</a></div>    

            <div class="biaosheng">五星評分:<span>16273次</span></div>
                      
    
    <div class="price">        
        <p><span class="price_n">&yen;30.40</span>
                        <span class="price_r">&yen;42.00</span>(<span class="price_s">7.2折</span>)
                    </p>
                    <p class="price_e"></p>
                <div class="buy_button">
                          <a ddname="加入購物車" name="" href="javascript:AddToShoppingCart('25345988');" class="listbtn_buy">加入購物車</a>
                        
                        <a ddname="加入收藏" id="addto_favorlist_25345988" name="" href="javascript:showMsgBox('addto_favorlist_25345988',encodeURIComponent('25345988&platform=3'), 'http://myhome.dangdang.com/addFavoritepop');" class="listbtn_collect">收藏</a>
     
        </div>

    </div>

  是不是很亂?不要急,我們慢慢來分析,首先我們要明確自己要提取圖書的哪部分信息,我們這里決定爬取它的:

排名,書名,圖片地址,作者,推薦指數,五星評分次數和價格。

  那么對這么大段的html文本,怎么提取每本書的相關信息呢?答案自然是通過正則表達式,在parse_result函數中,先構建了用來匹配的正則表達式(第16行),隨后對傳入的html文件執行匹配,獲取匹配結果(第19行),注意,這一步需要re模塊的支持(在第1行導入re模塊),re.compile是對匹配符的封裝,直接用re.match(匹配符,要匹配的原文本)可以達到相同的效果, 當然,這里沒有用re.match來執行匹配,而是用了re.findall,這是因為后者可以適用於多行文本的匹配。另外,re.compile后面的第2個參數,re.S是用來應對換行的,.匹配的單個字符不包括\n和\r,當遇到換行時,我們需要用到re.S。

  上面的這段表述可能不大清楚,具體re模塊的正則匹配用法請自行百度,配合自己動手實驗才能真正明白,這里只能描述個大概,另外,我們這里不會從頭開始講解正則表達式的種種細節,而是僅對代碼中用到的正則表達式進行分析,要了解更多正則表達式相關的消息,就需要您自行百度了,畢竟對一個程序員來說,自學能力還是很重要的。

  好,我們來看下代碼用到的正則表達式:

  一段段來分析,首先是:

<li>.*?list_num.*?(\d+).</div>

  .代表匹配除了\n和\r之外的任意字符,*代表匹配0次或多次,?跟在限制符(這里是*)后面是代表使用非貪婪模式匹配,因為默認的正則匹配是貪婪匹配,比如下面這段代碼:

import re
content = 'abcabc'
res = re.match('a.*c',content)
print(res.group())

  此時匹配時會匹配盡可能長的字符串,因此會輸出abcabc,而若把a.*c改為a.*c?,此時是非貪婪匹配,會匹配盡可能少的字符串,因此會輸出abc。

  然后是\d,代表匹配一個數字,+代表匹配1個或多個。因此上面的表達式匹配的就是html文本中下圖所示的部分:

  

  注意,\d+被括號括起來了,代表將匹配的這部分內容(即圖中的1這個數字)捕獲並作為1個元素存放到了一個數組中,所以現在匹配結果對應的數組中(即item)第一個元素是1,也就是排名。

   隨后是

.*?<img src="(.*?)"

  顯然它匹配的是下面這段: 

  此時會把括號中匹配到的圖片地址作為第2個元素存到數組中  

  剩下的匹配都是同樣的原理,並沒有什么值得注意的點,就不一一描述了,整個正則表達式一共有7對括號,因此有數組中存了7個元素,分別對應我們要提取的排名,書名,圖片地址,作者,推薦指數,五星評分次數和價格。

  re.findall進行了進行了匹配后返回一個數組items,里面存放了所有匹配成功的條目(item),每個item對應一次對正則表達式的成功匹配,也就是上面說的7個元素的數組。

  隨后程序中遍歷了items數組,將數組中每個item的數據封裝成一個表,並分別進行返回。

  這里出了問題,每次返回的都是items數組中的一個item,而main函數中卻對返回值又進一步遍歷了其中的元素,並調用write_item_to_file將每個元素寫到自定義的文件中(43-44行)。

  我們來看下write_item_to_file:

  第31行的:

    with open('book.txt', 'a', encoding='UTF-8') as f:

  是一種簡化寫法,等價於:

try:
    f = open('book.txt', 'a')
    print(f.read())
finally:
    if f:
        f.close()

  具體可以參考:https://www.cnblogs.com/yizhenfeng/p/7554620.html

  這里打開book.txt后(沒有會自動創建),用write函數將item轉換成的json格式的字符串寫入(json.dumps函數是將一個Python數據類型列表進行json格式的編碼(可以這么理解,json.dumps()函數是將字典轉化為字符串))

  而我們傳入write_item_to_file函數的是item中的一個元素,顯然不是一個表,這當然不對。顯然這里源代碼寫得有問題,在parse_result函數中,不應該遍歷匹配到的items並返回其中的每個item,而是應該直接返回items(對應正則表達式所有匹配結果),這樣,在main函數中,就可以正常地對items遍歷,抽出每個item(對應正則表達式的一組匹配結果),傳入write_item_to_file,后者在寫入時,對item進行json轉換,由於item是一個表,可以正常轉換,自然也能正常寫入。

  下面給出修改后的代碼:

 1 import requests
 2 import re
 3 import json
 4 
 5 def request_dandan(url):
 6     try:
 7         #同步請求
 8         response = requests.get(url)
 9         if response.status_code == 200:
10             return response.text
11     except requests.RequestException:
12          return None
13 
14 def parse_result(html):
15     pattern = re.compile('<li>.*?list_num.*?(\d+).</div>.*?<img src="(.*?)".*?class="name".*?title="(.*?)">.*?class="star">.*?class="tuijian">(.*?)</span>.*?class="publisher_info">.*?target="_blank">(.*?)</a>.*?class="biaosheng">.*?<span>(.*?)</span></div>.*?<p><span\sclass="price_n">&yen;(.*?)</span>.*?</li>',re.S)
16     items = re.findall(pattern, html)
17     return items
18     # for item in items:
19     #     yield {
20     #          'range': item[0],
21     #          'iamge': item[1],
22     #          'title': item[2],
23     #          'recommend': item[3],
24     #          'author': item[4],
25     #          'times': item[5],
26     #         'price': item[6]
27     #     }
28 
29 def write_item_to_file(item):
30     print('開始寫入數據 ====> ' + str(item))
31     with open('book.txt', 'a', encoding='UTF-8') as f:
32         f.write(json.dumps(item, ensure_ascii=False) + '\n')
33 
34 def main(page):
35     url = 'http://bang.dangdang.com/books/fivestars/01.00.00.00.00.00-recent30-0-0-1-' + str(page)
36     html = request_dandan(url)
37     items = parse_result(html)
38     for item in items:
39         write_item_to_file(item)
40 
41 if __name__ == "__main__":
42     for i in range(1,5):
43         main(i) 

  標紅的就是修改的部分,去試試吧,此時查看新生成的book.txt文件,結果如下: 

  至此,代碼修改成功。下面記錄一些額外的知識點,感興趣的可以看看:

  原先的錯誤代碼用到了yield,之前用C#寫代碼時會用到協程,里面就用到了yield關鍵子,那么yield在Python中是怎么用的呢?

  事實上,函數中一旦有語句被yield標記,那這個函數就不一樣了,此時直接調用它是不會調用的,而得理解成一種賦值效果,相當於暫存了這個函數,當要真正調用此函數時,需要用next驅動它,沒驅動一次,就相當於調用一次這個函數,而yield標記的語句實現的功能可以理解為是return,因此調用函數時,一旦運行到yield語句,就會返回,但返回后會記住當前運行到的yield語句位置,當下次再調用yield時,會從之前中斷的yield語句處繼續執行剩下的代碼。

  是不是感覺有點抽象?不要着急,我為您精心准備了一份資料,參考下面這篇文章,相信你很快就能明白它的使用方法:

  https://www.cnblogs.com/gausstu/p/9545519.html

  至此,要講的基本講完了。最后講一個小知識點吧,寫代碼時,在return或yield后面的大括號,是不能移到下一行中的,比如Python中:

return {
    'range': 1,
    'image':here
    }
return 
    {
    'range': 1,
    'image':here
    }

這兩種寫法,區別僅僅是第二種把大括號寫在了下面那行,但在Python中,是會報錯的,這點需要注意一下。

 

  以前碰到問題搜別人的博客時,總是各種嫌棄,嫌棄這個寫得不清楚,那個寫得太簡單,現在終於明白為什么會這樣了,主要是寫博客確實比較麻煩,舉個例子吧,同樣的知識,花一天能吸收完,但真要把它寫出來,並且寫得比較清晰,別人能看懂,基本還要再花一天。以前還好,博主一直在讀研,有大把的時間可以記錄,現在工作了,也沒啥時間了,雖然工作中學了不少東西,但實在沒時間記錄,畢竟空余時間若是都用來寫博客,就沒時間學更多的東西。

  很多人的做法是用降低博客的質量來彌補,比如有些問題,明明很多細節,就用一兩句話一筆帶過,導致的結果就是除了自己沒人能看懂,別人照着做的時候各種踩坑。說真的,這其實是一種很自私的行為,對記錄人來說,可能真的只需要記幾筆,提醒下自己關鍵點即可。但對那些碰到問題去搜解決方案的人來說,真的是一種煎熬,我相信大家都有這樣的體會:項目中碰到了一個問題,去網上搜索解決方案,結果花了半天時間,網上的方案各種不靠譜,各種不詳細,耗時又耗神。

  在我看來,即使有着上面提到的原因,這種做法也是不可原諒的,隨着垃圾信息的不斷增加,每個人獲取有用信息的成本必然不斷上升,到最后,受害的是所有人。

  可惜我無法改變這一切,我唯一能做到的就是,在我的博客中,盡可能將我的探索過程描述清楚,將每個細節展現給來看我博客的人,畢竟你們花了時間看我的博客,我也不能對不起你們。

  好了,發點牢騷而已,不要在意,有空我會陸續將工作中碰到的問題及解決方案逐漸記錄下來的,可能會有點慢,但好在足夠詳細。

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM