继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

06 Python3.6爬取全言乒乓连载文章并保存pdf和txt电子书

慕虎7371278
关注TA
已关注
手记 1125
粉丝 201
获赞 871
  • 思路:

这一阶段主要利用selenium来模拟Chrome浏览器获取所有的文章链接。首先要模拟点击不同的页内标签(如红色标注所示),但是由于每个标签下只默认显示十条,只有向下滚动触发js才能加载页内剩余的条目,这个过程属于异步加载。

webp

模拟点击不同的页内标签(如红色标注所示)


  • 分析实现
    这种规模的问题,一般会使用Beautifulsoup库+XHR调试或者selenium.webdriver,但是Beautifulsoup库+XHR调试有问题,在页面下滚捕捉query的时候,看起来像是有什么微妙的规律,但是真正更改query参数的时候,打开的网页还是一模一样,我不得其解,多究无益,果断止损放弃。

    webp

    更改query参数后,打开的网页还是一模一样,我不得其解,多究无益,果断放弃


    于是敲定使用selenium

  1. 列表中读取文章链接,打开链接,抓取段落存入txt文件对象,网页利用weasyprint库直接转pdf;

  • 思路:这一步给定了文章链接,由于Beautifulsoup的速度比selenium要快(selenium要打开浏览器),我采用Beautifulsoup

  1. 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

webp

txt

  • pdf

webp

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方法以后,你发现只能从当前激活列表标签下的元素里拿出数据,不在当前页面的数据拿不出来,是空字符串'',所以只能点击一次拿一次。类似有人问过这样的问题,就是元素拿不到了。

因此,如果得到的文本只为空,那么当前定位的元素可能被隐藏了。

  1. 判断是否被隐藏 。 driver.find_element_by_xx().is_displayed() ,如果得到 false的结果.那就说明被隐藏了。

  2. 怎么解决is_displayed()为false的元素,依然可以通过getAttribute()方法获取元素的属性. 由于webdriver spec的定义,Selenium WebDriver 只会与可见元素交互,所以获取隐藏元素的文本总是会返回空字符串。可是,在某些情况下,我们需要获取隐藏元素的文本。这些内容可以使用element.attribute('attributeName'), 通过textContent, innerText, innerHTML等属性获取。

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



作者:夏威夷的芒果
链接:https://www.jianshu.com/p/bd3d88a66068


打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP