序言
這是一篇集世界乒乓球運(yùn)動技術(shù)演進(jìn)显拜、發(fā)展和不同歷史階段著名運(yùn)動員簡介的百度貼吧帖子吃媒。作者全言是我很關(guān)注的一個吧友瓤介,我本科時候追過的一個他在百度貼吧乒乓球吧連載的一個長帖子吕喘,彼時我還是個乒乓小白,看帖子覺得他講技術(shù)演進(jìn)講得特別透徹刑桑,但是他有點問題氯质,老是對自己寫的東西不滿意,總是刪帖建帖刪帖建帖復(fù)制粘貼復(fù)制粘貼祠斧,我的貼吧收藏夾都跟丟了好幾次闻察,后來又去乒乓網(wǎng)、百家號搞連載琢锋,后來也都逐漸停更辕漂,后來他建了一個公眾號,把這個連載帖放在里面吴超,但是每篇文章都太短了钉嘹,需要點進(jìn)去退出來再點進(jìn)去,所以我用Python寫了一個爬蟲腳本鲸阻,把他這一系列的幾百多篇文章給導(dǎo)入成txt電子書文檔了跋涣。
資源
- 點此跳轉(zhuǎn)至騰訊微云下載電子書資源(txt)
- 點此跳轉(zhuǎn)至騰訊微云下載電子書資源(pdf)
- 缺陷:目前Python的支持網(wǎng)頁轉(zhuǎn)pdf的庫都僅支持html爬取,沒有一個支持js異步加載鸟悴,因此文章配圖未能爬取仆潮。
代碼相關(guān)準(zhǔn)備工作
- 任務(wù)
1. 爬取以下兩個網(wǎng)頁:網(wǎng)頁1、網(wǎng)頁2里面的所有文章鏈接遣臼,放入一個列表性置,再傳遞給下一步;
- 思路:
這一階段主要利用selenium
來模擬Chrome瀏覽器獲取所有的文章鏈接揍堰。首先要模擬點擊不同的頁內(nèi)標(biāo)簽(如紅色標(biāo)注所示)鹏浅,但是由于每個標(biāo)簽下只默認(rèn)顯示十條,只有向下滾動觸發(fā)js才能加載頁內(nèi)剩余的條目屏歹,這個過程屬于異步加載隐砸。
- 分析實現(xiàn)
這種規(guī)模的問題,一般會使用Beautifulsoup庫
+XHR調(diào)試
或者selenium.webdriver
蝙眶,但是Beautifulsoup庫
+XHR調(diào)試
有問題季希,在頁面下滾捕捉query
的時候,看起來像是有什么微妙的規(guī)律幽纷,但是真正更改query
參數(shù)的時候式塌,打開的網(wǎng)頁還是一模一樣,我不得其解友浸,多究無益峰尝,果斷止損放棄。
于是敲定使用selenium
火窒。
- 列表中讀取文章鏈接硼补,打開鏈接,抓取段落存入
txt文件對象
熏矿,網(wǎng)頁利用weasyprint庫直接轉(zhuǎn)pdf已骇;
- 思路:這一步給定了文章鏈接,由于
Beautifulsoup
的速度比selenium
要快(selenium
要打開瀏覽器)曲掰,我采用Beautifulsoup
。
- pdf合并奈辰。
使用Pypdf2中的PdfFileMerger
方法(from PyPDF2 import PdfFileMerger
)合并pdf栏妖,但是這種方法不帶書簽。
如果執(zhí)意添加書簽超鏈接奖恰,需要from PyPDF2 import PdfFileReader, PdfFileWriter
然后一遍addPage
一邊調(diào)用addBookmark
吊趾,具體使用方法參考
import time
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import os
import requests
from bs4 import BeautifulSoup
from weasyprint import HTML
import ssl
from PyPDF2 import PdfFileReader, PdfFileWriter, PdfFileMerger
outpath = './Table Tennis 24Years' #輸出到根目錄指定文件夾,如果沒有就創(chuàng)目錄
if not os.path.exists(outpath):
os.makedirs(outpath)
outpathpdf = './Table Tennis 24Years/PDF_folder'
if not os.path.exists(outpathpdf):
os.makedirs(outpathpdf)
#打開瀏覽器
# 運(yùn)行前先下載 chrome driver,下載地址是:https://sites.google.com/a/chromium.org/chromedriver/downloads瑟啃,點擊【Latest Release: ChromeDriver x.xx】進(jìn)入下載
driver = webdriver.Chrome(executable_path='/Users/miraco/PycharmProjects/grabnet/chromedriver') # Windows 需寫成'./chromedriver.exe'
driver.start_client() #網(wǎng)頁需要模擬瀏覽器點擊
url_pages = ('https://mp.weixin.qq.com/mp/homepage?__biz=MzI5MjY0MTY1Ng==&hid=2&sn=858963d6283870bc173bbb7076a4e620&scene=25#wechat_redirect',
'https://mp.weixin.qq.com/mp/homepage?__biz=MzI5MjY0MTY1Ng==&hid=6&sn=53bfd170c878ae8b06c868cf8c5c4e34&scene=25#wechat_redirect'
) #這是這兩個目標(biāo)網(wǎng)頁的網(wǎng)址论泛,我們要把網(wǎng)址里面的所有文章爬出來
tops_css = '#namespace_1 > div.tab_hd > div > div' #上方目錄表標(biāo)簽樣式
titles_css = '#namespace_1 > div.tab_bd > div > a > div.cont > h2' #標(biāo)簽下的題目的樣式
hrefs_css = '#namespace_1 > div.tab_bd > div > a' #每個標(biāo)簽下的超鏈接樣式
info_css = '#namespace_1 > div.tab_bd > div > a > div.cont > p' #
all_list = [] #這里面放所有文章的題目、鏈接蛹屿、簡介
def pgdown(): #頁面往下翻滾直到盡頭屁奏,多次翻滾保證完全加載
html_page = driver.find_element_by_tag_name('html') #拿到網(wǎng)頁對象
for i in range(8):
time.sleep(0.5)
html_page.send_keys(Keys.END) #模擬對著網(wǎng)頁按下鍵盤'END'的動作
def find_art(url): #要爬取給定url中的文章的題目、簡介错负、超鏈接
lists = [] #這個列表里放要此url可達(dá)的文章的題目坟瓢、梗概、鏈接
driver.get(url) #打開其中一個網(wǎng)頁
time.sleep(3) #等待網(wǎng)頁加載
buttons = driver.find_elements_by_css_selector(tops_css) #找到上方目錄表標(biāo)簽
for button in buttons: #按個激活標(biāo)簽
time.sleep(2) #等待網(wǎng)頁加載
button.click() #點擊標(biāo)簽
pgdown() #往下滾頁
titles = driver.find_elements_by_css_selector(titles_css) #找到所有每個標(biāo)簽下的題目對象
hrefs = driver.find_elements_by_css_selector(hrefs_css) #找到每個標(biāo)簽下的超鏈接對象
intros = driver.find_elements_by_css_selector(info_css) #找到每個題目下的簡介對象
for title, href, intro in zip(titles,hrefs,intros):
txt = title.text #題目對象轉(zhuǎn)文本
if '):' in txt: #因為正經(jīng)文章題目有括號冒號字樣犹撒,可以依此只找正經(jīng)編號文章折联,不找其他
ref = href.get_attribute('href') #超鏈接對象中提取超鏈接
lists.append([txt,ref,intro.text]) #符合要求的題目、超鏈接识颊、簡介作為一個子列表诚镰,放入大列表中
return lists
for url in url_pages: #這是這兩個目標(biāo)網(wǎng)頁的網(wǎng)址,都爬出來
all_list = all_list + find_art(url)
#得到的是[[a,b,c],[d,e,f],[,g,h,i],[j,k,l]]
#這里不能用append方法祥款,因為用append以后得到的是[[[a,b,c],[d,e,f]],[[,g,h,i],[j,k,l]]]
driver.quit() #關(guān)瀏覽器
print(all_list) #這里打印放所有文章的題目清笨、鏈接、簡介
#爬取到txt
#建立或?qū)σ延械拇嗣鹴xt進(jìn)行內(nèi)容清空
f = open(os.path.join(outpath,'Table Tennis 24 Years.txt'),'w')
f.close()
#開寫開爬刃跛,這里爬去使用selenium打開關(guān)閉瀏覽器太慢了函筋,直接上Beautifulsoup,嗖嗖的
f = open(os.path.join(outpath,'Table Tennis 24 Years.txt'),'a') #打開文件對象
f.write('本文檔內(nèi)所有文章皆由"全言乒乓"撰寫奠伪,Sober作為乒乓球迷苦于其內(nèi)容支離分散跌帐,使用基于Python3.6網(wǎng)絡(luò)爬蟲工具進(jìn)行文字整理首懈,版權(quán)屬于"全言乒乓",如侵權(quán)請聯(lián)系我刪除!\n\n\n')
def web2txt(f,url,intro): #給定txt對象谨敛、文章鏈接究履、簡介,將其寫入文件
web_page = requests.get(url)
soup = BeautifulSoup(web_page.text,'lxml')
title = soup.select('h2.rich_media_title')[0] #抓取文章頁內(nèi)的題目
f.write(title.text.strip() + ':' + intro.strip() + '\n\n') #題目+簡介寫進(jìn)文件
parapraghs = [i.text.strip() for i in soup.select('#js_content > p > span') if i.text.strip() != '' ] #抓取段落列表并文本化脸狸,strip()去掉前后多余的空格
for paragraph in parapraghs:
if '微信公眾號' not in paragraph: #判斷本段是不是頁末的廣告
f.write(paragraph.strip()+'\n\n') #不是廣告才寫進(jìn)去
else:
f.write('\n------本節(jié)完------'+'\n\n') #到廣告了寫上"本節(jié)完"
break
return f
ssl._create_default_https_context = ssl._create_unverified_context #weasyprint有時候強(qiáng)制要求ssl最仑,但是有時候會抽風(fēng)犯錯,為了避免ssl證書出問題炊甲,我們禁用ssl
for title ,url, intro in all_list:
print(f'正在整理文章:{title}') #表明進(jìn)度
f = web2txt(f,url,intro) #寫txt泥彤,并依照題目命名
HTML(url).write_pdf(os.path.join(outpathpdf,f'{title}.pdf')) #寫pdf,并依照題目命名
f.close() #關(guān)閉文件對象卿啡,得到txt文件
#再將pdf合并輸出
filelist = os.listdir(outpathpdf) #讀取文件夾里的文件名
pdfs = [ os.path.join(outpathpdf,file) for file in filelist if not os.path.isdir(file)] #摘取文件里的pdf放進(jìn)列表
pdfs.sort(key = lambda x : int(x.split('(')[1].split(')')[0])) #并按里面的數(shù)字排序吟吝,注意不能粗暴直接sort()排序,否則會出現(xiàn)10排在2前面的情況
print(pdfs)
#這段代碼是直接合并pdf颈娜,不帶書簽的
'''
merger = PdfFileMerger()
for pdf in pdfs:
merger.append(pdf) #按pdf順序合并
merger.write(os.path.join(outpath,'Table Tennis 24 Years.pdf')) #合并輸出
'''
#這段代碼是逐頁合并pdf剑逃,而且有超鏈接書簽的
output = PdfFileWriter()
output_Pages = 0 #文檔總頁數(shù)
for pdf in pdfs:
input = PdfFileReader(open(pdf,'rb')) #打開讀取文檔
pdf_name = pdf.split('/')[-1] #拿到文件名稱作為書簽名
page_Count = input.getNumPages() #讀取當(dāng)前小pdf的頁數(shù)
output_Pages += page_Count #總頁數(shù)累加計數(shù)
for iPage in range(page_Count):
output.addPage(input.getPage(iPage)) #小pdf內(nèi)逐頁添加到輸出pdf對象
output.addBookmark(pdf_name,pagenum =output_Pages-page_Count,parent=None) #在小pdf的首頁添加書簽
output.write(open(os.path.join(outpath,'Table Tennis 24 Years.pdf'), 'wb')) #合并輸出
運(yùn)行結(jié)果
-
txt
-
pdf
踩過的坑
-
phantomJS
我一開始想要使用網(wǎng)頁截圖再轉(zhuǎn)pdf,但是Webdriver的Chrome網(wǎng)頁截圖不支持滾動截圖官辽。其實selenium有兩種形式蛹磺,有頭的和無頭(headless)的,我用的是有頭的瀏覽器同仆,在以前開發(fā)者喜歡用的是PhantomJS
嫩码,但是selenium
不知道搞什么鬼懈凹,竟然在運(yùn)行phantomJS
時候提示最新版本的selenium
停止支持js
,建議我使用Chrome
或者Firefox
的無頭瀏覽器,
無頭瀏覽器就是沒有界面的靜默版本诀艰,也可以調(diào)用坷牛,但是肉眼看不見芯侥,不喜歡界面打擾的可以試試看下面的代碼缠犀。
chrome_options = Options()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--disable-gpu')
driver = webdriver.Chrome(executable_path='./chromedriver',chrome_options=chrome_options)
-
pdfkit
其實網(wǎng)頁轉(zhuǎn)pdf的庫還有一個叫pdfkit
,要預(yù)裝wkhtmltopdf
臭觉,而且轉(zhuǎn)換效果很差昆雀,還不如這個庫呢,不過weasyprint
雖說效果更好蝠筑,但是也是不支持異步加載的狞膘,有人在此項目的Github主頁里issue了為什么不能加載微信文章的插圖,作者也提到了這個問題什乙,本庫不支持js異步加載挽封。
-
爬取文章鏈接時的異步加載元素問題
在檢測文章的入口的css元素樣式的時候,如果點擊了頁上文章列表的新標(biāo)簽臣镣,那么在elements
中所查找的css元素個數(shù)會增多辅愿,但是并不意味著你可以把列表標(biāo)簽挨個點擊以后使用find_elements_by_css_selector
方法一網(wǎng)打盡智亮,你確實可以拿到元素,但是使用元素對象使用text
方法以后点待,你發(fā)現(xiàn)只能從當(dāng)前激活列表標(biāo)簽下的元素里拿出數(shù)據(jù)阔蛉,不在當(dāng)前頁面的數(shù)據(jù)拿不出來,是空字符串''
癞埠,所以只能點擊一次拿一次状原。類似有人問過這樣的問題,就是元素拿不到了苗踪。
因此颠区,如果得到的文本只為空,那么當(dāng)前定位的元素可能被隱藏了通铲。
- 判斷是否被隱藏 毕莱。 driver.find_element_by_xx().is_displayed() ,如果得到 false的結(jié)果.那就說明被隱藏了测暗。
-
怎么解決央串?
is_displayed()
為false的元素,依然可以通過getAttribute()方法獲取元素的屬性. 由于webdriver spec
的定義磨澡,Selenium WebDriver 只會與可見元素交互碗啄,所以獲取隱藏元素的文本總是會返回空字符串∥壬悖可是稚字,在某些情況下,我們需要獲取隱藏元素的文本厦酬。這些內(nèi)容可以使用element.attribute('attributeName')
, 通過textContent
,innerText
,innerHTML
等屬性獲取胆描。
-
weasyprint庫直接轉(zhuǎn)pdf時候ssl報錯
weasyprint.urls.URLFetchingError:
URLError: <urlopen error [SSL:CERTIFICATE_VERIFY_FAILED]
certificate verify failed (_ssl.c:777)
解決辦法;禁用ssl
import ssl
ssl._create_default_https_context = ssl._create_unverified_context