這幾天剛剛開始學習Python,就像寫個爬蟲小專案練練手,自從間書的“××豚”事件後搬到掘金,感覺掘金在各個方面做的都很不錯,尤其是文章的質量和寫文章的編輯器做的很舒服。
但是,我每次想要搜尋一個自己感興趣的關鍵字時,下面就會出現大量文章,想按照“點贊數”排序連按鈕也找不到,必須得一直向下一行行瀏覽才能找到我們需要的文章。所以,我在想能否利用剛學習的爬蟲做個功能:只需輸入關鍵字和通過被點贊數,就能自動給出一個列表,它包含了符合(點贊數大於我們設定的)我們需求的文章。說幹就開始,首先上結果圖:
下面開始正式的工作:
1.專案構成
程式主要分為:controller(主控制器)、downloader(下載器)、parser(解析器)、url_manager(url管理器)、outputer(輸出器)。2. URL管理器(url_manager)
URL管理器主要負責產生、維護需要爬取的網站連結,對於我們要爬取的網站“掘金”,主要分為兩類:靜態頁面URL,AJAX動態構建的頁面。
這兩種請求的URL的構成截然不同,並且返回內容也不同:靜態頁面URL返回HTML頁面,AJAX請求返回的是JSON字串。針對這兩種訪問方式,我們可以這樣編寫URL管理器:
class UrlManager(object):
def __init__(self):
self.new_urls = set() # 新的url的集合
# 構建訪問靜態頁面的url
def build_static_url(self, base_url, keyword):
static_url = base_url + '?query=' + keyword
return static_url
# 根據輸入的base_url(基礎地址)和params(引數字典)來構造一個新的url
# eg:https://search-merger-ms.juejin.im/v1/search?query=python&page=1&raw_result=false&src=web
# 引數中的start_page是訪問的起始頁數字,gap是訪問的頁數間隔
def build_ajax_url(self, base_url, params, start_page=0, end_page=4, gap=1):
if base_url is None:
print('Invalid param base_url!')
return None
if params is None or len(params)==0:
print('Invalid param request_params!')
return None
if end_page < start_page:
raise Exception('start_page is bigger than end_page!')
equal_sign = '=' #鍵值對內部連線符
and_sign = '&' #鍵值對之間連線符
# 將base_url和引數拼接成url放入集合中
for page_num in range(start_page, end_page, gap):
param_list = []
params['page'] = str(page_num)
for item in params.items():
param_list.append(equal_sign.join(item)) # 字典中每個鍵值對中間用'='連線
param_str = and_sign.join(param_list) # 不同鍵值對之間用'&'連線
new_url = base_url + '?' + param_str
self.new_urls.add(new_url)
return None
# 從url集合中獲取一個新的url
def get_new_url(self):
if self.new_urls is None or len(self.new_urls) == 0:
print('there are no new_url!')
return None
return self.new_urls.pop()
# 判斷集合中是否還有url
def has_more_url(self):
if self.new_urls is None or len(self.new_urls) == 0:
return False
else:
return True
複製程式碼
如上程式碼,在初始化函式__init__中維護了一個集合,建立好的URL將會放入到這個集合中。然後根據網址的結構分為基礎的網址+訪問引數,兩者之間通過'?'連結,引數之間通過'&'連結。通過函式build_ajax_url將兩者連線起來,構成完整的URL放入到集合中,可以通過get_new_url獲取集合中的一條URL,has_more_url判斷集合中是否還有未消費的URL。
2.下載器(html_downloader)
import urllib.request
class HtmlDownloader(object):
def download(self, url):
if url is None:
print('one invalid url is found!')
return None
response = urllib.request.urlopen(url)
if response.getcode() != 200:
print('response from %s is invalid!' % url)
return None
return response.read().decode('utf-8')
複製程式碼
這段程式碼比較簡單,使用urllib庫訪問URL並返回得到的返回資料。
3.JSON解析器(json_parser)
import json
from crawler.beans import result_bean
class JsonParser(object):
# 將json字元創解析為一個物件
def json_to_object(self, json_content):
if json_content is None:
print('parse error!json is None!')
return None
print('json', str(json_content))
return json.loads(str(json_content))
# 從JSON構成的物件中提取出文章的title、link、collectionCount等資料,並將其封裝成一個Bean物件,最後將這些物件新增到結果列表中
def build_bean_from_json(self, json_collection, baseline):
if json_collection is None:
print('build bean from json error! json_collection is None!')
list = json_collection['d'] # 文章的列表
result_list = [] # 結果的列表
for element in list:
starCount = element['collectionCount'] # 獲得的收藏數,即獲得的贊數
if int(starCount) > baseline: # 如果收藏數超過baseline,則勾結結果物件並新增到結果列表中
title = element['title']
link = element['originalUrl']
result = result_bean.ResultBean(title, link, starCount)
result_list.append(result) # 新增到結果列表中
print(title, link, starCount)
return result_list
複製程式碼
對於JSON的解析主要分為兩部:1.將JSON字串轉換為一個字典物件;2.將文章題目、連結、贊數等資訊從字典物件中提取出來,根據baseline判斷是否將這些資料封裝成結果物件並新增到結果列表中。
3.HTML解析器(html_parser)
我們可以通過訪問:'https://juejin.im/search?query=python'得到一個HTML網頁,但是隻有一頁資料,相當於訪問'https://search-merger-ms.juejin.im/v1/search?query=python&page=0&raw_result=false&src=web'獲得的資料量,但是區別是一個返回內容的格式是HTML,第二個返回的是JSON。這裡我們也將HTML的解析器也放到這裡,專案中可以不用到這個:
from bs4 import BeautifulSoup
from crawler.beans import result_bean
class HtmlParser(object):
# 建立BeautifulSoup物件,將html結構化
def build_soup(self, html_content):
self.soup = BeautifulSoup(html_content, 'html.parser')
return self.soup
# 根據獲得的贊數過濾得到符合條件的tag
def get_dom_by_star(self, baseline):
doms = self.soup.find_all('span', class_='count')
# 根據最少贊數過濾結果,只保留不小於baseline的節點
for dom in doms:
if int(dom.get_text()) < baseline:
doms.remove(dom)
return doms
# 根據節點構建結果物件並新增到列表中
def build_bean_from_html(self, baseline):
doms = self.get_dom_by_star(baseline)
if doms is None or len(doms)==0:
print('doms is empty!')
return None
results = []
for dom in doms:
starCount = dom.get_text() # 獲得的贊數
root = dom.find_parent('div', class_='info-box') #這篇文章的節點
a = root.find('a', class_='title', target='_blank') #包含了文章題目和連結的tag
link = 'https://juejin.im' + a['href'] + '/detail' #構造link
title = a.get_text()
results.append(result_bean.ResultBean(title, link, starCount))
print(link, title, starCount)
return results
複製程式碼
為了更加高效地解析HTML檔案,這裡需要用到'bs4'模組。
4.結果物件(result_bean)
結果物件是對爬蟲結果的一個封裝,將文章名、對應的連結、獲得的贊數封裝成一個物件:
# 將每條文章儲存為一個bean,其中包含:題目、連結、獲得的贊數 屬性
class ResultBean(object):
def __init__(self, title, link, starCount=10):
self.title = title
self.link = link
self.starCount = starCount
複製程式碼
5.HTML輸出器(html_outputer)
class HtmlOutputer(object):
def __init__(self):
self.datas = [] # 輸入結果列表
# 構建輸入資料(結果列表)
def build_data(self, datas):
if datas is None:
print('Invalid data for output!')
return None
# 判斷是應該追加還是覆蓋
if self.datas is None or len(self.datas)==0:
self.datas = datas
else:
self.datas.extend(datas)
# 輸出html檔案
def output(self):
fout = open('output.html', 'w', encoding='utf-8')
fout.write('<html>')
fout.write("<head><meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\">")
fout.write("<link rel=\"stylesheet\" href=\"http://cdn.static.runoob.com/libs/bootstrap/3.3.7/css/bootstrap.min.css\"> ")
fout.write("<script src=\"http://cdn.static.runoob.com/libs/bootstrap/3.3.7/js/bootstrap.min.js\"></script>")
fout.write("</head>")
fout.write("<body>")
fout.write("<table class=\"table table-striped\" width=\"200\">")
fout.write("<thead><tr><td><strong>文章</strong></td><td><strong>星數</strong></td></tr></thead>")
for data in self.datas:
fout.write("<tr>")
fout.write("<td width=\"100\"><a href=\"%s\" target=\"_blank\">%s</a></td>" % (data.link, data.title))
fout.write("<td width=\"100\"> %s</td>" % data.starCount)
fout.write("</tr>")
fout.write("</table>")
fout.write("</body>")
fout.write("</html>")
fout.close()
複製程式碼
將解析後得到的結果物件列表中的資料儲存在HTML表格中。
6.控制器(main_controller)
from crawler.url import url_manager
from crawler.downloader import html_downloader
from crawler.parser import html_parser, json_parser
from crawler.outputer import html_outputer
class MainController(object):
def __init__(self):
self.url_manager = url_manager.UrlManager()
self.downloader = html_downloader.HtmlDownloader()
self.html_parser = html_parser.HtmlParser()
self.html_outputer = html_outputer.HtmlOutputer()
self.json_paser = json_parser.JsonParser()
def craw(self, func):
def in_craw(baseline):
print('begin to crawler..')
results = []
while self.url_manager.has_more_url():
content = self.downloader.download(self.url_manager.get_new_url()) # 根據URL獲取靜態網頁
results.extend(func(content, baseline))
self.html_outputer.build_data(results)
self.html_outputer.output()
print('crawler end..')
print('call craw..')
return in_craw
def parse_from_json(self, content, baseline):
json_collection = self.json_paser.json_to_object(content)
results = self.json_paser.build_bean_from_json(json_collection, baseline)
return results
def parse_from_html(self, content, baseline):
self.html_parser.build_soup(content) # 使用BeautifulSoup將html網頁構建成soup樹
results = self.html_parser.build_bean_from_html(baseline)
return results
複製程式碼
在控制器中,通過__init__函式建立前面的幾個模組的例項。函式parse_from_json和parse_from_html分別負責從JSON和HTML中解析出結果;函式craw中利用閉包將解析函式抽象出來,使我們方便選擇需要的解析器,就將解析器作為引數'func'傳入craw函式中,這點類似於Java中對介面的使用,但是更加靈活,主函式中具體的用法可以是:
if __name__ == '__main__':
base_url = 'https://juejin.im/search' # 要爬取的HTML網站網址(不含引數)
ajax_base_url = 'https://search-merger-ms.juejin.im/v1/search' #要通過ajax訪問的網址(不含引數,返回JSON)
keyword = 'python' # 搜尋的關鍵字
baseline = 10 # 獲得的最少贊數量
# 建立控制器物件
crawler_controller = MainController()
static_url = crawler_controller.url_manager.build_static_url(base_url, keyword) # 構建靜態URL
# craw_html = crawler_controller.craw(crawler_controller.parse_from_html) # 選擇HTML解析器
# craw_html(static_url, baseline) #開始抓取
# ajax請求的網址例子:'https://search-merger-ms.juejin.im/v1/search?query=python&page=0&raw_result=false&src=web'
params = {} # 對應的請求引數
# 初始化請求引數
params['query'] = keyword
params['page'] = '1'
params['raw_result'] = 'false'
params['src'] = 'web'
crawler_controller.url_manager.build_ajax_url(ajax_base_url, params) # 構建ajax訪問的網址
craw_json = crawler_controller.craw(crawler_controller.parse_from_json) # 選擇JSON解析器
craw_json(baseline) #開始抓取
複製程式碼