序言
这是一篇集世界乒乓球运动技术演进、发展和不同历史阶段著名运动员简介的百度贴吧帖子。作者全言是我很关注的一个吧友,我本科时候追过的一个他在百度贴吧乒乓球吧连载的一个长帖子,彼时我还是个乒乓小白,看帖子觉得他讲技术演进讲得特别透彻,但是他有点问题,老是对自己写的东西不满意,总是删帖建帖删帖建帖复制粘贴复制粘贴,我的贴吧收藏夹都跟丢了好几次,后来又去乒乓网、百家号搞连载,后来也都逐渐停更,后来他建了一个公众号,把这个连载帖放在里面,但是每篇文章都太短了,需要点进去退出来再点进去,所以我用Python写了一个爬虫脚本,把他这一系列的几百多篇文章给导入成txt电子书文档了。
资源
- 点此跳转至腾讯微云下载电子书资源(txt)
- 点此跳转至腾讯微云下载电子书资源(pdf)
- 缺陷:目前Python的支持网页转pdf的库都仅支持html爬取,没有一个支持js异步加载,因此文章配图未能爬取。
代码相关准备工作
- 任务
1. 爬取以下两个网页:网页1、网页2里面的所有文章链接,放入一个列表,再传递给下一步;
- 思路:
这一阶段主要利用selenium
来模拟Chrome浏览器获取所有的文章链接。首先要模拟点击不同的页内标签(如红色标注所示),但是由于每个标签下只默认显示十条,只有向下滚动触发js才能加载页内剩余的条目,这个过程属于异步加载。
- 分析实现
这种规模的问题,一般会使用Beautifulsoup库
+XHR调试
或者selenium.webdriver
,但是Beautifulsoup库
+XHR调试
有问题,在页面下滚捕捉query
的时候,看起来像是有什么微妙的规律,但是真正更改query
参数的时候,打开的网页还是一模一样,我不得其解,多究无益,果断止损放弃。
于是敲定使用selenium
。
- 列表中读取文章链接,打开链接,抓取段落存入
txt文件对象
,网页利用weasyprint库直接转pdf;
- 思路:这一步给定了文章链接,由于
Beautifulsoup
的速度比selenium
要快(selenium
要打开浏览器),我采用Beautifulsoup
。
- pdf合并。
使用Pypdf2中的PdfFileMerger
方法(from PyPDF2 import PdfFileMerger
)合并pdf,但是这种方法不带书签。
如果执意添加书签超链接,需要from PyPDF2 import PdfFileReader, PdfFileWriter
然后一遍addPage
一边调用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' #输出到根目录指定文件夹,如果没有就创目录
if not os.path.exists(outpath):
os.makedirs(outpath)
outpathpdf = './Table Tennis 24Years/PDF_folder'
if not os.path.exists(outpathpdf):
os.makedirs(outpathpdf)
#打开浏览器
# 运行前先下载 chrome driver,下载地址是:https://sites.google.com/a/chromium.org/chromedriver/downloads,点击【Latest Release: ChromeDriver x.xx】进入下载
driver = webdriver.Chrome(executable_path='/Users/miraco/PycharmProjects/grabnet/chromedriver') # Windows 需写成'./chromedriver.exe'
driver.start_client() #网页需要模拟浏览器点击
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'
) #这是这两个目标网页的网址,我们要把网址里面的所有文章爬出来
tops_css = '#namespace_1 > div.tab_hd > div > div' #上方目录表标签样式
titles_css = '#namespace_1 > div.tab_bd > div > a > div.cont > h2' #标签下的题目的样式
hrefs_css = '#namespace_1 > div.tab_bd > div > a' #每个标签下的超链接样式
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') #拿到网页对象
for i in range(8):
time.sleep(0.5)
html_page.send_keys(Keys.END) #模拟对着网页按下键盘'END'的动作
def find_art(url): #要爬取给定url中的文章的题目、简介、超链接
lists = [] #这个列表里放要此url可达的文章的题目、梗概、链接
driver.get(url) #打开其中一个网页
time.sleep(3) #等待网页加载
buttons = driver.find_elements_by_css_selector(tops_css) #找到上方目录表标签
for button in buttons: #按个激活标签
time.sleep(2) #等待网页加载
button.click() #点击标签
pgdown() #往下滚页
titles = driver.find_elements_by_css_selector(titles_css) #找到所有每个标签下的题目对象
hrefs = driver.find_elements_by_css_selector(hrefs_css) #找到每个标签下的超链接对象
intros = driver.find_elements_by_css_selector(info_css) #找到每个题目下的简介对象
for title, href, intro in zip(titles,hrefs,intros):
txt = title.text #题目对象转文本
if '):' in txt: #因为正经文章题目有括号冒号字样,可以依此只找正经编号文章,不找其他
ref = href.get_attribute('href') #超链接对象中提取超链接
lists.append([txt,ref,intro.text]) #符合要求的题目、超链接、简介作为一个子列表,放入大列表中
return lists
for url in url_pages: #这是这两个目标网页的网址,都爬出来
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() #关浏览器
print(all_list) #这里打印放所有文章的题目、链接、简介
#爬取到txt
#建立或对已有的此名txt进行内容清空
f = open(os.path.join(outpath,'Table Tennis 24 Years.txt'),'w')
f.close()
#开写开爬,这里爬去使用selenium打开关闭浏览器太慢了,直接上Beautifulsoup,嗖嗖的
f = open(os.path.join(outpath,'Table Tennis 24 Years.txt'),'a') #打开文件对象
f.write('本文档内所有文章皆由"全言乒乓"撰写,Sober作为乒乓球迷苦于其内容支离分散,使用基于Python3.6网络爬虫工具进行文字整理,版权属于"全言乒乓",如侵权请联系我删除!\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] #抓取文章页内的题目
f.write(title.text.strip() + ':' + intro.strip() + '\n\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') #不是广告才写进去
else:
f.write('\n------本节完------'+'\n\n') #到广告了写上"本节完"
break
return f
ssl._create_default_https_context = ssl._create_unverified_context #weasyprint有时候强制要求ssl,但是有时候会抽风犯错,为了避免ssl证书出问题,我们禁用ssl
for title ,url, intro in all_list:
print(f'正在整理文章:{title}') #表明进度
f = web2txt(f,url,intro) #写txt,并依照题目命名
HTML(url).write_pdf(os.path.join(outpathpdf,f'{title}.pdf')) #写pdf,并依照题目命名
f.close() #关闭文件对象,得到txt文件
#再将pdf合并输出
filelist = os.listdir(outpathpdf) #读取文件夹里的文件名
pdfs = [ os.path.join(outpathpdf,file) for file in filelist if not os.path.isdir(file)] #摘取文件里的pdf放进列表
pdfs.sort(key = lambda x : int(x.split('(')[1].split(')')[0])) #并按里面的数字排序,注意不能粗暴直接sort()排序,否则会出现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 #文档总页数
for pdf in pdfs:
input = PdfFileReader(open(pdf,'rb')) #打开读取文档
pdf_name = pdf.split('/')[-1] #拿到文件名称作为书签名
page_Count = input.getNumPages() #读取当前小pdf的页数
output_Pages += page_Count #总页数累加计数
for iPage in range(page_Count):
output.addPage(input.getPage(iPage)) #小pdf内逐页添加到输出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')) #合并输出
运行结果
-
txt
-
pdf
踩过的坑
-
phantomJS
我一开始想要使用网页截图再转pdf,但是Webdriver的Chrome网页截图不支持滚动截图。其实selenium有两种形式,有头的和无头(headless)的,我用的是有头的浏览器,在以前开发者喜欢用的是PhantomJS
,但是selenium
不知道搞什么鬼,竟然在运行phantomJS
时候提示最新版本的selenium
停止支持js
,建议我使用Chrome
或者Firefox
的无头浏览器,
无头浏览器就是没有界面的静默版本,也可以调用,但是肉眼看不见,不喜欢界面打扰的可以试试看下面的代码。
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
其实网页转pdf的库还有一个叫pdfkit
,要预装wkhtmltopdf
,而且转换效果很差,还不如这个库呢,不过weasyprint
虽说效果更好,但是也是不支持异步加载的,有人在此项目的Github主页里issue了为什么不能加载微信文章的插图,作者也提到了这个问题,本库不支持js异步加载。
-
爬取文章链接时的异步加载元素问题
在检测文章的入口的css元素样式的时候,如果点击了页上文章列表的新标签,那么在elements
中所查找的css元素个数会增多,但是并不意味着你可以把列表标签挨个点击以后使用find_elements_by_css_selector
方法一网打尽,你确实可以拿到元素,但是使用元素对象使用text
方法以后,你发现只能从当前激活列表标签下的元素里拿出数据,不在当前页面的数据拿不出来,是空字符串''
,所以只能点击一次拿一次。类似有人问过这样的问题,就是元素拿不到了。
因此,如果得到的文本只为空,那么当前定位的元素可能被隐藏了。
- 判断是否被隐藏 。 driver.find_element_by_xx().is_displayed() ,如果得到 false的结果.那就说明被隐藏了。
-
怎么解决?
is_displayed()
为false的元素,依然可以通过getAttribute()方法获取元素的属性. 由于webdriver spec
的定义,Selenium WebDriver 只会与可见元素交互,所以获取隐藏元素的文本总是会返回空字符串。可是,在某些情况下,我们需要获取隐藏元素的文本。这些内容可以使用element.attribute('attributeName')
, 通过textContent
,innerText
,innerHTML
等属性获取。
-
weasyprint库直接转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