diff --git a/.gitattributes b/.gitattributes index 4cb342ff..f8c996b6 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,2 @@ docs/* linguist-documentation -tests/* linguist-vendored +tests/** linguist-vendored diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..fb179079 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,9 @@ +version: 2 + +build: + os: ubuntu-24.04 + tools: + python: "3.12" + +sphinx: + configuration: docs/conf.py diff --git a/.travis.yml b/.travis.yml index f5cd2e33..8af415d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: python python: - - "3.4" - "3.5" - "3.6" + - "3.7" install: - pip install -r requirements.txt coverage coveralls - python download_corpora.py diff --git a/README.rst b/README.rst index 576b359e..dcbee056 100644 --- a/README.rst +++ b/README.rst @@ -13,7 +13,6 @@ Newspaper3k: Article scraping & curation :target: https://coveralls.io/github/codelucas/newspaper :alt: Coverage status - Inspired by `requests`_ for its simplicity and powered by `lxml`_ for its speed: "Newspaper is an amazing python library for extracting & curating articles." @@ -132,6 +131,8 @@ If no language is specified, Newspaper will attempt to auto detect a language. >>> print(a.title) 港特首梁振英就住宅违建事件道歉 +Multi-lingual +============= If you are certain that an *entire* news source is in one language, **go ahead and use the same api :)** @@ -163,10 +164,87 @@ If you are certain that an *entire* news source is in one language, **go ahead a 两年双免0手续0利率 科鲁兹掀背金融轻松购_武汉车市_武汉汽 车网_新浪汽车_新浪网 -Documentation -------------- -Check out `The Documentation`_ for full and detailed guides using newspaper. +Scraping by topic: where do the URLs come from? +=============================================== + +``newspaper.build()`` is perfect when you know *which sites* to crawl. But the other question I get constantly is: "I want every article about **X**, across all publications — where do I get the URLs?" The answer is to search Google News for your keyword first, then feed the result links straight into newspaper for extraction. + +The easiest way to query Google News programmatically is the `Google News API`_ from `SerpApi - Search API`_ (they also cover Google Search, Google Maps, and more). The two libraries snap together in a few lines: + +.. code-block:: python + + # pip3 install google-search-results + from serpapi import GoogleSearch + from newspaper import Article + + search = GoogleSearch({ + "engine": "google_news", + "q": "electric vehicles", + "api_key": "YOUR_SERPAPI_KEY", # free plan at serpapi.com + }) + + for result in search.get_dict()["news_results"]: + article = Article(result["link"]) + article.download() + article.parse() + article.nlp() + print(article.title, "--", article.summary[:120]) + +This pattern of SerpApi for *discovery*, newspaper3k for *extraction*, is how most production news-monitoring pipelines are built, and it sidesteps writing a crawler for every source you care about. + +.. _`SerpApi - Search API`: https://serpapi.com?utm_source=newspaper3k_github +.. _`Google News API`: https://serpapi.com/google-news-api?utm_source=newspaper3k_github + + +Scraping at scale: avoiding IP blocks +===================================== + +Once you move past scraping a handful of articles, you'll hit the same wall every news scraper hits: 403s, captchas, rate limits, and silent shadow bans. Your code is fine — your IP is the problem. The fix is rotating residential proxies. + +I personally route my own newspaper3k pipelines through `Swiftproxy`_ — 80M+ residential IPs across 195+ countries, a 99.89% success rate, non-expiring traffic, and a free trial so you can pressure-test it before paying. Plugging it into newspaper3k takes about four lines: + +.. code-block:: python + + from newspaper import Article, Config + + config = Config() + config.proxies = { + 'http': 'http://USERNAME:PASSWORD@gate.swiftproxy.net:7777', + 'https': 'http://USERNAME:PASSWORD@gate.swiftproxy.net:7777', + } + # a real browser UA helps too + config.browser_user_agent = ( + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' + 'AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/124.0.0.0 Safari/537.36' + ) + config.request_timeout = 20 + + article = Article('https://example.com/some-news-story', config=config) + article.download() + article.parse() + print(article.title) + +The same ``config`` object works with ``newspaper.build()`` — every article fetched by the source will rotate through residential IPs automatically: + +.. code-block:: python + + import newspaper + paper = newspaper.build('http://cnn.com', config=config, memoize_articles=False) + for article in paper.articles: + article.download() + article.parse() + +Grab credentials and a free trial at `swiftproxy.net `_. Use code ``PROXY90`` for 10% off your first plan. + +.. _`Swiftproxy`: https://www.swiftproxy.net/?ref=codelucas + + +Docs +---- + +Check out `The Docs`_ for full and detailed guides using newspaper. Interested in adding a new language for us? Refer to: `Docs - Adding new languages `_ @@ -193,39 +271,43 @@ Features input code full name ar Arabic - ru Russian - nl Dutch + be Belarusian + bg Bulgarian + da Danish de German + el Greek en English es Spanish + et Estonian + fa Persian + fi Finnish fr French he Hebrew + hi Hindi + hr Croatian + hu Hungarian + id Indonesian it Italian + ja Japanese ko Korean + lt Lithuanian + mk Macedonian + nb Norwegian (Bokmål) + nl Dutch no Norwegian - fa Persian pl Polish pt Portuguese + ro Romanian + ru Russian + sl Slovenian + sr Serbian sv Swedish - hu Hungarian - fi Finnish - da Danish - zh Chinese - id Indonesian - vi Vietnamese sw Swahili + th Thai tr Turkish - el Greek uk Ukrainian - bg Bulgarian - hr Croatian - ro Romanian - sl Slovenian - sr Serbian - et Estonian - ja Japanese - be Belarusian - + vi Vietnamese + zh Chinese Get it now ---------- @@ -315,13 +397,51 @@ Planning on tweaking our full-text algorithm? Add the ``fulltext`` parameter:: $ python3 tests/unit_tests.py fulltext - Demo ---- View a working online demo here: http://newspaper-demo.herokuapp.com + This is another working online demo: http://newspaper.chinazt.cc/ + +Interested in scraping APIs & proxies? +====================================== + +Unlock the Web — the Smart Way +------------------------------ +`Click here to see SerpApi, scrape search engines easily with SerpApi - Search API`_. +Scrape Google Search, Google News, Google Maps, and more! + +.. image:: https://github.com/user-attachments/assets/9a80eeb4-72a8-43f1-9413-93c7a47b2bf6 + :target: https://serpapi.com/google-news-api?utm_source=newspaper3k_github + :alt: Scrape search engines easily with SerpApi - Search API. + +.. _`Click here to see SerpApi, scrape search engines easily with SerpApi - Search API`: https://serpapi.com?utm_source=newspaper3k_github + + +Power your scraping and automation at real-world scale +------------------------------------------------------ +`Click here to try Swiftproxy`_ — built for developers running scraping, automation, and data collection workflows at scale. Access 80M+ residential IPs from $0.7/GB, fast ISP proxies from $6/IP, global coverage across 195+ countries, non-expiring traffic, and a 99.89% success rate. Free trial available — use code ``PROXY90`` for 10% off. + +.. image:: https://github.com/user-attachments/assets/913f1fd6-20e9-4f37-89b7-ba6b0bd0724a + :target: https://www.swiftproxy.net/?ref=codelucas + :alt: Swiftproxy — residential and ISP proxies built for scrapers and developers. + +.. _`Click here to try Swiftproxy`: https://www.swiftproxy.net/?ref=codelucas + + +Stay private, fast, and fully in control +---------------------------------------- +`Click here to explore BestProxy`_, your go-to solution for premium residential proxies. BestProxy's proxies ensure smooth browsing, fast speeds, and total anonymity. `Get Started`_ today and experience the difference! + +.. image:: https://github.com/user-attachments/assets/1c6ef38c-f0c0-4db0-aad2-3ed9d6adf0b5 + :target: https://bestproxy.com/?keyword=b2vgzl0r + :alt: Experience BestProxy, smooth browsing, fast speeds, and total anonymity. + +.. _`Click here to explore BestProxy`: https://bestproxy.com/?keyword=b2vgzl0r +.. _`Get Started`: https://bestproxy.com/?keyword=b2vgzl0r + LICENSE ------- @@ -340,8 +460,12 @@ to talk about the future of this library and news extraction in general! .. _`python-goose's`: https://github.com/grangier/python-goose .. _`here`: https://github.com/codelucas/newspaper/blob/master/GOOSE-LICENSE.txt +.. _`https://www.paypal.me/codelucas`: https://www.paypal.me/codelucas +.. _`Venmo`: https://www.venmo.com/Lucas-Ou-Yang + .. _`Quickstart guide`: https://newspaper.readthedocs.io/en/latest/ -.. _`The Documentation`: https://newspaper.readthedocs.io +.. _`The Docs`: https://newspaper.readthedocs.io .. _`lxml`: http://lxml.de/ .. _`requests`: https://github.com/kennethreitz/requests .. _`Parse.ly`: http://parse.ly +.. _`It takes only one click`: https://tracking.gitads.io/?campaign=gitads&repo=newspaper&redirect=gitads.io diff --git a/docs/index.rst b/docs/index.rst index f969e0b4..b4e0bae7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -194,38 +194,43 @@ Features input code full name ar Arabic - ru Russian - nl Dutch + be Belarusian + bg Bulgarian + da Danish de German + el Greek en English es Spanish + et Estonian + fa Persian + fi Finnish fr French he Hebrew + hi Hindi + hr Croatian + hu Hungarian + id Indonesian it Italian + ja Japanese ko Korean + lt Lithuanian + mk Macedonian + nb Norwegian (Bokmål) + nl Dutch no Norwegian - fa Persian pl Polish pt Portuguese + ro Romanian + ru Russian + sl Slovenian + sr Serbian sv Swedish - hu Hungarian - fi Finnish - da Danish - zh Chinese - id Indonesian - vi Vietnamese sw Swahili + th Thai tr Turkish - el Greek uk Ukrainian - bg Bulgarian - hr Croatian - ro Romanian - sl Slovenian - sr Serbian - et Estonian - ja Japanese - be Belarusian + vi Vietnamese + zh Chinese Get it now @@ -347,6 +352,21 @@ Newspaper uses a lot of `python-goose's`_ parsing code. View their license `here Please feel free to `email & contact me`_ if you run into issues or just would like to talk about the future of this library and news extraction in general! +Sponsored by SerpApi +-------------------- + +`Scrape search engines easily with SerpApi - Search API`_. +Scrape Google Search, Google News, Google Maps, and more! Their `Google News API`_ +pairs perfectly with newspaper: use it to discover article URLs by keyword, then +extract them with ``Article``. + +.. image:: https://github.com/user-attachments/assets/9a80eeb4-72a8-43f1-9413-93c7a47b2bf6 + :target: https://serpapi.com/google-news-api?utm_source=newspaper3k_docs + :alt: Scrape search engines easily with SerpApi - Search API. + +.. _`Scrape search engines easily with SerpApi - Search API`: https://serpapi.com?utm_source=newspaper3k_docs +.. _`Google News API`: https://serpapi.com/google-news-api?utm_source=newspaper3k_docs + .. _`Lucas Ou-Yang`: http://codelucas.com .. _`email & contact me`: mailto:lucasyangpersonal@gmail.com .. _`python-goose's`: https://github.com/grangier/python-goose diff --git a/docs/user_guide/quickstart.rst b/docs/user_guide/quickstart.rst index b9f9ee95..24fe07b6 100644 --- a/docs/user_guide/quickstart.rst +++ b/docs/user_guide/quickstart.rst @@ -148,6 +148,25 @@ Initializing an ``Article`` by itself. Note the similar ``language=`` named paramater above. All the config parameters as described for ``Source`` objects also apply for ``Article`` objects! **Source and Article objects have a very similar api**. +Initializing an ``Article`` with the particular content-type ignoring. + +There is option to skip loading of articles with particular content-type, +that can be useful if it is not desired to have delays because of long PDF resources. +The default html value for the particular content type can be provided and then used in order to define the actual content-type of the article + +.. code-block:: pycon + + >>> from newspaper import Article + >>> pdf_defaults = {"application/pdf": "%PDF-", + "application/x-pdf": "%PDF-", + "application/x-bzpdf": "%PDF-", + "application/x-gzpdf": "%PDF-"} + >>> pdf_article = Article(url='https://www.adobe.com/pdf/pdfs/ISO32000-1PublicPatentLicense.pdf', + ignored_content_types_defaults=pdf_defaults) + >>> pdf_article.download() + >>> print(pdf_article.html) + %PDF- + There are endless possibilities on how we can manipulate and build articles. Downloading an Article @@ -271,3 +290,4 @@ of popular news source urls.. In case you need help choosing a news source! et Estonian ja Japanese be Belarusian + lt Lithuanian diff --git a/newspaper/article.py b/newspaper/article.py index e485a1dd..df0d9c43 100644 --- a/newspaper/article.py +++ b/newspaper/article.py @@ -8,6 +8,7 @@ import copy import os import glob +from urllib.parse import urlparse import requests @@ -124,6 +125,9 @@ def __init__(self, url, title='', source_url='', config=None, **kwargs): # Meta favicon field in HTML source self.meta_favicon = "" + # Meta site_name field in HTML source + self.meta_site_name = "" + # Meta tags contain a lot of structured data, e.g. OpenGraph self.meta_data = {} @@ -158,6 +162,23 @@ def build(self): self.parse() self.nlp() + def _parse_scheme_file(self, path): + try: + with open(path, "r") as fin: + return fin.read() + except OSError as e: + self.download_state = ArticleDownloadState.FAILED_RESPONSE + self.download_exception_msg = e.strerror + return None + + def _parse_scheme_http(self): + try: + return network.get_html_2XX_only(self.url, self.config) + except requests.exceptions.RequestException as e: + self.download_state = ArticleDownloadState.FAILED_RESPONSE + self.download_exception_msg = str(e) + return None + def download(self, input_html=None, title=None, recursion_counter=0): """Downloads the link's HTML content, don't use if you are batch async downloading articles @@ -166,11 +187,12 @@ def download(self, input_html=None, title=None, recursion_counter=0): infinite """ if input_html is None: - try: - html = network.get_html_2XX_only(self.url, self.config) - except requests.exceptions.RequestException as e: - self.download_state = ArticleDownloadState.FAILED_RESPONSE - self.download_exception_msg = str(e) + parsed_url = urlparse(self.url) + if parsed_url.scheme == "file": + html = self._parse_scheme_file(parsed_url.path) + else: + html = self._parse_scheme_http() + if html is None: log.debug('Download failed on URL %s because of %s' % (self.url, self.download_exception_msg)) return @@ -220,6 +242,9 @@ def parse(self): meta_favicon = self.extractor.get_favicon(self.clean_doc) self.set_meta_favicon(meta_favicon) + meta_site_name = self.extractor.get_meta_site_name(self.clean_doc) + self.set_meta_site_name(meta_site_name) + meta_description = \ self.extractor.get_meta_description(self.clean_doc) self.set_meta_description(meta_description) @@ -503,6 +528,9 @@ def set_meta_keywords(self, meta_keywords): def set_meta_favicon(self, meta_favicon): self.meta_favicon = meta_favicon + def set_meta_site_name(self, meta_site_name): + self.meta_site_name = meta_site_name + def set_meta_description(self, meta_description): self.meta_description = meta_description diff --git a/newspaper/cleaners.py b/newspaper/cleaners.py index 00fec2f1..47b6f1a8 100644 --- a/newspaper/cleaners.py +++ b/newspaper/cleaners.py @@ -41,7 +41,7 @@ def __init__(self, config): self.google_re = " google " self.entries_re = "^[^entry-]more.*$" self.facebook_re = "[^-]facebook" - self.facebook_braodcasting_re = "facebook-broadcasting" + self.facebook_broadcasting_re = "facebook-broadcasting" self.twitter_re = "[^-]twitter" self.tablines_replacements = ReplaceSequence()\ .create("\n", "\n\n")\ @@ -63,11 +63,12 @@ def clean(self, doc_to_clean): doc_to_clean = self.remove_nodes_regex(doc_to_clean, self.entries_re) doc_to_clean = self.remove_nodes_regex(doc_to_clean, self.facebook_re) doc_to_clean = self.remove_nodes_regex(doc_to_clean, - self.facebook_braodcasting_re) + self.facebook_broadcasting_re) doc_to_clean = self.remove_nodes_regex(doc_to_clean, self.twitter_re) doc_to_clean = self.clean_para_spans(doc_to_clean) doc_to_clean = self.div_to_para(doc_to_clean, 'div') doc_to_clean = self.div_to_para(doc_to_clean, 'span') + doc_to_clean = self.div_to_para(doc_to_clean, 'section') return doc_to_clean def clean_body_classes(self, doc): diff --git a/newspaper/configuration.py b/newspaper/configuration.py index d5cf7ae2..94688e70 100644 --- a/newspaper/configuration.py +++ b/newspaper/configuration.py @@ -14,7 +14,7 @@ from .parsers import Parser from .text import (StopWords, StopWordsArabic, StopWordsChinese, - StopWordsKorean, StopWordsHindi, StopWordsJapanese) + StopWordsKorean, StopWordsHindi, StopWordsJapanese, StopWordsThai) from .version import __version__ log = logging.getLogger(__name__) @@ -72,7 +72,7 @@ def __init__(self): self.verbose = False # for debugging self.thread_timeout_seconds = 1 - + self.ignored_content_types_defaults = {} # Set this to False if you want to recompute the categories # *every* time you build a `Source` object # TODO: Actually make this work @@ -116,6 +116,8 @@ def get_stopwords_class(language): return StopWordsArabic elif language == 'ja': return StopWordsJapanese + elif language == 'th': + return StopWordsThai return StopWords @staticmethod diff --git a/newspaper/extractors.py b/newspaper/extractors.py index 664ec2f5..96255401 100644 --- a/newspaper/extractors.py +++ b/newspaper/extractors.py @@ -153,7 +153,7 @@ def parse_byline(search_str): if len(mm) > 0: content = mm[0] else: - content = match.text or '' + content = match.text_content() or '' if len(content) > 0: authors.extend(parse_byline(content)) @@ -216,6 +216,8 @@ def parse_date_str(date_str): 'content': 'content'}, {'attribute': 'pubdate', 'value': 'pubdate', 'content': 'datetime'}, + {'attribute': 'name', 'value': 'publish_date', + 'content': 'content'}, ] for known_meta_tag in PUBLISH_DATE_TAGS: meta_tags = self.parser.getElementsByTag( @@ -472,6 +474,11 @@ def get_meta_type(self, doc): """ return self.get_meta_content(doc, 'meta[property="og:type"]') + def get_meta_site_name(self, doc): + """Returns site name of article, open graph protocol + """ + return self.get_meta_content(doc, 'meta[property="og:site_name"]') + def get_meta_description(self, doc): """If the article has meta description set in the source, use that """ diff --git a/newspaper/images.py b/newspaper/images.py index 3a9f56df..77476625 100644 --- a/newspaper/images.py +++ b/newspaper/images.py @@ -56,8 +56,8 @@ def image_entropy(img): def square_image(img): - """If the image is taller than it is wide, square it off. determine - which pieces to cut off based on the entropy pieces + """If the height of the image is greater than its width, then a square image is returned. + Pieces to cut is based on the entropy pieces. """ x, y = img.size while y > x: diff --git a/newspaper/network.py b/newspaper/network.py index 87f2549d..29f0e699 100644 --- a/newspaper/network.py +++ b/newspaper/network.py @@ -57,12 +57,12 @@ def get_html_2XX_only(url, config=None, response=None): headers = config.headers if response is not None: - return _get_html_from_response(response) + return _get_html_from_response(response, config) response = requests.get( url=url, **get_request_kwargs(timeout, useragent, proxies, headers)) - html = _get_html_from_response(response) + html = _get_html_from_response(response, config) if config.http_success_only: # fail if HTTP sends a non 2XX response @@ -71,7 +71,9 @@ def get_html_2XX_only(url, config=None, response=None): return html -def _get_html_from_response(response): +def _get_html_from_response(response, config): + if response.headers.get('content-type') in config.ignored_content_types_defaults: + return config.ignored_content_types_defaults[response.headers.get('content-type')] if response.encoding != FAIL_ENCODING: # return response as a unicode string html = response.text diff --git a/newspaper/outputformatters.py b/newspaper/outputformatters.py index db0b5a29..47a76467 100644 --- a/newspaper/outputformatters.py +++ b/newspaper/outputformatters.py @@ -141,9 +141,12 @@ def remove_empty_tags(self): def remove_trailing_media_div(self): """Punish the *last top level* node in the top_node if it's DOM depth is too deep. Many media non-content links are - eliminated: "related", "loading gallery", etc + eliminated: "related", "loading gallery", etc. It skips removal if + last top level node's class is one of NON_MEDIA_CLASSES. """ + NON_MEDIA_CLASSES = ('zn-body__read-all', ) + def get_depth(node, depth=1): """Computes depth of an lxml element via BFS, this would be in parser if it were used anywhere else besides this method @@ -163,5 +166,10 @@ def get_depth(node, depth=1): return last_node = top_level_nodes[-1] + + last_node_class = self.parser.getAttribute(last_node, 'class') + if last_node_class in NON_MEDIA_CLASSES: + return + if get_depth(last_node) >= 2: self.parser.remove(last_node) diff --git a/newspaper/resources/text/stopwords-hi.txt b/newspaper/resources/text/stopwords-hi.txt index d02051a8..2cf52d70 100644 --- a/newspaper/resources/text/stopwords-hi.txt +++ b/newspaper/resources/text/stopwords-hi.txt @@ -1,3 +1,209 @@ +अंदर +अत +अदि +अप +अपना +अपनि +अपनी +अपने +अभि +अभी +आदि +इंहिं +इंहें +इंहों +इतयादि +इत्यादि +इन +इनका +इन्हीं +इन्हें +इन्हों +इस +इसका +इसकि +इसकी +इसके +इसमें +इसि +इसी +इसे +उंहिं +उंहें +उंहों +उन +उनका +उनकि +उनकी +उनको +उन्हीं +उन्हें +उन्हों +उस +उसके +उसि +उसी +एक +एवं +एस +एसे +ऐसे +ओर +कइ +कई +करता +करते +करना +करने +करें +कहते +कहा +का +काफि +काफ़ी +कि +किंहें +किंहों +कितना +किन्हें +किन्हों +किया +किर +किस +किसि +किसी +किसे +की +कुछ +कुल +के +कोइ +कोई +कोन +कोनसा +कौनसा +गया +घर +जब +जहाँ +जहां +जा +जिंहें +जिंहों +जितना +जिधर +जिन +जिन्हें +जिन्हों +जिस +जिसे +जीधर +जेसा +जेसे +जैसा +जो +तक +तब +तरह +तिंहें +तिंहों +तिन +तिन्हें +तिन्हों +तिस +तिसे +थि +थी +थे +दबारा +दवारा +दिया +दुसरा +दुसरे +दूसरे +दो +द्वारा +न +नहिं +नहीं +ना +निचे +निहायत +ने +पहले +पुरा +पूरा +पे +फिर +बनि +बनी +बहि +बही +बहुत +बाद +बाला +बिलकुल +भि +भितर +भी +भीतर +मगर +मानो +मे +यदि +यहाँ +यहां +यहि +यही +या +यिह +ये +रखें +रवासा +रहा +रहे +ऱ्वासा +लिए +लिये +लेकिन +व +वगेरह +वरग +वर्ग +वहां +वहिं +वहीं +वाले +वुह +वे +वग़ैरह +संग +सकता +सबसे +सभि +सभी +साथ +साबुत +साभ +सारा +से +सो +हि +ही +हुअ +हुआ +हुइ +हुई +हुए +हे +हें +हो +होता +होति +होती +होते +होना +होने को नीचे सब @@ -6,7 +212,6 @@ तो कौन यह -एक और वहाँ था @@ -22,22 +227,11 @@ सकते हैं मुझे -एक -साथ -मुझे -अभी -तक अब -या हमें -भी -को -से -था क्या हम करेगा -वे -कहा इतना होगा + diff --git a/newspaper/resources/text/stopwords-it.txt b/newspaper/resources/text/stopwords-it.txt index 98ffee1d..73a4182f 100644 --- a/newspaper/resources/text/stopwords-it.txt +++ b/newspaper/resources/text/stopwords-it.txt @@ -1,133 +1,133 @@ -ad -al -allo -ai -agli -all -agl -alla -alle -con -col -coi -da -dal -dallo -dai -dagli -dall -dagl -dalla -dalle -di -del -dello -dei -degli -dell -degl -della -delle -in -nel -nello -nei -negli -nell -negl -nella -nelle -su -sul -sullo -sui -sugli -sull -sugl -sulla -sulle -per -tra -contro -io -tu -lui -lei -noi -voi -loro -mio -mia -miei -mie -tuo -tua -tuoi -tue -suo -sua -suoi -sue -nostro -nostra -nostri -nostre -vostro -vostra -vostri -vostre -mi -ti -ci -vi -lo -la -li -le -gli -ne -il -un -uno -una -ma -ed -se -perchè +ad +al +allo +ai +agli +all +agl +alla +alle +con +col +coi +da +dal +dallo +dai +dagli +dall +dagl +dalla +dalle +di +del +dello +dei +degli +dell +degl +della +delle +in +nel +nello +nei +negli +nell +negl +nella +nelle +su +sul +sullo +sui +sugli +sull +sugl +sulla +sulle +per +tra +contro +io +tu +lui +lei +noi +voi +loro +mio +mia +miei +mie +tuo +tua +tuoi +tue +suo +sua +suoi +sue +nostro +nostra +nostri +nostre +vostro +vostra +vostri +vostre +mi +ti +ci +vi +lo +la +li +le +gli +ne +il +un +uno +una +ma +ed +se +perchè perché perche -anche -come -dov -dove -che -chi -cui -non -più +anche +come +dov +dove +che +chi +cui +non +più piu -quale -quanto -quanti -quanta -quante -quello -quelli -quella -quelle -questo -questi -questa -queste -si -tutto -tutti -a -c -e -i -l -o +quale +quanto +quanti +quanta +quante +quello +quelli +quella +quelle +questo +questi +questa +queste +si +tutto +tutti +a +c +e +i +l +o ho hai ha diff --git a/newspaper/resources/text/stopwords-lt.txt b/newspaper/resources/text/stopwords-lt.txt new file mode 100644 index 00000000..a9fa60bd --- /dev/null +++ b/newspaper/resources/text/stopwords-lt.txt @@ -0,0 +1,165 @@ +taip +jas +be +kito +taps +juos +dvi +būti +jo +kita +juo +kokį +gero +tie +mes +bei +savo +mūsų +bus +rodo +jame +kam +prie +kada +itin +kuo +tiek +toks +ir +dar +nei +patį +visa +mums +kad +ko +arba +visą +pats +jis +pat +net +kurį +nėra +nė +pusė +pati +metu +ne +jam +jais +kuri +į +mano +mus +irgi +ta +šiuo +jų +vėl +vis +jei +su +šia +kiti +sau +na +virš +keli +būtų +jai +teks +o +namų +šalį +yra +šiol +šią +apie +kiek +daug +kai +gal +tų +jog +jos +gana +man +dėl +tą +jums +šis +gera +bet +per +negu +tarp +tokį +toli +liko +kaip +teko +nuo +tapo +būna +vos +jūsų +po +turi +ką +link +čia +šie +tai +jie +tuo +štai +ar +nors +tos +jau +ant +bent +esu +kol +pusę +vien +beje +nes +mane +koks +ši +iš +kitų +aš +juk +tu +kur +nori +tik +tuos +jūs +tas +tam +tada +jį +dalį +abu +pas +ten +ypač +šios +šio +tad +šių +iki +deja +viso +to +visų +ji +kas +lyg +save +šiek \ No newline at end of file diff --git a/newspaper/resources/text/stopwords-th.txt b/newspaper/resources/text/stopwords-th.txt new file mode 100644 index 00000000..34cc248f --- /dev/null +++ b/newspaper/resources/text/stopwords-th.txt @@ -0,0 +1,115 @@ +กล่าว +กว่า +กัน +กับ +การ +ก็ +ก่อน +ขณะ +ขอ +ของ +ขึ้น +คง +ครั้ง +ความ +คือ +จะ +จัด +จาก +จึง +ช่วง +ซึ่ง +ดัง +ด้วย +ด้าน +ตั้ง +ตั้งแต่ +ตาม +ต่อ +ต่าง +ต่างๆ +ต้อง +ถึง +ถูก +ถ้า +ทั้ง +ทั้งนี้ +ทาง +ที่ +ที่สุด +ทุก +ทํา +ทําให้ +นอกจาก +นัก +นั้น +นี้ +น่า +นํา +บาง +ผล +ผ่าน +พบ +พร้อม +มา +มาก +มี +ยัง +รวม +ระหว่าง +รับ +ราย +ร่วม +ลง +วัน +ว่า +สุด +ส่ง +ส่วน +สําหรับ +หนึ่ง +หรือ +หลัง +หลังจาก +หลาย +หาก +อยาก +อยู่ +อย่าง +ออก +อะไร +อาจ +อีก +เขา +เข้า +เคย +เฉพาะ +เช่น +เดียว +เดียวกัน +เนื่องจาก +เปิด +เปิดเผย +เป็น +เป็นการ +เพราะ +เพื่อ +เมื่อ +เรา +เริ่ม +เลย +เห็น +เอง +แต่ +แบบ +แรก +และ +แล้ว +แห่ง +โดย +ใน +ให้ +ได้ +ไป +ไม่ +ไว้ diff --git a/newspaper/text.py b/newspaper/text.py index 5e7002a8..23b4c6b1 100644 --- a/newspaper/text.py +++ b/newspaper/text.py @@ -195,3 +195,15 @@ def candidate_words(self, stripped_input): segmenter = tinysegmenter.TinySegmenter() tokens = segmenter.tokenize(stripped_input) return tokens + + +class StopWordsThai(StopWords): + """Thai segmentation + """ + def __init__(self, language='th'): + super(StopWordsThai, self).__init__(language='th') + + def candidate_words(self, stripped_input): + import pythainlp + tokens = pythainlp.word_tokenize(stripped_input) + return tokens diff --git a/newspaper/utils.py b/newspaper/utils.py index 3a9364c9..bfa44148 100644 --- a/newspaper/utils.py +++ b/newspaper/utils.py @@ -347,6 +347,7 @@ def get_available_languages(): two_dig_codes = [f.split('-')[1].split('.')[0] for f in stopword_files] for d in two_dig_codes: assert len(d) == 2 + two_dig_codes.sort() return two_dig_codes @@ -355,41 +356,43 @@ def print_available_languages(): """ language_dict = { 'ar': 'Arabic', - 'ru': 'Russian', - 'nl': 'Dutch', + 'be': 'Belarusian', + 'bg': 'Bulgarian', + 'da': 'Danish', 'de': 'German', + 'el': 'Greek', 'en': 'English', 'es': 'Spanish', + 'et': 'Estonian', + 'fa': 'Persian', + 'fi': 'Finnish', 'fr': 'French', 'he': 'Hebrew', + 'hi': 'Hindi', + 'hr': 'Croatian', + 'hu': 'Hungarian', + 'id': 'Indonesian', 'it': 'Italian', + 'ja': 'Japanese', 'ko': 'Korean', - 'no': 'Norwegian', + 'lt': 'Lithuanian', + 'mk': 'Macedonian', 'nb': 'Norwegian (Bokmål)', - 'fa': 'Persian', + 'nl': 'Dutch', + 'no': 'Norwegian', 'pl': 'Polish', 'pt': 'Portuguese', - 'sv': 'Swedish', - 'hu': 'Hungarian', - 'fi': 'Finnish', - 'da': 'Danish', - 'zh': 'Chinese', - 'id': 'Indonesian', - 'vi': 'Vietnamese', - 'mk': 'Macedonian', - 'tr': 'Turkish', - 'el': 'Greek', - 'uk': 'Ukrainian', - 'hi': 'Hindi', - 'sw': 'Swahili', - 'bg': 'Bulgarian', - 'hr': 'Croatian', 'ro': 'Romanian', + 'ru': 'Russian', 'sl': 'Slovenian', 'sr': 'Serbian', - 'et': 'Estonian', - 'ja': 'Japanese', - 'be': 'Belarusian' + 'sv': 'Swedish', + 'sw': 'Swahili', + 'th': 'Thai', + 'tr': 'Turkish', + 'uk': 'Ukrainian', + 'vi': 'Vietnamese', + 'zh': 'Chinese', } codes = get_available_languages() diff --git a/newspaper/version.py b/newspaper/version.py index bfcd8ea3..e2eab31a 100644 --- a/newspaper/version.py +++ b/newspaper/version.py @@ -7,5 +7,5 @@ __license__ = 'MIT' __copyright__ = 'Copyright 2014, Lucas Ou-Yang' -version_info = (0, 2, 7) +version_info = (0, 3, 0) __version__ = ".".join(map(str, version_info)) diff --git a/requirements.txt b/requirements.txt index f970cc4a..61974601 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,14 @@ beautifulsoup4>=4.4.1 -Pillow>=3.3.0 -PyYAML>=3.11 cssselect>=0.9.2 -lxml>=3.6.0 -nltk>=3.2.1 -requests>=2.10.0 -feedparser>=5.2.1 -tldextract>=2.0.1 feedfinder2>=0.0.4 +feedparser>=5.2.1 jieba3k>=0.35.1 +lxml>=3.6.0 +nltk>=3.2.1 +Pillow>=3.3.0 +pythainlp>=1.7.2 python-dateutil>=2.5.3 +PyYAML>=3.11 +requests>=2.10.0 tinysegmenter==0.3 # TODO(codelucas): Investigate making this >=0.3 +tldextract>=2.0.1 \ No newline at end of file diff --git a/setup.py b/setup.py index e8cf2545..5569c7cb 100755 --- a/setup.py +++ b/setup.py @@ -21,7 +21,10 @@ if sys.argv[-1] == 'publish': - os.system('python3 setup.py sdist upload -r pypi') + # PYPI now uses twine for package management. + # For this to work you must first `$ pip3 install twine` + os.system('python3 setup.py sdist bdist_wheel') + os.system('twine upload dist/*') sys.exit() @@ -44,7 +47,7 @@ setup( name='newspaper3k', - version='0.2.7', + version='0.3.0', description='Simplified python article discovery & extraction.', long_description=readme, author='Lucas Ou-Yang', diff --git a/tests/data/html/thai_article.html b/tests/data/html/thai_article.html new file mode 100644 index 00000000..72afa4c7 --- /dev/null +++ b/tests/data/html/thai_article.html @@ -0,0 +1,629 @@ + + + + + + + + + + + + + + + ผล DNA ยืนยัน ศพลอยแม่น้ำโขงเป็นคนสนิท อ.สุรชัย | ประชาไท Prachatai.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+ +
+ + + +
+ +
+
+
+
+
+
+
+
+
+
+ +
+
+ + + +
+
+
+
+
+
+
+
+
+
+ +
+
+ + + + + + + + + + +
+
+
+
+
+
+
+
+ +
+
+ +

ผล DNA ยืนยัน ศพลอยแม่น้ำโขงเป็นคนสนิท อ.สุรชัย

+
+
+ +
+
+ + +
+
+

+

21 ม.ค.2561 เวลาประมาณ 12.00 น. ผู้สื่อข่าวได้รับแจ้งจากนาย ต.(ไม่ประสงค์เปิดเผยชื่อ) บุตรชายของ 'ภูชนะ' คนสนิทของนายสุรชัย แซ่ด่าน ที่หายไปว่า ผลการตรวจ DNA โดยใช้เนื้อเยื่อของศพที่ถูกสังหารด้วยการมัดแขน รัดคอ ทุบจนใบหน้าเละ และท้องถูกผ่ายัดเสาปูนที่ลอยมาติดที่ อ.ธาตุพนม จ.นครพนม นั้น เมื่อตรวจสอบแล้วมีความเกี่ยวพันทางสายเลือดกับนาย ต.จริง 

+

ภูชนะ (นามแฝง) เป็นคนใกล้ชิดของนายสุรชัย ด่านวัฒนานุสรณ์ หรือ สุรชัย แซ่ด่าน นักเคลื่อนไหวทางการเมืองที่ลี้ภัยออกจากประเทศไทยและได้หายตัวไปจากที่พักในประเทศเพื่อนบ้านในช่วงคืนวันที่ 12-13 ธ.ค.2561 พร้อมกันกับสุรชัยและคนสนิทอีกคนหนึ่ง

+

ปรานี ด่านวัฒนานุสรณ์ ภรรยาของสุรชัยกล่าวว่า ได้ทราบข่าวจากบุตรชายของภูชนะแล้ว แต่ยังไม่ขอพูดอะไร โดยเบื้องต้นได้ทำใจแต่แรกแล้วว่าเหตุการณ์ลักษณะนี้จะต้องเกิดขึ้นสักวันหนึ่ง

+

ขณะที่ นาย ว. พี่เขยของ 'กาสะลอง' อีกหนึ่งผู้ลี้ภัยที่ได้หายไปพร้อมกับนายสุรชัยกล่าวกับผู้สื่อข่าวว่า หลังจากได้ทราบผลการตรวจสอบ DNA จากลูกชายของภูชนะ ทางญาติของกาสะลองก็ได้ติดต่อไปที่พนักงานสอบสวนเจ้าของคดีและได้คำตอบว่า จะได้ทราบผลการตรวจสอบ DNA ภายในเวลา 2-3 วันนี้

+

สุรชัย (78 ปี) ภูชนะ (54 ปี) กาสะลอง (47 ปี) เป็นนักเคลื่อนไหวทางการเมือง และเป็นผู้ลี้ภัยจากเหตุการณ์รัฐประหาร 2557 ไปยังประเทศเพื่อนบ้านได้หายออกจากที่พักในประเทศเพื่อนบ้านโดยที่ไม่มีใครสามารถติดต่อได้จนปัจจุบันนับเป็นเวลานาน 1 เดือน กับอีก 10 วัน

+

เรื่องที่เกี่ยวข้อง
ลืออุ้ม อ.สุรชัย ผู้ลี้ภัยหายจากที่พัก 12 วันแล้ว
ตร.เร่งหาเบาะแสคดีฆ่ายัดเสาถ่วงน้ำโขง - เมียไม่เชื่อเป็น 'สุรชัย แซ่ด่าน'

+ +
+
+ + +
+ เท่าไรก็ได้ การสนับสนุนจากคุณ คือการร่วมสร้างและรักษาสื่อเสรี ‘ประชาไท’ ... ร่วมสนับสนุนเรา
โอนเงิน พร้อมเพย์ PromptPay "มูลนิธิสื่อเพื่อการศึกษาของชุมชน" 0993000060423
โอนเงิน PayPal คลิกที่นี่ https://paypal.me/prachatai (รายงานยอดบริจาคสนับสนุน) +
+
+ +
ติดตามประชาไทอัพเดท ได้ที่:
เฟซบุ๊ก https://fb.me/prachatai
ทวิตเตอร์ https://twitter.com/prachatai
LINE ไอดี = @prachatai
+ + +
+
+
+
+

แสดงความคิดเห็น

+ +
+ +
+
+
+
+
+
+
+
+
+
+
+
+ +
+

+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/tests/data/text/thai.txt b/tests/data/text/thai.txt new file mode 100644 index 00000000..b2ce780d --- /dev/null +++ b/tests/data/text/thai.txt @@ -0,0 +1,15 @@ +21 ม.ค.2561 เวลาประมาณ 12.00 น. ผู้สื่อข่าวได้รับแจ้งจากนาย ต.(ไม่ประสงค์เปิดเผยชื่อ) บุตรชายของ 'ภูชนะ' คนสนิทของนายสุรชัย แซ่ด่าน ที่หายไปว่า ผลการตรวจ DNA โดยใช้เนื้อเยื่อของศพที่ถูกสังหารด้วยการมัดแขน รัดคอ ทุบจนใบหน้าเละ และท้องถูกผ่ายัดเสาปูนที่ลอยมาติดที่ อ.ธาตุพนม จ.นครพนม นั้น เมื่อตรวจสอบแล้วมีความเกี่ยวพันทางสายเลือดกับนาย ต.จริง + +ภูชนะ (นามแฝง) เป็นคนใกล้ชิดของนายสุรชัย ด่านวัฒนานุสรณ์ หรือ สุรชัย แซ่ด่าน นักเคลื่อนไหวทางการเมืองที่ลี้ภัยออกจากประเทศไทยและได้หายตัวไปจากที่พักในประเทศเพื่อนบ้านในช่วงคืนวันที่ 12-13 ธ.ค.2561 พร้อมกันกับสุรชัยและคนสนิทอีกคนหนึ่ง + +ปรานี ด่านวัฒนานุสรณ์ ภรรยาของสุรชัยกล่าวว่า ได้ทราบข่าวจากบุตรชายของภูชนะแล้ว แต่ยังไม่ขอพูดอะไร โดยเบื้องต้นได้ทำใจแต่แรกแล้วว่าเหตุการณ์ลักษณะนี้จะต้องเกิดขึ้นสักวันหนึ่ง + +ขณะที่ นาย ว. พี่เขยของ 'กาสะลอง' อีกหนึ่งผู้ลี้ภัยที่ได้หายไปพร้อมกับนายสุรชัยกล่าวกับผู้สื่อข่าวว่า หลังจากได้ทราบผลการตรวจสอบ DNA จากลูกชายของภูชนะ ทางญาติของกาสะลองก็ได้ติดต่อไปที่พนักงานสอบสวนเจ้าของคดีและได้คำตอบว่า จะได้ทราบผลการตรวจสอบ DNA ภายในเวลา 2-3 วันนี้ + +สุรชัย (78 ปี) ภูชนะ (54 ปี) กาสะลอง (47 ปี) เป็นนักเคลื่อนไหวทางการเมือง และเป็นผู้ลี้ภัยจากเหตุการณ์รัฐประหาร 2557 ไปยังประเทศเพื่อนบ้านได้หายออกจากที่พักในประเทศเพื่อนบ้านโดยที่ไม่มีใครสามารถติดต่อได้จนปัจจุบันนับเป็นเวลานาน 1 เดือน กับอีก 10 วัน + +เรื่องที่เกี่ยวข้อง + +ลืออุ้ม อ.สุรชัย ผู้ลี้ภัยหายจากที่พัก 12 วันแล้ว + +ตร.เร่งหาเบาะแสคดีฆ่ายัดเสาถ่วงน้ำโขง - เมียไม่เชื่อเป็น 'สุรชัย แซ่ด่าน' \ No newline at end of file diff --git a/tests/unit_tests.py b/tests/unit_tests.py index 2ed69c93..69c05adf 100644 --- a/tests/unit_tests.py +++ b/tests/unit_tests.py @@ -25,6 +25,7 @@ import newspaper from newspaper import Article, fulltext, Source, ArticleException, news_pool +from newspaper.article import ArticleDownloadState from newspaper.configuration import Configuration from newspaper.urls import get_domain @@ -177,6 +178,8 @@ def test_download_html(self): self.setup_stage('download') html = mock_resource_with('cnn_article', 'html') self.article.download(html) + self.assertEqual(self.article.download_state, ArticleDownloadState.SUCCESS) + self.assertEqual(self.article.download_exception_msg, None) self.assertEqual(75406, len(self.article.html)) @print_test @@ -219,6 +222,7 @@ def test_parse_html(self): TITLE = 'After storm, forecasters see smooth sailing for Thanksgiving' LEN_IMGS = 46 META_LANG = 'en' + META_SITE_NAME = 'CNN' self.article.parse() self.article.nlp() @@ -237,6 +241,7 @@ def test_parse_html(self): self.assertEqual(TITLE, self.article.title) self.assertEqual(LEN_IMGS, len(self.article.imgs)) self.assertEqual(META_LANG, self.article.meta_lang) + self.assertEqual(META_SITE_NAME, self.article.meta_site_name) self.assertEqual('2013-11-27 00:00:00', str(self.article.publish_date)) @print_test @@ -322,6 +327,26 @@ def test_nlp_body(self): self.assertCountEqual(KEYWORDS, self.article.keywords) +class TestDownloadScheme(unittest.TestCase): + @print_test + def test_download_file_success(self): + url = "file://" + os.path.join(HTML_FN, "cnn_article.html") + article = Article(url=url) + article.download() + self.assertEqual(article.download_state, ArticleDownloadState.SUCCESS) + self.assertEqual(article.download_exception_msg, None) + self.assertEqual(75406, len(article.html)) + + @print_test + def test_download_file_failure(self): + url = "file://" + os.path.join(HTML_FN, "does_not_exist.html") + article = Article(url=url) + article.download() + self.assertEqual(0, len(article.html)) + self.assertEqual(article.download_state, ArticleDownloadState.FAILED_RESPONSE) + self.assertEqual(article.download_exception_msg, "No such file or directory") + + class ContentExtractorTestCase(unittest.TestCase): """Test specific element extraction cases""" @@ -705,6 +730,17 @@ def test_japanese_fulltext_extract2(self): self.assertEqual(text, article.text) self.assertEqual(text, fulltext(article.html, 'ja')) + @print_test + def test_thai_fulltext_extract(self): + url = 'https://prachatai.com/journal/2019/01/80642' + article = Article(url=url, language='th') + html = mock_resource_with('thai_article', 'html') + article.download(html) + article.parse() + text = mock_resource_with('thai', 'txt') + self.assertEqual(text, article.text) + self.assertEqual(text, fulltext(article.html, 'th')) + class TestNewspaperLanguagesApi(unittest.TestCase): @print_test @@ -712,6 +748,25 @@ def test_languages_api_call(self): newspaper.languages() +class TestDownloadPdf(unittest.TestCase): + + @print_test + def test_article_pdf_ignoring(self): + empty_pdf = "%PDF-" # empty PDF constant + a = Article(url='https://www.adobe.com/pdf/pdfs/ISO32000-1PublicPatentLicense.pdf', + ignored_content_types_defaults={"application/pdf": empty_pdf, + "application/x-pdf": empty_pdf, + "application/x-bzpdf": empty_pdf, + "application/x-gzpdf": empty_pdf}) + a.download() + self.assertEqual(empty_pdf, a.html) + + @print_test + def test_article_pdf_fetching(self): + a = Article(url='https://www.adobe.com/pdf/pdfs/ISO32000-1PublicPatentLicense.pdf') + a.download() + self.assertNotEqual('%PDF-', a.html) + if __name__ == '__main__': argv = list(sys.argv) if 'fulltext' in argv: