网页数据抓取怎么写(本节书摘来自异步社区《用Python写网络爬虫》第2章)

优采云 发布时间: 2021-12-15 20:19

  网页数据抓取怎么写(本节书摘来自异步社区《用Python写网络爬虫》第2章)

  本节摘自异步社区《Writing Web Crawlers in Python》一书第2章2.2,作者[澳大利亚]理查德劳森(Richard Lawson),李斌译,更多章节内容可在云栖社区“异步社区”公众号查看。

  2.2 三种网络爬虫方法

  现在我们已经了解了网页的结构,我们将介绍三种捕获其中数据的方法。首先是正则表达式,然后是流行的 BeautifulSoup 模块,最后是强大的 lxml 模块。

  2.2.1 正则表达式

  如果您对正则表达式不熟悉,或者需要一些提示,可以查看完整介绍。

  当我们使用正则表达式抓取区域数据时,首先需要尝试匹配

  元素的内容如下。

   >>> import re

>>> url = 'http://example.webscraping.com/view/United

-Kingdom-239'

>>> html = download(url)

>>> re.findall('(.*?)', html)

['/places/static/images/flags/gb.png',

'244,820 square kilometres',

'62,348,447',

'GB',

'United Kingdom',

'London',

'EU',

'.uk',

'GBP',

'Pound',

'44',

'@# #@@|@## #@@|@@# #@@|@@## #@@|@#@ #@@|@@#@ #@@|GIR0AA',

'^(([A-Z]\\d{2}[A-Z]{2})|([A-Z]\\d{3}[A-Z]{2})|([A-Z]{2}\\d{2}

[A-Z]{2})|([A-Z]{2}\\d{3}[A-Z]{2})|([A-Z]\\d[A-Z]\\d[A-Z]{2})

|([A-Z]{2}\\d[A-Z]\\d[A-Z]{2})|(GIR0AA))$',

'en-GB,cy-GB,gd',

'IE ']

  从上面的结果可以看出,在很多国家属性中都使用了标签。为了隔离area属性,我们可以只选择第二个元素,如下图。

   >>> re.findall('(.*?)', html)[1]

'244,820 square kilometres'

  虽然现在可以使用这个方案,但是如果网页发生变化,该方案很可能会失败。比如表变了,去掉了第二行的土地面积数据。如果我们现在只抓取数据,我们可以忽略这种可能的未来变化。但是,如果我们希望将来再次捕获数据,则需要提供更健壮的解决方案,以尽可能避免这种布局更改的影响。为了让正则表达式更健壮,我们可以改变它的父元素

   >>> re.findall('Area: (.*?)', html)

