Skip to content

Commit 2099239

Browse files
Fix Thesaurus.com scraping by parsing HTML directly
Thesaurus.com removed the embedded JSON state that was previously used to extract synonyms and definitions. This commit updates the scraper to parse the HTML structure directly using BeautifulSoup. Changes: - Updated `Synonym.parse_data` to scrape definitions, parts of speech, synonyms, and antonyms from the DOM. - Updated `Synonym.extract_info` to handle the new list-based data structure. - Implemented `Synonym.plain` method which was missing and causing a crash when using the `--plain` flag. - Updated `test/test_main.py` to reflect the new internal data structure.
1 parent 722b209 commit 2099239

2 files changed

Lines changed: 204 additions & 123 deletions

File tree

src/synonym_cli/cli.py

Lines changed: 181 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,181 @@
1-
from urllib.parse import quote
2-
import requests
3-
from bs4 import BeautifulSoup
4-
import json
5-
from rich.table import Table
6-
from rich.console import Console
7-
from rich import box
8-
from rich import print as rprint
9-
10-
11-
class Synonym:
12-
def __init__(self, word):
13-
self.word = quote(word)
14-
self.data = None
15-
16-
def fetch_data(self):
17-
try:
18-
url = f"https://www.thesaurus.com/browse/{self.word}"
19-
response = requests.get(url)
20-
response.raise_for_status()
21-
except requests.exceptions.RequestException as e:
22-
print(f"Error during requests to {url} : {str(e)}")
23-
return None
24-
return response.text
25-
26-
def parse_data(self, response_text):
27-
soup = BeautifulSoup(response_text, "html.parser")
28-
script = soup.find("script", {"id": "preloaded-state"})
29-
if script is not None:
30-
script_text = script.string
31-
start = script_text.find("{")
32-
end = script_text.rfind("}") + 1
33-
json_text = script_text[start:end]
34-
data = json.loads(json_text)
35-
self.data = data
36-
return data
37-
38-
def extract_info(self):
39-
if self.data:
40-
results_data = self.data["tuna"]["resultsData"]
41-
if results_data is None:
42-
rprint(
43-
f"\n[cayn]Oops, it looks like thesaurus.com doesn't recognize '{self.word}' as a valid word."
44-
)
45-
return []
46-
47-
definition_data = results_data["definitionData"]
48-
definitions = definition_data["definitions"]
49-
return self.format_definitions(definitions)
50-
return []
51-
52-
def format_definitions(self, definitions):
53-
colors = {
54-
"100": "[rgb(252,232,197)]",
55-
"50": "[rgb(220,221,187)]",
56-
"10": "[rgb(191,182,155)]",
57-
"-100": "[grey74]",
58-
"-50": "[grey58]",
59-
"-10": "[grey42]",
60-
}
61-
62-
definitions_synonyms_antonyms = []
63-
for definition in definitions:
64-
def_text = definition.get("definition", "N/A")
65-
pos = definition.get("pos", "N/A")
66-
synonyms = [
67-
colors[syn["similarity"]] + syn["term"] + "[/]"
68-
for syn in definition.get("synonyms", [])
69-
]
70-
antonyms = [
71-
colors[ant["similarity"]] + ant["term"] + "[/]"
72-
for ant in definition.get("antonyms", [])
73-
]
74-
definitions_synonyms_antonyms.append(
75-
(f"{pos}. {def_text}", synonyms, antonyms)
76-
)
77-
return definitions_synonyms_antonyms
78-
79-
def rich(self):
80-
response_text = self.fetch_data()
81-
if response_text:
82-
self.parse_data(response_text)
83-
definitions_synonyms_antonyms = self.extract_info()
84-
self.display_definitions(definitions_synonyms_antonyms)
85-
86-
def display_definitions(self, definitions_synonyms_antonyms):
87-
console = Console()
88-
for definition, synonyms, antonyms in definitions_synonyms_antonyms:
89-
def_split = definition.split(".")
90-
part_of_speech = def_split[0]
91-
def_text = def_split[1].strip()
92-
table = Table(box=box.SQUARE, leading=1)
93-
table.add_column(
94-
"[blue]❯ " + def_text + " [gray50](" + part_of_speech + ")"
95-
)
96-
table.add_row(
97-
"🔵[cyan3]synonyms:[/cyan3] " + "[grey50],[/grey50] ".join(synonyms)
98-
)
99-
if antonyms:
100-
table.add_row(
101-
"🟤[grey74]antonyms:[/grey74] "
102-
+ "[grey74],[/grey74] ".join(antonyms)
103-
)
104-
console.print(table)
105-
console.print(
106-
f"[grey42][link=https://www.thesaurus.com/browse/{self.word}]thesaurus.com↗[/link]",
107-
justify="right",
108-
)
1+
from urllib.parse import quote
2+
import requests
3+
from bs4 import BeautifulSoup
4+
import json
5+
from rich.table import Table
6+
from rich.console import Console
7+
from rich import box
8+
from rich import print as rprint
9+
10+
11+
class Synonym:
12+
def __init__(self, word):
13+
self.word = quote(word)
14+
self.data = None
15+
16+
def fetch_data(self):
17+
try:
18+
url = f"https://www.thesaurus.com/browse/{self.word}"
19+
response = requests.get(url)
20+
response.raise_for_status()
21+
except requests.exceptions.RequestException as e:
22+
print(f"Error during requests to {url} : {str(e)}")
23+
return None
24+
return response.text
25+
26+
def parse_data(self, response_text):
27+
soup = BeautifulSoup(response_text, "html.parser")
28+
definitions = []
29+
30+
# Find all definition blocks
31+
def_blocks = soup.find_all("div", {"class": "definition-block"})
32+
33+
for block in def_blocks:
34+
# Extract POS and Definition
35+
header = block.find("div", {"class": "definition-header"})
36+
pos_tag = header.find("div", {"class": "part-of-speech-label"})
37+
pos = pos_tag.get_text(strip=True) if pos_tag else "N/A"
38+
39+
def_tag = header.find("div", {"class": "definition"})
40+
definition = def_tag.get_text(strip=True) if def_tag else "N/A"
41+
42+
synonyms = []
43+
antonyms = []
44+
45+
panels = block.find_all("section", {"class": "synonym-antonym-panel"})
46+
for panel in panels:
47+
label_div = panel.find("div", {"class": "synonym-antonym-panel-label"})
48+
if not label_div:
49+
continue
50+
51+
label = label_div.get_text(strip=True)
52+
is_synonym = "Synonyms" in label
53+
is_antonym = "Antonyms" in label
54+
55+
if not (is_synonym or is_antonym):
56+
continue
57+
58+
# Find all word chips
59+
word_chips = panel.find_all("a", {"class": "word-chip"})
60+
for chip in word_chips:
61+
term = chip.get_text(strip=True)
62+
63+
# Extract similarity from class
64+
classes = chip.get("class", [])
65+
similarity = "0"
66+
for cls in classes:
67+
if cls.startswith("similarity-"):
68+
similarity = cls.replace("similarity-", "")
69+
break
70+
71+
item = {"term": term, "similarity": similarity}
72+
73+
if is_synonym:
74+
synonyms.append(item)
75+
elif is_antonym:
76+
antonyms.append(item)
77+
78+
definitions.append(
79+
{
80+
"definition": definition,
81+
"pos": pos,
82+
"synonyms": synonyms,
83+
"antonyms": antonyms,
84+
}
85+
)
86+
87+
self.data = definitions
88+
return definitions
89+
90+
def extract_info(self):
91+
if self.data:
92+
return self.format_definitions(self.data)
93+
94+
rprint(
95+
f"\n[cayn]Oops, it looks like thesaurus.com doesn't recognize '{self.word}' as a valid word."
96+
)
97+
return []
98+
99+
def format_definitions(self, definitions):
100+
colors = {
101+
"100": "[rgb(252,232,197)]",
102+
"50": "[rgb(220,221,187)]",
103+
"10": "[rgb(191,182,155)]",
104+
"-100": "[grey74]",
105+
"-50": "[grey58]",
106+
"-10": "[grey42]",
107+
"0": "[grey42]", # Default fallback
108+
}
109+
110+
definitions_synonyms_antonyms = []
111+
for definition in definitions:
112+
def_text = definition.get("definition", "N/A")
113+
pos = definition.get("pos", "N/A")
114+
synonyms = [
115+
colors.get(syn["similarity"], "[grey42]") + syn["term"] + "[/]"
116+
for syn in definition.get("synonyms", [])
117+
]
118+
antonyms = [
119+
colors.get(ant["similarity"], "[grey74]") + ant["term"] + "[/]"
120+
for ant in definition.get("antonyms", [])
121+
]
122+
definitions_synonyms_antonyms.append(
123+
(f"{pos}. {def_text}", synonyms, antonyms)
124+
)
125+
return definitions_synonyms_antonyms
126+
127+
def plain(self):
128+
response_text = self.fetch_data()
129+
if response_text:
130+
self.parse_data(response_text)
131+
if not self.data:
132+
print(
133+
f"Oops, it looks like thesaurus.com doesn't recognize '{self.word}' as a valid word."
134+
)
135+
return
136+
137+
for definition_data in self.data:
138+
def_text = definition_data.get("definition", "N/A")
139+
pos = definition_data.get("pos", "N/A")
140+
print(f"{pos}. {def_text}")
141+
142+
synonyms = [s["term"] for s in definition_data.get("synonyms", [])]
143+
if synonyms:
144+
print(f"synonyms: {', '.join(synonyms)}")
145+
146+
antonyms = [a["term"] for a in definition_data.get("antonyms", [])]
147+
if antonyms:
148+
print(f"antonyms: {', '.join(antonyms)}")
149+
print()
150+
151+
def rich(self):
152+
response_text = self.fetch_data()
153+
if response_text:
154+
self.parse_data(response_text)
155+
definitions_synonyms_antonyms = self.extract_info()
156+
if definitions_synonyms_antonyms:
157+
self.display_definitions(definitions_synonyms_antonyms)
158+
159+
def display_definitions(self, definitions_synonyms_antonyms):
160+
console = Console()
161+
for definition, synonyms, antonyms in definitions_synonyms_antonyms:
162+
def_split = definition.split(".")
163+
part_of_speech = def_split[0]
164+
def_text = def_split[1].strip()
165+
table = Table(box=box.SQUARE, leading=1)
166+
table.add_column(
167+
"[blue]❯ " + def_text + " [gray50](" + part_of_speech + ")"
168+
)
169+
table.add_row(
170+
"🔵[cyan3]synonyms:[/cyan3] " + "[grey50],[/grey50] ".join(synonyms)
171+
)
172+
if antonyms:
173+
table.add_row(
174+
"🟤[grey74]antonyms:[/grey74] "
175+
+ "[grey74],[/grey74] ".join(antonyms)
176+
)
177+
console.print(table)
178+
console.print(
179+
f"[grey42][link=https://www.thesaurus.com/browse/{self.word}]thesaurus.com↗[/link]",
180+
justify="right",
181+
)

