如何使用无头使用 puppeteer 下载文件:真的?

Posted

技术标签:

【中文标题】如何使用无头使用 puppeteer 下载文件:真的?【英文标题】:How to download file with puppeteer using headless: true? 【发布时间】:2018-08-21 01:18:36 【问题描述】:

为了从http://niftyindices.com/resources/holiday-calendar 网站下载csv 文件,我一直在运行以下代码:

const puppeteer = require('puppeteer');

(async () => 
const browser = await puppeteer.launch(headless: true);
const page = await browser.newPage();

await page.goto('http://niftyindices.com/resources/holiday-calendar');
await page._client.send('Page.setDownloadBehavior', behavior: 'allow', 
downloadPath: '/tmp')
await page.click('#exportholidaycalender');
await page.waitFor(5000);
await browser.close();
)();

使用headless: false 它可以工作,它将文件下载到/Users/user/Downloadsheadless: true 不起作用。

我在 macOS Sierra (MacBook Pro) 上使用 puppeteer 版本 1.1.1 运行此程序,它将 Chromium 版本 66.0.3347.0 拉入 .local-chromium/ 目录并使用 npm initnpm i --save puppeteer 进行设置。

知道有什么问题吗?

提前感谢您的时间和帮助,

【问题讨论】:

我在创建browser 对象时使用--enable-logging 运行了这个,我在下载过程中看到了这个:[0313/104723.451228:VERBOSE1:navigator_impl.cc(200)] Failed Provisional Load: data:application/csv;charset=utf-8,%22SR.%20NO.... error_description: , showing_repost_interstitial: 0, frame_id: 4 【参考方案1】:

昨天我花了几个小时研究this thread 和 Stack Overflow,试图弄清楚如何通过在经过身份验证的会话中单击无头模式下的下载链接来让 Puppeteer 下载 csv 文件。这里接受的答案在我的情况下不起作用,因为下载不会触发targetcreated,并且无论出于何种原因,下一个答案都没有保留经过身份验证的会话。 This article 拯救了这一天。简而言之,fetch。希望这对其他人有所帮助。

const res = await this.page.evaluate(() =>

    return fetch('https://example.com/path/to/file.csv', 
        method: 'GET',
        credentials: 'include'
    ).then(r => r.text());
);

【讨论】:

这可能适用于某些下载,但不适用于服务器需要发布请求并且注意不将内容作为响应正文返回的情况,而是作为具有类型的文件下载八位字节流。 我在下载大文本文件 (70MB) 时遇到问题,即使使用无头 false。该页面永远不会完全加载。使用 fetch 就像一个魅力。谢谢!【参考方案2】:

这个页面通过创建一个逗号分隔的字符串来下载一个csv,并通过设置数据类型来强制浏览器下载它

let uri = "data:text/csv;charset=utf-8," + encodeURIComponent(content);
window.open(uri, "Some CSV");

这在 chrome 上会打开一个新标签。

您可以点击此活动并将内容实际下载到文件中。不确定这是否是最好的方法,但效果很好。

const browser = await puppeteer.launch(
  headless: true
);
browser.on('targetcreated', async (target) => 
    let s = target.url();
    //the test opens an about:blank to start - ignore this
    if (s == 'about:blank') 
        return;
    
    //unencode the characters after removing the content type
    s = s.replace("data:text/csv;charset=utf-8,", "");
    //clean up string by unencoding the %xx
    ...
    fs.writeFile("/tmp/download.csv", s, function(err) 
        if(err) 
            console.log(err);
            return;
        
        console.log("The file was saved!");
    ); 
);

const page = await browser.newPage();
.. open link ...
.. click on download link ..

【讨论】:

完美!作品!这也不需要page._client 存在。【参考方案3】:

问题是浏览器在下载完成之前关闭。

您可以从响应中获取文件大小和文件名,然后使用监视脚本从下载的文件中检查文件大小,以关闭浏览器。

这是一个例子:

    const filename = "set this with some regex in response";
    const dir = "watch folder or file";
    
    // Download and wait for download
        await Promise.all([
            page.click('#DownloadFile'),
           // Event on all responses
            page.on('response', response => 
                // If response has a file on it
                if (response._headers['content-disposition'] === `attachment;filename=$filename`) 
                   // Get the size
                    console.log('Size del header: ', response._headers['content-length']);
                    // Watch event on download folder or file
                     fs.watchFile(dir, function (curr, prev) 
                       // If current size eq to size from response then close
                        if (parseInt(curr.size) === parseInt(response._headers['content-length'])) 
                            browser.close();
                            this.close();
                        
                    );
                
            )
        ]);

即使响应搜索的方式可以改进,但我希望你会发现这很有用。

【讨论】:

【参考方案4】:

我找到了一种等待浏览器功能下载文件的方法。这个想法是等待谓词的响应。在我的例子中,URL 以“/data”结尾。

我只是不喜欢将文件内容加载到缓冲区中。

await page._client.send('Page.setDownloadBehavior', 
    behavior: 'allow',
    downloadPath: download_path,
);

await frame.focus(report_download_selector);
await Promise.all([
    page.waitForResponse(r => r.url().endsWith('/data')),
    page.keyboard.press('Enter'),
]);

【讨论】:

这对我有用 - 谢谢!无论我的银行是什么,我都找不到任何其他的方法来工作。无论我如何尝试拦截请求或使用相同的标头发出单独的请求等,后端似乎以某种方式识别它不是来自他们的前端并返回错误页面。这虽然有效。【参考方案5】:

