仅通过下载网页的相关部分来抓取标题

时间:2017-05-22 23:27:26

标签: python html performance web-scraping

我想用Python抓取网页的标题。我需要为成千上万的网站做到这一点,所以它必须快速。我之前看过像retrieving just the title of a webpage in python这样的问题,但是我发现所有这些问题都是在检索标题之前下载整个页面,这看起来非常低效,因为大多数情况下标题都包含在HTML的前几行中。

是否可以只下载网页的部分,直到找到标题?

我已尝试过以下操作,但page.readline()会下载整个页面。

import urllib2
print("Looking up {}".format(link))
hdr = {'User-Agent': 'Mozilla/5.0',
       'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
       'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.3',
       'Accept-Encoding': 'none',
       'Accept-Language': 'en-US,en;q=0.8',
       'Connection': 'keep-alive'}
req = urllib2.Request(link, headers=hdr)
page = urllib2.urlopen(req, timeout=10)
content = ''
while '</title>' not in content:
    content = content + page.readline()

- 编辑 -

请注意,我当前的解决方案使用BeautifulSoup仅限于处理标题,因此我可以优化的唯一地方可能无法读取整个页面。

title_selector = SoupStrainer('title')
soup = BeautifulSoup(page, "lxml", parse_only=title_selector)
title = soup.title.string.strip()

- 编辑2 -

我发现BeautifulSoup本身会将内容拆分为self.current_data中的多个字符串  变量(see this function in bs4),但我不确定如何修改代码,基本上在找到标题后停止阅读所有剩余的内容。一个问题可能是重定向应该仍然有效。

- 编辑3 -

所以这是一个例子。我有一个链接www.xyz.com/abc,我必须通过任何重定向(几乎所有我的链接使用一点点链接缩短)。我对重定向后出现的标题和域感兴趣。

- 编辑4 -

非常感谢您的所有帮助! Kul-Tigin的答案非常有效并且已被接受。我会保留赏金,直到它耗尽,看看是否有更好的答案(如时间测量比较所示)。

- 编辑5 -

对于任何感兴趣的人:我已经将接受的答案计时大约是使用BeautifulSoup4的现有解决方案的两倍。

6 个答案:

答案 0 :(得分:14)

您可以通过启用requests的流模式推迟下载整个响应正文。

  

Requests 2.14.2 documentation - Advanced Usage

     

默认情况下,当您发出请求时,响应的正文是   立即下载。您可以覆盖此行为并推迟   下载响应正文,直到您访问 Response.content   带有stream参数的属性:

     

...

     

