說明
五一將至,又到了學習的季節。目前流行的各大書單主打的都是豆瓣8.0評分書籍,卻很少有人來聊聊這9.0評分的書籍長什么樣子。剛好最近學了學python爬蟲,那就拿豆瓣讀書來練練手。
爬蟲
本來思路是直接爬豆瓣的書籍目錄,將評分9.0以上的書篩選出來,一打開發現事情並不簡單,幾千萬本書可不好爬 = =,於是轉化一下思路,看有沒有類似的書單。
一搜還真有,找到一個9.0評分的榜單,大大減少了工作量,這樣就不用先爬一下整站書籍來篩選了。看了看榜單,應該是某位好心的書友手工整理的,更新時間為2018-12-25,目前一共530本,分為22頁,也就是說22次訪問就能搞定了,不會給豆瓣的服務器造成壓力。
目標
目標URL:https://www.douban.com/doulist/1264675/?start=0&sort=seq&playable=0&sub_type=4
數據量:530
預計訪問次數:22
數據存儲: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
- 設置UA
- 使用代理ip訪問網頁
- 解析網頁數據
- 存儲/分析
這個爬蟲還是比較簡陋的,在獲取代理並校驗代理ip可用性這一步花了較多時間,優化的話,可以用多線程來進行代理ip可用性檢測,得到一定數量的代理ip后,多線程進行網頁訪問和數據解析,然后再存儲到數據庫中。不過要使用多線程的話復雜度就會大大提升了,在這個小爬蟲里,因為只需要爬22頁數據,所以沒有使用的必要。
還有一個重要的問題就是這里沒有對異常信息進行處理,運行中途如果出錯就會導致前功盡棄,要考慮好大部分異常情況並不容易。
當然,整個過程並沒有上文描述的這樣簡單,調試過程還是花了不少時間,應該沒有用過 BeautifulSoup 模塊,摸索了不少時間才能初步使用它。
作為python的初學者而言,用python最舒服的感受便是好用的模塊確實多,用 BeautifulSoup 模塊來進行網頁解析確實比直接正則解析要方便的多,而且更容易控制。
個人覺得爬蟲只是用來獲取數據的一個手段,用python也好,java也好,沒有優劣之分,能實現想要的達成的目的即可,用什么語言順手就用什么語言。將數據爬取下來后,便可以進行后續的數據分析,可視化等工作了。使用工具不是目的,只是手段,這一點我也是花了很長時間才慢慢理解。就像使用爬蟲來獲取數據來進行數據分析,從數據中挖掘想要的信息並用於指導實踐才是真正產生價值的地方。作為技術人員,很容易產生的誤區便是把技術當做一切,而不重視業務,殊不知真正創造價值的正是業務的制定者和執行者,技術最終都是為業務服務的。
本文到此就告一段落了,希望能對你有所幫助,也歡迎關注我的公眾號進行留言交流。