['244,820 square kilometres']

  这个迭代版本看起来更好,但是还有很多其他的更新网页的方式,也会让正则表达式不尽人意。比如把双引号改成单引号,

  在标签之间添加额外的空格,或更改 area_label 等。以下是尝试支持这些可能性的改进版本。

   >>> re.findall('.*?>> from bs4 import BeautifulSoup

>>> broken_html = 'AreaPopulation'

>>> # parse the HTML

>>> soup = BeautifulSoup(broken_html, 'html.parser')

>>> fixed_html = soup.prettify()

>>> print fixed_html

Area

Population

  从上面的执行结果可以看出,Beautiful Soup 可以正确解析缺失的引号并关闭标签,除了添加和

  标签使它成为一个完整的 HTML 文档。现在我们可以使用 find() 和 find_all() 方法来定位我们需要的元素。

   >>> ul = soup.find('ul', attrs={'class':'country'})

>>> ul.find('li') # returns just the first match

Area

>>> ul.find_all('li') # returns all matches

[Area, Population]

  如果想知道所有的方法和参数,可以参考BeautifulSoup的官方文档,其网址为:。

  下面是使用该方法提取样本国家面积数据的完整代码。

   >>> from bs4 import BeautifulSoup

>>> url = 'http://example.webscraping.com/places/view/

United-Kingdom-239'

>>> html = download(url)

>>> soup = BeautifulSoup(html)

>>> # locate the area row

>>> tr = soup.find(attrs={'id':'places_area__row'})

>>> td = tr.find(attrs={'class':'w2p_fw'}) # locate the area tag

>>> area = td.text # extract the text from this tag

>>> print area

244,820 square kilometres

  这段代码虽然比正则表达式代码复杂,但更容易构建和理解。此外,我们不需要担心布局的微小变化,例如额外的空间和标签属性。

  2.2.3 Lxml

  Lxml 是一个基于 libxml2 的 Python 包,一个 XML 解析库。这个模块是用C语言编写的,解析速度比Beautiful Soup快,但是安装过程比较复杂。您可以参考最新的安装说明。

  与 Beautiful Soup 一样,使用 lxml 模块的第一步是将潜在的非法 HTML 解析为统一格式。以下是使用该模块解析相同不完整 HTML 的示例。

   >>> import lxml.html

>>> broken_html = 'AreaPopulation'

>>> tree = lxml.html.fromstring(broken_html) # parse the HTML

>>> fixed_html = lxml.html.tostring(tree, pretty_print=True)

>>> print fixed_html

Area

Population

  同理,lxml 也可以正确解析属性两边缺失的引号并关闭标签,但是模块没有添加额外的和

  标签。

  解析输入内容后,进入选择元素的步骤。这时候lxml有几种不同的方法,比如XPath选择器和类似于Beautiful Soup的find()方法。但是,在这个和后续的例子中,我们将使用 CSS 选择器,因为它更简洁,可以在第 5 章解析动态内容时重复使用。此外,一些有 jQuery 选择器经验的读者会更熟悉它。

  以下是使用 lxml 的 CSS 选择器提取区域数据的示例代码。

   >>> tree = lxml.html.fromstring(html)

>>> td = tree.cssselect('tr#places_area__row > td.w2p_fw')[0]

>>> area = td.text_content()

>>> print area

244,820 square kilometres

  CSS 选择器的关键代码行以粗体显示。这行代码首先找到id为places_area__row的表格行元素,然后选择class为w2p_fw的表格数据子标签。

  CSS 选择器

  CSS 选择器表示用于选择元素的模式。以下是一些常用选择器的示例。

   选择所有标签:*

选择<a>标签:a

选择所有class="link"的元素:.link

选择class="link"的<a>标签:a.link

选择id="home"的<a>标签:a#home

选择父元素为<a>标签的所有子标签:a > span

选择<a>标签内部的所有标签:a span

选择title属性为"Home"的所有<a>标签:a[title=Home]

  W3C提出了CSS3规范,其网址为`/2011/REC-css3-selectors-20110929/。

  Lxml 已经实现了大部分 CSS3 属性,不支持的功能可以在这里找到。

  需要注意的是,在 lxml 的内部实现中,CSS 选择器实际上被转换为等效的 XPath 选择器。

  2.2.4 性能对比

  为了更好地评估本章介绍的三种捕获方法的权衡,我们需要比较它们的相对效率。一般情况下,爬虫会在一个网页中提取多个字段。因此,为了让对比更加真实,本章我们将为每个爬虫实现一个扩展版本,以提取该国网页中的每一个可用数据。首先,我们需要回到Firebug,查看国家页面其他功能的格式,如图2.4。

  

  从Firebug的显示可以看出,表中的每一行都有一个以places_开头,以__row结尾的ID。这些行中收录的国家数据的格式与上面的示例相同。下面是使用上述信息提取所有可用国家数据的实现代码。

   FIELDS = ('area', 'population', 'iso', 'country', 'capital',

'continent', 'tld', 'currency_code', 'currency_name', 'phone',

'postal_code_format', 'postal_code_regex', 'languages',

'nei*敏*感*词*ours')

import re

def re_scraper(html):

results = {}

for field in FIELDS:

results[field] = re.search('.*?(.*?)' % field, html).groups()[0]

return results

from bs4 import BeautifulSoup

def bs_scraper(html):

soup = BeautifulSoup(html, 'html.parser')

results = {}

for field in FIELDS:

results[field] = soup.find('table').find('tr',

id='places_%s__row' % field).find('td',

class_='w2p_fw').text

return results

import lxml.html

def lxml_scraper(html):

tree = lxml.html.fromstring(html)

results = {}

for field in FIELDS:

results[field] = tree.cssselect('table > tr#places_%s__row

> td.w2p_fw' % field)[0].text_content()

return results

  获取结果

  现在我们已经完成了所有爬虫的代码实现,接下来我们将使用以下代码片段来测试这三种方法的相对性能。

   import time

NUM_ITERATIONS = 1000 # number of times to test each scraper

html = download('http://example.webscraping.com/places/view/

United-Kingdom-239')

for name, scraper in [('Regular expressions', re_scraper),

('BeautifulSoup', bs_scraper),

('Lxml', lxml_scraper)]:

# record start time of scrape

start = time.time()

for i in range(NUM_ITERATIONS):

if scraper == re_scraper:

re.purge()

result = scraper(html)

# check scraped result is as expected

assert(result['area'] == '244,820 square kilometres')

# record end time of scrape and output the total

end = time.time()

print '%s: %.2f seconds' % (name, end – start)

  在这段代码中,每个爬虫都会执行1000次,每次都会检查爬取结果是否正确,然后打印总时间。这里使用的下载函数还是上一章定义的那个。请注意,我们在粗体代码行中调用了 re.purge() 方法。默认情况下,正则表达式模块会缓存搜索结果。为了和其他爬虫比较公平,我们需要使用这个方法来清除缓存。

  下面是在我的电脑上运行脚本的结果。

  $ python performance.py

Regular expressions: 5.50 seconds

BeautifulSoup: 42.84 seconds

Lxml: 7.06 seconds

  由于硬件条件的不同,不同计算机的执行结果也会有一定的差异。但是,每种方法之间的相对差异应该具有可比性。从结果可以看出,在抓取我们的示例网页时,Beautiful Soup 比其他两种方法慢 6 倍以上。其实这个结果是符合预期的,因为lxml和正则表达式模块是用C语言写的,而BeautifulSoup是纯Python写的。一个有趣的事实是 lxml 的行为几乎与正则表达式一样。由于 lxml 必须在搜索元素之前将输入解析为内部格式,因此会产生额外的开销。当捕获同一个网页的多个特征时,会减少这种初始分析的开销,lxml 将更具竞争力。

  2.2.5 结论

  表2.1总结了每种爬取方法的优缺点。

  

  如果你的爬虫的瓶颈是下载网页,而不是提取数据,那么较慢的方法(比如 Beautiful Soup)不是问题。如果你只需要抓取少量数据,又想避免额外的依赖,那么正则表达式可能更合适。但是,一般情况下,lxml 是捕获数据的最佳选择,因为该方法快速且健壮,而正则表达式和 Beautiful Soup 仅在某些场景下有用。

  2.2.6 为链接爬虫添加爬取回调

  我们已经学习了如何抓取国家数据,接下来我们需要将其集成到上一章的链接爬虫中。为了复用这个爬虫代码去抓取其他网站,我们需要添加一个回调参数来处理抓取行为。callback 是一个在某个事件发生后会被调用的函数(在这个例子中,它会在网页下载后被调用)。爬取回调函数收录url和html两个参数,可以返回要爬取的URL列表。下面是它的实现代码,可以看到在Python中实现这个功能非常简单。

   def link_crawler(..., scrape_callback=None):

...

links = []

if scrape_callback:

links.extend(scrape_callback(url, html) or [])

...

  在上面的代码片段中,我们大胆地展示了抓取回调函数的新增代码。如果想获取本版链接爬虫的完整代码,可以访问org/wswp/code/src/tip/chapter02/link_crawler.py。

  现在,我们只需要自定义传入的scrape_callback函数,然后我们就可以使用这个爬虫抓取其他网站。下面修改lxml抓取示例的代码,使其能够在回调函数中使用。

   def scrape_callback(url, html):

if re.search('/view/', url):

tree = lxml.html.fromstring(html)

row = [tree.cssselect('table > tr#places_%s__row >

td.w2p_fw' % field)[0].text_content() for field in

FIELDS]

print url, row

  上面的回调函数会抓取国家数据并显示出来。不过一般情况下,在爬取网站的时候,我们更倾向于复用这些数据,所以下面我们将对其功能进行扩展,将结果数据保存到一个CSV表中。代码如下所示。

   import csv

class ScrapeCallback:

def __init__(self):

self.writer = csv.writer(open('countries.csv', 'w'))

self.fields = ('area', 'population', 'iso', 'country',

'capital', 'continent', 'tld', 'currency_code',

'currency_name', 'phone', 'postal_code_format',

'postal_code_regex', 'languages',

'nei*敏*感*词*ours')

self.writer.writerow(self.fields)

def __call__(self, url, html):

if re.search('/view/', url):

tree = lxml.html.fromstring(html)

row = []

for field in self.fields:

row.append(tree.cssselect('table >

tr#places_{}__row >

td.w2p_fw'.format(field))

[0].text_content())

self.writer.writerow(row)

  为了实现这个回调,我们使用回调类而不是回调函数来维护csv中writer属性的状态。在构造方法中实例化csv的writer属性,然后在__call__方法中进行多次写操作。请注意 __call__ 是一个特殊的方法,当对象作为函数被调用时会被调用。这也是链接爬虫中cache_callback的调用方法。换句话说,scrape_callback(url, html) 和调用 scrape_callback.__call__(url, html) 是等价的。如果想深入了解Python的特殊类方法,可以参考。

  下面是如何编写链接爬虫的回调。

   link_crawler('http://example.webscraping.com/', '/(index|view)',

max_depth=-1, scrape_callback=ScrapeCallback())

  现在,当我们运行这个使用回调的爬虫时,程序会将结果写入CSV文件,我们可以使用Excel或LibreOffice等应用程序查看该文件,如图2.5。

  

  这是成功的!我们完成了第一个工作数据爬虫。

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线