Python网络编程 11.3 网络抓取、Ch11小结

关于网络抓取的第一个建议是:不到迫不得已,不要进行网络抓取。

除了直接进行抓取之外,还有许多可以获取数据的方法。直接使用这些数据源对程序员和网站本身来说都能减少花销。很多情况下,如果直接进行抓取,就需要解析成千上万个页面的HTML。许多网站(比如谷歌和雅虎)都提供了核心服务的API,这样用户就可以不用抓取并解析原始的HTML了。

需要抓取的情况,比如,如果能够通过google搜索到需要的数据,但是google却没有提供可供下载的链接或APi,那就只能进行网络抓取了。此时需要牢记几条规则。首先,找一下要抓取的网站是否包含一个“服务条款”页面。然后,找找看有没有一个robot.txt文件,该文件会指出可以通过搜索引擎下载的URL以及禁止通过搜索引擎下载的URL。这样可以帮助我们避免下载到除了广告之外完全相同的内容,可以帮助网站控制负载。

如果遵循网站的服务条款和robots.txt,那么由于负载过高而导致IP被禁用的可能性也会大大降低。

在最普遍的情况下,抓取网站的过程需要使用我们前面学习的有关HTTP以及Web浏览器对HTTP的处理方式的内容。

  • GET和POST方法,以及如何将HTTP方法、路径和头信息结合起来构造HTTP请求。
  • 状态码和HTTP响应的结构,包括请求成功、重定向、暂时失败以及用就失败之间的区别。
  • 基于HTTP的认证,包括服务器响应与客户端请求中的HTTP认证。
  • 基于表单的认证以及如何设置后续请求认证过程中需要提供的cookie。
  • 基于Javascript的认证。在这种认证方式中,浏览器本身不需要提交表单,而是可以在登录表单内直接向Web服务器发送POST请求。
  • 在HTTP响应中提供隐藏表单字段或者全新的cookie来为网站提供对CSRF攻击的保护。
  • 以下两点的不同之处:
    1.在查询或操作时直接将数据添加至URL,然后对该URL地址进行GET请求         
    2.在操作时向服务器发送POST请求,将数据放在请求体中传输。
  • 比较用于从浏览器表单发送编码数据的POST URL与用于直接在前端Javascript代码中与服务器进行交互的URL的异同。对于后者,传输的数据可能会采用JSON或其他对程序员很友好的格式。
抓取复杂网站时常常需要进行大量的实验和代码微调,还需要一直使用浏览器的Web开发者工具了解网站的原理。在开发者工具中,有3个面板是最重要的。元素(Elements)面板可以实时展示元素之间的层级结构。即使Javascript已经向原始工文档中添加或删除了部分内容,元素面板也能够展示出最新的文档。重新加载页面时,可以在网络(Network)面板看到在生成完整页面过程中进行的HTTP请求和响应(包括通过Javascript发送的请求和响应)。可以在控制台(Console)面板中看到页面的错误,其中包括用户浏览页面时不会发现的错误。

程序员需要处理两类需要自动化抓取的问题。
第一类是抓取整个页面。在需要下载大量数据时会这么做。首先,我们可能需要先登录网站,获取到所需的cookie,然后不断进行GET操作。而在使用这些GET操作下载页面时,有可能需要通过岭以西GET操作来访问页面中的链接。这与Web搜索引擎在获取网站包含的页面时使用的“爬虫”程序类似。
另一种类型是针对一到两个页面的特定部分进行抓取,而不是抓取整个网站。我们有时候可能只想要获取某个特定页面上的某部分数据,比如希望在shell命令提示符中输出从某个天气预报页面抓取的温度;有时候可能想要自动化地进行一些本来需要在浏览器中进行的操作,比如通过客户支付或者是列出昨天的信用卡交易记录检查账户是否被盗用。在进行这一类抓取时,需要在点击量、表单和认证的问题上多加小心。因为网站可能会使用网页内的Javascript来阻止尝试非法访问账户信息的自动化脚本,所以除了Python本身之外,通常还需要一个全功能的浏览器来查看Javascript。

在使用自动化程序抓取网站之前,一定不要忘了检查服务条款和网站的robots.txt。正常的用户在浏览网站时都会在页面上进行一些点击,然后停下来略读或仔细阅读某些内容。因此,就算是自动化程序因为一些没有考虑到的边界情况而没有成功发送请求,它所造成的负载也会相当大,所以需要做好IP被禁用的准备。

11.3.1 获取页面

