Python爬虫一一第2章、数据抓取

我们需要让这个爬虫从每个网页中抽取一些数据,然后实现某些事情,这种做法也被称为抓取(scraping)

2.1 分析网页

右键单击选择View page source选项,获取网页源代码

2.2 三种网页抓取方法

2.2.1 正则表达式

当我们使用正则表达式获取面积数据时,首先需要尝试匹配<td>元素中为w2p_fw的内容,如下所示:

实现代码如下:

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import urllib.request
# 下载url网页,proxy是支持代理功能,初始值为None,想要设置就直接传参数即可
def download(url, user_agent = 'brain', proxy = None, num_retries = 2):
    print ('Downloading:', url)
    headers = {'User-agent':user_agent} # 设置用户代理,而不使用python默认的用户代理Python-urllib/3.6
    request = urllib.request.Request(url,headers=headers)

    opener = urllib.request.build_opener()
    if proxy: # 如果设置了proxy,那么就进行以下设置以实现支持代理功能
        proxy_params = {urllib.parse.urlparse(url).scheme: proxy}
        opener.add_handler(urllib.request.ProxyHandler(proxy_params))

    try:
        html = opener.open(request).read()
    except urllib.request.URLError as e: # 下载过程中出现问题
        print('Download error:', e.reason)
        html = None
        if num_retries>0: # 错误4XX发生在请求存在问题,而5XX错误则发生在服务端存在问题,所以在发生5XX错误时重试下载
            if hasattr(e,'code') and 500 <= e.code < 600:
                # recursively retry 5xx HTTP errors
                return download(url, user_agent, proxy, num_retries-1)
    return html

import re
url = 'http://example.webscraping.com/places/default/view/Afghanistan-1'
html = download(url)
html = str(html)
src = re.findall('<td class="w2p_fw">(.*?)</td>', html)
for target in src:
    print(target)

运行结果:

我们可以看到很多属性都使用了<td class="w2p_fw">标签,想要分离出面积属性,我们可以只选择其中的第二个元素,如下所示:

虽然现在可以使用这个方案,但是如果网页发生变化,该方案就可能会失效,所以我们可以将其父元素<tr>属性也添加进来,这样该元素对应ID属性就唯一了:

修改版本,让其更适用:

import re
url = 'http://example.webscraping.com/places/default/view/Afghanistan-1'
html = download(url)
html = str(html)
src = re.findall('<tr id="places_area__row">.*?<td class=["\']w2p_fw["\']>(.*?)</td>', html)
print(src)

虽然正则表达式更容易适应未来变化,但又存在难以构造、可读性差的问题,此外,还有一些微小的布局变化也会使该正则表达式无法满足,所以我们在接下来介绍一些更好的方法。

2.2.2 Beautiful Soup

Beautiful Soup 是一个可以从HTML或XML文件中提取数据的Python库.它能够通过你喜欢的转换器实现惯用的文档导航,查找,修改文档的方式.

详细介绍网站:https://www.crummy.com/software/BeautifulSoup/bs4/doc/index.zh.html

BeautifulSoup可以对不规则html(如引号缺失和标签未闭合情况进行整合),下面我们使用BeautifulSoup重新对Aera进行抓取:

from bs4 import BeautifulSoup
url = 'http://example.webscraping.com/places/default/view/Afghanistan-1'
html = download(url)
# html = str(html)
soup = BeautifulSoup(html,'html.parser')
tr = soup.find(attrs = {'id':'places_area__row'})
td = tr.find(attrs = {'class':'w2p_fw'})
area = td.text
print(area)

运行结果:

2.2.3 Lxml

Lxml是基于libxml2这一XML解析库的Python封装。该模块使用C语言编写,解析速度比BeautifulSoup更快,不过安装过程也更为复杂,不过如果你使用的是Pycharm编译器的话,跟其他模块安装是一样的,找到File->Setting->Project:xxx->Project Interpreter页面,点击右边的+按钮,输入Lxml搜索并安装即可。

有关Lxml的相关介绍参考:https://lxml.de/

同样,Lxml也可以正确解析属性两侧缺失的引号,并闭合标签。

lxml有几种不同的方法,比如XPath选择器和类似Beautiful Soup的find()方法。不过,我们将使用CSS选择器,因为它更加简洁。

下面是使用lxml的CSS选择器抽取面试数据的示例代码:

import lxml.html
from lxml.cssselect import CSSSelector
url = 'http://example.webscraping.com/places/default/view/Afghanistan-1'
html = download(url)
tree = lxml.html.fromstring(html)
td = tree.cssselect('tr#places_area__row > td.w2p_fw')[0]
area = td.text_content()
print(area)

运行结果:

2.2.4 性能对比

