4
\$\begingroup\$

Seeing a csv in table form is nicer then viewing it as row text, for example the csv from Calculate food company sales for the year (with headers added by me) looks much nicer in this table form than in plain-text:

<table border="1">

<tr>
<th>    Kind</th>
<th> Brand</th>
<th> Sales in 2014</th>
<th> Sales in 2015</th>
</tr>

<tr>
<td>    Cereal</td>
<td>Magic Balls</td>
<td>2200</td>
<td>2344
</tr>

<tr>
<td>    Cereal</td>
<td>Kaptain Krunch</td>
<td>3300</td>
<td>3123
</tr>

<tr>
<td>    Cereal</td>
<td>Coco Bongo</td>
<td>1800</td>
<td>2100
</tr>

<tr>
<td>    Cereal</td>
<td>Sugar Munch</td>
<td>4355</td>
<td>6500
</tr>

<tr>
<td>    Cereal</td>
<td>Oats n Barley</td>
<td>3299</td>
<td>5400
</tr>

<tr>
<td>    Sugar Candy</td>
<td>Pop Rocks</td>
<td>546</td>
<td>982
</tr>

<tr>
<td>    Sugar Candy</td>
<td>Lollipop</td>
<td>1233</td>
<td>1544
</tr>

<tr>
<td>    Sugar Candy</td>
<td>Gingerbud</td>
<td>2344</td>
<td>2211
</tr>

<tr>
<td>    Sugar Candy</td>
<td>Respur</td>
<td>1245</td>
<td>2211
</tr>

<tr>
<td>    Chocolate</td>
<td>Coco Jam</td>
<td>3322</td>
<td>4300
</tr>

<tr>
<td>    Chocolate</td>
<td>Larkspur</td>
<td>1600</td>
<td>2200
</tr>

<tr>
<td>    Chocolate</td>
<td>Mighty Milk</td>
<td>1234</td>
<td>2235
</tr>

<tr>
<td>    Chocolate</td>
<td>Almond Berry</td>
<td>998</td>
<td>1233
</tr>

<tr>
<td>    Condiments</td>
<td>Peanut Butter</td>
<td>3500</td>
<td>3902
</tr>

<tr>
<td>    Condiments</td>
<td>Hot Sauce</td>
<td>1234</td>
<td>1560
</tr>

<tr>
<td>    Condiments</td>
<td>Jelly</td>
<td>346</td>
<td>544
</tr>

<tr>
<td>    Condiments</td>
<td>Spread</td>
<td>2334</td>
<td>5644</tr></table>

To view a .csv file, I just translate it into HTML and call the browser on it.

The code is short because the task is simple, but I feel like it could be written better:

"""
This utility shows a csv file in table format
by translating it to html and calling the browser on the newly created file.

The html file is not deleted after being viewed
and has name `original_file.split('.')[0] + '.html'`

Example usage:
  python3 csview.py example.csv
"""
import webbrowser
import sys

def csv_to_html(csv):
    START = '''<table border="1">\n\n'''
    END = '''</tr></table>'''

    lines = [line for line in csv.split("\n") if line]

    html_lines = ["<th>" + lines[0].replace(',', '</th>\n<th>') + '</th>'] +\
                 ["<td>" + line.replace(',', '</td>\n<td>') for line in lines[1:]]
    body = '<tr>\n' + '\n</tr>\n\n<tr>\n'.join(html_lines)
    return START + body + END

if __name__ == "__main__":
    html_filename = sys.argv[1].split('.')[0] + '.html'
    with open(html_filename, "w+") as out_file:
        with open(sys.argv[1]) as in_file:
            out_file.write(csv_to_html(in_file.read()))
    webbrowser.open(html_filename)
\$\endgroup\$
  • \$\begingroup\$ I'd say this is good tight code. You could ask the codegolf forum to trim it just for fun, but I'd say it's fine the way it is. \$\endgroup\$ – Barry Carter Feb 6 '16 at 16:18
2
\$\begingroup\$

Use Modules

As a joke, I'd say that to write good Python, you need to be lazy. Python comes with batteries included, and using them usually has a very good impact on functionality, readability, security and performance while reducing workload.

For example, with the csv module, as suggested by others, it can parse several formats, your intent is clear, it supports separators and line breaks inside cells with quoting and is implemented in C.

I also suggest os.path.splitext and xml.etree.ElementTree. Once again, intent is made more clear, you gain in functionality (special support for funny file names for splitext, automatic escaping for etree), though you may argue with the performance argument for this specific case.

import webbrowser
import sys
import xml.etree.ElementTree as eTree
import os.path
import csv

