如今的大多數動態網站,都是由瀏覽器端經過js發起ajax請求,拿到數據後再渲染完成頁面展現。這種狀況下采集數據,經過腳本發起http的get請求,拿到DOM文檔頁面後再解析提取有用數據的方法是行不通的。而後又有人會想到經過F12打開瀏覽器控制檯分析服務端api,再模擬請求相應的api來拿到咱們想要的數據,這種思路在一些狀況下可行,可是不少大型網站都會採起一些反爬策略,出於安全性考慮,每每對接口增長了安全驗證,好比只有設置了相關的header和cookie,才能對頁面進行請求;還有的對請求來源也作了限制等等,這個時候經過這種方式採集數據就更加困難了。咱們還有其餘有效的方法嗎?固然,python作爬蟲很是的簡單,咱們先來了解一下Selenium和Selectors,而後經過爬取美團網上商家信息的例子總結一下數據採集的一些技巧:css
我以家附近朝陽大悅城中的一家美食店爲例進行數據採集,網址是:python
https://www.meituan.com/meishi/40453459/
源碼地址mysql
咱們要抓取的第一部分數據是商家的基本信息,包括商家名稱、地址、電話、營業時間,分析多個美食類商家咱們可知,這些商家的web界面在佈局上基本是一致的,因此咱們的爬蟲能夠寫的比較通用。爲了防止對商家數據的重複抓取,咱們將商家的網址信息也存儲到數據表中。
第二部分要抓取的數據是美食店的招牌菜,每一個店鋪基本都有本身的特點菜,咱們將這些數據也保存下來,用另外的一張數據表存儲。
最後一部分咱們要抓取的數據是用戶的評論,這部分數據對咱們來講是頗有價值的,未來咱們能夠經過對這部分數據的分析,提取更多關於商家的信息。咱們要抓取的這部分信息有:評論者暱稱、星級、評論內容、評論時間,若是有圖片,咱們也要將圖片的地址以列表的形式存下來。linux
咱們存儲數據使用的數據庫是Mysql,Python有相關的ORM,項目中咱們使用peewee。可是在創建數據表時建議採用原生的sql,這樣咱們能靈活的控制字段屬性,設置引擎和字符編碼格式等。使用Python的ORM也能夠達到效果,可是ORM是對數據庫層的封裝,像sqlite、sqlserver數據庫和Mysql仍是有些許差異的,使用ORM只能使用這些數據庫共有的部分。下面是存儲數據須要用到的數據表sql:git
CREATE TABLE `merchant` ( #商家表 `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL COMMENT '商家名稱', `address` varchar(255) NOT NULL COMMENT '地址', `website_address` varchar(255) NOT NULL COMMENT '網址', `website_address_hash` varchar(32) NOT NULL COMMENT '網址hash', `mobile` varchar(32) NOT NULL COMMENT '電話', `business_hours` varchar(255) NOT NULL COMMENT '營業時間', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4; CREATE TABLE `recommended_dish` ( #推薦菜表 `id` int(11) NOT NULL AUTO_INCREMENT, `merchant_id` int(11) NOT NULL COMMENT '商家id', `name` varchar(255) NOT NULL COMMENT '推薦菜名稱', PRIMARY KEY (`id`), KEY `recommended_dish_merchant_id` (`merchant_id`), CONSTRAINT `recommended_dish_ibfk_1` FOREIGN KEY (`merchant_id`) REFERENCES `merchant` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=309 DEFAULT CHARSET=utf8mb4; CREATE TABLE `evaluate` ( #評論表 `id` int(11) NOT NULL AUTO_INCREMENT, `merchant_id` int(11) NOT NULL COMMENT '商家id', `user_name` varchar(255) DEFAULT '' COMMENT '評論人暱稱', `evaluate_time` datetime NOT NULL COMMENT '評論時間', `content` varchar(10000) DEFAULT '' COMMENT '評論內容', `star` tinyint(4) DEFAULT '0' COMMENT '星級', `image_list` varchar(1000) DEFAULT '' COMMENT '圖片列表', PRIMARY KEY (`id`), KEY `evaluate_merchant_id` (`merchant_id`), CONSTRAINT `evaluate_ibfk_1` FOREIGN KEY (`merchant_id`) REFERENCES `merchant` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=8427 DEFAULT CHARSET=utf8mb4;
相應的咱們也可使用Python的ORM建立管理數據表,後邊具體分析到代碼時會講到peewee對mysql數據庫的一些經常使用操作,好比查詢數據,插入數據庫數據並返回id;批量插入數據庫等,讀者可蒐集相關資料系統學習。
meituan_spider/models.py代碼:github
from peewee import * # 鏈接數據庫 db = MySQLDatabase("meituan_spider", host="127.0.0.1", port=3306, user="root", password="root", charset="utf8") class BaseModel(Model): class Meta: database = db # 商家表,用來存放商家信息 class Merchant(BaseModel): id = AutoField(primary_key=True, verbose_name="商家id") name = CharField(max_length=255, verbose_name="商家名稱") address = CharField(max_length=255, verbose_name="商家地址") website_address = CharField(max_length=255, verbose_name="網絡地址") website_address_hash = CharField(max_length=32, verbose_name="網絡地址的md5值,爲了快速索引") mobile = CharField(max_length=32, verbose_name="商家電話") business_hours = CharField(max_length=255, verbose_name="營業時間") # 商家推薦菜表,存放菜品的推薦信息 class Recommended_dish(BaseModel): merchant_id = ForeignKeyField(Merchant, verbose_name="商家外鍵") name = CharField(max_length=255, verbose_name="推薦菜名稱") # 用戶評價表,存放用戶的評論信息 class Evaluate(BaseModel): id = CharField(primary_key=True) merchant_id = ForeignKeyField(Merchant, verbose_name="商家外鍵") user_name = CharField(verbose_name="用戶名") evaluate_time = DateTimeField(verbose_name="評價時間") content = TextField(default="", verbose_name="評論內容") star = IntegerField(default=0, verbose_name="評分") image_list = TextField(default="", verbose_name="圖片") if __name__ == "__main__": db.create_tables([Merchant, Recommended_dish, Evaluate])
代碼比較簡單,可是讓代碼運行起來,須要安裝前邊提到的工具包:selenium、scrapy,另外使用peewee也須要安裝,這些包均可以經過pip進行安裝;另外selenium驅動瀏覽器還須要安裝相應的driver,由於我本地使用的是chrome瀏覽器,因此我下載了相關版本的chromedriver,這個後邊會使用到。請讀者自行查閱python操做selenium須要作的準備工做,先手動搭建好相關環境。接下來詳細分析代碼;源代碼以下:web
from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.common.exceptions import NoSuchElementException from scrapy import Selector from models import * import hashlib import os import re import time import json chrome_options = Options() # 設置headless模式,這種方式下無啓動界面,可以加速程序的運行 # chrome_options.add_argument("--headless") # 禁用gpu防止渲染圖片 chrome_options.add_argument('disable-gpu') # 設置不加載圖片 chrome_options.add_argument('blink-settings=imagesEnabled=false') # 經過頁面展現的像素數計算星級 def star_num(num): numbers = { "16.8": 1, "33.6": 2, "50.4": 3, "67.2": 4, "84": 5 } return numbers.get(num, 0) # 解析商家內容 def parse(merchant_id): weblink = "https://www.meituan.com/meishi/{}/".format(merchant_id) # 啓動selenium browser = webdriver.Chrome(executable_path="/Users/guozhaoran/python/tools/chromedriver", options=chrome_options) browser.get(weblink) # 不重複爬取數據 hash_weblink = hashlib.md5(weblink.encode(encoding='utf-8')).hexdigest() existed = Merchant.select().where(Merchant.website_address_hash == hash_weblink) if (existed): print("數據已經爬取") os._exit(0) time.sleep(2) # print(browser.page_source) #獲取到網頁渲染後的內容 sel = Selector(text=browser.page_source) # 提取商家的基本信息 # 商家名稱 name = "".join(sel.xpath("//div[@id='app']//div[@class='d-left']//div[@class='name']/text()").extract()).strip() detail = sel.xpath("//div[@id='app']//div[@class='d-left']//div[@class='address']//p/text()").extract() address = "".join(detail[1].strip()) mobile = "".join(detail[3].strip()) business_hours = "".join(detail[5].strip()) # 保存商家信息 merchant_id = Merchant.insert(name=name, address=address, website_address=weblink, website_address_hash=hash_weblink, mobile=mobile, business_hours=business_hours ).execute() # 獲取推薦菜信息 recommended_dish_list = sel.xpath( "//div[@id='app']//div[@class='recommend']//div[@class='list clear']//span/text()").extract() # 遍歷獲取到的數據,批量插入數據庫 dish_data = [{ 'merchant_id': merchant_id, 'name': i } for i in recommended_dish_list] Recommended_dish.insert_many(dish_data).execute() # 也能夠遍歷list,一條條插入數據庫 # for dish in recommended_dish_list: # Recommended_dish.create(merchant_id=merchant_id, name=dish) # 查看連接一共有多少頁的評論 page_num = 0 try: page_num = sel.xpath( "//div[@id='app']//div[@class='mt-pagination']//ul[@class='pagination clear']//li[last()-1]//span/text()").extract_first() page_num = int("".join(page_num).strip()) # page_num = int(page_num) except NoSuchElementException as e: print("改商家沒有用戶評論信息") os._exit(0) # 當有用戶評論數據,每頁每頁的讀取用戶數據 if (page_num): i = 1 number_pattern = re.compile(r"\d+\.?\d*") chinese_pattern = re.compile(u"[\u4e00-\u9fa5]+") illegal_str = re.compile(u'[^0-9a-zA-Z\u4e00-\u9fa5.,,。?「」]+', re.UNICODE) while (i <= page_num): # 獲取評論區元素 all_evalutes = sel.xpath( "//div[@id='app']//div[@class='comment']//div[@class='com-cont']//div[2]//div[@class='list clear']") for item in all_evalutes: # 獲取用戶暱稱 user_name = item.xpath(".//div[@class='info']//div[@class='name']/text()").extract()[0] # 獲取用戶評價星級 star = item.xpath( ".//div[@class='info']//div[@class='source']//div[@class='star-cont']//ul[@class='stars-ul stars-light']/@style").extract_first() starContent = "".join(star).strip() starPx = number_pattern.search(starContent).group() starNum = star_num(starPx) # 獲取評論時間 comment_time = "".join( item.xpath(".//div[@class='info']//div[@class='date']//span/text()").extract_first()).strip() evaluate_time = chinese_pattern.sub('-', comment_time, 3)[:-1] + ' 00:00:00' # 獲取評論內容 comment_content = "".join( item.xpath(".//div[@class='info']//div[@class='desc']/text()").extract_first()).strip() comment_filter_content = illegal_str.sub("", comment_content) # 若是有圖片,獲取圖片 image_container = item.xpath( ".//div[@class='noShowBigImg']//div[@class='imgs-content']//div[contains(@class, 'thumbnail')]//img/@src").extract() image_list = json.dumps(image_container) Evaluate.insert(merchant_id=merchant_id, user_name=user_name, evaluate_time=evaluate_time, content=comment_filter_content, star=starNum, image_list=image_list).execute() i = i + 1 if (i < page_num): next_page_ele = browser.find_element_by_xpath( "//div[@id='app']//div[@class='mt-pagination']//span[@class='iconfont icon-btn_right']") next_page_ele.click() time.sleep(10) sel = Selector(text=browser.page_source) if __name__ == "__main__": parse("5451106")
爲了讓爬蟲更加通用,咱們的解析函數經過接收商家"參數id"來摘取不一樣商家的網頁內容。selenium經過webdriver驅動web瀏覽器:ajax
weblink = "https://www.meituan.com/meishi/{}/".format(merchant_id) # 啓動selenium browser = webdriver.Chrome(executable_path="/Users/guozhaoran/python/tools/chromedriver", options=chrome_options) browser.get(weblink)
其中executable_path就是以前咱們下載好的相關版本的chromedriver可執行文件,另外selenium啓動web瀏覽器以前還能夠設置一些參數:正則表達式
chrome_options = Options() # 設置headless模式,這種方式下無啓動界面,可以加速程序的運行 # chrome_options.add_argument("--headless") # 禁用gpu防止渲染圖片 chrome_options.add_argument('disable-gpu') # 設置不加載圖片 chrome_options.add_argument('blink-settings=imagesEnabled=false')
設置--headless可讓chrome不啓動前臺界面運行,有點相似於守護進程,不過在調試代碼的過程當中咱們能夠不設置這個參數,這樣就能看到程序對瀏覽器中的網頁具體進行了哪些操做。另外咱們還能夠經過disable-gpu、blink-settings=imagesEnabled=false使瀏覽器解析網頁過程當中不加載圖片來提升瀏覽器渲染網頁的速度;由於咱們數據中存儲的圖片數據也只是路徑而已。selenium作爬蟲的一個缺點是效率比較低,爬取速度慢,可是經過設置這些優化參數,也是能夠極大提高爬蟲抓取速度的。sql
前邊提到過,爲了避免重複爬取數據,咱們會對要抓取的商家進行hash校驗:
# 不重複爬取數據 hash_weblink = hashlib.md5(weblink.encode(encoding='utf-8')).hexdigest() existed = Merchant.select().where(Merchant.website_address_hash == hash_weblink) if (existed): print("數據已經爬取") os._exit(0)
若是商家數據沒有被爬取過,咱們就獲取到網頁數據進行解析:
time.sleep(2) # print(browser.page_source) #獲取到網頁渲染後的內容 sel = Selector(text=browser.page_source)
sleep兩秒是由於browser對象解析網頁須要時間,不過這個時間通常會很快,這裏是爲了使程序更加穩妥;以後構造一個選擇器對頁面數據進行解析:
# 提取商家的基本信息 # 商家名稱 name = "".join(sel.xpath("//div[@id='app']//div[@class='d-left']//div[@class='name']/text()").extract()).strip() detail = sel.xpath("//div[@id='app']//div[@class='d-left']//div[@class='address']//p/text()").extract() address = "".join(detail[1].strip()) mobile = "".join(detail[3].strip()) business_hours = "".join(detail[5].strip()) # 保存商家信息 merchant_id = Merchant.insert(name=name, address=address, website_address=weblink, website_address_hash=hash_weblink, mobile=mobile, business_hours=business_hours ).execute()
解析商家基本信息是經過xpath語法定位到相關元素而後提取文本信息,爲了保證提取的數據都是不爲空的字符串,進行了字符串拼接;最後將解析到的數據插入到商家數據表,peewee的insert方法返回了主鍵id,在後邊採集數據入庫時會使用到。
提取商家特點菜信息邏輯比較簡單,提取出來的數據返回一個list,python解析數據類型很是的方便,不過數據入庫時有不一樣的方案,能夠批量插入也能夠循環遍歷列表插入,這裏咱們採用批量插入。這樣效率會更高。
# 獲取推薦菜信息 recommended_dish_list = sel.xpath( "//div[@id='app']//div[@class='recommend']//div[@class='list clear']//span/text()").extract() # 遍歷獲取到的數據,批量插入數據庫 dish_data = [{ 'merchant_id': merchant_id, 'name': i } for i in recommended_dish_list] Recommended_dish.insert_many(dish_data).execute() # 也能夠遍歷list,一條條插入數據庫 # for dish in recommended_dish_list: # Recommended_dish.create(merchant_id=merchant_id, name=dish)
用戶信息的提取是數據抓取中最難的部分了,基本思路就是咱們首先查看有多少頁的用戶評論,而後再一頁一頁的解析用戶評論信息。期間咱們能夠經過selenium模擬瀏覽器的點擊事件進行翻頁,入庫的時候還要注意對文本進行清洗,由於評論中不少的表情字符是不符合數據表字段設計的編碼規範的,另外點擊了下一頁以後,程序必定要sleep一段時間,由於網站的數據發生了更新,要進行頁面數據的從新獲取。咱們先來看看如何獲取一共有多少頁的用戶評論數據,網站的分頁圖以下:
這裏咱們重點關注兩個按鈕,一個是下一頁,另外一個是最後一頁的數字,這是咱們想要的信息,不過有些商家可能沒有相關的用戶評論,頁面上也沒有相關的元素,程序仍是要作一下兼容性處理的:
# 查看連接一共有多少頁的評論 page_num = 0 try: page_num = sel.xpath( "//div[@id='app']//div[@class='mt-pagination']//ul[@class='pagination clear']//li[last()-1]//span/text()").extract_first() page_num = int("".join(page_num).strip()) # page_num = int(page_num) except NoSuchElementException as e: print("改商家沒有用戶評論信息") os._exit(0)
接下來就是像獲取商場特點菜同樣獲取一條條的評論數據了,只是過程比較繁瑣而已,咱們的基本思路就是這樣:
if (page_num): i = 1 ... while (i <= page_num): ... i = i + 1 if (i < page_num): next_page_ele = browser.find_element_by_xpath( "//div[@id='app']//div[@class='mt-pagination']//span[@class='iconfont icon-btn_right']") next_page_ele.click() time.sleep(10) sel = Selector(text=browser.page_source)
咱們判斷程序解析是否到了最後一頁,若是沒有,經過模擬點擊下一頁得到新頁面,程序sleep是爲了給瀏覽器解析新頁面數據留下時間。詳細的解析過程咱們挑幾個重點說一下:
# 經過頁面展現的像素數計算星級 def star_num(num): numbers = { "16.8": 1, "33.6": 2, "50.4": 3, "67.2": 4, "84": 5 } return numbers.get(num, 0) ... # 獲取用戶評價星級 star = item.xpath( ".//div[@class='info']//div[@class='source']//div[@class='star-cont']//ul[@class='stars-ul stars-light']/@style").extract_first() starContent = "".join(star).strip() starPx = number_pattern.search(starContent).group() starNum = star_num(starPx)
number_pattern = re.compile(r"\d+\.?\d*") chinese_pattern = re.compile(u"[\u4e00-\u9fa5]+") illegal_str = re.compile(u'[^0-9a-zA-Z\u4e00-\u9fa5.,,。?「」]+', re.UNICODE) while (i <= page_num): ... comment_content = "".join( item.xpath(".//div[@class='info']//div[@class='desc']/text()").extract_first()).strip() comment_filter_content = illegal_str.sub("", comment_content)
image_container = item.xpath( ".//div[@class='noShowBigImg']//div[@class='imgs-content']//div[contains(@class, 'thumbnail')]//img/@src").extract() image_list = json.dumps(image_container)
下邊是程序運行過程當中數據抓取的截圖:程序的思路很簡潔,真實的企業應用中,可能會更多的考慮爬蟲的效率和穩定性。通常linux服務器下程序發生錯誤,都會記錄有相關的日誌,selenium也只能是無界面的運行,程序中沒有用到太多的高級特性,其實一個爬蟲架構中要包含的技術點有不少,好比多線程的數據爬取,還有針對驗證碼反爬的驗證(本示例中第一次打開美團頁面也須要驗證,我手動處理了一次)等等,這裏算是起一個拋磚引玉的目的吧。不過程序中使用到的文本處理技巧、數據分析提取等都是爬蟲中常常會使用到的,很高興在這裏和你們一塊分享。