假如我们需要爬取国家网页中的每个可用数据,通过Inspect网页我们可以看到,如下数据:

表格中的每一行都拥有一个以places起始且以row结束的ID,下面我们分别用上述三种方法实现抓取:

(1)正则表达式

import re
def re_scraper(html):
    results = {}
    for field in FIELDS:
        #results[field] = re.findall('<tr id="places_%s__row">.*?<td class=["\']w2p_fw["\']>(.*?)</td>' % field, html)
        results[field] = re.search('<tr id="places_%s__row">.*?<td class=["\']w2p_fw["\']>(.*?)</td>' % field, html).groups()[0]
        print(results[field])
    return results

(2).BeautifulSoup

from bs4 import BeautifulSoup
def bs_scraper(html):
    soup = BeautifulSoup(html, 'html.parser')
    results = {}
    for filed in FIELDS:
        results[filed] = soup.find('table').find('tr',id='places_%s__row' % filed).find('td',class_='w2p_fw').text
        print(results[filed])
    return results

(3).Lxml

import lxml.html
def lxml_scraper(html):
    tree = lxml.html.fromstring(html)
    results = {}
    for filed in FIELDS:
        td = tree.cssselect('tr#places_%s__row > td.w2p_fw' % filed)[0]
        results[filed] = td.text_content()
        print(results[filed])
    return results

我们分别对上述三种方式进行调用,测试结果均如下所示:

url = 'http://example.webscraping.com/places/default/view/Afghanistan-1'
html = download(url)
html = str(html)
# re_scraper(html)/bs_scraper(html)
lxml_scraper(html)

抓取结果:测试三种方法的抓取性能。

import time
NUM_ITERATIONS = 1000 # number of times to test each scraper
url = 'http://example.webscraping.com/places/default/view/Afghanistan-1'
html = download(url)
html = str(html)
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))

运行结果:

从运行结果我们可以看到,三种抓取结果的对比:

总结:

如果你的爬虫瓶颈是下载网页,而不是抽取数据 的话,那 么使用较慢的方法 (如 Beautiful Soup )也不成问题。如果只需抓取少量数据 , 并且想要避免额外依赖的话 , 那么正则表达式可能更加适合 。 不过 , 通常情况下 , l xml 是抓取数据的最好选 择 , 这是因为该方法既快速又健壮 , 而正则表达式 和Beautifl Soup 只在某些特定场景下有用
 

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

前面我们已经介绍了如何抓取国家数据,接下来我们需要将其集成到上一章的链接爬虫当中,我们需要添加一个callback参数处理抓取行为。callback是一个函数,在发生某个特定事件之后会调用该函数(本例中,会在网页下载完成后调用)。该抓取callback函数包含url和html两个参数,并且返回一个爬虫的URL列表,下面是实现代码:

接下来,我们实现callback回调,下面我们对其功能进行扩展,把得到的结果数据保存到CSV表格中,其代码如下:

import csv
class ScrapeCallback:
    def __init__(self):
        self.writer = csv.writer(open('countries.csv','w'))
        self.fields = FIELDS
        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)

最后,我们向链接爬虫传入回调的代码写法如下:

# 只想找http://example.webscraping.com/places/default/index... or http://example.webscraping.com/places/default/view...
link_crawler('http://example.webscraping.com', '/places/default'+'/(index|view)', max_depth=2, scrape_callback = ScrapeCallback())

这样,我们就实现了数据抓取并保存到CSV文件,如下所示:

完整代码示例如下:

#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import urllib.request
import urllib.parse # 将url链接从相对路径(浏览器可懂但python不懂)转为绝对路径(python也懂了)
import re #正则表达式
import urllib.robotparser # 爬取数据前解析网站robots.txt文件,避免爬取网站所禁止或限制的
import datetime # 下载限速功能所需模块
import lxml.html
from lxml.cssselect import CSSSelector

# 下载url网页,proxy是支持代理功能,初始值为None,想要设置就直接传参数即可
def download(url, user_agent = 'brain', proxy = None, num_retries = 2):
    print ('Downloading:', url)
    headers = {'User-agent':user_agent} # 设置用户代理,而不使用python默认的用户代理Python-urllib/3.6
    request = urllib.request.Request(url,headers=headers)

    opener = urllib.request.build_opener()
    if proxy: # 如果设置了proxy,那么就进行以下设置以实现支持代理功能
        proxy_params = {urllib.parse.urlparse(url).scheme: proxy}
        opener.add_handler(urllib.request.ProxyHandler(proxy_params))

    try:
        html = opener.open(request).read()
    except urllib.request.URLError as e: # 下载过程中出现问题
        print('Download error:', e.reason)
        html = None
        if num_retries>0: # 错误4XX发生在请求存在问题,而5XX错误则发生在服务端存在问题,所以在发生5XX错误时重试下载
            if hasattr(e,'code') and 500 <= e.code < 600:
                # recursively retry 5xx HTTP errors
                return download(url, user_agent, proxy, num_retries-1)
    return html