我需要从登录后下载一个文件,该文件由 Puppeteer 处理。 targetcreated 未被触发。最后,在从 Puppeteer 实例复制 cookie 后,我使用 request 下载。

在这种情况下,我正在流式传输文件,但您也可以轻松保存它。

    res.writeHead(200, 
        "Content-Type": 'application/octet-stream',
        "Content-Disposition": `attachment; filename=secretfile.jpg`
    );
    let cookies = await page.cookies();
    let jar = request.jar();
    for (let cookie of cookies) 
        jar.setCookie(`$cookie.name=$cookie.value`, "http://secretsite.com");
    
    try 
        var response = await request( url: "http://secretsite.com/secretfile.jpg", jar ).pipe(res);
     catch(err) 
        console.trace(err);
        return res.send( status: "error", message: err );
    

【讨论】:

【参考方案6】:

我发现的一种方法是使用addScriptTag 方法。适用于无头 FalseTrue

使用这个可以下载任何类型的网页。现在考虑网页打开一个类似的链接:https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.mp4

网页,意味着将下载 mp4 文件,使用以下脚本;

    await page.addScriptTag('content':'''
    function fileName()
        link = document.location.href
        return link.substring(link.lastIndexOf('/')+1);
    
    async function save() 
        bl = await fetch(document.location.href).then(r => r.blob()); 
        var a = document.createElement("a");
        a.href = URL.createObjectURL(bl);
        a.download = fileName();
        a.hidden = true;
        document.body.appendChild(a);
        a.innerhtml = "download";
        a.click();
    
    save()
    '''
    )

【讨论】:

效果很好,谢谢!【参考方案7】:

我有一个更困难的变体,使用 Puppeteer Sharp。在开始下载之前,我需要同时设置 HeadersCookies

本质上,在单击按钮之前,我必须处理多个响应并在下载时处理单个响应。获得特定响应后,我必须为远程服务器附加标头和 cookie,以在响应中发送可下载的数据。

await using (var browser = await Puppeteer.LaunchAsync(new LaunchOptions  Headless = true, Product = Product.Chrome ))
await using (var page = await browser.NewPageAsync())

    ...
    // Handle multiple responses and process the Download
    page.Response += async (sender, responseCreatedEventArgs) =>
    
        if (!responseCreatedEventArgs.Response.Headers.ContainsKey("Content-Type"))
            return;

        // Handle the response with the Excel download
        var contentType = responseCreatedEventArgs.Response.Headers["Content-Type"];
        if (contentType.Contains("application/vnd.ms-excel"))
        
            string getUrl = responseCreatedEventArgs.Response.Url;

            // Add the cookies to a container for the upcoming Download GET request
            var pageCookies = await page.GetCookiesAsync();
            var cookieContainer = BuildCookieContainer(pageCookies);

            await DownloadFileRequiringHeadersAndCookies(getUrl, fullPath, cookieContainer, cancellationToken);
        
    ;

    await page.ClickAsync("button[id^='next']");

    // NEED THIS TIMEOUT TO KEEP THE BROWSER OPEN WHILE THE FILE IS DOWNLOADING!
    await page.WaitForTimeoutAsync(1000 * configs.DownloadDurationEstimateInSeconds);

像这样填充 Cookie 容器:

private CookieContainer BuildCookieContainer(IEnumerable<CookieParam> cookies)

    var cookieContainer = new CookieContainer();
        
    foreach (var cookie in cookies)
    
        cookieContainer.Add(new Cookie(cookie.Name, cookie.Value, cookie.Path, cookie.Domain));
    

    return cookieContainer;

DownloadFileRequiringHeadersAndCookies的详细信息是here。如果您下载文件的需求比较简单,您可以使用此线程或链接线程中提到的其他方法。

【讨论】:

【参考方案8】:

setDownloadBehaviorheadless: true 模式下工作正常,文件最终会被下载,但在完成时会引发异常,所以对于我来说,一个简单的包装器有助于忘记这个问题并完成工作:

const fs = require('fs');    
function DownloadMgr(page, downloaddPath) 
    if(!fs.existsSync(downloaddPath))
        fs.mkdirSync(downloaddPath);
    
    var init = page.target().createCDPSession().then((client) => 
        return client.send('Page.setDownloadBehavior', behavior: 'allow', downloadPath: downloaddPath)
    );
    this.download = async function(url) 
        await init;
        try
            await page.goto(url);
        catch(e)
        return Promise.resolve();
    


var path = require('path');
var DownloadMgr = require('./classes/DownloadMgr');
var downloadMgr = new DownloadMgr(page, path.resolve('./tmp'));
await downloadMgr.download('http://file.csv');

【讨论】:

这不会等待下载完全完成。如何等待?【参考方案9】:

我有另一个解决这个问题的方法,因为这里的答案都不适合我。

我需要登录一个网站,然后下载一些 .csv 报告。 Headed 很好,无论我尝试什么,headed 都失败了。查看网络错误,下载被中止,但我无法(快速)确定原因。

所以,我拦截了请求并使用 node-fetch 在 puppeteer 之外发出请求。这需要复制获取选项、正文、标头并添加访问 cookie。

祝你好运。

【讨论】:

请分享您的代码,否则这并没有真正的帮助。

以上是关于如何使用无头使用 puppeteer 下载文件:真的?的主要内容,如果未能解决你的问题,请参考以下文章

如何通过无头 chrome 管理登录会话?

无头浏览器是啥?它有啥用?

在阿尔卑斯字体问题上的木偶/无头Chromium

Puppeteer - 谷歌推出的自动化测试工具库

puppeteer环境搭建——新自动化工具(同webdriver)

无头浏览器检测