目標
1 打開安居客二手房頁面,如 https://nanning.anjuke.com/sale/?from=navigation 。得到如下頁面。
通過分析發現,每個主頁有60個二手房信息。一共有50個主頁(一般類似網站都只提供50個主頁)。
2 打開其中一個二手房的信息后,跳轉到如下頁面。我們的目標是要得到下圖所示框起來的“房屋信息”的內容。
也就是我們需要爬取 50 * 60 = 3000 個“房屋信息”
思路
1 獲取50個主頁源碼。
如果使用本機ip進行reques請求主頁源碼后,安居客的反爬機制會檢測出我們的請求,提示如下頁面。為了解決這個問題,應該使用代理IP代替本機ip。
本次使用蘑菇隧道代理IP,並且對官方接口進行了部分修改。在定義的接口函數中,使用while循環和 try : ... except : ... 的目的是為了能夠在請求url失敗的時候重新進行請求。使用 if 語句來判斷請求的次數是否超過8次,如果超過8次那么就退出請求。請求安居客的頁面時,有時候雖然請求失敗了(即請求到的內容不是我們需要的內容),但是程序也不會報錯,而是返回一個無效的頁面,這個無效的頁面的字符串長度小於1000。因此可以使用 if 語句來判斷請求到的內容的字符串長度是否大於1000,如果大於1000就說明獲取到的內容是我們所需要的。注意:當蘑菇隧道代理的代理IP每秒請求(並發)為1時,如果請求失敗,需要重新請求,需要在重新請求前延時等待一秒。即添加 time.sleep(1)
2 獲取主頁源碼后,使用xpath抓取每個主頁的60個二手房的跳轉鏈接。如下圖所示。
3 使用requests請求獲取到的跳轉鏈接。
4 使用xpath對跳轉鏈接的源碼獲取房屋信息的內容。
獲取到詳細頁的源碼后,通過觀察發現,房屋信息的每一個小信息(包括字段和對應內容)都存放在一個<li>標簽里。這個小信息的字段是<li>標簽里的第一個<div>標簽的文本,字段對應的內容是<li>標簽里的第二個<div>標簽的文本。
我們需要成對地將一個小信息的字段和內容爬取出來,做法是:
①將小信息的標簽用xpath定位。
我第一次在對應代碼位置通過點擊右鍵的Copy的Cope XPath定位的路徑,得到定位的路徑 " //*[@id="content"]/div[3]/div[1]/div[3]/div/div[1]/ul/li[1] ",這個路徑是通過id來定位的,但是使用xpath查找這個路徑時,獲取不到結果。因此后面我改為使用class定位就能獲取到結果,即" //*//li[@class="houseInfo-detail-item"] "。總結:xpath有時候如果使用id定位不到,建議換其他屬性定位。
因為每一個小信息的class屬性都是一樣的,所以獲取到一個列表,這個列表的元素是每一個小信息。
②對獲取到的列表進行遍歷,每次遍歷的結果是一個小信息。對這個小信息用xpath方法獲取第一個div文本(字段)和第二個div文本(對應的內容)。
③對得到的字段和對應的內容進行數據的清洗。然后以鍵值對的形式添加到一個空字典。
5 通過多個詳情頁觀察發現,存在一些詳情頁的房屋信息的小信息個數不同。詳情頁的小信息最多有18個。有的詳情頁小信息個數少於18個。針對這個問題,解決的方法是:
創建一個列表,這個列表有18個字段。對這個列表進行遍歷,使用 not in 方法判斷每次遍歷出的字段是否在字典的鍵里,如果不在,那么就以鍵值對的方式給字典添加這個不在的字段和對應的內容 '暫無' 。
1 import requests 2 from lxml import etree 3 import re 4 5 # 該函數請求網頁時使用的代理IP是蘑菇隧道代理,該函數用於請求網頁源代碼。 6 def mogu_suidaodaili(url,appKey): 7 # 蘑菇隧道代理服務器地址 8 ip_port = 'secondtransfer.moguproxy.com:9001' 9 proxy = {"http": "http://" + ip_port, "https": "https://" + ip_port} 10 headers = { 11 "Proxy-Authorization": 'Basic ' + appKey, 12 "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0) Gecko/20100101 Firefox/6.0", 13 "Accept-Language": "zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4"} 14 num = 0 15 while 1: 16 if num != 8: 17 try: 18 r = requests.get(url=url, headers=headers, proxies=proxy, verify=False, allow_redirects=False) 19 if r.status_code == 302 or r.status_code == 301: 20 loc = r.headers['Location'] 21 print(loc) 22 url_f = loc 23 r = requests.get(url_f, headers=headers, proxies=proxy, verify=False, allow_redirects=False) 24 return r.content.decode('utf-8') 25 if len(r.content.decode('utf-8')) > 1000 : 26 return r.content.decode('utf-8') 27 else: 28 num = num + 1 29 print('當前代理ip請求失敗,正在進行第' + str(num) + '次重新請求') 30 except: 31 num = num + 1 32 print('當前代理ip請求失敗,正在進行第'+str(num)+'次重新請求') 33 else: 34 return '請求8次失敗,退出請求' 35 36 def get_jump_url(home_page): 37 etree_object = etree.HTML(home_page) 38 jump_url = etree_object.xpath('//*[@id="houselist-mod-new"]/li/div[2]/div[1]/a/@href') 39 return jump_url 40 41 def get_information(detail_page): 42 etree_object = etree.HTML(detail_page) 43 # 因為我使用在對應代碼位置通過點擊右鍵的Copy的Cope XPath定位的路徑,即//*[@id="content"]/div[3]/div[1]/div[3]/div/div[1]/ul/li[1]。這個路徑是通過id來定位的,但是這個路徑獲取不到內容,所以改為使用class屬性定位。有時候如果使用id定位不到,建議換其他屬性定位。 44 informations = etree_object.xpath('//*//li[@class="houseInfo-detail-item"]') 45 46 dict = {} 47 for information in informations: 48 ziduan = information.xpath('div[1]//text()')[0] 49 content = information.xpath('div[2]//text()') 50 # str.strip(str) 這個方法移除字符串頭尾指定的字符或字符序列 51 ziduan = ziduan.strip(':') 52 53 # '指定分隔符'.join(seq) 這個方法將序列中的元素以指定分隔符連接生成一個新的字符串 54 content = ''.join(content) 55 56 # re.sub(pattern, repl, string, count) 替換函數,將規則表達式pattern匹配到的字符串替換為repl指定的字符串,參數count用於指定最大替換次數。正則表達式中 \s 用於匹配任意空白字符(包括\t\n\r\f\v) 57 pattern = '\s' 58 content = re.sub(pattern,'',content) 59 60 # 因為遍歷出的一個content值的尾部有\ue003,所以需要用到strip方法去掉。 61 content = content.strip('\ue003') 62 63 #dict.update(dict2) 這個方法把字典dict2的鍵/值對更新到dict里。 64 dict.update({ziduan:content}) 65 66 list = ['所屬小區','房屋戶型','房屋單價','所在位置','建築面積','參考首付','建造年代','房屋朝向','房屋類型','所在樓層','裝修程度','產權年限','配套電梯','房本年限','產權性質','唯一住房','一手房源','測試'] 67 for i in list: 68 # dict.keys() 這個方法以列表返回一個字典所有的鍵。 69 if i not in dict.keys(): 70 dict[i] = '暫無' 71 return dict 72 73 if __name__ == '__main__': 74 appKey = ****** 75 for page in range(50): 76 page = page + 1 77 url = 'https://nanning.anjuke.com/sale/p'+str(page)+'/#filtersort' 78 print('=============================現在請求的是第'+str(page)+'頁================================') 79 home_page = mogu_suidaodaili(url=url,appKey=appKey) 80 jump_urls = get_jump_url(home_page) 81 for jump_url in jump_urls: 82 detail_page = mogu_suidaodaili(url=jump_url,appKey=appKey) 83 print(get_information(detail_page))
補充
當我們后期如果想做數據分析的話,就會需要大量的信息。我們發現標簽為 區域:全部;售價:全部;面積:全部;房型:全部 的50個主頁提供的信息並不能滿足數據分析的數量。
要想獲取更多信息,可以依次抓取標簽順序如下的的50個主頁提供的信息:
區域:青秀;售價:50萬以下;面積:50²以下;房型:一室
區域:青秀;售價:50萬以下;面積:50²以下;房型:二室
。。。
區域:青秀;售價:50萬以下;面積:50m²以下;房型:五室以上
區域:青秀;售價:50萬以下;面積:50-70m²;房型:一室
。。。
區域:青秀;售價:50萬以下;面積:50-70m²;房型:五室以上
。。。
區域:其他;售價:300萬以上;面積:300m²以上;房型:五室以上