source: orange/Orange/OrangeWidgets/Prototypes/OWCSVFileImport.py @ 10533:b6ce40e06f68

Revision 10533:b6ce40e06f68, 14.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 2 years ago (diff)

Bug fixes to csv importer, added skip initial whitespace and missing values flag option.

Line 
1"""
2<name>CSV File import</name>
3<description>Import .csv file</description>
4
5"""
6import os
7import csv
8from StringIO import StringIO
9
10from PyQt4 import QtCore, QtGui
11
12from OWWidget import *
13import OWGUI
14import Orange
15
16#from OWFile import FileNameContextHandler as PathContextHandler
17from OWDataTable import ExampleTableModel
18
19Slot = pyqtSlot
20
21class standard_icons(object):
22    def __init__(self, qwidget=None, style=None):
23        self.qwidget = qwidget
24        if qwidget is None:
25            self.style = QApplication.instance().style()
26        else:
27            self.style = qwidget.style()
28   
29    @property
30    def dir_open_icon(self):
31        return self.style.standardIcon(QStyle.SP_DirOpenIcon)
32   
33    @property
34    def reload_icon(self):
35        return self.style.standardIcon(QStyle.SP_BrowserReload)
36   
37   
38class OWCSVFileImport(OWWidget):
39#    contextHandlers = {"": PathContextHandler("")}
40    settingsList = ["recent_files"]
41   
42    DELIMITERS = [("Tab", "\t"),
43                  ("Comma", ","),
44                  ("Semicolon", ";"),
45                  ("Space", " "),
46                  ("Others", None),
47                  ]
48   
49    def __init__(self, parent=None, signalManager=None, 
50                 title="CSV File Import"):
51       
52        OWWidget.__init__(self, parent, signalManager, title, 
53                          wantMainArea=False)
54       
55        self.inputs = []
56        self.outputs = [("Data", Orange.data.Table)]
57       
58        # Settings
59        self.delimiter = ","
60        self.other_delimiter = None
61        self.quote = '"'
62        self.missing = ""
63       
64        self.skipinitialspace = True
65        self.has_header = True
66        self.has_orange_header = True
67       
68        self.recent_files = []
69       
70        # Hints for the recent files
71        self.hints = {}
72       
73        self.loadSettings()
74       
75        self.recent_files = filter(os.path.exists, self.recent_files)
76       
77        layout = QHBoxLayout()
78        box = OWGUI.widgetBox(self.controlArea, "File", orientation=layout)
79        self.style().standardIcon(QStyle.SP_DirOpenIcon)
80        icons = standard_icons(self)
81       
82        self.recent_combo = QComboBox(self, objectName="recent_combo",
83                                      toolTip="Recent files.",
84                                      activated=self.on_select_recent)
85        self.recent_combo.addItems([os.path.basename(p) for p in self.recent_files])
86       
87        self.browse_button = QPushButton("...", icon=icons.dir_open_icon,
88                                         toolTip="Browse filesystem",
89                                         clicked=self.on_open_dialog)
90       
91#        self.reload_button = QPushButton("Reload", icon=icons.reload_icon,
92#                                         clicked=self.reload_file)
93       
94        layout.addWidget(self.recent_combo, 2)
95        layout.addWidget(self.browse_button)
96#        layout.addWidget(self.reload_button)
97
98        #################
99        # Cell separators
100        #################
101        grid_layout = QGridLayout()
102        grid_layout.setVerticalSpacing(4)
103        grid_layout.setHorizontalSpacing(4)
104        box = OWGUI.widgetBox(self.controlArea, "Cell Separator",
105                              orientation=grid_layout)
106       
107        button_group = QButtonGroup(box)
108        QObject.connect(button_group, 
109                        SIGNAL("buttonPressed(int)"),
110                        self.delimiter_changed
111                        )
112       
113        for i, (name, char) in  enumerate(self.DELIMITERS[:-1]):
114            button = QRadioButton(name, box,
115                                  toolTip="Use %r as cell separator" % char)
116           
117            button_group.addButton(button, i)
118            grid_layout.addWidget(button, i / 3, i % 3)
119           
120        button = QRadioButton("Other", box,
121                              toolTip="Use other character")
122       
123        button_group.addButton(button, i + 1)
124        grid_layout.addWidget(button, i / 3 + 1, 0)
125        self.delimiter_button_group = button_group
126           
127        self.delimiter_edit = QLineEdit(objectName="delimiter_edit",
128                                        text=self.other_delimiter or self.delimiter,
129                                        editingFinished=self.delimiter_changed,
130                                        toolTip="Cell delimiter character.")
131       
132        grid_layout.addWidget(self.delimiter_edit, i / 3 + 1, 1, -1, -1)
133       
134        preset = [d[1] for d in self.DELIMITERS[:-1]]
135        if self.delimiter in preset:
136            index = preset.index(self.delimiter)
137            b = button_group.button(index)
138            b.setChecked(True)
139            self.delimiter_edit.setEnabled(False)
140        else:
141            button.setChecked(True)
142            self.delimiter_edit.setEnabled(True)
143       
144        ###############
145        # Other options
146        ###############
147        form = QFormLayout()
148        box = OWGUI.widgetBox(self.controlArea, "Other Options",
149                              orientation=form)
150       
151        self.quote_edit = QLineEdit(objectName="quote_edit",
152                                    text=self.quote,
153                                    editingFinished=self.quote_changed,
154                                    toolTip="Text quote character.")
155       
156        form.addRow("Quote", self.quote_edit)
157       
158        self.missing_edit = QLineEdit(objectName="missing_edit",
159                                      text=self.missing,
160                                      editingFinished=self.missing_changed,
161                                      toolTip="Missing value flags (separated by a comma)."
162                                      )
163       
164        form.addRow("Missing values", self.missing_edit)
165       
166        self.skipinitialspace_check = \
167                QCheckBox(objectName="skipinitialspace_check",
168                          checked=self.skipinitialspace,
169                          text="Skip initial whitespace",
170                          toolTip="Skip any whitespace at the beginning of each cell.",
171                          toggled=self.skipinitialspace_changed
172                          )
173               
174        form.addRow(self.skipinitialspace_check)
175               
176        self.has_header_check = \
177                QCheckBox(objectName="has_header_check",
178                          checked=self.has_header,
179                          text="Header line",
180                          toolTip="Use the first line as a header",
181                          toggled=self.has_header_changed
182                          )
183       
184        form.addRow(self.has_header_check)
185       
186        self.has_orange_header_check = \
187                QCheckBox(objectName="has_orange_header_check",
188                          checked=self.has_orange_header,
189                          text="Has orange variable type definitions",
190                          toolTip="Use second and third line as a orange style '.tab' format feature definitions.",
191                          toggled=self.has_orange_header_changed
192                          )
193               
194        form.addRow(self.has_orange_header_check)
195       
196        box = OWGUI.widgetBox(self.controlArea, "Preview")
197        self.preview_view = QTableView()
198        box.layout().addWidget(self.preview_view)
199       
200        OWGUI.button(self.controlArea, self, "Send", callback=self.send_data)
201       
202        self.selected_file = None
203        self.data = None
204       
205        self.resize(450, 500)
206        if self.recent_files:
207            QTimer.singleShot(1, lambda: self.set_selected_file(self.recent_files[0]))
208       
209    def on_select_recent(self, recent):
210        if isinstance(recent, int):
211            recent = self.recent_files[recent]
212       
213        self.set_selected_file(recent)
214   
215    def on_open_dialog(self): 
216        last = os.path.expanduser("~/Documents")
217        path = QFileDialog.getOpenFileName(self, "Open File", last)
218        path = unicode(path)
219        if path:
220            basedir, name = os.path.split(path)
221            self.recent_combo.insertItem(0, name)
222            self.recent_combo.setCurrentIndex(0)
223            self.recent_files.insert(0, path)
224            self.set_selected_file(path)
225   
226    @Slot(int)
227    def delimiter_changed(self, index):
228        self.delimiter = self.DELIMITERS[index][1]
229        if self.delimiter is None:
230            self.other_delimiter = str(self.delimiter_edit.text())
231        self.update_preview()
232   
233    def quote_changed(self):
234        if self.quote_edit.text():
235            self.quote = str(self.quote_edit.text())
236            self.update_preview()
237           
238    def missing_changed(self):
239        self.missing = str(self.missing_edit.text())
240        self.update_preview()
241           
242    def has_header_changed(self):
243        self.has_header = self.has_header_check.isChecked()
244        self.update_preview()
245       
246    def has_orange_header_changed(self):
247        self.has_orange_header = self.has_orange_header_check.isChecked()
248        self.update_preview()
249       
250    def skipinitialspace_changed(self):
251        self.skipinitialspace = self.skipinitialspace_check.isChecked()
252        self.update_preview()
253           
254    def set_selected_file(self, filename):
255        if filename in self.hints:
256            hints = self.hints[filename]
257        else:
258            hints = sniff_csv(filename)
259            self.hints[filename] = hints
260       
261        delimiter = hints["delimiter"]
262       
263        # Update the widget state (GUI) from the saved hints for the file
264        preset = [d[1] for d in self.DELIMITERS[:-1]]
265        if delimiter not in preset:
266            self.delimiter = None
267            self.other_delimiter = delimiter
268            index = len(self.DELIMITERS) - 1
269            button = self.delimiter_button_group.button(index)
270            button.setChecked(True)
271            self.delimiter_edit.setText(self.other_delimiter)
272            self.delimiter_edit.setEnabled(True)
273        else:
274            self.delimiter = delimiter
275            index = preset.index(delimiter)
276            button = self.delimiter_button_group.button(index)
277            button.setChecked(True)
278            self.delimiter_edit.setEnabled(False)
279       
280        self.quote = hints["quotechar"]
281        self.quote_edit.setText(self.quote)
282       
283        self.missing = hints["DK"] or ""
284        self.missing_edit.setText(self.missing)
285       
286        self.has_header = hints["has_header"]
287        self.has_header_check.setChecked(self.has_header)
288       
289        self.has_orange_header = hints["has_orange_header"]
290        self.has_orange_header_check.setChecked(self.has_orange_header)
291       
292        self.skipinitialspace = hints["skipinitialspace"]
293        self.skipinitialspace_check.setChecked(self.skipinitialspace)
294       
295        self.selected_file = filename
296        self.selected_file_head = []
297        with open(self.selected_file, "rb") as f:
298            for i, line in zip(range(30), f):
299                self.selected_file_head.append(line)
300       
301        self.update_preview()
302       
303    def update_preview(self):
304        self.error(0)
305        if self.selected_file:
306            head = StringIO("".join(self.selected_file_head))
307            hints = self.hints[self.selected_file]
308           
309            # Save hints for the selected file
310            hints["quote"] = self.quote
311            hints["delimiter"] = self.delimiter or self.other_delimiter
312            hints["has_header"] = self.has_header
313            hints["has_orange_header"] = self.has_orange_header
314            hints["skipinitialspace"] = self.skipinitialspace
315            hints["DK"] = self.missing or None
316            try:
317                data = Orange.data.io.load_csv(head, delimiter=self.delimiter,
318                                               quotechar=self.quote,
319                                               has_header=self.has_header,
320                                               has_types=self.has_orange_header,
321                                               has_annotations=self.has_orange_header,
322                                               skipinitialspace=self.skipinitialspace,
323                                               DK=self.missing or None
324                                               )
325            except Exception, ex:
326                self.error(0, "Cannot parse (%r)" % ex)
327                data = None
328               
329            if data is not None:
330                model = ExampleTableModel(data, None, self)
331            else:
332                model = None
333            self.preview_view.setModel(model)
334           
335    def send_data(self):
336        self.error(0)
337        if self.selected_file:
338            hints = self.hints[self.selected_file]
339            try:
340                data = Orange.data.io.load_csv(self.selected_file,
341                                               delimiter=self.delimiter,
342                                               quotechar=self.quote,
343                                               has_header=self.has_header,
344                                               has_annotations=self.has_orange_header,
345                                               skipinitialspace=self.skipinitialspace,
346                                               DK=self.missing or None
347                                               )
348            except Exception, ex:
349                self.error(0, "An error occurred while loading the file:\n\t%r" % ex)
350                data = None
351            self.data = data
352        self.send("Data", self.data)
353       
354       
355def sniff_csv(file):
356    snifer = csv.Sniffer()
357    if isinstance(file, basestring):
358        file = open(file, "rb")
359   
360    sample = file.read(2 ** 20) # max 1MB sample
361    dialect = snifer.sniff(sample)
362    has_header = snifer.has_header(sample)
363   
364    return {"delimiter": dialect.delimiter,
365            "doublequote": dialect.doublequote,
366            "escapechar": dialect.escapechar,
367            "quotechar": dialect.quotechar,
368            "quoting": dialect.quoting,
369            "skipinitialspace": dialect.skipinitialspace,
370            "has_header": has_header,
371            "has_orange_header": False,
372            "skipinitialspace": True,
373            "DK": None,
374            }
375   
376if __name__ == "__main__":
377    import sys
378    app = QApplication(sys.argv)
379    w = OWCSVFileImport()
380    w.show()
381    app.exec_()
382    w.saveSettings()
383       
384       
385       
386       
Note: See TracBrowser for help on using the repository browser.