要在Python程序中查看Web页面的内容,可以使用下面3类方法来获取web页面。
  • 使用Python库直接发起GET或POST请求。将Requests库作为首选解决方案,并且使用Session对象来维护cookie与连接池。
  • 曾经有一些工具是介于全功能Web浏览器和Python程序之间的,能够提供基本的Web浏览器功能,因此可以用于解析
    表单元素,这样我们就可以像在全功能浏览器中一样构造并向服务器发送HTTP请求。其中,Mechanize是最流行的工具,但是它似乎已经不再更新了,原因可能是现在的许多网站都非常复杂,浏览器必须启用Javascript才能够正常访问这些网站。
  • 也可以使用一个真正的Web浏览器。在接下来的例子中,我们将使用Selenium的WebDriver库来控制浏览器。不过现在仍然有大量关于如何在不起动完整浏览器窗口的前提下进行浏览器操作的研究。这些工具通常会先创建一个WebKit实例,该实例不会链接至某个真实的浏览器窗口。在Javascript社区内,PhantomJS是非常流行的一个方法,Ghost.py则是Python社区内正在研究的提供该功能的工具。
在得到了要访问的URL之后,需要使用的算法就相当简单了。根据列出的URL,依次发送HTTP请求,然后保存或查看得到的内容。只有在无法事先获取需要访问的URL时,问题才会变得复杂。此时我们需要在抓取过程中动态获取要访问的URL,而且必须记录曾经访问过的URL,以防重复访问已经访问过的URL或是发生死循环。
下面的代码展示了一个并不复杂但有特定目标的抓取程序。该程序用于登录账单应用程序,然后获取用户已经赚取的收入。在运行该代码前,需要先在窗口中运行咱们之前的账单应用服务器程序 app_improved.py。
import argparse,bs4,lxml.html,requests
from selenium import webdriver
from urllib.parse import urljoin

ROW = '{:>12}  {}'
def download_page_with_requests(base):
    session = requests.Session()
    response = session.post(urljoin(base,'/login'),
                            {'username':'brandon','password':'pswd'})
    assert response.url == urljoin(base,'/')
    return response.text

def download_page_with_selenium(base):
    browser= webdriver.Chrome()
    browser.get(base)
    assert browser.current_url == urljoin(base,'/login')
    css = browser.find_element_by_css_selector
    css('input[name="username"]').send_keys('')
    css('input[name="password"]').send_keys('')
    css('input[name="password"]').submit()
    assert browser.current_url == urljoin(base,'/')
    return browser.page_source

def scrape_with_soup(text):
    soup = bs4.BeautifulSoup(text)
    total = 0
    for li in soup.find_all('li','to'):
        dollars = int(li.get_text().split()[0].lstrip('$'))
        memo = li.find('i').get_text()
        total += dollars
        print(ROW.format(dollars,memo))
    print(ROW.format('-'* 8,'-' * 30))
    print(ROW.format(total,'Total payments made'))

def scrape_with_lxml(text):
    root = lxml.html.document_fromstring(text)
    total = 0
    for li in root.cssselect('li.to'):
        dollars = int(li.text_content().split()[0].lstrip('$'))
        memo = li.cssselect('i')[0].text_content()
        total += dollars
        print(ROW.format(dollars, memo))
    print(ROW.format('-' * 8, '-' * 30))
    print(ROW.format(total, 'Total payments made'))

def main():
    url = 'http://127.0.0.1:5000/'
    choice1 = input('Choose:\n\t1 to download page with selenium'
                   '\t2 to download with requests')
    choice2 = input('Choose:\n\t3 to scrape with lxml'
                    '\t4 to scrape with soup\n\t')
    if choice1 == 1 :
        text = download_page_with_selenium(url)
    else:
        text = download_page_with_requests(url)
    if choice2 == 3:
        scrape_with_lxml(text)
    else:
        scrape_with_soup(text)
if __name__ == '__main__':
    main()
使用requests和selenium的不同之处:
  • 使用前者时, 我们需要自己打开网站,了解登录表单结构,然后根据了解到的内容填写用于登录的post()方法。一旦这么做了之后,如果网站的登录表单将来有变化,代码将会一无所知。代码中直接硬编码了'username'和'password',而这两个输入名将来有可能会发生改变。
    因此,至少使用这种方式编写的Requests代码是与浏览器不同的。Requests并没有打开登录页面,也没有访问表单。这种方法只是假设已经存在登录页面,但是却绕过了对页面的访问 ,直接通过登录页面中的表单发送POST请求。显然,只要登录表单中使用了一个私钥来防止对用户名密码的大量猜测尝试,这个方法就无法成功了。此时,需要在POST前,先进行一次GET请求,来获取/login页面,并得到私钥,然后将私钥与用户名和密码结合起来发送后续的POST请求。
  • mscrape.py中基于Selenium的代码则使用了一种不同的方法。基于selenium的方法就像是用浏览器的真实用户一样,先访问表单,然后选择元素开始填写。填写完成后,selenium也像用户一样点击按钮提交表单。只要selenium的CSS选择器能够正确识别表单字段,代码就能够成功登录。由于selenium其实就是通过直接操作FireFox或Chrome这样的浏览器进行登陆,所以即使表单中使用了私钥签名或特殊的Javascript代码发送POST请求,也能够成功登录。
    然而,selenium要比Requests慢得多。而且,因为要启动浏览器,所以使用selenium的代码在第一次运行时尤其慢。但是,也可以使用selenium快速地进行一些操作,而此时直接使用Python的话可能会需要花上好几个小时来做实验。还可以采用混合方法来完成一些难度较高的抓取任务,这种方法挺有意思的。为了避免在等待浏览器上浪费大多,能否使用selenium来登录并获取必要的cookie,然后将cookie传输回给Requests,使用Requests来进行大量的页面抓取呢?