如果您在发出请求时将stream设置为True,则除非您使用所有数据或致电 Response.close 。   这可能导致连接效率低下。如果您在使用stream=True时发现自己部分阅读请求正文(或根本没有阅读它们),则应考虑使用contextlib.closing(documented here

因此,使用此方法,您可以按块读取响应块,直到遇到title标记。由于重定向将由库处理,您可以随时使用。

这是一个容易出错的代码,使用 Python 2.7.10 3.6.0 进行测试:

try:
    from HTMLParser import HTMLParser
except ImportError:
    from html.parser import HTMLParser

import requests, re
from contextlib import closing

CHUNKSIZE = 1024
retitle = re.compile("<title[^>]*>(.*?)</title>", re.IGNORECASE | re.DOTALL)
buffer = ""
htmlp = HTMLParser()
with closing(requests.get("http://example.com/abc", stream=True)) as res:
    for chunk in res.iter_content(chunk_size=CHUNKSIZE, decode_unicode=True):
        buffer = "".join([buffer, chunk])
        match = retitle.search(buffer)
        if match:
            print(htmlp.unescape(match.group(1)))
            break

答案 1 :(得分:4)

  

问题:...我可以优化的唯一地方可能无法在整个页面中阅读。

这不会读取整个页面。

  

注意:如果您在中间剪切Unicode序列,则Unicode .decode()raise Exception。使用.decode(errors='ignore')删除这些序列。

例如:

import re
try:
    # PY3
    from urllib import request
except:
    import urllib2 as request

for url in ['http://www.python.org/', 'http://www.google.com', 'http://www.bit.ly']:
    f = request.urlopen(url)
    re_obj = re.compile(r'.*(<head.*<title.*?>(.*)</title>.*</head>)',re.DOTALL)
    Found = False
    data = ''
    while True:
        b_data = f.read(4096)
        if not b_data: break

        data += b_data.decode(errors='ignore')
        match = re_obj.match(data)
        if match:
            Found = True
            title = match.groups()[1]
            print('title={}'.format(title))
            break

    f.close()
  

<强>输出
  title =欢迎使用Python.org
  标题=谷歌
  title = Bitly | URL Shortener和链接管理平台

使用Python测试:3.4.2和2.7.9

答案 2 :(得分:2)

您正在使用标准REST请求抓取网页,我不知道任何只返回标题的请求,因此我认为不可能。

我知道这并不一定只能帮助获得标题,但我通常会使用BeautifulSoup进行任何网页抓取。这更容易。这是一个例子。

代码:

import requests
from bs4 import BeautifulSoup

urls = ["http://www.google.com", "http://www.msn.com"]

for url in urls:
    r = requests.get(url)
    soup = BeautifulSoup(r.text, "html.parser")

    print "Title with tags: %s" % soup.title
    print "Title: %s" % soup.title.text
    print

输出:

Title with tags: <title>Google</title>
Title: Google

Title with tags: <title>MSN.com - Hotmail, Outlook, Skype, Bing, Latest News, Photos &amp; Videos</title>
Title: MSN.com - Hotmail, Outlook, Skype, Bing, Latest News, Photos & Videos

答案 3 :(得分:2)

你想要的东西我认为无法完成,因为网页的设置方式,你可以在解析任何内容之前获得请求的响应。通常不会有流媒体&#34;如果遇到<title>,则停止向我提供数据&#34;旗。如果有爱看到它,但有一些东西可以帮助你。请记住,并非所有网站都尊重这一点。因此,有些网站会强制您下载整个页面源,然后才能对其进行操作。但是很多它们都允许你指定一个范围标题。所以在请求示例中:

import requests

targeturl = "http://www.urbandictionary.com/define.php?term=Blarg&page=2"
rangeheader = {"Range": "bytes=0-150"}

response = requests.get(targeturl, headers=rangeheader)

response.text

你得到了

'<!DOCTYPE html>\n<html lang="en-US" prefix="og: http://ogp.me/ns#'

现在当然在这里遇到了这个问题 如果您指定的范围太短而无法获取页面标题,该怎么办? 目标是什么? (速度与准确性的结合) 如果页面不尊重Range,会发生什么? (大多数时候你只是得到你没有它的整个回应。)

我不知道这对你有帮助吗?希望如此。但我做过类似的事情,只能获取下载检查的文件头。

EDIT4:

所以我想到了另一种可能有帮助的hacky事情。几乎每个页面都有一个404页面找不到页面。我们也许能够利用这个优势。而不是请求常规页面。请求类似的东西。

http://www.urbandictionary.com/nothing.php

一般页面将包含大量信息,链接和数据。但404页面只不过是一条消息,而且(在这种情况下)是一个视频。而且通常没有视频。只是一些文字。

但你也注意到标题仍然出现在这里。所以也许我们可以在任何页面上请求我们知道不存在的东西,例如。

X5ijsuUJSoisjHJFk948.php

并为每个页面获取404。这样你只需下载一个非常小而简约的页面。而已。这将显着减少您下载的信息量。从而提高速度和效率。

下面是这个方法的问题:你需要检查一下这个页面是否提供了自己的404版本。大多数页面都有它,因为它看起来不错。及其标准做法包括一个。但并非所有人都这样做。确保处理这种情况。

但我认为这可能值得一试。在数千个站点的过程中,它将为每个html节省许多ms的下载时间。

EDIT5:

正如我们所讨论的那样,因为您对重定向的网址感兴趣。我们可能会使用http头部请求。哪个不会得到网站内容。只是标题。所以在这种情况下:

response = requests.head('http://myshortenedurl.com/5b2su2')

用tunyurl替换我的shortenedurl跟随。

>>>response
<Response [301]>

很好,所以我们知道这会重定向到某个东西。

>>>response.headers['Location']
'http://stackoverflow.com'

现在我们知道网址重定向到的位置,而不实际关注或下载任何网页来源。现在我们可以应用之前讨论的任何其他技术。

下面是一个例子,使用请求和lxml模块并使用404页面的想法。 (请注意,我必须用bit替换bit.ly,因此堆栈溢出不会生气。)

#!/usr/bin/python3

import requests
from lxml.html import fromstring

links = ['http://bit'ly/MW2qgH',
         'http://bit'ly/1x0885j',
         'http://bit'ly/IFHzvO',
         'http://bit'ly/1PwR9xM']

for link in links:

    response = '<Response [301]>'
    redirect = ''

    while response == '<Response [301]>':
        response = requests.head(link)
        try:
            redirect = response.headers['Location']
        except Exception as e:
            pass

    fakepage = redirect + 'X5ijsuUJSoisjHJFk948.php'

    scrapetarget = requests.get(fakepage)
    tree = fromstring(scrapetarget.text)
    print(tree.findtext('.//title'))

所以这里我们得到404页面,它将遵循任意数量的重定向。现在继续这个输出:

Urban Dictionary error
Page Not Found - Stack Overflow
Error 404 (Not Found)!!1
Kijiji: Page Not Found

所以你可以看到我们确实得到了冠军。但我们发现该方法存在一些问题。即一些标题添加的东西,有些只是没有一个好的标题。那就是那个方法的问题。然而,我们也可以尝试范围方法。这样做的好处是标题是正确的,但有时我们可能会错过它,有时我们必须下载整个页面来获取它。增加所需时间。

同样归功于alecxe这部分快速而又脏的脚本

tree = fromstring(scrapetarget.text)
print(tree.findtext('.//title'))

以范围方法为例。在链接中的链接循环:将try catch语句之后的代码更改为:

rangeheader = {"Range": "bytes=0-500"}

scrapetargetsection = requests.get(redirect, headers=rangeheader)
tree = fromstring(scrapetargetsection.text)
print(tree.findtext('.//title'))

输出是:

None
Stack Overflow
Google
Kijiji: Free Classifieds in...

这里我们看到都市词典没有标题或我错过了返回的字节。在任何这些方法中都存在权衡。接近总体准确度的唯一方法是下载我认为的每个页面的整个源。

答案 4 :(得分:2)

使用urllib你可以设置Range标头来请求一定范围的字节,但是会有一些后果:

  • 它取决于服务器以兑现请求
  • 您认为您正在寻找的数据在所需的范围内(但是您可以使用不同的范围标头进行另一个请求以获取下一个字节 - 即先下载前300个字节,如果可以的话,再获取另外300个字节&#39;在第一个结果中找到标题 - 300个字节的2个请求仍然比整个文档便宜得多)
  • (编辑) - 为避免标题标记在两个远程请求之间分割,使您的范围重叠,请参阅&#39; range_header_overlapped&#39;我的example code

    中的功能

    import urllib

    req = urllib.request.Request(&#39; http://www.python.org/&#39;)

    req.headers [&#39;范围&#39;] =&#39;字节=%S-%S&#39; %(0,300)

    f = urllib.request.urlopen(req)

    只是为了验证服务器是否接受了我们的范围:

    content_range = f.headers.get(&#39;内容范围&#39)

    打印(content_range)

答案 5 :(得分:0)

我的代码也解决了在块之间分割标题标记的情况。

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
"""
Created on Tue May 30 04:21:26 2017
====================
@author: s
"""

import requests
from string import lower
from html.parser import HTMLParser

#proxies = { 'http': 'http://127.0.0.1:8080' }
urls = ['http://opencvexamples.blogspot.com/p/learning-opencv-functions-step-by-step.html',
        'http://www.robindavid.fr/opencv-tutorial/chapter2-filters-and-arithmetic.html',
        'http://blog.iank.org/playing-capitals-with-opencv-and-python.html',
        'http://docs.opencv.org/3.2.0/df/d9d/tutorial_py_colorspaces.html',
        'http://scikit-image.org/docs/dev/api/skimage.exposure.html',
        'http://apprize.info/programming/opencv/8.html',
        'http://opencvexamples.blogspot.com/2013/09/find-contour.html',
        'http://docs.opencv.org/2.4/modules/imgproc/doc/geometric_transformations.html',
        'https://github.com/ArunJayan/OpenCV-Python/blob/master/resize.py']

class TitleParser(HTMLParser):
    def __init__(self):
        HTMLParser.__init__(self)
        self.match = False
        self.title = ''
    def handle_starttag(self, tag, attributes):
        self.match = True if tag == 'title' else False
    def handle_data(self, data):
        if self.match:
            self.title = data
            self.match = False

def valid_content( url, proxies=None ):
    valid = [ 'text/html; charset=utf-8',
              'text/html',
              'application/xhtml+xml',
              'application/xhtml',
              'application/xml',
              'text/xml' ]
    r = requests.head(url, proxies=proxies)
    our_type = lower(r.headers.get('Content-Type'))
    if not our_type in valid:
        print('unknown content-type: {} at URL:{}'.format(our_type, url))
        return False
    return our_type in valid

def range_header_overlapped( chunksize, seg_num=0, overlap=50 ):
    """
    generate overlapping ranges
    (to solve cases when title tag splits between them)

    seg_num: segment number we want, 0 based
    overlap: number of overlaping bytes, defaults to 50
    """
    start = chunksize * seg_num
    end = chunksize * (seg_num + 1)
    if seg_num:
        overlap = overlap * seg_num
        start -= overlap
        end -= overlap
    return {'Range': 'bytes={}-{}'.format( start, end )}

def get_title_from_url(url, proxies=None, chunksize=300, max_chunks=5):
    if not valid_content(url, proxies=proxies):
        return False
    current_chunk = 0
    myparser = TitleParser()
    while current_chunk <= max_chunks:
        headers = range_header_overlapped( chunksize, current_chunk )
        headers['Accept-Encoding'] = 'deflate'
        # quick fix, as my locally hosted Apache/2.4.25 kept raising
        # ContentDecodingError when using "Content-Encoding: gzip"
        # ContentDecodingError: ('Received response with content-encoding: gzip, but failed to decode it.', 
        #                  error('Error -3 while decompressing: incorrect header check',))
        r = requests.get( url, headers=headers, proxies=proxies )
        myparser.feed(r.content)
        if myparser.title:
            return myparser.title
        current_chunk += 1
    print('title tag not found within {} chunks ({}b each) at {}'.format(current_chunk-1, chunksize, url))
    return False