docx2csv 0.1.1__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: docx2csv
3
+ Version: 0.1.4
4
+ Summary: Extracts tables from .docx files and saves them as csv or xlsx
5
+ Author-email: Ivan Begtin <ivan@begtin.tech>
6
+ License: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/ivbeg/docx2csv
8
+ Keywords: docx,converter,tables
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: BSD License
12
+ Classifier: Natural Language :: English
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: click>=8.1.6
23
+ Requires-Dist: openpyxl>=3.1.2
24
+ Requires-Dist: python-docx>=0.8.11
25
+ Provides-Extra: xls
26
+ Requires-Dist: xlwt>=1.3.0; extra == "xls"
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7.0; extra == "dev"
29
+ Requires-Dist: pytest-cov; extra == "dev"
30
+ Requires-Dist: flake8; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # docx2csv
34
+
35
+ Extracts tables from .docx files and saves them as CSV, TSV, XLSX, or JSON.
36
+
37
+ [![CI](https://github.com/ivbeg/docx2csv/actions/workflows/ci.yml/badge.svg)](https://github.com/ivbeg/docx2csv/actions/workflows/ci.yml)
38
+ [![PyPI](https://img.shields.io/pypi/v/docx2csv.svg)](https://pypi.org/project/docx2csv/)
39
+ [![Python](https://img.shields.io/pypi/pyversions/docx2csv.svg)](https://pypi.org/project/docx2csv/)
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ pip install docx2csv
45
+ ```
46
+
47
+ For XLS (Excel 97) output support:
48
+
49
+ ```bash
50
+ pip install docx2csv[xls]
51
+ ```
52
+
53
+ ## Quick Start
54
+
55
+ ### Command Line
56
+
57
+ ```bash
58
+ # Extract all tables as separate CSV files
59
+ docx2csv extract document.docx
60
+
61
+ # Extract as XLSX
62
+ docx2csv extract document.docx --format xlsx
63
+
64
+ # Extract as single CSV file with all tables
65
+ docx2csv extract document.docx --format csv --singlefile
66
+
67
+ # Filter tables by minimum row count
68
+ docx2csv extract document.docx --sizefilter 3
69
+
70
+ # Specify output location
71
+ docx2csv extract document.docx --output results.csv
72
+
73
+ # Analyze a document (list tables without extracting)
74
+ docx2csv analyze document.docx
75
+ ```
76
+
77
+ ### Python API
78
+
79
+ ```python
80
+ from docx2csv import extract_tables, extract, analyze
81
+
82
+ # Get table data as Python objects
83
+ tables = extract_tables('document.docx')
84
+ for table in tables:
85
+ print(f"Table {table['id']}: {table['num_rows']} rows x {table['num_cols']} cols")
86
+ for row in table['data']:
87
+ print(row)
88
+
89
+ # Extract to file
90
+ extract('document.docx', format='xlsx', output='output.xlsx')
91
+
92
+ # Analyze document structure
93
+ info = analyze('document.docx')
94
+ ```
95
+
96
+ ## Output Formats
97
+
98
+ | Format | Extension | Description |
99
+ |--------|-----------|-------------|
100
+ | CSV | `.csv` | Comma-separated values |
101
+ | TSV | `.tsv` | Tab-separated values |
102
+ | XLSX | `.xlsx` | Excel 2007+ (via openpyxl) |
103
+ | XLS | `.xls` | Excel 97 (requires `pip install docx2csv[xls]`) |
104
+ | JSON | `.json` | Structured JSON with metadata |
105
+
106
+ ## Requirements
107
+
108
+ - [click](https://github.com/pallets/click)
109
+ - [python-docx](https://github.com/python-openxml/python-docx)
110
+ - [openpyxl](https://openpyxl.readthedocs.io/)
111
+
112
+ ## Contributing
113
+
114
+ Contributions are welcome! Every little bit helps.
115
+
116
+ 1. Fork the repo on GitHub
117
+ 2. Clone your fork: `git clone https://github.com/your-username/docx2csv.git`
118
+ 3. Create a branch: `git checkout -b name-of-your-bugfix-or-feature`
119
+ 4. Make your changes and add tests
120
+ 5. Run tests: `pytest`
121
+ 6. Commit and push: `git commit -m "Your message" && git push origin your-branch`
122
+ 7. Submit a pull request
123
+
124
+ ## Credits
125
+
126
+ - **Ivan Begtin** - Author and maintainer
127
+ - **Vsevolod Oparin** - Optimized table extraction code
128
+
129
+ ## License
130
+
131
+ BSD 3-Clause License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,99 @@
1
+ # docx2csv
2
+
3
+ Extracts tables from .docx files and saves them as CSV, TSV, XLSX, or JSON.
4
+
5
+ [![CI](https://github.com/ivbeg/docx2csv/actions/workflows/ci.yml/badge.svg)](https://github.com/ivbeg/docx2csv/actions/workflows/ci.yml)
6
+ [![PyPI](https://img.shields.io/pypi/v/docx2csv.svg)](https://pypi.org/project/docx2csv/)
7
+ [![Python](https://img.shields.io/pypi/pyversions/docx2csv.svg)](https://pypi.org/project/docx2csv/)
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install docx2csv
13
+ ```
14
+
15
+ For XLS (Excel 97) output support:
16
+
17
+ ```bash
18
+ pip install docx2csv[xls]
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ### Command Line
24
+
25
+ ```bash
26
+ # Extract all tables as separate CSV files
27
+ docx2csv extract document.docx
28
+
29
+ # Extract as XLSX
30
+ docx2csv extract document.docx --format xlsx
31
+
32
+ # Extract as single CSV file with all tables
33
+ docx2csv extract document.docx --format csv --singlefile
34
+
35
+ # Filter tables by minimum row count
36
+ docx2csv extract document.docx --sizefilter 3
37
+
38
+ # Specify output location
39
+ docx2csv extract document.docx --output results.csv
40
+
41
+ # Analyze a document (list tables without extracting)
42
+ docx2csv analyze document.docx
43
+ ```
44
+
45
+ ### Python API
46
+
47
+ ```python
48
+ from docx2csv import extract_tables, extract, analyze
49
+
50
+ # Get table data as Python objects
51
+ tables = extract_tables('document.docx')
52
+ for table in tables:
53
+ print(f"Table {table['id']}: {table['num_rows']} rows x {table['num_cols']} cols")
54
+ for row in table['data']:
55
+ print(row)
56
+
57
+ # Extract to file
58
+ extract('document.docx', format='xlsx', output='output.xlsx')
59
+
60
+ # Analyze document structure
61
+ info = analyze('document.docx')
62
+ ```
63
+
64
+ ## Output Formats
65
+
66
+ | Format | Extension | Description |
67
+ |--------|-----------|-------------|
68
+ | CSV | `.csv` | Comma-separated values |
69
+ | TSV | `.tsv` | Tab-separated values |
70
+ | XLSX | `.xlsx` | Excel 2007+ (via openpyxl) |
71
+ | XLS | `.xls` | Excel 97 (requires `pip install docx2csv[xls]`) |
72
+ | JSON | `.json` | Structured JSON with metadata |
73
+
74
+ ## Requirements
75
+
76
+ - [click](https://github.com/pallets/click)
77
+ - [python-docx](https://github.com/python-openxml/python-docx)
78
+ - [openpyxl](https://openpyxl.readthedocs.io/)
79
+
80
+ ## Contributing
81
+
82
+ Contributions are welcome! Every little bit helps.
83
+
84
+ 1. Fork the repo on GitHub
85
+ 2. Clone your fork: `git clone https://github.com/your-username/docx2csv.git`
86
+ 3. Create a branch: `git checkout -b name-of-your-bugfix-or-feature`
87
+ 4. Make your changes and add tests
88
+ 5. Run tests: `pytest`
89
+ 6. Commit and push: `git commit -m "Your message" && git push origin your-branch`
90
+ 7. Submit a pull request
91
+
92
+ ## Credits
93
+
94
+ - **Ivan Begtin** - Author and maintainer
95
+ - **Vsevolod Oparin** - Optimized table extraction code
96
+
97
+ ## License
98
+
99
+ BSD 3-Clause License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,10 @@
1
+ # -*- coding: utf-8 -*-
2
+ __version__ = '0.1.4'
3
+ __author__ = "Ivan Begtin (ivan@begtin.tech)"
4
+ __license__ = "BSD"
5
+
6
+
7
+ from .converter import extract, extract_tables, analyze
8
+
9
+
10
+ __all__ = ["extract", "extract_tables", "analyze"]
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env python
2
+ """The main entry point.
3
+
4
+ """
5
+ import sys
6
+
7
+
8
+ def main():
9
+ try:
10
+ from .core import cli
11
+ exit_status = cli()
12
+ except KeyboardInterrupt:
13
+ print("Ctrl-C pressed. Aborting")
14
+ sys.exit(0)
15
+
16
+
17
+ if __name__ == '__main__':
18
+ main()
@@ -0,0 +1,263 @@
1
+ # -*- coding: utf8 -*-
2
+
3
+ import csv
4
+ import os
5
+ import json
6
+ import datetime
7
+ import warnings
8
+
9
+ from typing import List, Dict, Any, Optional
10
+
11
+ import openpyxl
12
+
13
+ from docx import Document
14
+ from docx.table import Table
15
+
16
+
17
+ def _validate_input(filename: str) -> None:
18
+ """Validate that filename is a readable .docx file."""
19
+ if not os.path.exists(filename):
20
+ raise FileNotFoundError("File not found: %s" % filename)
21
+ if not os.path.isfile(filename):
22
+ raise ValueError("Path is not a file: %s" % filename)
23
+ if not filename.lower().endswith('.docx'):
24
+ warnings.warn("File does not have .docx extension: %s" % filename)
25
+
26
+
27
+ def __extract_table(table: Table, strip_space: bool = False) -> List[List[str]]:
28
+ """Extracts table data from a table object using the public API.
29
+
30
+ Uses table.rows and row.cells instead of private XML attributes
31
+ (_tbl.tr_lst, tc_lst, grid_span, vMerge) for compatibility with
32
+ future python-docx releases.
33
+
34
+ Note: Merged cells are handled by tracking cell identity. Vertically
35
+ merged continuation cells replicate the value of the merge origin.
36
+ Horizontally duplicated cells (from gridSpan) are deduplicated.
37
+ """
38
+ results: List[List[str]] = []
39
+ # Track the last value per column for vertical merge continuation
40
+ col_values: Dict[int, str] = {}
41
+
42
+ for row in table.rows:
43
+ r: List[str] = []
44
+ seen_tcs: set = set()
45
+ col_idx: int = 0
46
+
47
+ for cell in row.cells:
48
+ tc_id: int = id(cell._tc)
49
+
50
+ # Skip duplicates from horizontal merge (gridSpan)
51
+ if tc_id in seen_tcs:
52
+ continue
53
+ seen_tcs.add(tc_id)
54
+
55
+ # Check for vertical merge continuation by examining the
56
+ # cell's XML for the vMerge element with no val attribute
57
+ # (which means "continue" in OOXML)
58
+ tc_element = cell._tc
59
+ vmerge = tc_element.find(
60
+ '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}vMerge'
61
+ )
62
+
63
+ if vmerge is not None and vmerge.get(
64
+ '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val'
65
+ ) is None:
66
+ # vMerge continuation — reuse the value from the same column
67
+ # in a previous row
68
+ if col_idx in col_values:
69
+ value = col_values[col_idx]
70
+ else:
71
+ value = cell.text.replace("\n", " ")
72
+ else:
73
+ value = cell.text.replace("\n", " ")
74
+
75
+ if strip_space:
76
+ value = value.strip()
77
+
78
+ col_values[col_idx] = value
79
+ r.append(value)
80
+ col_idx += 1
81
+
82
+ results.append(r)
83
+
84
+ return results
85
+
86
+
87
+ def __store_table(tabdata: List[List[str]], filename: str, format: str = "csv") -> None:
88
+ """Saves table data as csv/tsv/xls/xlsx file."""
89
+ if format == "csv":
90
+ with open(filename, "w", encoding='utf8') as f:
91
+ w = csv.writer(f, delimiter=",")
92
+ for row in tabdata:
93
+ w.writerow(row)
94
+ elif format == 'tsv':
95
+ with open(filename, 'w', encoding='utf8') as f:
96
+ w = csv.writer(f, delimiter='\t')
97
+ for row in tabdata:
98
+ w.writerow(row)
99
+ elif format == 'xls':
100
+ try:
101
+ import xlwt
102
+ except ImportError:
103
+ raise ImportError(
104
+ "XLS output requires xlwt. Install with: pip install docx2csv[xls]"
105
+ )
106
+ workbook = xlwt.Workbook()
107
+ ws = __xls_table_to_sheet(tabdata, workbook.add_sheet("0"))
108
+ workbook.save(filename)
109
+ elif format == "xlsx":
110
+ workbook = openpyxl.Workbook()
111
+ ws = __xlsx_table_to_sheet(tabdata, workbook.create_sheet("0"))
112
+ workbook.save(filename)
113
+
114
+
115
+ def __xls_table_to_sheet(table: List[List[str]], ws: Any) -> Any:
116
+ """Write table data to an XLS worksheet."""
117
+ rn: int = 0
118
+ for row in table:
119
+ cn: int = 0
120
+ for c in row:
121
+ ws.write(rn, cn, c)
122
+ cn += 1
123
+ rn += 1
124
+ return ws
125
+
126
+
127
+ def __xlsx_table_to_sheet(table: List[List[str]], ws: Any) -> Any:
128
+ """Write table data to an XLSX worksheet."""
129
+ for row in table:
130
+ ws.append(row)
131
+ return ws
132
+
133
+
134
+ def extract_tables(filename: str, strip_space: bool = True) -> List[Dict[str, Any]]:
135
+ """Extracts tables from .DOCX files.
136
+
137
+ Args:
138
+ filename: Path to the .docx file.
139
+ strip_space: If True, strip leading/trailing whitespace from cell values.
140
+
141
+ Returns:
142
+ List of dicts with keys: id, num_cols, num_rows, style, data.
143
+ """
144
+ _validate_input(filename)
145
+ tables: List[Dict[str, Any]] = []
146
+ document = Document(filename)
147
+ n: int = 0
148
+ for table in document.tables:
149
+ n += 1
150
+ info: Dict[str, Any] = {}
151
+ info['id'] = n
152
+ info['num_cols'] = len(table.columns)
153
+ info['num_rows'] = len(table.rows)
154
+ info['style'] = table.style.name
155
+ tdata = __extract_table(table, strip_space=strip_space)
156
+ info['data'] = tdata
157
+ tables.append(info)
158
+ return tables
159
+
160
+
161
+ def extract(
162
+ filename: str,
163
+ format: str = "csv",
164
+ sizefilter: int = 0,
165
+ singlefile: bool = False,
166
+ output: Optional[str] = None,
167
+ strip_space: bool = True,
168
+ ) -> None:
169
+ """Extracts tables from .docx files and saves them as csv, xls or xlsx files.
170
+
171
+ Args:
172
+ filename: Path to the .docx file.
173
+ format: Output format — 'csv', 'tsv', 'xls', 'xlsx', or 'json'.
174
+ sizefilter: Exclude tables with fewer than this many rows.
175
+ singlefile: If True, write all tables to a single file.
176
+ output: Output file path. Default: same directory as input.
177
+ strip_space: If True, strip leading/trailing whitespace from cell values.
178
+ """
179
+ _validate_input(filename)
180
+ tables = extract_tables(filename, strip_space=strip_space)
181
+ name: str = filename.rsplit(".", 1)[0]
182
+ fmt: str = format.lower()
183
+ n: int = 0
184
+ lfilter: int = int(sizefilter)
185
+
186
+ if singlefile:
187
+ if fmt == "xls":
188
+ try:
189
+ import xlwt
190
+ except ImportError:
191
+ raise ImportError(
192
+ "XLS output requires xlwt. Install with: pip install docx2csv[xls]"
193
+ )
194
+ workbook = xlwt.Workbook()
195
+ for t in tables:
196
+ if lfilter > len(t['data']):
197
+ continue
198
+ n += 1
199
+ __xls_table_to_sheet(t['data'], workbook.add_sheet(str(n)))
200
+ destname = output if output else name + ".%s" % (fmt)
201
+ workbook.save(destname)
202
+ elif fmt == "xlsx":
203
+ workbook = openpyxl.Workbook()
204
+ for t in tables:
205
+ if lfilter > len(t['data']):
206
+ continue
207
+ n += 1
208
+ __xlsx_table_to_sheet(t['data'], workbook.create_sheet(str(n)))
209
+ destname = output if output else name + ".%s" % (fmt)
210
+ workbook.save(destname)
211
+ elif fmt == "json":
212
+ report: Dict[str, Any] = {
213
+ 'filename': filename,
214
+ 'timestamp': datetime.datetime.now().isoformat(),
215
+ 'num_tables': len(tables),
216
+ 'tables': tables,
217
+ }
218
+ destname = output if output else name + ".%s" % (fmt)
219
+ with open(destname, 'w', encoding='utf8') as f:
220
+ json.dump(report, f, ensure_ascii=False, indent=4)
221
+ elif fmt in ("csv", "tsv"):
222
+ delimiter: str = "," if fmt == "csv" else "\t"
223
+ destname = output if output else name + ".%s" % fmt
224
+ with open(destname, "w", encoding='utf8') as f:
225
+ w = csv.writer(f, delimiter=delimiter)
226
+ for i, t in enumerate(tables):
227
+ if lfilter > len(t['data']):
228
+ continue
229
+ if i > 0:
230
+ w.writerow([])
231
+ for row in t['data']:
232
+ w.writerow(row)
233
+ else:
234
+ for t in tables:
235
+ if lfilter > len(t['data']):
236
+ continue
237
+ n += 1
238
+ destname = output if output else name + "_%d.%s" % (n, fmt)
239
+ __store_table(t['data'], destname, fmt)
240
+
241
+
242
+ def analyze(filename: str) -> List[Dict[str, Any]]:
243
+ """Analyzes .docx file and returns table metadata.
244
+
245
+ Args:
246
+ filename: Path to the .docx file.
247
+
248
+ Returns:
249
+ List of dicts with keys: id, num_cols, num_rows, style.
250
+ """
251
+ _validate_input(filename)
252
+ tableinfo: List[Dict[str, Any]] = []
253
+ document = Document(filename)
254
+ n: int = 0
255
+ for table in document.tables:
256
+ n += 1
257
+ info: Dict[str, Any] = {}
258
+ info['id'] = n
259
+ info['num_cols'] = len(table.columns)
260
+ info['num_rows'] = len(table.rows)
261
+ info['style'] = table.style.name
262
+ tableinfo.append(info)
263
+ return tableinfo
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf8 -*-
3
+
4
+ import sys
5
+
6
+ import click
7
+
8
+ import docx2csv
9
+ from docx.opc.exceptions import PackageNotFoundError
10
+
11
+
12
+ @click.group()
13
+ def cli1():
14
+ """Extracts tables from DOCX files as CSV or XLSX.
15
+
16
+ Use command: "docx2csv extract <filename>" to run extraction.
17
+ It will create files like filename_1.csv, filename_2.csv for each table found.
18
+
19
+ """
20
+ pass
21
+
22
+
23
+ @cli1.command()
24
+ @click.argument('filename')
25
+ @click.option('--format', '-f', default='csv', help='Output format: CSV, TSV, XLSX')
26
+ @click.option('--singlefile', '-s', is_flag=True, show_default=True, default=False, help='Outputs single file with multiple tables')
27
+ @click.option('--sizefilter', '-i', default=0, help='Filters table by minimum number of rows')
28
+ @click.option('--output', '-o', default=None, help='Choose location of output file, default same location as input')
29
+ def extract(filename, format, sizefilter, singlefile, output):
30
+ """
31
+ Extracts tables from DOCX files as CSV or TSV or XLSX.
32
+
33
+ Use command: "docx2csv extract <filename>" to run extraction.
34
+ It will create files like filename_1.csv, filename_2.csv for each table found.
35
+ """
36
+ try:
37
+ docx2csv.extract(filename, format, sizefilter, singlefile, output)
38
+ except FileNotFoundError as e:
39
+ click.echo("Error: %s" % e, err=True)
40
+ sys.exit(1)
41
+ except ValueError as e:
42
+ click.echo("Error: %s" % e, err=True)
43
+ sys.exit(1)
44
+ except PackageNotFoundError:
45
+ click.echo("Error: '%s' is not a valid .docx file." % filename, err=True)
46
+ sys.exit(1)
47
+
48
+
49
+ @click.group()
50
+ def cli2():
51
+ """Analyses of DOCX file, lists all existing tables
52
+ """
53
+ pass
54
+
55
+
56
+ @cli2.command()
57
+ @click.argument('filename')
58
+ def analyze(filename):
59
+ """
60
+ Analyzes .docx file and finds tables
61
+ """
62
+ from pprint import pprint
63
+ try:
64
+ tableinfo = docx2csv.analyze(filename)
65
+ pprint(tableinfo)
66
+ except FileNotFoundError as e:
67
+ click.echo("Error: %s" % e, err=True)
68
+ sys.exit(1)
69
+ except ValueError as e:
70
+ click.echo("Error: %s" % e, err=True)
71
+ sys.exit(1)
72
+ except PackageNotFoundError:
73
+ click.echo("Error: '%s' is not a valid .docx file." % filename, err=True)
74
+ sys.exit(1)
75
+
76
+
77
+ cli = click.CommandCollection(sources=[cli1, cli2])