思路:
这一阶段主要利用selenium来模拟Chrome浏览器获取所有的文章链接。首先要模拟点击不同的页内标签(如红色标注所示),但是由于每个标签下只默认显示十条,只有向下滚动触发js才能加载页内剩余的条目,这个过程属于异步加载。
模拟点击不同的页内标签(如红色标注所示)
分析实现
这种规模的问题,一般会使用Beautifulsoup库+XHR调试或者selenium.webdriver,但是Beautifulsoup库+XHR调试有问题,在页面下滚捕捉query的时候,看起来像是有什么微妙的规律,但是真正更改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 timefrom selenium import webdriverfrom selenium.webdriver.common.keys import Keysimport osimport requestsfrom bs4 import BeautifulSoupfrom weasyprint import HTMLimport sslfrom 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 listsfor 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证书出问题,我们禁用sslfor 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
txtpdf
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_optinotallow=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:
解决办法;禁用sslimport ssl
ssl._create_default_https_context = ssl._create_unverified_context
作者:夏威夷的芒果