1

生成PDF的需求多么?

你没有碰到过要将网页转换为PDF文件的需求? 图片呢?
如何保存一个网页的快照? 截图? 页面过长怎办,系统自带的截图工具没法滚屏
海外网页被墙,通过在香港服部署一PDF生成服务可以免除无谓的浏览其它垃圾/非法网址。
公司管理层需要在公司集成办公系统上查看其它系统的资料,比如hr系统上员工简历,营销系统上的某报表...

需求还是不少的。

使用历史

个人先后使用过 wkhtmltopdf , phantomjs ,puppeteer。
wkhtmltopdf 使用起来最简单,功能比较单一,看名字就知道了,在windows下直接就是一个exe,可在自己的项目中fork/startProcess 开一个新进程,并传入网页和保存的路径地址,通过 wkhtmltopdf -h 可以查看各参数和说明。如果你要生成的页面是集团内部服务,那么使用它是最方便的。

phantomjs ,同样通过名字就知道它和js 结合的非常紧密。它使用webkit内核,同pupp,用户在可在页面内注入js,像在浏览器上操作页面和元素一样操作它,但它已停止更新了。

puppeteer 是google 提供的 node 库用来操作chrome,有些人称之为无头浏览器,在linux上使用会配套下载chromium。功能上完全可以作为phantomjs 替代品,正因为它操作的实际是chrome浏览器,对于分析页面数据,模拟和破解前端逻辑非常给力。

除此之外,其实还有很多工具,网上能搜出一大把,这里就不多说了。

使用puppeteer实现

'use strict'
 
const Service = require("egg").Service;
const puppeteer = require('puppeteer');

class pupp extends Service {
    async Create(tourl, savePath,ops) {
        ops = ops || {};
        //console.log("pupp.js ", tourl, savePath,ops);
        const browser = await puppeteer.launch({
                args : [
                    '--no-sandbox',
                    '--disable-setuid-sandbox',
                    '--disable-gpu',
                    '--disable-dev-shm-usage',
                    '--no-first-run',
                    '--no-zygote',
                    //'--single-process'
                ],
                ignoreHTTPSErrors:true
            });
        const page = await browser.newPage();
        // console.log("before go to:");
        await page.setUserAgent("Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36");
        /*
        await page.setExtraHTTPHeaders({
        "accept-language":"zh-CN,zh;q=0.9,en;q=0.8",
        "cookie":"csrfToken=V04e-JSpCtM_BEr_VH2-73N9",
        "cache-control":"max-age=0"
        });*/
         
        if(ops.cookies)
        {
          console.log("setcookies return :",await page.setCookie(...ops.cookies));
          await page.waitFor(200);
        }
        await page.goto(tourl, {
            waitUntil : 'networkidle2',
            timeout : 60000
        });
           
        if(ops.navigation)
          await page.waitForNavigation({waitUntil:'networkidle2'});//没有这个,weibo 相关页面会是空白
         
        //await page.screenshot({path: 'screenshot.png'});
        //不然会有href 显示在页面
        page.emulateMedia('screen');
 
        let scrollEnable = true;
        let scrollStep = 1000; //每次滚动的步长
        let scrollCount = 0;
        while (scrollEnable) {
            if (scrollCount++ > 5) //最多允许滚动5次
            {
                console.error("too many scroll, jumpout ", tourl);
                scrollEnable = false;
                break;
            }
 
            scrollEnable = await page.evaluate((scrollStep) => {
                        let scrollTop = document.scrollingElement.scrollTop;
                        document.scrollingElement.scrollTop = scrollTop + scrollStep;
                        return document.body.clientHeight > scrollTop + scrollStep ? true : false
                    }, scrollStep);
            await page.waitFor(200)
        }
        page.waitFor(2000);
 
        await page.pdf({
            path : savePath,
            width : "1280px",
            height : "1960px",
            printBackground:true
        });
        await browser.close();
 
    }
    /**
    将cookie string 转换为cookie object 的数组
    */
    cvtCookie(ck_line)
    {
        let arr = fs.readFileSync("./old_cookies.txt").toString().split(";");
        let objcks = arr.map(aline=>{
          let ind = aline.indexOf("=");
          return {name:aline.substr(0,ind),value:aline.substr(ind+1)};
        });
    }
}
 
//延时
function waitMs(ms) {
    if (typeof ms != "number")
        return Promise.resolve(0);
    return new Promise((resolve, rej) => {
            setTimeout(() => {
                    resolve(1);
                }, ms);
        });
}
 
module.exports = pupp;

以上是生成pdf文件的核心代码,使用的是egg框架,不熟悉并不影响阅读。有如下几点可留意一下

1:有些注释掉的代码没有去掉,有些代码也可以注释,取决于你真实的测试情况,许多网页的渲染方式不一样,有些还混合着多种js异步渲染。

2:文中的一些具体参数都是经过反复多次验证效果后添加或调整过的,官网上并不会有这些内容,有时一个默认参数就会让效果迥异。

3:代码中有段cookie设置,这是因为很多网页用户不登陆不能看,所以需要前置的用户登陆,并获取登陆成功后的cookie,进而在此步使用。需要留意模拟登陆后的cookie(甚至手动在浏览器登陆后保存cookie)使用的浏览器请求头应尽量保持一致,以免服务端将它区分为两个不同用户。

4:文中有个滚动页面的逻辑,因为有些页面仅显示一屏,当用户滚动鼠标到文末时才会继续加载,就像一些手机app,最多往下拉五次,拉太多没多大意义还浪费时间。

一点废话

puppeteer用作生成pdf有些大材小用了,用作分析和操作页面元素获取数据都十分方便的。个人认为它不太适合大批量的爬取数据,效率较低。

另外,要获取别人的页面就绕不开需要登陆和验证码,如果不是过于频繁的请求,如上文所述,直接用户手动在浏览器输入帐密,然后再将浏览器的cookie保存下来,交由上面代码处理就够了。但如果是将服务部署在服务器,这种办法就失去了作用。笔者就用过一个网上下载的新浪微博登陆库,并使用一种很“挫”的方式填充验证码,工作良好,有兴趣的同学可以留言向我了解。

下一篇,将讲述使用 selinum 获取股票数据


neveryield
49 声望4 粉丝

资深互联网 noiser