Python поиск по удаленному файлу с использованием HTTP

Как мне найти определенную позицию в удаленном (HTTP) файле, чтобы я мог загрузить только эту часть?

Допустим, байты в удаленном файле были: 1234567890

Я хочу искать 4 и скачать оттуда 3 байта, чтобы у меня было 456

а также, как я могу проверить, существует ли удаленный файл? Я попытался, os.path.isfile(), но он возвращает False, когда я передаю URL удаленного файла.

4 ответа

Решение

Если вы загружаете удаленный файл через HTTP, вам нужно установить Range заголовок.

Проверьте в этом примере, как это можно сделать. Выглядит так:

myUrlclass.addheader("Range","bytes=%s-" % (existSize))

РЕДАКТИРОВАТЬ: я только что нашел лучшую реализацию. Этот класс очень прост в использовании, как видно из строки документации.

class HTTPRangeHandler(urllib2.BaseHandler):
"""Handler that enables HTTP Range headers.

This was extremely simple. The Range header is a HTTP feature to
begin with so all this class does is tell urllib2 that the 
"206 Partial Content" reponse from the HTTP server is what we 
expected.

Example:
    import urllib2
    import byterange

    range_handler = range.HTTPRangeHandler()
    opener = urllib2.build_opener(range_handler)

    # install it
    urllib2.install_opener(opener)

    # create Request and set Range header
    req = urllib2.Request('http://www.python.org/')
    req.header['Range'] = 'bytes=30-50'
    f = urllib2.urlopen(req)
"""

def http_error_206(self, req, fp, code, msg, hdrs):
    # 206 Partial Content Response
    r = urllib.addinfourl(fp, hdrs, req.get_full_url())
    r.code = code
    r.msg = msg
    return r

def http_error_416(self, req, fp, code, msg, hdrs):
    # HTTP's Range Not Satisfiable error
    raise RangeError('Requested Range Not Satisfiable')

Обновление: "лучшая реализация" перенесена в https://github.com/excid3/urlgrabber в файле byterange.py.

Я настоятельно рекомендую использовать библиотеку запросов. Это просто лучшая библиотека HTTP, которую я когда-либо использовал. В частности, чтобы выполнить то, что вы описали, вы должны сделать что-то вроде:

import requests

url = "http://www.sffaudio.com/podcasts/ShellGameByPhilipK.Dick.pdf"

# Retrieve bytes between offsets 3 and 5 (inclusive).
r = requests.get(url, headers={"range": "bytes=3-5"})

# If a 4XX client error or a 5XX server error is encountered, we raise it.
r.raise_for_status()

Я не нашел никаких существующих реализаций файлового интерфейса с функцией seek() для URL-адресов HTTP, поэтому я развернул свою простую версию: https://github.com/valgur/pyhttpio. Это зависит от urllib.request но, вероятно, может быть легко модифицирован для использования requests, если необходимо.

Полный код:

import cgi
import time
import urllib.request
from io import IOBase
from sys import stderr