11.3.2 抓取页面

当网站返回CSV、JSON或其他容易识别的数据格式的数据时,我们当然可以使用标准库模块或是第三方库来解析数据,并进行相应的处理。但是如果返回的信息是原始HTML,该怎么办?
在Chrome 或 Firefox审查元素的话,就可以浏览可折叠的文档元素树,这样可读性就高了很多。当然,前提是HTML已经进行了合理的格式化,而且即使某些标记存在错误,依然不妨碍我们在浏览器中查看需要的数据。之前就已经发现了使用实时审查元素也存在一个问题,即我们看到的文档可能已经被网页内运行的Javascript修改了,与原始的HTML不同。
要查看这样的页面,有两个办法。
第一,禁用浏览器的Javascript,然后重新载入正在阅读的页面,这时在审查元素面板中看到的就是没有经过任何修改的原始HTML,与通过Python代码下载的文档一致。
第二,使用某种用于对原始HTML格式进行整理的程序,比如W3C发布的tidy包,该包在Debian和Ubuntu平台上都可以使用。

11.3.3 递归抓取

11.4小结

HTTP是专为万维网设计的。万维网通过超链接将海量文档连接起来,每个超链接都用URL来表示其指向的页面或页面中的某个小节。用户可以直接点击超链接来访问它所指向的页面。Python标准库也提供了用于解析及构造URL的方法。此外,还可以使用标准库提供的功能根据页面的基URL地址将相对URL转化为绝对URL。

Web应用程序通常会在对HTTP请求进行相应的服务器程序中连接持久化的数据存储(如数据库),然后构造作为响应的HTML。在这一过程中有一点是十分重要的,即应该使用数据库本身提供的功能来引用由Web外部传递来的不可信信息。也可以在Python中使用DB-API 2.0和任何ORM来正确地引用不可信信息。

Web框架各不相同,有的只提供最简单的功能,有的则提供了全栈式服务。如果使用简单的Web框架,就需要自己选择模板语言、ORM或是其他持久层方案。而全栈式的框架则内置了工具来提供这些功能。无论选择哪种框架,都可以在自己的代码中支持静态URL即/person/123/这样包含可变组件的URL。这些框架同样会提供生成与返回模板的方法,以及返回重定向信息或HTTP错误的功能。

每个网站编写者都会遇到一个大麻烦:在像Web这样一个复杂的系统中,组件之间的交互可能会适得用户违背了自己的操作本意,或是允许用户损害其他人的利益。在代码中设计与外部网络的接口时,一定要考虑跨站脚本攻击、跨站请求伪造以及对用户隐私攻击的可能性。在编写会从URL路径、URL查询字符串、POST请求或文件上传等途径接收数据的代码之前,一定要彻底理解这些安全威胁。

我们通常会在全栈式的框架以及轻量级的框架之间进行权衡。像Django这样的全栈式解决方案鼓励用户全部使用它所提供的刚弄工具,而它会为用户提供一个很不错的默认配置(比如自动提供表单的CSRF保护);而Flask这样的轻量级框架则要求我们自己选择其他工具,相互结合,形成最终的解决方案。此时我们就需要理解所有用到的组件。例如,如果选择Flask来开发应用程序,但是却不知道要提供CSRF保护,那么最后开发出的应用程序就无法抵御CSRF攻击了。

要抓取一个Web页面,就需要对网站的工作原理有透彻的理解,这样才能在脚本中模拟正常的用户交互——包括登录、填写以及提交表单这些复杂操作。在Python中,有很多方法可以用来获取和解析页面。目前,Requests和selenium是最流行的用来获取页面的库,而Beautiful Soup和lxml则是人们解析页面时最喜欢使用的方案。




《Python网络编程》第三版一共有18章,我的博客的学习过程将会只包括现在我们已经学习了的前11章,也就是关于网络编程基础以及HTTP的部分。12~18章不会出现在后面的博客中。它们分别是: 

Ch 12 电子邮件的构造与解析

Ch 13 SMTP(简单邮件传输协议)

Ch 14 POP(邮局协议)

Ch 15 IMAP(Internet 消息访问协议)

Ch 16 telnet和SSH

Ch 17 FTP

Ch 18 RPC

你可能感兴趣的:(Python网络编程)