五一將至,又到了學習的季節。目前流行的各大書單主打的都是豆瓣8.0評分書籍,卻不多有人來聊聊這9.0評分的書籍長什麼樣子。恰好最近學了學python爬蟲,那就拿豆瓣讀書來練練手。html
原本思路是直接爬豆瓣的書籍目錄,將評分9.0以上的書篩選出來,一打開發現事情並不簡單,幾千萬本書可很差爬 = =,因而轉化一下思路,看有沒有相似的書單。java
一搜還真有,找到一個9.0評分的榜單,大大減小了工做量,這樣就不用先爬一下整站書籍來篩選了。看了看榜單,應該是某位好心的書友手工整理的,更新時間爲2018-12-25,目前一共530本,分爲22頁,也就是說22次訪問就能搞定了,不會給豆瓣的服務器形成壓力。python
目標URL:https://www.douban.com/doulist/1264675/?start=0&sort=seq&playable=0&sub_type=4git
數據量:530github
預計訪問次數:22web
數據存儲:csv正則表達式
抓取內容格式:書籍名稱 做者 評分 評價人數 出版社 出版年 封面連接數據庫
有了小目標,接下來就是用剛學的 python 來現學現賣了。服務器
先來定一下步驟:多線程
# 設置headers # 獲取代理 # 獲取網頁數據 # 解析書籍數據 # 存入csv文件
而後一步步來填坑便可,先來設置headers,主要是設置UA來繞過訪問限制:
url = 'https://www.douban.com/doulist/1264675/?start=0&sort=seq&playable=0&sub_type=4' logging.basicConfig(level=logging.DEBUG) ua = UserAgent() # 設置headers headers = {'User-Agent': ua.random}
固然,只設置UA也無法逃過訪問限制,IP限制這一關仍是存在的,因此須要使用代理來繞開。
因此先來爬一爬代理的數據,弄一批能用的代理IP下來:
# 獲取代理數據 def get_proxies(proxy_url, dis_url, page=10): proxy_list = [] for i in range(1, page + 1): tmp_ua = UserAgent() tmp_headers = {'User-Agent': tmp_ua.random} html_str = get_web_data(proxy_url + str(i), tmp_headers) soup = BeautifulSoup(html_str.content, "lxml") ips = soup.find('tbody').find_all('tr') for ip_info in ips: tds = ip_info.find_all('td') ip = tds[0].get_text() port = tds[1].get_text() ip_str = ip + ":" + port tmp = {"http": "http://" + ip_str} if check_proxy(dis_url, tmp): logging.info("ip:%s is available", ip_str) proxy_list.append(ip_str) time.sleep(1) return proxy_list # 檢測代理ip是否可用 def check_proxy(url, proxy): try: tmp_ua = UserAgent() tmp_headers = {'User-Agent': tmp_ua.random} res = requests.get(url, proxies=proxy, timeout=1, headers=tmp_headers) except: return False else: return True
這裏其實有兩個函數,一個是get_proxies函數,用來從代理頁面爬數據,這裏選用的是快代理,一個是check_proxy函數,用來檢測該ip是否能訪問目標頁面,若是能訪問,則將其添加到可用代理列表。
而後是獲取網頁內容,這裏使用requests模塊來獲取網頁內容:
# 獲取網頁數據 def get_web_data(url, headers, proxies=[]): try: data = requests.get(url, proxies=proxies, timeout=3, headers=headers) except requests.exceptions.ConnectionError as e: logging.error("請求錯誤,url:", url) logging.error("錯誤詳情:", e) data = None except: logging.error("未知錯誤,url:", url) data = None return data
接下來進行網頁內容解析,藉助一下BeautifulSoup模塊和re正則模塊來解析網頁元素。
# 解析書籍數據 def parse_data(data): if data is None: return None # 處理編碼 charset = chardet.detect(data.content) data.encoding = charset['encoding'] # 正則表達式匹配做者出版社信息 author_pattern = re.compile(r'(做者: (.*))?[\s|\S]*出版社: (.*)[\s|\S]*出版年: (.*)') # 解析標籤 soup = BeautifulSoup(data.text, 'lxml') book_list = soup.find_all("div", class_="bd doulist-subject") list = [] for book in book_list: book_map = {} book_name = book.find('div', class_='title').get_text().strip() book_map['book_name'] = book_name rate_point = book.find('div', class_='rating').find('span', class_='rating_nums').get_text().strip() book_map['rate_point'] = rate_point rate_number = book.find('div', class_='rating').find('span', class_='').get_text().strip()[1:-4] book_map['rate_number'] = rate_number tmp = book.find('div', class_='abstract').get_text().strip() m = author_pattern.match(tmp) if m != None: author = m.group(1) if author == None: author = '' publisher = m.group(3) publish_date = m.group(4) book_map['author'] = author book_map['publisher'] = publisher book_map['publish_date'] = publish_date pic_link = book.find('div', class_='post').a.img['src'] book_map['pic_link'] = pic_link list.append(book_map) logging.info("書名:《%s》,做者:%s,評分:%s,評分人數:%s,出版社:%s,出版年:%s,封面連接:%s", book_name, author, rate_point, rate_number, publisher, publish_date, pic_link) return list
而後將結果存入csv文件中:
# 存入csv文件 def save_to_csv(filename, books): with open(filename, 'a', newline='', encoding='utf-8') as f: writer = csv.DictWriter(f, fieldnames=books[0].keys()) for book in books: writer.writerow(book) f.close()
這樣,咱們總體的代碼就差很少成型了,所有代碼以下:
#!/usr/bin/python # -*- coding: utf-8 -*- """ auth: Frank date: 2019-04-27 desc: 爬取豆瓣讀書評分9.0以上書籍並存入csv文件 目標URL:https://www.douban.com/doulist/1264675/?start=0&sort=seq&playable=0&sub_type=4 數據量:530 預計訪問次數:22 數據存儲:csv 抓取內容格式:書籍名稱 做者 做者國籍 評分 評價人數 出版社 出版年 封面連接 """ import logging import os import random import urllib.robotparser import time import requests import re import chardet from bs4 import BeautifulSoup from fake_useragent import UserAgent import csv # 獲取網頁數據 def get_web_data(url, headers, proxies=[]): try: data = requests.get(url, proxies=proxies, timeout=3, headers=headers) except requests.exceptions.ConnectionError as e: logging.error("請求錯誤,url:", url) logging.error("錯誤詳情:", e) data = None except: logging.error("未知錯誤,url:", url) data = None return data # 解析書籍數據 def parse_data(data): if data is None: return None # 處理編碼 charset = chardet.detect(data.content) data.encoding = charset['encoding'] # 正則表達式匹配做者出版社信息 author_pattern = re.compile(r'(做者: (.*))?[\s|\S]*出版社: (.*)[\s|\S]*出版年: (.*)') # 解析標籤 soup = BeautifulSoup(data.text, 'lxml') book_list = soup.find_all("div", class_="bd doulist-subject") list = [] for book in book_list: book_map = {} book_name = book.find('div', class_='title').get_text().strip() book_map['book_name'] = book_name rate_point = book.find('div', class_='rating').find('span', class_='rating_nums').get_text().strip() book_map['rate_point'] = rate_point rate_number = book.find('div', class_='rating').find('span', class_='').get_text().strip()[1:-4] book_map['rate_number'] = rate_number tmp = book.find('div', class_='abstract').get_text().strip() m = author_pattern.match(tmp) if m is not None: author = m.group(1) if author is None: author = '' publisher = m.group(3) publish_date = m.group(4) book_map['author'] = author book_map['publisher'] = publisher book_map['publish_date'] = publish_date pic_link = book.find('div', class_='post').a.img['src'] book_map['pic_link'] = pic_link list.append(book_map) logging.info("書名:《%s》,做者:%s,評分:%s,評分人數:%s,出版社:%s,出版年:%s,封面連接:%s", book_name, author, rate_point, rate_number, publisher, publish_date, pic_link) return list # 存入csv文件 def save_to_csv(filename, books): with open(filename, 'a', newline='', encoding='utf-8') as file: writer = csv.DictWriter(file, fieldnames=books[0].keys()) for tmp_book in books: writer.writerow(tmp_book) # 獲取代理數據 def get_proxies(proxy_url, dis_url, page=10): proxy_list = [] for i in range(1, page + 1): tmp_ua = UserAgent() tmp_headers = {'User-Agent': tmp_ua.random} html_str = get_web_data(proxy_url + str(i), tmp_headers) soup = BeautifulSoup(html_str.content, "lxml") ips = soup.find('tbody').find_all('tr') for ip_info in ips: tds = ip_info.find_all('td') ip = tds[0].get_text() port = tds[1].get_text() ip_str = ip + ":" + port tmp = {"http": "http://" + ip_str} if check_proxy(dis_url, tmp): logging.info("ip:%s is available", ip_str) proxy_list.append(ip_str) time.sleep(1) return proxy_list # 檢測代理ip是否可用 def check_proxy(url, proxy): try: tmp_ua = UserAgent() tmp_headers = {'User-Agent': tmp_ua.random} res = requests.get(url, proxies=proxy, timeout=1, headers=tmp_headers) except: return False else: return True def get_random_ip(ip_list): proxy = random.choice(ip_list) proxies = {'http': 'http://' + proxy} return proxies if __name__ == '__main__': logging.basicConfig(level=logging.INFO) url = 'https://www.douban.com/doulist/1264675/?start=' file_path = os.path.dirname(os.path.realpath(__file__)) + os.sep + 'douban.csv' f = open(file_path, 'w') f.close() # 獲取代理 proxies = get_proxies("https://www.kuaidaili.com/free/intr/", url, 5) # 設置headers ua = UserAgent() result_list = [] for num in range(0, 530, 25): headers = {'User-Agent': ua.random} logging.info('headers:%s', headers) data = get_web_data(url + str(num), headers, get_random_ip(proxies)) book = parse_data(data) save_to_csv(file_path, book) time.sleep(1)
來運行一下:
最終爬下來的文件:
源碼以及爬下來的數據都放到了github:https://github.com/MFrank2016/douban_spider
要運行該文件,除了須要安裝import中的模塊,還須要安裝一個lxml模塊才能運行。
其實寫爬蟲的思路都是差很少的,大概分爲幾步:
這個爬蟲仍是比較簡陋的,在獲取代理並校驗代理ip可用性這一步花了較多時間,優化的話,能夠用多線程來進行代理ip可用性檢測,獲得必定數量的代理ip後,多線程進行網頁訪問和數據解析,而後再存儲到數據庫中。不過要使用多線程的話複雜度就會大大提高了,在這個小爬蟲裏,由於只須要爬22頁數據,因此沒有使用的必要。
還有一個重要的問題就是這裏沒有對異常信息進行處理,運行中途若是出錯就會致使前功盡棄,要考慮好大部分異常狀況並不容易。
固然,整個過程並無上文描述的這樣簡單,調試過程仍是花了很多時間,應該沒有用過 BeautifulSoup 模塊,摸索了很多時間才能初步使用它。
做爲python的初學者而言,用python最舒服的感覺即是好用的模塊確實多,用 BeautifulSoup 模塊來進行網頁解析確實比直接正則解析要方便的多,並且更容易控制。
我的以爲爬蟲只是用來獲取數據的一個手段,用python也好,java也好,沒有優劣之分,能實現想要的達成的目的便可,用什麼語言順手就用什麼語言。將數據爬取下來後,即可以進行後續的數據分析,可視化等工做了。使用工具不是目的,只是手段,這一點我也是花了很長時間才慢慢理解。就像使用爬蟲來獲取數據來進行數據分析,從數據中挖掘想要的信息並用於指導實踐纔是真正產生價值的地方。做爲技術人員,很容易產生的誤區即是把技術當作一切,而不重視業務,卻不知真正創造價值的正是業務的制定者和執行者,技術最終都是爲業務服務的。
本文到此就告一段落了,但願能對你有所幫助,也歡迎關注個人公衆號進行留言交流。