class SeekableHTTPFile(IOBase):
    def __init__(self, url, name=None, repeat_time=-1, debug=False):
        """Allow a file accessible via HTTP to be used like a local file by utilities
         that use `seek()` to read arbitrary parts of the file, such as `ZipFile`.
        Seeking is done via the 'range: bytes=xx-yy' HTTP header.

        Parameters
        ----------
        url : str
            A HTTP or HTTPS URL
        name : str, optional
            The filename of the file.
            Will be filled from the Content-Disposition header if not provided.
        repeat_time : int, optional
            In case of HTTP errors wait `repeat_time` seconds before trying again.
            Negative value or `None` disables retrying and simply passes on the exception (the default).
        """
        super().__init__()
        self.url = url
        self.name = name
        self.repeat_time = repeat_time
        self.debug = debug
        self._pos = 0
        self._seekable = True
        with self._urlopen() as f:
            if self.debug:
                print(f.getheaders())
            self.content_length = int(f.getheader("Content-Length", -1))
            if self.content_length < 0:
                self._seekable = False
            if f.getheader("Accept-Ranges", "none").lower() != "bytes":
                self._seekable = False
            if name is None:
                header = f.getheader("Content-Disposition")
                if header:
                    value, params = cgi.parse_header(header)
                    self.name = params["filename"]

    def seek(self, offset, whence=0):
        if not self.seekable():
            raise OSError
        if whence == 0:
            self._pos = 0
        elif whence == 1:
            pass
        elif whence == 2:
            self._pos = self.content_length
        self._pos += offset
        return self._pos

    def seekable(self, *args, **kwargs):
        return self._seekable

    def readable(self, *args, **kwargs):
        return not self.closed

    def writable(self, *args, **kwargs):
        return False

    def read(self, amt=-1):
        if self._pos >= self.content_length:
            return b""
        if amt < 0:
            end = self.content_length - 1
        else:
            end = min(self._pos + amt - 1, self.content_length - 1)
        byte_range = (self._pos, end)
        self._pos = end + 1
        with self._urlopen(byte_range) as f:
            return f.read()

    def readall(self):
        return self.read(-1)

    def tell(self):
        return self._pos

    def __getattribute__(self, item):
        attr = object.__getattribute__(self, item)
        if not object.__getattribute__(self, "debug"):
            return attr

        if hasattr(attr, '__call__'):
            def trace(*args, **kwargs):
                a = ", ".join(map(str, args))
                if kwargs:
                    a += ", ".join(["{}={}".format(k, v) for k, v in kwargs.items()])
                print("Calling: {}({})".format(item, a))
                return attr(*args, **kwargs)

            return trace
        else:
            return attr

    def _urlopen(self, byte_range=None):
        header = {}
        if byte_range:
            header = {"range": "bytes={}-{}".format(*byte_range)}
        while True:
            try:
                r = urllib.request.Request(self.url, headers=header)
                return urllib.request.urlopen(r)
            except urllib.error.HTTPError as e:
                if self.repeat_time is None or self.repeat_time < 0:
                    raise
                print("Server responded with " + str(e), file=stderr)
                print("Sleeping for {} seconds before trying again".format(self.repeat_time), file=stderr)
                time.sleep(self.repeat_time)

Небольшой пример использования:

url = "https://www.python.org/ftp/python/3.5.0/python-3.5.0-embed-amd64.zip"
f = SeekableHTTPFile(url, debug=True)
zf = ZipFile(f)
zf.printdir()
zf.extract("python.exe")

Изменить: На самом деле в этом ответе есть практически идентичная, хотя и несколько более минимальная реализация: /questions/3995972/est-li-biblioteka-dlya-izvlecheniya-fajla-iz-udalennogo-zip/3995986#3995986

AFAIK, это невозможно с помощью fseek() или подобного. Вам нужно использовать заголовок HTTP Range для достижения этой цели. Этот заголовок может или не может поддерживаться сервером, поэтому ваш пробег может отличаться.

import urllib2

myHeaders = {'Range':'bytes=0-9'}

req = urllib2.Request('http://www.promotionalpromos.com/mirrors/gnu/gnu/bash/bash-1.14.3-1.14.4.diff.gz',headers=myHeaders)

partialFile = urllib2.urlopen(req)

s2 = (partialFile.read())

РЕДАКТИРОВАТЬ: Это, конечно, предполагается, что под удаленным файлом вы подразумеваете файл, хранящийся на сервере HTTP...

Если нужный файл находится на FTP-сервере, FTP позволяет только указать начальное смещение, а не диапазон. Если это то, что вы хотите, то следующий код должен это сделать (не проверено!)

import ftplib
fileToRetrieve = 'somefile.zip'
fromByte = 15
ftp = ftplib.FTP('ftp.someplace.net')
outFile = open('partialFile', 'wb')
ftp.retrbinary('RETR '+ fileToRetrieve, outFile.write, rest=str(fromByte))
outFile.close()

Я думаю, что ключ к вашему вопросу в том, что вы сказали "URL удаленного файла". Это означает, что вы используете HTTP-URL для загрузки файла с помощью операции HTTP "get".

Так что я просто выполнил в Google поиск "HTTP get" и нашел это для вас:

http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html

Похоже, что вы можете указать диапазон байтов в HTTP получить.

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

Другие вопросы по тегам