导读:简单地说,我们的目标是编写一个网络爬虫,帮你自动搜索飞往特定目的地,时间在一个弹性范围(在首选日期前后最多3天)内的航班价格。它会把搜索结果保存在一个 Excel 表格中,并把精炼过的统计信息通过电子邮件发送给你。
显然,我们要找的就是对应时段中最优惠的机票。
<tt-image data-tteditor-tag="tteditorTag" contenteditable="false" class="syl1561969731399 ql-align-center" data-render-status="finished" data-syl-blot="image" style="box-sizing: border-box; cursor: text; text-align: left; color: rgb(34, 34, 34); font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", "Helvetica Neue", Arial, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial; display: block;"> image<input class="pgc-img-caption-ipt" placeholder="图片描述(最多50字)" value="" style="box-sizing: border-box; outline: 0px; color: rgb(102, 102, 102); position: absolute; left: 187.5px; transform: translateX(-50%); padding: 6px 7px; max-width: 100%; width: 375px; text-align: center; cursor: text; font-size: 12px; line-height: 1.5; background-color: rgb(255, 255, 255); background-image: none; border: 0px solid rgb(217, 217, 217); border-radius: 4px; transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1) 0s;"></tt-image>
在现实生活中,爬虫的用途完全取决于你。我曾经用它安排过两次假期旅行,还搜索过一些回我老家的短途航班信息。
如果你想要弄得专业一点,你可以把这个程序放在服务器上(一个简单的树莓派就够了),让它每天运行上一两次。程序将会把统计结果发到你的邮箱里,我也建议你把生成的 Excel 表格保存到网盘中(比如 Dropbox),这样你就能方便地在任何地方查阅数据。
它会搜索一个“弹性”的日期范围,以便查找在你首选日期前后 3 天内的航班信息。尽管这个脚本一次只能查询一对目的地(出发-到达),但你很容易就能对它进行调整,以在每个循环内运行多次。你甚至可能发现一些标注错误的超低票价……那简直是"棒极了"(不提倡钻这种空子)。
<tt-image data-tteditor-tag="tteditorTag" contenteditable="false" class="syl1561969731409 ql-align-center" data-render-status="finished" data-syl-blot="image" style="box-sizing: border-box; cursor: text; text-align: left; color: rgb(34, 34, 34); font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", "Helvetica Neue", Arial, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial; display: block;"> image<input class="pgc-img-caption-ipt" placeholder="图片描述(最多50字)" value="" style="box-sizing: border-box; outline: 0px; color: rgb(102, 102, 102); position: absolute; left: 187.5px; transform: translateX(-50%); padding: 6px 7px; max-width: 100%; width: 375px; text-align: center; cursor: text; font-size: 12px; line-height: 1.5; background-color: rgb(255, 255, 255); background-image: none; border: 0px solid rgb(217, 217, 217); border-radius: 4px; transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1) 0s;"></tt-image>
Python学习交流群:1004391443
我目前还没发现这类出错的机票,不过我想我会成功的!
01 又一个爬虫?
当我第一次开始做网络抓取工作的时候,我对这个方面……并不是太感兴趣。没错,这是真心话。我那时候更希望搞些预测性的建模,或是金融分析,甚至一些语义情绪分析之类的“高级”项目。
但事实证明,想方设法编写出第一个网络爬虫的过程,还是相当有趣的。随着我学习的不断深入,我逐渐意识到,网络抓取正是驱动互联网本身能够运行的主要推手。
你可能觉得我是章口就莱,但如果你知道,Google 最开始就是建立在 Larry Page 用 Java 和 Python 写的一个爬虫上的呢?
这个爬虫差不多是字面意义上地把整个互联网都抓了下来(即使现在也是如此),以便当你在搜索框里输入关键字的时候,程序能够给你提供最佳的答案。网络爬虫在互联网上的实际应用几乎是无穷无尽的。退一万步说,就算你从事的是数据科学中的其他领域,你仍然需要一些网络抓取技能来帮你从互联网上获取数据。
<tt-image data-tteditor-tag="tteditorTag" contenteditable="false" class="syl1561969731417 ql-align-center" data-render-status="finished" data-syl-blot="image" style="box-sizing: border-box; cursor: text; text-align: left; color: rgb(34, 34, 34); font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", "Helvetica Neue", Arial, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial; display: block;"> image<input class="pgc-img-caption-ipt" placeholder="图片描述(最多50字)" value="" style="box-sizing: border-box; outline: 0px; color: rgb(102, 102, 102); position: absolute; left: 187.5px; transform: translateX(-50%); padding: 6px 7px; max-width: 100%; width: 375px; text-align: center; cursor: text; font-size: 12px; line-height: 1.5; background-color: rgb(255, 255, 255); background-image: none; border: 0px solid rgb(217, 217, 217); border-radius: 4px; transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1) 0s;"></tt-image>
02 “你喜欢旅行吗?”
这个简单而无害的问题,常常能得到别人肯定的答复,偶尔还会收获一两个之前的冒险故事。我想大部分人应该都同意,旅行是体验新文化,拓展自己眼界的好办法。但是,如果问题变成“你喜欢订机票的过程吗?”,我想大家的热情一定会打上许多折扣吧……
这就是 Python 大显身手的时候啦。
第一个挑战是,该选择从哪个平台获取信息。这并不是个容易的决定。最后,我选择了 Kayak。在这个过程中,我也考虑过 Momondo、Skyscanner、Expedia 以及一些其他的网站,不过对初学者来说,这些网站的人机验证实在是……比较无情。
在选过几次“哪个是红绿灯,哪个是人行道和自行车”,点过几次“我不是机器人”之后,我觉得还是 Kayak 比较友好一点——虽然如果你在短时间内同时读取太多页面的话,它也会给你弹一些安全检查什么的。
我目前让脚本大约每隔 4 到 6 个小时就抓一次网页,虽然偶尔会出现一些小问题,但总体上还是比较 OK 的。如果你发现脚本开始一直碰到验证码,你可以试着手动提交验证,然后重启脚本,也可以等上几个小时再让爬虫访问这个网站,那时候验证码应该就消失了。
你也可以试着把这些代码用在其他平台上,欢迎大家在下面留言分享你的成果!
在真正开始之前,我要强调很重要的一点:如果你还不熟悉网络抓取,或者如果你不知道为什么某些网站费尽全力要阻止爬虫,那么在你写下第一行爬虫代码之前,请先 Google 一下“网络爬虫礼仪”。如果你像疯子一样毫无节制地扒别人的网站,可能很快就会碰一鼻子灰。
译注:也请大家严格遵守我国互联网和计算机系统方面的各种法律法规及相关规定,不要滥用爬虫技术。
<tt-image data-tteditor-tag="tteditorTag" contenteditable="false" class="syl1561969731429 ql-align-center" data-render-status="finished" data-syl-blot="image" style="box-sizing: border-box; cursor: text; text-align: left; color: rgb(34, 34, 34); font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", "Helvetica Neue", Arial, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial; display: block;"> image<input class="pgc-img-caption-ipt" placeholder="图片描述(最多50字)" value="" style="box-sizing: border-box; outline: 0px; color: rgb(102, 102, 102); position: absolute; left: 187.5px; transform: translateX(-50%); padding: 6px 7px; max-width: 100%; width: 375px; text-align: center; cursor: text; font-size: 12px; line-height: 1.5; background-color: rgb(255, 255, 255); background-image: none; border: 0px solid rgb(217, 217, 217); border-radius: 4px; transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1) 0s;"></tt-image>
03 系紧安全带
准备,加速!
在你导入所需的库,并打开一个 Chrome 页面之后,我们需要定义一些之后会在循环中调用的函数。主要的程序结构应该差不多类似这样:
- 一个函数负责启动爬虫,指出我们需要查找的城市和日期
- 这个函数获取到最初的搜索结果,按照“最优”方式排序航班列表,然后点击“载入更多”
- 另外一个函数爬取整个页面,返回一个 dataframe 数据表对象
- 重复上面的步骤 2 和 3,用“最便宜”和“最快速”的方式排序列表
- 简单地对价格进行统计(最低价、平均价),然后形成简要汇总表,发送到指定邮箱,并把对应的 dataframe 保存成 Excel 表格文件,放在指定目录中
- 每隔 X 小时就重复一遍上面的每一步
通常情况下,每一个 Selenium 项目都是从一个网页驱动框架(webdriver)开始的。我现在用的是 Chromedriver,它使用的是 Chrome 内核。当然,你也可以选择其他的选项,比如无头浏览器 PhantomJS,或者干脆是火狐,都很不错。下载完,往文件夹里一丢就完事了。
请各位大佬读者注意,我写这篇文章并不是为了展示什么新技术。没错,已经有更先进的方法来寻找更便宜的机票,然而我只希望我的文章能给读者带来一些简单而实用的东西。
<pre spellcheck="false" style="box-sizing: border-box; margin: 5px 0px; padding: 5px 10px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 16px; line-height: inherit; font-family: inherit; vertical-align: baseline; cursor: text; counter-reset: list-1 0 list-2 0 list-3 0 list-4 0 list-5 0 list-6 0 list-7 0 list-8 0 list-9 0; background-color: rgb(240, 240, 240); border-radius: 3px; white-space: pre-wrap; color: rgb(34, 34, 34); letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">from time import sleep, strftime
from random import randint
import pandas as pd
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import smtplib
from email.mime.multipart import MIMEMultipart
</pre>
上面这些就是我们的脚本所需的代码库啦。我将用 randint() 让爬虫在每次搜索之间暂停上随机的几秒钟,这是基本上每个爬虫都会有的功能。
<pre spellcheck="false" style="box-sizing: border-box; margin: 5px 0px; padding: 5px 10px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 16px; line-height: inherit; font-family: inherit; vertical-align: baseline; cursor: text; counter-reset: list-1 0 list-2 0 list-3 0 list-4 0 list-5 0 list-6 0 list-7 0 list-8 0 list-9 0; background-color: rgb(240, 240, 240); border-radius: 3px; white-space: pre-wrap; color: rgb(34, 34, 34); letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">driver = webdriver.Chrome(executable_path=chromedriver_path)
sleep(2)
</pre>
开头这几行将会打开一个空白的 Chrome 页面。当你运行它的时候,你将会看到一个空白的 Chrome 浏览器窗口出现了,我们接下来就将让爬虫在这个窗口里工作。
那么,先让我们在另外一个窗口中手动打开 kayak.com 检查一下吧。选择你的出发和到达城市,以及出发日期。在选择日期的时候,记得选上“± 3 天”的选项。我写代码的时候是按这个选项来调试的,所以如果只想搜索某个指定日期的话,需要对代码进行一些调整。
<tt-image data-tteditor-tag="tteditorTag" contenteditable="false" class="syl1561969731448 ql-align-center" data-render-status="finished" data-syl-blot="image" style="box-sizing: border-box; cursor: text; text-align: left; color: rgb(34, 34, 34); font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", "Helvetica Neue", Arial, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial; display: block;"> image<input class="pgc-img-caption-ipt" placeholder="图片描述(最多50字)" value="" style="box-sizing: border-box; outline: 0px; color: rgb(102, 102, 102); position: absolute; left: 187.5px; transform: translateX(-50%); padding: 6px 7px; max-width: 100%; width: 375px; text-align: center; cursor: text; font-size: 12px; line-height: 1.5; background-color: rgb(255, 255, 255); background-image: none; border: 0px solid rgb(217, 217, 217); border-radius: 4px; transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1) 0s;"></tt-image>
我会在之后说明需要调整的地方,不过如果你在尝试的时候遇到问题,欢迎在下面留言哈。
接下来,我们按下搜索按钮,把地址栏里的链接地址复制下来。这个地址长得应该类似下面代码中的那个字符串。我把这个字符串赋值给 kayak 变量,并用 webdriver 的 get 方法来访问这个地址。
<pre spellcheck="false" style="box-sizing: border-box; margin: 5px 0px; padding: 5px 10px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 16px; line-height: inherit; font-family: inherit; vertical-align: baseline; cursor: text; counter-reset: list-1 0 list-2 0 list-3 0 list-4 0 list-5 0 list-6 0 list-7 0 list-8 0 list-9 0; background-color: rgb(240, 240, 240); border-radius: 3px; white-space: pre-wrap; color: rgb(34, 34, 34); letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">kayak = "https://www.kayak.com/flights/LTS-SIN/2019-07-29-flexible/2019-08-15-flexible?sort=bestflight_a"
driver.get(kayak)
sleep(3)
</pre>
这时你的搜索结果页面应该出现了。
如果我在几分钟内连续执行这个命令两三次,网站就会弹出一个验证码,阻止后续的访问。这种情况下,你可以直接手动完成验证,并继续测试你需要搜索的内容,直到下一个验证码跳出来为止。
就我个人的测试而言,头一次的搜索总是不会有问题的,所以如果你还没有跳过验证码的能力,你可以试着先手动完成验证,然后再让爬虫以较低的频率执行搜索操作。——毕竟你完全没必要每隔10分钟就搜索一次价格,对吧?
04 XPath:一个萝卜一个坑
目前,我们打开了一个窗口,读取了一个网页,为了能确切地获取到价格和其他信息,我们需要用到 XPath 或者 CSS 的选择器。今天的例子中,我选择用 XPath 来定位页面上的元素,因为我觉得这个例子里并不是太需要用到 CSS——当然,如果你能做到混合使用 CSS 来进行定位,那当然更完美。
用 XPath 来在页面中进行跳转有的时候还是容易把人搞晕,即使你用了网上那些文章中的技巧,比如在“检查元素”中直接右键“复制 XPath”等方式来获取对应网页元素的 XPath 信息,也不见得就是最佳的办法--有的时候,这样获取的链接太特殊了,很快就不能再用了。
译注:这里个人推荐一下我个人之前买过的《Python 爬虫开发从入门到实战》(谢乾坤 著),里面比较详细地介绍了 XPath 语法,以及如何构造 XPath 的知识,当然 Selenium 的模拟登录和处理验证码等黑科技的介绍也少不了。建议学有余力的同学可以看一看。
<pre spellcheck="false" style="box-sizing: border-box; margin: 5px 0px; padding: 5px 10px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 16px; line-height: inherit; font-family: inherit; vertical-align: baseline; cursor: text; counter-reset: list-1 0 list-2 0 list-3 0 list-4 0 list-5 0 list-6 0 list-7 0 list-8 0 list-9 0; background-color: rgb(240, 240, 240); border-radius: 3px; white-space: pre-wrap; color: rgb(34, 34, 34); letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">cheap_results = '//a[@data-code = "price"]'
driver.find_element_by_xpath(cheap_results).click()
</pre>
那么,让我们用 Python 来选中最便宜的搜索结果。上面的代码中,那个字符串就是 XPath 选择器。你可以在网页中任意元素上点击右键,选择 检查,当开发者工具弹出时,你就可以在窗口中看到你选中的元素的代码了。
<tt-image data-tteditor-tag="tteditorTag" contenteditable="false" class="syl1561969731462 ql-align-center" data-render-status="finished" data-syl-blot="image" style="box-sizing: border-box; cursor: text; text-align: left; color: rgb(34, 34, 34); font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", "Helvetica Neue", Arial, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial; display: block;"> image<input class="pgc-img-caption-ipt" placeholder="图片描述(最多50字)" value="" style="box-sizing: border-box; outline: 0px; color: rgb(102, 102, 102); position: absolute; left: 187.5px; transform: translateX(-50%); padding: 6px 7px; max-width: 100%; width: 375px; text-align: center; cursor: text; font-size: 12px; line-height: 1.5; background-color: rgb(255, 255, 255); background-image: none; border: 0px solid rgb(217, 217, 217); border-radius: 4px; transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1) 0s;"></tt-image>
为了说明一下我前面提到过的,直接在开发者工具中复制 XPath 可能存在的问题,大家可以对比一下这两个 XPath 代码:
这是在开发者工具中,右键点击并选择 复制XPath 命令后,你得到的 XPath 字符串:
<pre spellcheck="false" style="box-sizing: border-box; margin: 5px 0px; padding: 5px 10px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 16px; line-height: inherit; font-family: inherit; vertical-align: baseline; cursor: text; counter-reset: list-1 0 list-2 0 list-3 0 list-4 0 list-5 0 list-6 0 list-7 0 list-8 0 list-9 0; background-color: rgb(240, 240, 240); border-radius: 3px; white-space: pre-wrap; color: rgb(34, 34, 34); letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">'//*[@id="wtKI-price_aTab"]/div[1]/div/div/div[1]/div/span/span'
</pre>
这是我实际使用的定位“最便宜”结果的 XPath 语句:
<pre spellcheck="false" style="box-sizing: border-box; margin: 5px 0px; padding: 5px 10px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 16px; line-height: inherit; font-family: inherit; vertical-align: baseline; cursor: text; counter-reset: list-1 0 list-2 0 list-3 0 list-4 0 list-5 0 list-6 0 list-7 0 list-8 0 list-9 0; background-color: rgb(240, 240, 240); border-radius: 3px; white-space: pre-wrap; color: rgb(34, 34, 34); letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">cheap_results = '//a[@data-code = "price"]'
</pre>
看出问题了吗?
很明显,后面这种写法更简明易懂。它在网页上搜索,并定位一个 data-code 属性等于 "price" 的元素。
而前面这种写法,它先定位一个 ID 是 wtKI-price_aTab 的元素,然后找它的第一个子 div然后往下找 4 层 div 以及 2 层 span …… 怎么说呢,它这次应该是会成功的吧,但一旦网页层次有变化,你的代码就废了。
还是回到上面这个例子,这个 ID 是 wtKI-price_aTab 的元素,只要你刷新一下页面,它的 ID 就变了——事实上,这个 wtKI 是自动生成的字符串,它在每次搜索的时候都会不一样。也就是说,只要一刷新页面,你这段代码就没法正常工作了。
所以,我的忠告是:花点时间认真了解网页结构特征,熟悉 XPath,肯定不亏。
<tt-image data-tteditor-tag="tteditorTag" contenteditable="false" class="syl1561969731476 ql-align-center" data-render-status="finished" data-syl-blot="image" style="box-sizing: border-box; cursor: text; text-align: left; color: rgb(34, 34, 34); font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", "Helvetica Neue", Arial, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial; display: block;"> image<input class="pgc-img-caption-ipt" placeholder="图片描述(最多50字)" value="" style="box-sizing: border-box; outline: 0px; color: rgb(102, 102, 102); position: absolute; left: 187.5px; transform: translateX(-50%); padding: 6px 7px; max-width: 100%; width: 375px; text-align: center; cursor: text; font-size: 12px; line-height: 1.5; background-color: rgb(255, 255, 255); background-image: none; border: 0px solid rgb(217, 217, 217); border-radius: 4px; transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1) 0s;"></tt-image>
不过,在没那么“复杂”的网站上,直接用复制 XPath 也是可以的完成任务的。具体问题具体分析吧!
在完成了上面的这些步骤之后,搜索结果应该已经显示出来了。那么,如果我们要把所有搜索结果的字符串都读取出来,保存在一个列表对象里面,该怎么做呢?小菜一碟。
观察这个页面,我们能看出,每一个搜索结果都属于 resultWrapper 类下的一个对象。那么,我们只需要用 xpath 把所有包含这个类的元素都抓下来,再弄个循环把它们塞进列表里就完事了。如果你能理解这个部分,那接下来的绝大部分代码应该都难不住你啦。
基本上,它的工作方式就是指向你想要的某个对象(比如这里的 resultWrapper),用某种方式(XPath 选择器)把文字都抓下来,然后把内容都放在某个方便读取的对象(先是 flight_containers,接着是 flights_list )中,就搞定咯。
这段的代码类似这样:
<pre spellcheck="false" style="box-sizing: border-box; margin: 5px 0px; padding: 5px 10px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 16px; line-height: inherit; font-family: inherit; vertical-align: baseline; cursor: text; counter-reset: list-1 0 list-2 0 list-3 0 list-4 0 list-5 0 list-6 0 list-7 0 list-8 0 list-9 0; background-color: rgb(240, 240, 240); border-radius: 3px; white-space: pre-wrap; color: rgb(34, 34, 34); letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">xp_results_table = '//*[@class = "resultWrapper"]'
flight_containers = driver.find_elements_by_xpath(xp_results_table)
flights_list = [flight.text for flight in flight_containers]
列出前 3 个结果
flights_list[0:3]
</pre>
运行结果如下:
<tt-image data-tteditor-tag="tteditorTag" contenteditable="false" class="syl1561969731485 ql-align-center" data-render-status="finished" data-syl-blot="image" style="box-sizing: border-box; cursor: text; text-align: left; color: rgb(34, 34, 34); font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", "Helvetica Neue", Arial, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial; display: block;"> image<input class="pgc-img-caption-ipt" placeholder="图片描述(最多50字)" value="" style="box-sizing: border-box; outline: 0px; color: rgb(102, 102, 102); position: absolute; left: 187.5px; transform: translateX(-50%); padding: 6px 7px; max-width: 100%; width: 375px; text-align: center; cursor: text; font-size: 12px; line-height: 1.5; background-color: rgb(255, 255, 255); background-image: none; border: 0px solid rgb(217, 217, 217); border-radius: 4px; transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1) 0s;"></tt-image>
我把前三行显示出来,这样我们就能比较直观地看出程序有没有正确地获取到我们需要的信息。不过,为了方便处理多页数据,我打算单独爬取每个页面上的每个元素,最后再整合进数据表中。
05 全速起飞!
首先,也是最容易的函数,就是实现「加载更多」功能。我们的目标是,在一页里尽可能多地获取航班信息,同时又不触发验证码检查。所以,我的做法是,在一页内容载入进来之后,点一下(就一下!)页面上的「加载更多结果」按钮。
这基本上和我们上面讲过的代码没啥区别,只多了一个 try 语句——我加上这行是因为有的时候这个按钮会没能正确加载,而我不希望程序在这种情况下就此崩溃。
要开启这个功能,你只需要在 start_kayak 函数中把 # load_more() 前面的注释去掉就行啦。
那么,在拉拉杂杂地说了这么多之后(有的时候我真的容易跑题),我们终于到了实际抓取页面内容的函数啦!
我已经把页面上大部分需要处理的元素都丢给 page_scrape 函数来处理了。有的时候,处理结果的列表中会混杂插入第一站和第二站的经停信息,我用一个简单的方法把它们分开,存在 section_a_list 和 section_b_list 两个变量中。这个函数还返回一个数据表对象 flights_df 以便我们可以把各种不同排序的结果分门别类,并最后整合在一起。
我试着让变量名看起来比较清晰易懂一些。请记住,带有 A 的变量与行程第一段相关,而 B 与第二段相关。让我们看看下一个函数吧。
<tt-image data-tteditor-tag="tteditorTag" contenteditable="false" class="syl1561969731493 ql-align-center" data-render-status="finished" data-syl-blot="image" style="box-sizing: border-box; cursor: text; text-align: left; color: rgb(34, 34, 34); font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", "Helvetica Neue", Arial, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial; display: block;"> image<input class="pgc-img-caption-ipt" placeholder="图片描述(最多50字)" value="" style="box-sizing: border-box; outline: 0px; color: rgb(102, 102, 102); position: absolute; left: 187.5px; transform: translateX(-50%); padding: 6px 7px; max-width: 100%; width: 375px; text-align: center; cursor: text; font-size: 12px; line-height: 1.5; background-color: rgb(255, 255, 255); background-image: none; border: 0px solid rgb(217, 217, 217); border-radius: 4px; transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1) 0s;"></tt-image>
06 什么,还有其他函数?
是的。目前我们已经载入了一个页面,构建了一个读取更多内容的函数,以及一个爬取并处理内容的函数。其实,我大可以在这里就把文章结束掉,你还是可以用这段代码来打开某个页面,并读取对应的内容。
但我之前提到过,我们的程序要能自动保存、发邮件通知你等等,这些功能我都已经放在 start_kayak 函数里面啦!
首先,你需要指定出发/到达的城市和乘机日期。接着,程序将会根据你输入的数据,构造对应的 kayak 地址字符串,再打开这个网页地址——这会直达“最佳”排序的搜索结果页面。在第一次爬取之后,我就悄摸摸地把页面顶部的价格和时间对照表给存了下来。
我将用这个表格来计算出最低价格和平均价等数据,和 Kayak 的预测推荐数据(一般在页面的左上角)一起用电子邮件发给你。如果你的搜索不含有弹性日期的话,就不会有一个对照表对象出现在页面上,那么你的这段代码就可能会出问题。
我使用 Outlook 帐户(hotmail.com)进行了一下电子邮件发送测试。如果你没有 Outlook,也可以在网上搜索到许多替代方案,还有其他方法可以实现。如果您已经拥有 Hotmail 帐户,那你直接替换一下相关的账户信息应该就能用了。
译注:请注意,永远不要把密码直接写在代码里,版本管理系统里的东西是删不掉的。
如果你想要理解这些代码的每个部分到底产生了什么作用,请把它们复制出来,在函数外运行它,观察一下。只有这样,你才能真正理解其中的原理。
<tt-image data-tteditor-tag="tteditorTag" contenteditable="false" class="syl1561969731501 ql-align-center" data-render-status="finished" data-syl-blot="image" style="box-sizing: border-box; cursor: text; text-align: left; color: rgb(34, 34, 34); font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", "Helvetica Neue", Arial, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial; display: block;"> image<input class="pgc-img-caption-ipt" placeholder="图片描述(最多50字)" value="" style="box-sizing: border-box; outline: 0px; color: rgb(102, 102, 102); position: absolute; left: 187.5px; transform: translateX(-50%); padding: 6px 7px; max-width: 100%; width: 375px; text-align: center; cursor: text; font-size: 12px; line-height: 1.5; background-color: rgb(255, 255, 255); background-image: none; border: 0px solid rgb(217, 217, 217); border-radius: 4px; transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1) 0s;"></tt-image>
07 把所有代码都用上
在写完了上面这些代码之后,我们需要把这些函数都组装起来,让它们开始工作。
为了保持例子的简单,我们不妨就用一个简单的循环来重复调用它们。在循环的前面,我加了四个“花哨”(并不)的提示,让你可以直接输入出发和到达的城市,以及搜索的日期范围(用的就是 input 函数)。
不过在我们测试的时候,我并不想每次都输入这些变量,所以我在下面写入了 4 个测试数据,在实际使用的时候,你只需要把这 4 个测试数据注释掉就好啦。
如果你已经看到了这里,恭喜!你已经学完了今天这个短短的教程。
对于学有余力的读者,可以考虑一下如何改进我们这段简单的小程序,比如我想到的有:使用微信机器人,把搜索结果文字通过微信发给你自己;使用 VPN 或是其他更隐蔽的方式从多个服务器同时获取搜索结果;把保存搜索结果的 Excel 文件作为附件发送;使用更高级的功能来搞定验证码等问题……等等等等。
<tt-image data-tteditor-tag="tteditorTag" contenteditable="false" class="syl1561969731508 ql-align-center" data-render-status="finished" data-syl-blot="image" style="box-sizing: border-box; cursor: text; text-align: left; color: rgb(34, 34, 34); font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", "Helvetica Neue", Arial, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial; display: block;"> image<input class="pgc-img-caption-ipt" placeholder="图片描述(最多50字)" value="" style="box-sizing: border-box; outline: 0px; color: rgb(102, 102, 102); position: absolute; left: 187.5px; transform: translateX(-50%); padding: 6px 7px; max-width: 100%; width: 375px; text-align: center; cursor: text; font-size: 12px; line-height: 1.5; background-color: rgb(255, 255, 255); background-image: none; border: 0px solid rgb(217, 217, 217); border-radius: 4px; transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1) 0s;"></tt-image>
网友评论