-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathbs4utils.py
More file actions
160 lines (150 loc) · 4.73 KB
/
Copy pathbs4utils.py
File metadata and controls
160 lines (150 loc) · 4.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
#!/usr/bin/env python3
''' Some little utility functions for working with the soup from `beautifulsoup4`.
'''
from typing import Iterable
from bs4 import BeautifulSoup, Tag as BS4Tag, NavigableString
from typeguard import typechecked
from cs.lex import printt
DISTINFO = {
'keywords': ["python3"],
'classifiers': [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Topic :: Text Processing",
],
'install_requires': [
'beautifulsoup4',
],
}
# TODO: find_all(...,recursive=False) does this? apparently not?
def child_tags(tag, child_name: str = None) -> Iterable[BS4Tag]:
''' A generator yielding the immediate child tags of `child`
whose tag name is `child_name`.
If `child_name` is `None`, yield all the immediate child tags,
skipping things like strings and comments.
'''
for child in tag.children:
if isinstance(child, BS4Tag) and (child_name is None
or child.name == child_name):
yield child
def table_grid(table: BS4Tag) -> list[list]:
''' Given a `<TABLE>` tag, return a `list[list]` representing
the text contents of the table in a grid.
This is pretty simple minded, with initial support for `colspan=`
but no support for `rowspan=`.
`colspan` is supported by associating the same datum with multiple cells.
`<TH>` and `<TR>` rows are supported but not differentiated.
Only `<TH>` and `<TR>` which are immediate children of the `<TABLE>` tag
are recognised.
Only `<TD>` which are immediate children of `<TH>` or `<TR>` are recognised.
'''
# TODO: rowspan=
# TODO: pad rows? optionally?
rows = []
for tx in child_tags(table):
if tx.name in ('th', 'tr'):
row = []
for td in child_tags(tx, 'td'):
datum = td.text.strip()
colspan = td.get("colspan", 1)
try:
colspan = int(colspan)
except ValueError:
colspan = 1
for _ in range(colspan):
row.append(datum)
rows.append(row)
return rows
@typechecked
def tabulate_soup(
tag: BS4Tag | NavigableString
) -> list[list[str, str] | tuple]:
''' Return a table describing `soup` for use with `cs.lex.printt`.
Connect tags with their child tags using Unicode box characters.
'''
table = []
if isinstance(tag, NavigableString):
text = str(tag).strip()
if text:
table.append(['', text])
else:
# A tag with interior content.
attrs = dict(tag.attrs)
label = tag.name
# pop off the id attribute if present, include in the label
try:
id_attr = attrs.pop('id')
except KeyError:
pass
else:
label += f' #{id_attr}'
# pop off the name attribute if present, include in the label
try:
id_attr = attrs.pop('name')
except KeyError:
pass
else:
label += f' {id_attr!r}'
children = list(
child for child in tag.children if isinstance(child, NavigableString)
or child.name not in ('script', 'style')
)
# count the subtags which aren't strings
nsubtags = sum(
not isinstance(child, NavigableString) for child in children
)
bigrows = []
if not attrs and len(children) == 1 and isinstance(children[0],
NavigableString):
# The super compact form:
# a tag with no attrs and some text puts the text beside the tag name.
assert nsubtags == 0
text = f'{str(children[0]).strip()}'
table.append([label, text])
else:
attr_text = "\n".join(
f'{attr}={value!r}' for attr, value in sorted(attrs.items())
)
table.append([label, attr_text])
if children:
subtable = []
for child in children:
subtable.extend(tabulate_soup(child))
table.append(tuple(subtable))
return table
def printt_soup(tag: BS4Tag, **printt_kw):
''' Print the contents of the soup via `cs.lex.printt`
using `tabulate_soup` to make the table.
'''
if isinstance(tag, BS4Tag) and tag.name == 'html':
table = []
if tag.head:
table.extend(tabulate_soup(tag.head))
table.extend(tabulate_soup(tag.body))
else:
table = tabulate_soup(tag)
printt(*table, **printt_kw)
if __name__ == '__main__':
for html in (
'foo',
'<h1>foo</h1>',
'''
<html>
<head>
<title>title here</title>
</head>
<body>
<h1 id="3" attr="zot" attr2="2">heading 1</h1>
body here
<h1>second heading</h1>
second
third
</body>
</html>
''',
):
print("======================================")
print(html)
print("--------------------------------------")
soup = BeautifulSoup(html, features="lxml")
printt_soup(soup)