puppeteer进阶之12306买票

puppeteer进阶之12306买票


开始准备工作

安装

  1. cnpm i puppeteer
  2. 因为要使用到async, await异步处理方法,所以最好是把nodejs升级到7.6.0以上

几个常用的API

  1. puppeteer.launch([options])

    • options <Object> 启动浏览器时的配置,可能存在如下字段:

      • ignoreHTTPSErrors <boolean> 是否忽略在导航阶段 HTTPS引发的错误,默认为false

      • headless <boolean> 是否以一种 headless mode的模式来启动浏览器。 只要devtools 选项不为true,那么此选项的默认值就是 true

      • executablePath <string> ChromiumChrome 的运行路径而不是安装路径。 如果 executablePath 是一个相对目录, 那么它应该是相对于 current working directory

      • slowMo <number> 通过改变此值来减缓 Puppeteer的操作速度,单位是毫秒,当你想知道具体发生了什么情况的时候,此参数将会比较有用。

      • args <Array<string>> 传递给浏览器实例的额外参数,这些参数的可选列表可见与此

      • ignoreDefaultArgs <boolean> 将会使 puppeteer.defaultArgs()失效。 属于比较危险的选项,需要小心使用。默认为 false

      • handleSIGINT <boolean> 程序终止信号,通过按下 Ctrl-C 来关闭浏览器进程,默认为 true

      • handleSIGTERM <boolean> 程序结束信号,当接收到程序结束信号时,关闭浏览器进程,默认为 true

      • timeout <number> 等待浏览器实例启动的超时时间,单位为 ms,默认是30000 (30秒),设置为 0标识禁用此选项。

      • devtools <boolean>是否为每个标签页自动打开 DevTools面板,如果为 true, 那么 headless选项将被设置为 false

          demo代码  
          `const browser = await puppeteer.launch({headless:false,slowMo:150});`
          //打开浏览器并设置浏览器为有头模式,并且减慢操作时间为150ms
        
  2. browser.newPage()

    • returns: <Promise<Page>>
      • 返回一个新的 Page Promise对象,Page将在一个默认的浏览器上下文中创建。
        const page = await browser.newPage();

    创建一个page对象,后面就用这个对象进行操作

  3. page.goto(url, options)

    • url <string> 目标页面的url. url中应该包含协议头,例如 https://

      • timeout <number> 连接超时时间,单位毫秒, 默认是 30秒, 如果设置为 0则表示禁用此选项。 此值也可以被 page.setDefaultNavigationTimeout(timeout) 方法修改。

      • waitUntil <string|Array<string>> 确认navigation成功的条件, 默认是load。如果给定的值是一个事件组成的数组,那么只有当数组中的所有事件全部被触发后才会认为 navigation成功,可选的事件列表如下:

      • load - 当 load 事件触发时,则认为 navigation导航结束。

      • domcontentloaded - 当 DOMContentLoaded 事件触发时,则认为 navigation导航结束。

      • networkidle0 - 如果在 500ms内发起的http请求数为0,则认为导航结束。

      • networkidle2 - 如果在 500ms内发起的http请求数为不超过 2条,则认为导航结束。

           `await page.goto('https://kyfw.12306.cn/otn/login/init')`
           //页面跳转到12306的登录页
        
  4. page.type(selector, text[, options])

    • selector <string> 文本框(包括 texareainput)的选择器。如果选择器匹配出了多个元素,则只会选择第一个匹配的元素上。

    • text <string> 将要输入到文本框内的文字。

    • options <Object>

    • delay <number> 按键输入的间隔速度,单位为ms。默认为0.

    • returns: <Promise>

    • Sends a keydown, keypress/input, and keyup event for each character in the text.

    • 效果等同于page.click()

       demo代码
       await page.type('#username', 'yourtelephone')
       await  page.type('#password', 'yourpassword')
       <input id="username" tabindex="1" name="loginUserDTO.user_name" type="text" class="inptxt w200" style="color: rgb(153, 153, 153); width: 309px;">
       //上面这段是从12306网站登录页截取下来的,这里的`#username`就是html元素中的id标识,所以我们只要在网站中去找到这个唯一标识,然后获取它就可以给他赋值了
      
  5. page.waitForNavigation(options)

    • options <Object> Navigation 参数,允许存在以下属性

    • timeout <number> navigation超时时间(ms),默认是 30 seconds, 取值 0则表示禁用此选项。也可以使用 page.setDefaultNavigationTimeout(timeout) 方法来改变默认值。

    • waitUntil <string|Array<string>> navigation导航成功的界限, 默认是 load. 如果给定的值是一个事件名称组成的数组,那么只有当数组中的所有事件全部被触发后才会认为 navigation成功,可选的事件列表如下:

    • load - 当 load 事件触发时,则认为 navigation导航结束。

    • domcontentloaded - 当 DOMContentLoaded 事件触发时,则认为 navigation导航结束。

    • networkidle0 - 如果在 500ms内发起的http请求数为0,则认为导航结束。

    • networkidle2 - 如果在 500ms内发起的http请求数为不超过 2条,则认为导航结束。

    • returns: <Promise<Response>> Promise which resolves to the main resource response. In case of multiple redirects, the navigation will resolve with the response of the last redirect.

    适应于当页面重定向到一个新的url或者reload的场景,例如,当执行了一段可能间接导致页面跳转的代码:

     await page.waitForNavigation({
         waitUntil: 'domcontentloaded'
             })
         console.log(page.url())
         console.log('填写验证码后登录')
         await page.waitForNavigation({
         waitUntil: 'load'
         })
    

    这里因为是手动输入图片验证码,所以设置一个等待函数,在点击登录按钮后直接跳转。

  6. page.waitForSelector(selector[, options])

    • selector <string> 被等待的元素的选择器selector

    • options <Object>可选参数:

    • visible <boolean> 出现在DOM中的元素必须是可见的(visible),例如,不能有 display: none 或者 visibility: hidden CSS 属性。默认是 false。

    • hidden <boolean>元素不存在于DOM中(包括一开始就不存在,或者存在了又被移除掉),或者是被隐藏了, 例如, 具有 display: none 或者 visibility: hidden CSS 属性。默认是false.

    • timeout <number> 最大等待时间(ms)。默认是30000 (30 秒)。取值为的 0则表示禁用此参数。

    • returns: <Promise<ElementHandle>> 当在 DOM中找到 selector指定的元素时,Promise 将会 resolves 这个元素的ElementHandle。

    等到 selector选择器选择的元素出现在页面中,如果在调用此方法的同时选择器选取的元素就已经存在于页面中了,
    则此方法将会立即返回结果,如果超过了最大等待时间 timeout之后,选择器还没有匹配到元素,则将会抛出错误。

    demo中需要等待预定车票的元素出现才会点击跳转:

     await  page.waitForSelector('#selectYuding,#my12306page')
     await page.goto('https://kyfw.12306.cn/otn/leftTicket/init')
    
  7. page.evaluate(pageFunction, …args)

    • pageFunction <function|string> 将在page context中执行的函数

    • ...args <…Serializable|JSHandle> 传递给 pageFunction的参数

    • returns: <Promise<Serializable>> Promise which resolves to the return value of pageFunction

    • 如果传递给 page.evaluate的 pageFunction函数返回一个 Promise,则page.evaluate将会等待得到resolve后,才会返回它自己的值。

    • 如果传递给 page.evaluate的 pageFunction函数返回一个 non-Serializable的值,则page.evaluate将会返回 undefined。

       await page.evaluate(() => {
           document.querySelector('#fromStation').value = "SHH";
           document.querySelector('#toStation').value = "TJP";
               })
       // const fromStationText = await page.$("#fromStationText");
       // await fromStationText.click();
       // await page.keyboard.type("BJP");
       // const toStationText = await page.$("#toStationText");
       // await toStationText.click();
       // await page.keyboard.type("SHH");
           await page.tap('#date_icon_1');
       //填写表单
       console.log('开始填写日期');
       await page.evaluate((month, day) => {
           let cals = document.querySelectorAll('.cal');
           let target = Array.from(cals).filter((cal) => cal.querySelector('.month input').value === month)[0];
           let days = target.querySelectorAll('.cal-cm .cell')
           let theDay = Array.from(days).filter(dayel => dayel.innerText === day + '\n')[0];
           theDay.click()
           }, month, day)
      

    这里有个坑需要避开,就是12306的输入出发地和到达站时候,如果你直接给他的输入框赋值是不能成功的,因为他有两个input框,一个能看见,一个隐藏的,当你在input框输入你的到达地时候必须要选择一下才能成功赋值,并且那个隐藏的input框的value值会变化,并且他是以SHH形式标识上海:

     <input id="fromStation" type="hidden" value="SHH" name="leftTicketDTO.from_station">
     <input type="text" id="fromStationText" class="inp-txt inp_selected" value="" name="leftTicketDTO.from_station_name">
    

    所以在输入出发地和到达地的时候,直接获取起作用的那个隐藏的input框,然后给他赋值就可以了。上面的代码后半部分是在给输入框赋值结束后,对时间选择器进行赋值,但是在evaluate中好像只能用原生的选择器document.querySelectorAll$$()不起作用,赋值结束后点击查询按钮,出来列车的列表。

    await page.tap('#query_ticket') //点击查询按钮
    console.log('开始查询');
    await page.waitForSelector('tr[datatran]');
    let tra = await page.$('[datatran="G216"]');//通过datatran="G216"查找G216车次
    await page.evaluate(() => {
        var trainId  = document.querySelector('[datatran="G216"]').id
        console.log(this.trainId)
        let tr = document.querySelector('#'+trainId.replace('price','ticket'))
        let yuding_btns = tr.querySelector('td:last-child a')//看有没有预定的btn
        yuding_btns.click()
        })
        page.on('load',async () => {
        await page.tap('#normalPassenger_0')//点选第一个默认乘客
        wait page.tap('#submitOrder_id')//提交按钮
        await page.tap('#qr_submit_id') //确认按钮
        })
  1. 完整代码

