Запишите название, загрузив соответствующую часть веб-страницы

Я хотел бы очистить только название веб-страницы, используя Python. Мне нужно сделать это для тысяч сайтов, чтобы это было быстро. Я видел предыдущие вопросы, такие как получение только заголовка веб-страницы в 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  переменная (см. эту функцию в bs4), но я не уверен, как изменить код, чтобы в основном остановить чтение всего оставшегося содержимого после того, как заголовок был найденный. Одна из проблем может заключаться в том, что перенаправления должны по-прежнему работать.

- Редактировать 3 -

Итак, вот пример. У меня есть ссылка www.xyz.com/abc, и я должен следовать этому через любые переадресации (почти все мои ссылки используют bit.ly вид сокращения ссылок). Меня интересуют как заголовок, так и домен, который возникает после любых перенаправлений.

- Изменить 4 -

Большое спасибо за вашу помощь! Ответ Кул-Тигина очень хорошо работает и был принят. Я сохраню щедрость, пока она не закончится, хотя для того, чтобы увидеть, появляется ли лучший ответ (как показано, например, сравнением измерения времени).

- Изменить 5 -

Для всех, кого это интересует: я решил, что принятый ответ будет примерно вдвое быстрее моего существующего решения с помощью BeautifulSoup4.

Ответ 1

Вы можете отложить загрузку всего тела ответа, включив режим потока requests.

Запросы 2.14.2 документация - Расширенное использование

По умолчанию, когда вы делаете запрос, тело ответа является скачан сразу. Вы можете отменить это поведение и отложить загружая тело ответа, пока не получите доступ к Response.contentатрибут с параметром stream:

...

Если вы устанавливаете stream на True при выполнении запроса, запросы не могут освободить соединение обратно в пул, если вы не потребляете все данные или не вызываете Response.close. Это может привести к неэффективности соединений. Если вы обнаружите, что частично читаете тела запросов (или не читаете их вообще) при использовании stream=True, вам следует рассмотреть возможность использования contextlib.closing(зарегистрированного здесь)

Итак, с помощью этого метода вы можете прочитать кусок ответа куском до тех пор, пока не столкнетесь с тегом заголовка. Поскольку переадресация будет обрабатываться библиотекой, вы будете готовы к работе.

Здесь код с ошибкой, протестированный с помощью 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

Ответ 2

Вопрос:... единственное место, которое я могу оптимизировать, скорее всего, не будет прочитано на всей странице.

Это не читает всю страницу.

Примечание: Unicode .decode() будет raise Exception, если вы вырезаете последовательность Unicode посередине. Используя .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
название = Google
title= Битл | Удлинитель URL и платформа управления ссылками

Протестировано с помощью Python: 3.4.2 и 2.7.9

Ответ 3

Вы очищаете веб-страницы, используя стандартные запросы 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

Ответ 4

вид вещи, которую вы хотите, я не думаю, что это можно сделать, так как способ настройки сети, вы получаете ответ на запрос до того, как все разобрано. обычно нет потока "если встречается <title>, а затем прекратите давать мне данные". если есть любовь, чтобы увидеть это, но есть что-то, что может вам помочь. имейте в виду, что не все сайты уважают это. поэтому некоторые сайты заставят вас загрузить весь источник страницы, прежде чем вы сможете действовать на нем. но многие из них позволят вам указать заголовок диапазона. поэтому в примере запросов:

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:

поэтому я подумал о другой хакерской вещи, которая могла бы помочь. почти на каждой странице есть страница с 404 страницами, не найденная. мы могли бы использовать это в наших интересах. вместо того, чтобы запрашивать обычную страницу. запросите что-то вроде этого.

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

общая страница будет содержать массу информации, ссылок, данных. но страница 404 - это не что иное, как сообщение и (в данном случае) видео. и обычно нет видео. просто текст.

но вы также заметите, что заголовок все еще появляется здесь. поэтому, возможно, мы можем просто запросить то, что, как мы знаем, не существует на любой странице, например.

X5ijsuUJSoisjHJFk948.php

и получить 404 для каждой страницы. таким образом вы загружаете очень маленькую и минималистичную страницу. больше ничего. что значительно сократит объем загружаемой вами информации. тем самым увеличивая скорость и эффективность.

возникает проблема с этим методом: вам нужно как-то проверить, не показывает ли страница свою собственную версию 404. У большинства страниц это есть, потому что она хорошо выглядит с сайтом. и его стандартная практика включает один. но не все из них. не забудьте обработать этот случай.

но я думаю, что это может быть что-то стоящее. в течение тысяч сайтов, это спасло бы много ms времени загрузки для каждого html.

EDIT5:

о чем мы говорили, поскольку вас интересуют URL-адреса, которые перенаправляются. мы могли бы использовать требование заголовка http. которые не получат контент сайта. просто заголовки. поэтому в этом случае:

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

замените мой shortenedurl на tunyurl, чтобы следовать дальше.

>>>response
<Response [301]>

nice, поэтому мы знаем, что это перенаправляет что-то.

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

теперь мы знаем, где URL перенаправляется без фактического последующего его или загрузки любого источника страницы. теперь мы можем применить любые другие ранее обсуждавшиеся методы.

Вот пример, используя запросы и модули lxml и используя идею страницы 404. (имейте в виду, мне нужно заменить 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 за эту часть моего быстрого и грязного script

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...

здесь мы видим, что городской словарь не имеет названия или ive пропустил его в возвращаемых байтах. в любом из этих методов есть компромиссы. единственный способ приблизиться к полной точности - это загрузить весь источник для каждой страницы, о которой я думаю.

Ответ 5

с помощью urllib вы можете установить заголовок Range для запроса определенного диапазона байтов, но есть некоторые последствия:

  • это зависит от сервера для выполнения запроса.
  • вы предполагаете, что данные, которые вы ищете, находятся в пределах требуемого диапазона (однако вы можете сделать другой запрос, используя другой заголовок диапазона, чтобы получить следующие байты - т.е. загрузить первые 300 байтов и получить еще 300, только если вы не можете найти название внутри первый результат - 2 запроса из 300 байт по-прежнему намного дешевле, чем весь документ)
  • (изменить) - чтобы избежать ситуаций, когда тег заголовка разделяется между двумя запрошенными запросами, сделайте перекрытие диапазонов, см. функцию "range_header_overlapped" в моем примере

    import urllib

    req = urllib.request.Request('http://www.python.org/')

    req.headers ['Range'] = 'bytes =% s-% s'% (0, 300)

    f = urllib.request.urlopen(req)

    , чтобы убедиться, что сервер принял наш диапазон:

    content_range = f.headers.get( 'Content-Range')

    печать (content_range)

Ответ 6

мой код также решает случаи, когда тег заголовка разделяется между кусками.

#!/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