下面我们将介绍三种抓取网页数据的方法, 首先是正则表达式, 然后是流行的 BeautifulSoup 模块, 最后是强大的 lxml 模块.
1. 正则表达式
? 如果你对正则表达式还不熟悉, 或是需要一些提示时, 可以查阅 Regular Expression HOWTO https://docs.python.org/2/howto/regex.html 获得完整介绍.
? 当我们使用正则表达式抓取国家面积数据时, 首先要尝试匹配元素中的内容, 如下所示:
- >>> import re
- >>> import urllib2
- >>> url = 'http://example.webscraping.com/view/United-Kingdom-239'
- >>> html = urllib2.urlopen(url).read()
- >>> re.findall('<td class="w2p_fw">(.*?)</td>', html)
- ['<img src="/places/static/images/flags/gb.png"/>', '244,820 square kilometres', '62,348,447', 'GB', 'United Kingdom', 'London', '<a href="/continent/EU">EU</a>', '.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', '<div><a href="/iso/IE">IE </a></div>']
- >>>
? 从上述结果看出, 多个国家属性都使用了 < td class="w2p_fw">标签. 要想分离出面积属性, 我们可以只选择其中的第二个元素, 如下所示:
- >>> re.findall('<td class="w2p_fw">(.*?)</td>', html)[1]
- '244,820 square kilometres'
? 虽然现在可以使用这个方案, 但是如果网页发生变化, 该方案很可能就会失效. 比如表格发生了变化, 去除了第二行中的国土面积数据. 如果我们只在现在抓取数据, 就可以忽略这种未来可能发生的变化. 但是, 如果我们希望未来还能再次抓取该数据, 就需要给出更加健壮的解决方案, 从而尽可能避免这种布局变化所带来的影响. 想要该正则表达式更加健壮, 我们可以将其父元素 < tr > 也加入进来. 由于该元素具有 ID 属性, 所以应该是唯一的.
- >>> re.findall('<tr id="places_area__row"><td class="w2p_fl"><label for="places_area"id="places_area__label">Area: </label></td><td class="w2p_fw">(.*?)</td>', html)
- ['244,820 square kilometres']
? 这个迭代版本看起来更好一些, 但是网页更新还有很多其他方式, 同样可以让该正则表达式无法满足. 比如, 将双引号变为单引号,<td > 标签之间添加多余的空格, 或是变更 area_label 等. 下面是尝试支持这些可能性的改进版本.
>>> re.findall('<tr id="places_area__row">.*?<td\s*class=["\']w2p_fw["\']>(.*?)</td>',html)['244,820 square kilometres']
? 虽然该正则表达式更容易适应未来变化, 但又存在难以构造, 可读性差的问题. 此外, 还有一些微小的布局变化也会使该正则表达式无法满足, 比如在 < td > 标签里添加 title 属性.
? 从本例中可以看出, 正则表达式为我们提供了抓取数据的快捷方式, 但是, 该方法过于脆弱, 容易在网页更新后出现问题. 幸好还有一些更好的解决方案, 后期将会介绍.
2. Beautiful Soup
? Beautiful Soup 是一个非常流行的 Python 模块. 该模块可以解析网页, 并提供定位内容的便捷接口. 如果你还没有安装该模块, 可以使用下面的命令安装其最新版本(需要先安装 pip, 请自行百度):
pip install beautifulsoup4
? 使用 Beautiful Soup 的第一步是将已下载的 HTML 内容解析为 soup 文档. 由于大多数网页都不具备良好的 HTML 格式, 因此 Beautiful Soup 需要对其实际格式进行确定. 例如, 在下面这个简单网页的列表中, 存在属性值两侧引号缺失和标签未闭合的问题.
- <ul class=country>
- <li>Area
- <li>Population
- </ul>
? 如果 Population 列表项被解析为 Area 列表项的子元素, 而不是并列的两个列表项的话, 我们在抓取时就会得到错误的结果. 下面让我们看一下 Beautiful Soup 是如何处理的.
- >>> from bs4 import BeautifulSoup
- >>> broken_html = '<ul class=country><li>Area<li>Population</ul>'
- >>> # parse the HTML
- >>> soup = BeautifulSoup(broken_html, 'html.parser')
- >>> fixed_html = soup.prettify()
- >>> print fixed_html
- <ul class="country">
- <li>
- Area
- <li>
- Population
- </li>
- </li>
- </ul>
? 从上面的执行结果中可以看出, Beautiful Soup 能够正确解析缺失的引号并闭合标签. 现在可以使用 find() 和 find_all() 方法来定位我们需要的元素了.
- >>> ul = soup.find('ul', attrs={'class':'country'})
- >>> ul.find('li') # return just the first match
- <li>Area<li>Population</li></li>
- >>> ul.find_all('li') # return all matches
- [<li>Area<li>Population</li></li>, <li>Population</li>]
Note: 由于不同版本的 Python 内置库的容错能力有所区别, 可能处理结果和上述有所不同, 具体请参考: https://www.crummy.com/software/BeautifulSoup/bs4/doc/#installing-a-parser . 想了解全部方法和参数, 可以查阅 Beautiful Soup 的 官方文档 https://www.crummy.com/software/BeautifulSoup/bs4/doc/
? 下面是使用该方法抽取示例国家面积数据的完整代码.
- >>> from bs4 import BeautifulSoup
- >>> import urllib2
- >>> url = 'http://example.webscraping.com/view/United-Kingdom-239'
- >>> html = urllib2.urlopen(url).read()
- >>> # locate the area row
- >>> tr = soup.find(attrs={'id':'places_area__row'})
- >>> # locate the area tag
- >>> td = tr.find(attrs={'class':'w2p_fw'})
- >>> area = td.text # extract the text from this tag
- >>> print area
- 244,820 square kilometres
? 这段代码虽然比正则表达式的代码更加复杂, 但更容易构造和理解. 而且, 像多余的空格和标签属性这种布局上的小变化, 我们也无需再担心了.
3. Lxml
? Lxml 是基于 libxml2 这一 XML 解析库的 Python 封装. 该模块使用 C 语言 编写, 解析速度比 Beautiful Soup 更快, 不过安装过程也更为复杂. 最新的安装说明可以参考 http://lxml.de/installation.html .**
? 和 Beautiful Soup 一样, 使用 lxml 模块的第一步也是将有可能不合法的 HTML 解析为统一格式. 下面是使用该模块解析一个不完整 HTML 的例子:
- >>> import lxml.html
- >>> broken_html = '<ul class=country><li>Area<li>Population</ul>'
- >>> # parse the HTML
- >>> tree = lxml.html.fromstring(broken_html)
- >>> fixed_html = lxml.html.tostring(tree, pretty_print=True)
- >>> print fixed_html
- <ul class="country">
- <li>Area</li>
- <li>Population</li>
- </ul>
? 同样地, lxml 也可以正确解析属性两侧缺失的引号, 并闭合标签, 不过该模块没有额外添加 <html> 和 <body> 标签.
? 解析完输入内容之后, 进入选择元素的步骤, 此时 lxml 有几种不同的方法, 比如 XPath 选择器和类似 Beautiful Soup 的 find() 方法. 不过, 后续我们将使用 CSS 选择器, 因为它更加简洁, 并且能够在解析动态内容时得以复用. 此外, 一些拥有 jQuery 选择器相关经验的读者会对其更加熟悉.
? 下面是使用 lxml 的 CSS 选择器抽取面积数据的示例代码:
- >>> import urllib2
- >>> import lxml.html
- >>> url = 'http://example.webscraping.com/view/United-Kingdom-239'
- >>> html = urllib2.urlopen(url).read()
- >>> 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
? * 行代码首先会找到 ID 为 places_area__row 的表格行元素, 然后选择 class 为 w2p_fw 的表格数据子标签.
? CSS 选择器表示选择元素所使用的模式, 下面是一些常用的选择器示例:
- # -*- coding: utf-8 -*-
- import csv
- import time
- import urllib2
- import re
- import timeit
- from bs4 import BeautifulSoup
- import lxml.html
- def regex_scraper(html):
- results = {}
- for field in FIELDS:
- results[field] = re.search('<tr id="places_{}__row">.*?<td class="w2p_fw">(.*?)</td>'.format(field), html).groups()[0]
- return results
- def beautiful_soup_scraper(html):
- soup = BeautifulSoup(html, 'html.parser')
- results = {}
- for field in FIELDS:
- results[field] = soup.find('table').find('tr', id='places_{}__row'.format(field)).find('td', class_='w2p_fw').text
- return results
- def lxml_scraper(html):
- tree = lxml.html.fromstring(html)
- results = {}
- for field in FIELDS:
- results[field] = tree.cssselect('table> tr#places_{}__row> td.w2p_fw'.format(field))[0].text_content()
- return results
- def main():
- times = {}
- html = urllib2.urlopen('http://example.webscraping.com/view/United-Kingdom-239').read()
- NUM_ITERATIONS = 1000 # number of times to test each scraper
- times[name] = []
- # record start time of scrape
- start = time.time()
- for i in range(NUM_ITERATIONS):
- if scraper == regex_scraper:
- # the regular expression module will cache results
- # so need to purge this cache for meaningful timings
- re.purge() # * 行代码
- result = scraper(html)
- # check scraped result is as expected
- assert(result['area'] == '244,820 square kilometres')
- times[name].append(time.time() - start)
- # record end time of scrape and output the total
- end = time.time()
- print '{}: {:.2f} seconds'.format(name, end - start)
- writer = csv.writer(open('times.csv', 'w'))
- header = sorted(times.keys())
- writer.writerow(header)
- for row in zip(*[times[scraper] for scraper in header]):
- writer.writerow(row)
- if __name__ == '__main__':
- main()
来源: http://www.bubuko.com/infodetail-2582241.html