const puppeteer = require('puppeteer');
const month = '八月';
const day = '30';
(async () => {

  const browser = await puppeteer.launch({headless:false,slowMo:150});
  const page = await browser.newPage();
  await page.setViewport({width:1920, height:1080});
  await page.setJavaScriptEnabled(true);
  // page.on('console', msg => console.log('PAGE LOG:', msg.text()));
  await page.goto('https://kyfw.12306.cn/otn/login/init')
  await page.type('#username', 'yourtele')
  await  page.type('#password', 'yourkey')
  await page.waitForNavigation({
    waitUntil: 'domcontentloaded'
  })
  console.log(page.url())
  console.log('填写验证码后登录')
  await page.waitForNavigation({
    waitUntil: 'load'
  })
  await  page.waitForSelector('#selectYuding,#my12306page')
  await page.goto('https://kyfw.12306.cn/otn/leftTicket/init')
  
  await page.evaluate(() => {
    document.querySelector('#fromStation').value = "SHH";
    document.querySelector('#toStation').value = "TJP";
  })
  // const fromStationText = await page.$("#fromStationText");
  // await fromStationText.click();
  // await page.keyboard.type("BJP");
  // const toStationText = await page.$("#toStationText");
  // await toStationText.click();
  // await page.keyboard.type("SHH");
  await page.tap('#date_icon_1');
  //填写表单
  console.log('开始填写日期');
  await page.evaluate((month, day) => {
    let cals = document.querySelectorAll('.cal');
    let target = Array.from(cals).filter((cal) => cal.querySelector('.month input').value === month)[0];
    let days = target.querySelectorAll('.cal-cm .cell')
    let theDay = Array.from(days).filter(dayel => dayel.innerText === day + '\n')[0];
    theDay.click()
  }, month, day)
  //点击查询
  await page.tap('#query_ticket')
  console.log('开始查询');
    await page.waitForSelector('tr[datatran]');
    let tra = await page.$('[datatran="G216"]');
   await page.evaluate(() => {
      var trainId  = document.querySelector('[datatran="G216"]').id
      console.log(this.trainId)
      let tr = document.querySelector('#'+trainId.replace('price','ticket'))
      let yuding_btns = tr.querySelector('td:last-child a')//看有没有预定的btn
        yuding_btns.click()
    })
  page.on('load',async () => {
    await page.tap('#normalPassenger_0')
    await page.tap('#submitOrder_id')
    await page.tap('#qr_submit_id')
  })

  await browser.close();
})();

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,302评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,232评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,337评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,977评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,920评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,194评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,638评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,319评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,455评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,379评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,426评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,106评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,696评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,786评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,996评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,467评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,043评论 2 341

推荐阅读更多精彩内容

  • 1.puppeteer简介 puppeteer是一个node库,是Google chrome团队官方的无界面(he...
    伊人风采_690d阅读 7,604评论 0 11
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,566评论 18 139
  • 问答题47 /72 常见浏览器兼容性问题与解决方案? 参考答案 (1)浏览器兼容问题一:不同浏览器的标签默认的外补...
    _Yfling阅读 13,722评论 1 92
  • 这篇文章在介绍官网的同时使用了比较多的脚本示例,示例里遇到的问题有部分在本篇文章进行了解释,还有一篇文章专门记录了...
    顾顾314阅读 12,890评论 3 32
  • 一直以来…我都以为自己是最好的,但是到了现在这个年纪…我才知道,有一样东西我是不能好好把握的…那,就是爱情。你也许...
    明天盛夏阅读 95评论 0 1