Repository: pandas-dev/pandas

Issue:
ENH read_excel error when accessing AWS S3 URL
Summary: read_excel is unable to read a file using the same S3 URL syntax as read_csv.  read_excel should support accessing S3 data in the same manner as read_csv

read_excel fails with the following error:

``` python
>>> import pandas as pd
>>> df = pd.read_excel("s3://my-bucket/my_file.xlsx")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib64/python2.6/site-packages/pandas/io/excel.py", line 163, in read_excel
    io = ExcelFile(io, engine=engine)
  File "/usr/local/lib64/python2.6/site-packages/pandas/io/excel.py", line 206, in __init__
    self.book = xlrd.open_workbook(io)
  File "/usr/local/lib/python2.6/site-packages/xlrd/__init__.py", line 394, in open_workbook
    f = open(filename, "rb")
IOError: [Errno 2] No such file or directory: 's3://my-bucket/my_file.xlsx'
>>> 
```

read_csv on the other hand is able to successfully read a csv file in the same S3 bucket using the same URL syntax:

``` python
>>> import pandas as pd
>>> df = pd.read_csv("s3://my-bucket/my_file.csv")
>>> len(df.index)
1187
>>>
```

For the record, read_csv can also see the xlsx file but returns parse errors when attempting to tokenize the data.

``` python
>>> import pandas as pd
>>> df = pd.read_csv("s3://my-bucket/my_file.xlsx")
Exception pandas.parser.CParserError: CParserError('Error tokenizing data. C error: Expected 9 fields in line 210, saw 10\n',) in 'pandas.parser.TextReader._tokenize_rows' ignored
>>> 
```

read_excel successfully reads and parses a local copy of the xlsx file

``` python
>>> import pandas as pd
>>> df = pd.read_excel("my_file.xlsx")
>>> len(df.index)
221
>>> 
```

Pandas version string and dependencies:

``` python
>>> pd.show_versions()

INSTALLED VERSIONS
------------------
commit: None
python: 2.6.9.final.0
python-bits: 64
OS: Linux
OS-release: 3.14.48-33.39.amzn1.x86_64
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: None
LANG: en_US.UTF-8

pandas: 0.17.0
nose: 1.3.4
pip: 6.1.1
setuptools: 12.2
Cython: None
numpy: 1.10.1
scipy: 0.16.0
statsmodels: None
IPython: None
sphinx: None
patsy: None
dateutil: 2.4.2
pytz: 2015.7
blosc: None
bottleneck: None
tables: None
numexpr: None
matplotlib: None
openpyxl: None
xlrd: 0.9.4
xlwt: None
xlsxwriter: None
lxml: None
bs4: None
html5lib: None
httplib2: None
apiclient: None
sqlalchemy: None
pymysql: None
psycopg2: None
>>> 
```



Discussion:
This is almost a trivial enhancement, just add `_is_s3_url` [here](https://github.com/pydata/pandas/blob/master/pandas/io/excel.py#L202)

post a file link as an example and i'll put it on our test s3 bucket.


Resolving patch:
diff --git a/pandas/io/excel.py b/pandas/io/excel.py
--- a/pandas/io/excel.py
+++ b/pandas/io/excel.py
@@ -11,7 +11,7 @@
 
 from pandas.core.frame import DataFrame
 from pandas.io.parsers import TextParser
-from pandas.io.common import _is_url, _urlopen, _validate_header_arg
+from pandas.io.common import _is_url, _urlopen, _validate_header_arg, get_filepath_or_buffer, _is_s3_url
 from pandas.tseries.period import Period
 from pandas import json
 from pandas.compat import (map, zip, reduce, range, lrange, u, add_metaclass,
@@ -199,7 +199,10 @@ def __init__(self, io, **kwds):
             raise ValueError("Unknown engine: %s" % engine)
 
         if isinstance(io, compat.string_types):
-            if _is_url(io):
+            if _is_s3_url(io):
+                buffer, _, _ = get_filepath_or_buffer(io)
+                self.book = xlrd.open_workbook(file_contents=buffer.read())
+            elif _is_url(io):
                 data = _urlopen(io).read()
                 self.book = xlrd.open_workbook(file_contents=data)
             else:


Test patch:
diff --git a/pandas/io/tests/test_excel.py b/pandas/io/tests/test_excel.py
--- a/pandas/io/tests/test_excel.py
+++ b/pandas/io/tests/test_excel.py
@@ -65,6 +65,13 @@ def _skip_if_no_excelsuite():
     _skip_if_no_openpyxl()
 
 
+def _skip_if_no_boto():
+    try:
+        import boto  # NOQA
+    except ImportError:
+        raise nose.SkipTest('boto not installed, skipping')
+
+
 _seriesd = tm.getSeriesData()
 _tsd = tm.getTimeSeriesData()
 _frame = DataFrame(_seriesd)[:10]
@@ -429,6 +436,15 @@ def test_read_from_http_url(self):
         local_table = self.get_exceldf('test1')
         tm.assert_frame_equal(url_table, local_table)
 
+    @tm.network(check_before_test=True)
+    def test_read_from_s3_url(self):
+        _skip_if_no_boto()
+
+        url = ('s3://pandas-test/test1' + self.ext)
+        url_table = read_excel(url)
+        local_table = self.get_exceldf('test1')
+        tm.assert_frame_equal(url_table, local_table)
+
     @slow
     def test_read_from_file_url(self):