def csv_to_html(csvhandle):
    # Init
    table = eTree.Element("table", border="1")
    reader = csv.reader(csvhandle)

    # Header
    headline = eTree.SubElement(table, "tr")
    for column in next(reader):
        elem = eTree.SubElement(headline, "th")
        elem.text = column

    # Content
    for row in reader:
        row_elem = eTree.SubElement(table, "tr")
        for cell in row:
            elem = eTree.SubElement(row_elem, "td")
            elem.text = cell

    return eTree.tostring(table, method="html", encoding="unicode")

if __name__ == "__main__":
    csv_filename = sys.argv[1]
    html_filename = os.path.splitext(csv_filename)[0] + ".html"
    # With construct, just to remind you that several with_items are allowed.
    # Comes in handy when dealing with a bigger number of nested with statements
    with open(html_filename, "w+") as out_file, open(csv_filename) as in_file:
        out_file.write(csv_to_html(in_file))
    webbrowser.open(html_filename)

If you can, you could also use lxml instead of xml.etree, which produces better HTML and is faster, but the module is not standard.

\$\endgroup\$
  • \$\begingroup\$ Very good first post, it shows deep knowledge of Python modules. I hope You will stick around :) \$\endgroup\$ – Caridorc Feb 7 '16 at 20:30
4
\$\begingroup\$

Constants

They are better defined outside of functions; there is no need in reinitializing them each call.

Also they are not balanced, fixing it will avoid a weird body = '<tr>\n' + ....

Files management

Just in case the path of the input file contained a dot, I recommend you use rsplit('.', 1).

Also you should try to open the input file before the ouput one. Just in case an error happen during the access to the file, you won't create an empty output file.

And since you're dealing with CSV files, why not use the csv module? It will allow you to handle edge-case data (such as fields containing comas or newlines) easily.

Wrap stuff into tags

Since you're outputing HTML, it may be a better idea to build it as such: by wrapping stuff into tags.

You could define a PATTERN = "<{tag}>{{}}</{tag}>" and a formatter:

def formatter(tag):
    return PATTERN.format(tag=tag).format

map that formatter at will to wrap stuff into tags.

Proposed improvements

import webbrowser
import sys
import csv

PATTERN = "<{tag}>{{}}</{tag}>"

def formatter(tag):
    return PATTERN.format(tag=tag).format

def csv_to_html(csv_file):
    reader = csv.reader(csv_file)

    header = formatter('tr')(
        '\n'.join(map(formatter('th'), next(reader)))
    )

    data = '\n'.join(
        map(formatter('tr'),
            '\n'.join(map(formatter('td'), row))
            for row in reader
        )
    )

    return formatter('table')('\n'.join((header, data)))

if __name__ == "__main__":
    csv_filename = sys.argv[1]
    html_filename = csv_filename.rsplit('.', 1)[0] + '.html'
    with open(csv_filename) as in_file:
        with open(html_filename, "w+") as out_file:
            out_file.write(csv_to_html(in_file))
    webbrowser.open(html_filename)
\$\endgroup\$
  • \$\begingroup\$ Why did you make formatter a partial function? What are the benefits over giving it two arguments? \$\endgroup\$ – Caridorc Feb 7 '16 at 11:10
  • 1
    \$\begingroup\$ @Caridorc Because map, on it's own can only apply 1 argument. But you could make it so it accept 2 args and use generator expressions instead. \$\endgroup\$ – 409_Conflict Feb 7 '16 at 12:05
  • \$\begingroup\$ Lots of good points here. You can optionally join the two with statements with a comma to avoid double indentation. \$\endgroup\$ – holroy Feb 7 '16 at 15:11
4
\$\begingroup\$

You can't just go about doing string substitutions like that. Every serialization format has some kind of escaping mechanism. Examples:

  • SQL strings are delimited by '. To include a literal apostrophe, you use ''. If you interpolate strings into your SQL queries without escaping, then you end up with a Bobby Tables situation (a.k.a ).
  • HTML has special characters <, >, ", and &, which should be written as &lt;, &gt; &quot;, and &amp;, or numerically like &#46;, or in hexadecimal like &#x26;. If you neglect to perform HTML escaping, then you might end up with arbitrary HTML, including possibly a <script> tag injected into the document.
  • CSV is only loosely specified, but RFC 4180 is a rough summary of the de facto practices. If there are literal commas in your data, then the field can be "quoted, like this".

These are just a few examples, but if you think about it, every data format has such special characters (YAML, HTTP URLs, LDAP search filters, Javascript string literals, C printf() format strings, …). Ignore that fact, and you have a security hole.

To parse CSV, use the csv module. To escape HTML, use the cgi.escape() function.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.