test/test_main.py

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
1-
from synonym_cli.cli import Synonym
2-
3-
4-
def test_req(capsys):
5-
with capsys.disabled():
6-
word = Synonym("lighter")
7-
fetch = word.fetch_data()
8-
data = word.parse_data(fetch)
9-
results_data = data["tuna"]["resultsData"]
10-
definition_data = results_data["definitionData"]
11-
definitions = definition_data["definitions"]
12-
13-
assert type(definitions) == list
14-
assert definitions[0]["synonyms"][0]["targetSlug"] == "raft"
15-
assert definitions[0]["synonyms"][0]["similarity"] == "100"
1+
from synonym_cli.cli import Synonym
2+
3+
4+
def test_req(capsys):
5+
with capsys.disabled():
6+
word = Synonym("lighter")
7+
fetch = word.fetch_data()
8+
definitions = word.parse_data(fetch)
9+
10+
# New structure: definitions is a list of dicts
11+
assert type(definitions) == list
12+
assert len(definitions) > 0
13+
14+
# Check first definition structure
15+
first_def = definitions[0]
16+
assert "synonyms" in first_def
17+
assert type(first_def["synonyms"]) == list
18+
assert len(first_def["synonyms"]) > 0
19+
20+
# Check first synonym
21+
first_syn = first_def["synonyms"][0]
22+
assert first_syn["term"] == "raft"
23+
assert first_syn["similarity"] == "100"

0 commit comments

Comments
 (0)