Python Web-Scraping with BeautifulSoup
if word in matching_words:
    matching_words[word] += 1
else:
    matching_words[word] = 1

If you're checking if a dictionary has a key before adding to it, a defaultdict may be a better option:
from collections import defaultdict

matching_words = defaultdict(int)
matching_words[word] += 1

int returns a 0 when called without arguments, and that 0 is used as a default value for the dictionary when the key doesn't exist.

fulltext = ''
for p in ps:
    fulltext += p.get_text()

This isn't very efficient. Performance of += on strings has gotten better in later versions of Python, but it's still generally slower. The typical alternative is using join:
pieces = [p.get_text() for p in ps]
fulltext = &quot;&quot;.join(pieces)

# Or just

fulltext = &quot;&quot;.join([p.get_text() for p in ps])

Then similarly in sort_and_format:
output_string = &quot;&quot;.join([f&quot;{r}\t{dict[r]}\n&quot;] for r in ordered_keys])


In sort_and_format, you've named the parameter dict. This is suboptimal for a couple reasons:

dict is a generic name that doesn't properly describe the data.
dict is the name of a built-in class, and shadowing it makes your code more confusing, and prevents you from using the built-in.

Indicating the type can be helpful though, so I might introduce type hints here
from typing import Dict

def sort_and_format(words: Dict[str, int]) -&gt; str:
    . . .

This says that the functions accepts a Dictionary mapping strings to ints, and returns a string

Also for sort_and_format, I've found that when you start sticking and into names, that can suggest that the function is doing too much. You may find that the code will make more sense if the sorting and formatting happen separately. That functions can handle purely formatting, and can be handed a sequence to work with instead. If that sequence is sorted, great, if not, also great. It doesn't matter for the purposes of formatting what the sort order is.