# 爬取网站的下载限速功能的类的实现,每次在download下载前使用
class Throttle:
    """Add a delay between downloads to the same domain
    """
    def __init__(self, delay):
        # amount of delay between downloads for each domain
        self.delay = delay
        # timestamp of when a domain was last accessed
        #记录上次访问的时间,小知识timestamp:时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)
        #起至现在的总秒数。
        self.domains = {}

    def wait(self, url):
        domain = urllib.parse.urlparse(url).netloc
        last_accessed = self.domains.get(domain) # 记录每个域名上次访问的时间

        if self.delay > 0 and last_accessed is not None:
            # 外部延时与访问时间间隔(当前访问时间及上次访问时间)比较
            sleep_secs = self.delay - (datetime.datetime.now() - last_accessed).seconds
            if sleep_secs > 0: # 访问时间间隔小于外部时延的话,执行睡眠操作
                # domain has been accessed recently
                # so need to sleep
                time.sleep(sleep_secs)
        # update the last accessed time
        self.domains[domain] = datetime.datetime.now()

"""先下载 seed_url 网页的源代码,然后提取出里面所有的链接URL,接着对所有匹配到的链接URL与link_regex 进行匹配,
如果链接URL里面有link_regex内容,就将这个链接URL放入到队列中,下一次 执行 while crawl_queue: 就对这个链接URL进行同样的操作。
反反复复,直到 crawl_queue 队列为空,才退出函数。"""
def link_crawler(seed_url, link_regex, max_depth = 2, scrape_callback = None):
    """Crawl from the given seed URL following links matched by link_regex
    """
    crawl_queue = [seed_url]
    # keep track which URL's have seen before
    # seen = set(crawl_queue)
    seen = {seed_url: 0}  # 初始化seed_url访问深度为0
    while crawl_queue:
        url = crawl_queue.pop()

        # 爬取前解析网站robots.txt,检查是否可以爬取网站,避免爬取网站禁止或限制的
        rp = urllib.robotparser.RobotFileParser()
        rp.set_url(seed_url+'/robots.txt')
        rp.read()
        user_agent = 'brain'
        if rp.can_fetch(user_agent,url): # 解析后发现如果可以正常爬取网站,则继续执行

            # 爬取网站的下载限速功能的类的调用,每次在download下载前使用
            throttle = Throttle(delay = 5) # 这里实例网站robots.txt中的delay值为5
            throttle.wait(url)

            html = download(url)
            html = html.decode('utf-8') # 需要转换为UTF-8 / html = str(html)
            links = []
            if scrape_callback:
                links.extend(scrape_callback(url, html) or [])
            # filter for links matching our regular expression
            if html == None:
                continue
            depth = seen[url]  # 用于避免爬虫陷阱的记录爬取深度的depth
            if depth != max_depth:
                for link in get_links(html):
                    if re.match(link_regex, link):
                        link = urllib.parse.urljoin(seed_url,link)
                        if link not in seen:
                            # seen.add(link)
                            seen[link] = depth + 1  # 在之前的爬取深度上加1
                            crawl_queue.append(link)
        else:
            print("Blocked by %s robots,txt" % url)
            continue


def get_links(html):
    """Return a list of links from html
    """
    # a regular expression to extract all links from the webpage
    webpage_regex = re.compile('<a[^>]+href=["\'](.*?)["\']', re.IGNORECASE)
    # list of all links from the webpage
    return webpage_regex.findall(html)


FIELDS = ('area','population','iso','country','capital',
          'continent', 'tld', 'currency_code', 'currency_name', 'phone',
          'postal_code_format', 'postal_code_regex', 'languages', 'neighbours')

def scrape_callback(url, html):
    if re.search('/view/', url):
        tree = lxml.html.fromstring(html)
        row = [tree.cssselect('tr#places_%s__row > td.w2p_fw' % field)[0].text_content() for field in FIELDS]
        print(url,row)

import csv
class ScrapeCallback:
    def __init__(self):
        self.writer = csv.writer(open('countries.csv','w',newline='')) # 加入newline=”''这个参数后生成的文件就不会出现空行了
        self.fields = FIELDS
        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)

# 只想找http://example.webscraping.com/places/default/index... or http://example.webscraping.com/places/default/view...
link_crawler('http://example.webscraping.com', '/places/default'+'/(index|view)', max_depth=2, scrape_callback = ScrapeCallback())

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值