├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── MANIFEST ├── README.rst ├── extractcontent3 ├── __init__.py └── extractcontent3.py ├── setup.py └── tests ├── __init__.py ├── blog.html └── test_extractcontent3.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build/ 3 | dist/ 4 | 5 | *.*~ 6 | .python-version -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | - "nightly" 5 | script: 6 | - pytest 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007, Cybozu Labs Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | The views and conclusions contained in the software and documentation are those 25 | of the authors and should not be interpreted as representing official policies, 26 | either expressed or implied, of the FreeBSD Project. 27 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | include *.txt 2 | recursive-include extractcontent *.txt *.py 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ExtractContent3 2 | =============== 3 | 4 | .. image:: https://img.shields.io/pypi/v/extractcontent3.svg 5 | :target: https://pypi.python.org/pypi/extractcontent3 6 | 7 | .. image:: https://img.shields.io/pypi/l/extractcontent3.svg 8 | :target: https://pypi.python.org/pypi/extractcontent3 9 | 10 | .. image:: https://img.shields.io/pypi/pyversions/extractcontent3.svg 11 | :target: https://pypi.python.org/pypi/extractcontent3 12 | 13 | .. image:: https://travis-ci.org/kanjirz50/python-extractcontent3.svg?branch=master 14 | :target: https://travis-ci.org/kanjirz50/python-extractcontent3 15 | 16 | ExtractContent3はPython3で動作する、HTMLから本文を抽出するモジュールです。 17 | このモジュールは、ExtractContent RubyモジュールをPython用に書き直したpython-extracontentを改造したものです。 18 | 19 | Usage 20 | ------------ 21 | 22 | .. code-block:: python 23 | 24 | from extractcontent3 import ExtractContent 25 | extractor = ExtractContent() 26 | 27 | # オプション値を指定する 28 | opt = {"threshold":50} 29 | extractor.set_option(opt) 30 | 31 | html = open("index.html").read() # 解析対象HTML 32 | extractor.analyse(html) 33 | text, title = extractor.as_text() 34 | html, title = extractor.as_html() 35 | title = extractor.extract_title(html) 36 | 37 | Installation 38 | ------------ 39 | 40 | .. code-block:: bash 41 | 42 | # pypi 43 | $ pip install extractcontent3 44 | 45 | # Githubからのインストール 46 | $ pip install git+https://github.com/kanjirz50/python-extractcontent3 47 | 48 | Option 49 | ------------- 50 | 51 | .. code-block:: python 52 | 53 | """ 54 | オプションの種類: 55 | 名称 / デフォルト値 56 | 57 | threshold / 100 58 | 本文と見なすスコアの閾値 59 | 60 | min_length / 80 61 | 評価を行うブロック長の最小値 62 | 63 | decay_factor / 0.73 64 | 減衰係数 65 | 小さいほど先頭に近いブロックのスコアが高くなります 66 | 67 | continuous_factor / 1.62 68 | 連続ブロック係数 69 | 大きいほどブロックを連続と判定しにくくなる 70 | 71 | punctuation_weight / 10 72 | 句読点に対するスコア  73 | 大きいほど句読点が存在するブロックを本文と判定しやすくなる 74 | 75 | punctuations r"(?is)([\u3001\u3002\uff01\uff0c\uff0e\uff1f]|\.[^A-Za-z0-9]|,[^0-9]|!|\?)" 76 | 句読点を抽出する正規表現 77 | 78 | waste_expressions / r"(?i)Copyright|All Rights Reserved" 79 | フッターに含まれる特徴的なキーワードを指定した正規表現 80 | 81 | debug / False 82 | Trueの場合、ブロック情報を出力 83 | """ 84 | 85 | 謝辞 86 | ---- 87 | 88 | オリジナル版の作成者やForkで改良を加えた方々に感謝します。 89 | 90 | - Copyright of the original implementation:: (c)2007/2008/2009 Nakatani Shuyo / Cybozu labs Inc. All rights reserved 91 | - http://rubyforge.org/projects/extractcontent/ 92 | - http://labs.cybozu.co.jp/blog/nakatani/2007/09/web_1.html 93 | - https://github.com/petitviolet/python-extractcontent 94 | - https://github.com/yono/python-extractcontent 95 | -------------------------------------------------------------------------------- /extractcontent3/__init__.py: -------------------------------------------------------------------------------- 1 | from .extractcontent3 import ExtractContent 2 | -------------------------------------------------------------------------------- /extractcontent3/extractcontent3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import re 4 | import unicodedata 5 | from functools import reduce 6 | 7 | 8 | class ExtractContent(object): 9 | 10 | # convert character to entity references 11 | CHARREF = { 12 | "nbsp": " ", 13 | "lt": "<", 14 | "gt": "<", 15 | "amp": "&", 16 | "laquo": "\x00\xab", 17 | "raquo": "\x00\xbb", 18 | } 19 | 20 | # Default option parameters. 21 | option = { 22 | "threshold": 100, 23 | # threshold for score of the text 24 | "min_length": 80, 25 | # minimum length of evaluated blocks 26 | "decay_factor": 0.73, 27 | # decay factor for block score 28 | "continuous_factor": 1.62, 29 | # continuous factor for block score 30 | # ( the larger, the harder to continue ) 31 | "punctuation_weight": 10, 32 | # score weight for punctuations 33 | "punctuations": (r"(?is)([\u3001\u3002\uff01\uff0c\uff0e\uff1f]" 34 | r"|\.[^A-Za-z0-9]|,[^0-9]|!|\?)"), 35 | # punctuation characters 36 | "waste_expressions": r"(?i)Copyright|All Rights Reserved", 37 | # characteristic keywords including footer 38 | "debug": False, 39 | # if true, output block information to stdout 40 | } 41 | 42 | def __init__(self, opt=None): 43 | if opt is not None: 44 | self.option.update(opt) 45 | self.title = '' 46 | self.body = '' 47 | 48 | def set_option(self, opt): 49 | """ 50 | Sets option parameters to default. 51 | Parameter opt is given as Dictionary. 52 | """ 53 | self.option.update(opt) 54 | 55 | def analyse(self, html, opt=None): 56 | """ 57 | Analyses the given HTML text, extracts body and title. 58 | """ 59 | # flameset or redirect 60 | if re.search((r"(?i)<\/frameset>|]*url"), html) is not None: 62 | return ["", self.extract_title(html)] 63 | 64 | # option parameters 65 | if opt: 66 | self.option.update(opt) 67 | 68 | # header & title 69 | header = re.match(r"(?s)", html) 70 | if header is not None: 71 | html = html[:header.end()] 72 | self.title = self.extract_title(html[0:header.start()]) 73 | else: 74 | self.title = self.extract_title(html) 75 | 76 | # Google AdSense Section Target 77 | html = re.sub((r"(?is).*?"), 79 | "", html) 80 | if re.search(r"(?is)", 81 | html) is not None: 82 | result = re.findall((r"(?is).*?"), 84 | html) 85 | html = "\n".join(result) 86 | 87 | # eliminate useless text 88 | html = self._eliminate_useless_tags(html) 89 | 90 | # heading tags including title 91 | # self.title = title 92 | html = re.sub(r"(?s)(\s*(.*?)\s*)", 93 | self._estimate_title, html) 94 | 95 | # extract text blocks 96 | factor = 1.0 97 | continuous = 1.0 98 | body = '' 99 | score = 0 100 | bodylist = [] 101 | block_list = self._split_to_blocks(html) 102 | for block in block_list: 103 | if self._has_only_tags(block): 104 | continue 105 | 106 | if len(body) > 0: 107 | continuous /= self.option["continuous_factor"] 108 | 109 | # ignore link list block 110 | notlinked = self._eliminate_link(block) 111 | if len(notlinked) < self.option["min_length"]: 112 | continue 113 | 114 | # calculate score of block 115 | c = (len(notlinked) + self._count_pattern(notlinked, self.option["punctuations"]) * self.option["punctuation_weight"]) * factor 116 | factor *= self.option["decay_factor"] 117 | not_body_rate = self._count_pattern(block, self.option["waste_expressions"]) + self._count_pattern(block, r"amazon[a-z0-9\.\/\-\?&]+-22") / 2.0 118 | if not_body_rate > 0: 119 | c *= (0.72 ** not_body_rate) 120 | c1 = c * continuous 121 | if self.option["debug"]: 122 | print("----- %f*%f=%f %d \n%s" % (c, continuous, c1, len(notlinked), 123 | self._strip_tags(block)[0:100])) 124 | 125 | # tread continuous blocks as cluster 126 | if c1 > self.option["threshold"]: 127 | body += block + "\n" 128 | score += c1 129 | continuous = self.option["continuous_factor"] 130 | elif c > self.option["threshold"]: # continuous block end 131 | bodylist.append((body, score)) 132 | body = block + "\n" 133 | score = c 134 | continuous = self.option["continuous_factor"] 135 | 136 | bodylist.append((body, score)) 137 | body = reduce(lambda x, y: x if x[1] >= y[1] else y, bodylist) 138 | self.body = body[0] 139 | return self.as_text() 140 | 141 | def as_html(self): 142 | return (self.body, self.title) 143 | 144 | def as_text(self): 145 | return (self._strip_tags(self.body), self.title) 146 | 147 | def extract_title(self, st): 148 | result = re.search(r"(?s)]*>\s*(.*?)\s*", st) 149 | if result is not None: 150 | return self._strip_tags(result.group(0)) 151 | else: 152 | return "" 153 | 154 | def _split_to_blocks(self, html): 155 | block_list = \ 156 | re.split((r"]*>|]*class\s*=\s*" 157 | r"[\"']?(?:posted|plugin-\w+)['\"]?[^>]*>"), html) 158 | return block_list 159 | 160 | # Count a pattern from text. 161 | def _count_pattern(self, text, pattern): 162 | result = re.search(pattern, text) 163 | if result is None: 164 | return 0 165 | else: 166 | return len(result.span()) 167 | 168 | def _estimate_title(self, match): 169 | """ 170 | h? タグの記述がタイトルと同じかどうか調べる 171 | """ 172 | striped = self._strip_tags(match.group(2)) 173 | if len(striped) >= 3 and self.title.find(striped) != -1: 174 | return "
%s
" % (striped) 175 | else: 176 | return match.group(1) 177 | 178 | def _eliminate_useless_tags(self, html): 179 | """ 180 | Eliminates useless tags 181 | """ 182 | # Eliminate useless symbols 183 | html = re.sub(r"[\u2018-\u201d\u2190-\u2193\u25a0-\u25bd\u25c6-\u25ef\u2605-\u2606]", "", html) 184 | # Eliminate useless html tags 185 | html = \ 186 | re.sub(r"(?is)<(script|style|select|noscript)[^>]*>.*?", 187 | "", html) 188 | html = re.sub(r"(?s)", "", html) 189 | html = re.sub(r"/s", "", html) 190 | html = re.sub((r"(?s)]*class\s*=\s*['\"]?alpslab-slide" 191 | r"[\"']?[^>]*>.*?"), "", html) 192 | html = re.sub((r"(?is)]*(id|class)\s*=\s*['\"]" 193 | r"?\S*more\S*[\"']?[^>]*>"), "", html) 194 | return html 195 | 196 | def _has_only_tags(self, st): 197 | """ 198 | Checks if the given block has only tags without text. 199 | """ 200 | st = re.sub(r"(?is)<[^>]*>", "", st) 201 | st = re.sub(r" ", "", st) 202 | st = st.strip() 203 | return len(st) == 0 204 | 205 | def _eliminate_link(self, html): 206 | """ 207 | eliminate link tags 208 | """ 209 | count = 0 210 | notlinked, count = re.subn(r"(?is)]*>.*?<\/a\s*>", "", html) 211 | notlinked = re.sub(r"(?is)]*>.*?", "", notlinked) 212 | notlinked = self._strip_tags(notlinked) 213 | # returns empty string when html contains many links or list of links 214 | if (len(notlinked) < 20 * count) or (self._islinklist(html)): 215 | return "" 216 | return notlinked 217 | 218 | def _islinklist(self, st): 219 | """ 220 | determines whether a block is link list or not 221 | """ 222 | result = re.search(r"(?is)<(?:ul|dl|ol)(.+?)", st) 223 | if result is not None: 224 | listpart = result.group(1) 225 | outside = re.sub(r"(?is)<(?:ul|dl)(.+?)", "", st) 226 | outside = re.sub(r"(?is)<.+?>", "", outside) 227 | outside = re.sub(r"\s+", "", outside) 228 | list = re.split(r"]*>", listpart) 229 | rate = self._evaluate_list(list) 230 | return len(outside) <= len(st) / (45 / rate) 231 | return False 232 | 233 | def _evaluate_list(self, list): 234 | """ 235 | estimates how much degree of link list 236 | """ 237 | if len(list) == 0: 238 | return 1 239 | hit = 0 240 | href = re.compile("", "", html) 251 | # Convert from wide character to ascii 252 | if st and type(st) != str: 253 | st = unicodedata.normalize("NFKC", st) 254 | st = re.sub(r"[\u2500-\u253f\u2540-\u257f]", "", st) # 罫線(keisen) 255 | st = re.sub(r"&(.*?);", lambda x: self.CHARREF.get(x.group(1), 256 | x.group()), st) 257 | st = re.sub(r"[ \t]+", " ", st) 258 | st = re.sub(r"\n\s*", "\n", st) 259 | return st 260 | 261 | if __name__ == "__main__": 262 | pass 263 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from codecs import open 3 | from os import path 4 | 5 | here = path.abspath(path.dirname(__file__)) 6 | 7 | with open(path.join(here, "README.rst"), encoding="utf-8") as f: 8 | long_description = f.read() 9 | 10 | setup( 11 | name="extractcontent3", 12 | version="0.0.1", 13 | description="", 14 | long_description=long_description, 15 | license="BSD 2-Clause", 16 | url="https://github.com/kanjirz50/python-extractcontent3", 17 | packages=find_packages(exclude=["contrib", "docs", "tests"]), 18 | install_requires=[], 19 | dependency_links=[], 20 | python_requires='~=3.3', 21 | ) 22 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kanjirz50/python-extractcontent3/52c16984957c57f53d679f34b9d068cbefb5dd6a/tests/__init__.py -------------------------------------------------------------------------------- /tests/blog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | Xonshを使ってみた - かんちゃんの備忘録 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 | 82 | 83 | 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 | 127 | 128 | 129 | 130 | 134 | 135 | 136 | 137 | 140 | 141 |
142 | 143 |
144 | 145 | 146 |
147 |
148 |
149 |
150 |
151 |

かんちゃんの備忘録

152 | 153 |

プログラミングや言語処理、ガジェットなど個人の備忘録です。(メモ書き+α)

154 | 155 |
156 |
157 |
158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 |
168 |
169 |
170 |
171 |
172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 |
189 |
190 |
191 | 198 |

199 | Xonshを使ってみた 200 |

201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 |
編集
211 | 212 | 213 | 214 |
215 | 216 |

【Xonsh Advent Calendar 2017の13日目の記事です。】

217 | 218 |

Xonshがいいという話を聞いて、これは使ってみないと!と思い使ってみました。

219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 |

その備忘録です。

227 | 228 | 240 | 241 |

Xonshとは

242 | 243 |

the xonsh shell

244 | 245 |

~ こちらトム少佐からXonsh地上管制へ ~

246 | 247 |

XonshはPythonにより動作する、クロスプラットフォームUnixのようなシェル言語であるコマンドプロンプトです。 248 | この言語はPython 3.4+の上位互換で、慣れ親しんだBashやIPythonなどの基本的なシェル命令を追加したものです。 249 | LinuxやMac OSX、Windowsといったメジャーなシステム上で動作します。 250 | Xonshは、普段使いで上級者も初級者も同じように使えるように作られています。

251 | 252 |

勢いでxonshトップページの冒頭を訳してみましたが、This is major Tom to ground xonshtrolは、デビッド・ボウイスペイス・オディティThis is major Tom to ground controlをもじっているようで、英語力のない自分には理解しがたいです(汗)

253 | 254 |

とにかく冒頭からは、Pythonで作られた良い感じのシェルだということが分かったので使ってみます。

255 | 256 |

導入

257 | 258 |

Python 3.4以上の環境を用意して、pip install xonshでインストールできます。

259 | 260 |

ログインシェルにする場合は、システムにPython 3.4以上をインストールしておくと安心かと思います。

261 | 262 |

使ってみた

263 | 264 |

Tutorial見ながら使ってみて、普段使っているzshとは異なる機能や文法に触れました。 265 | (ノリで記事を書いてみたら、ばんくしさんの1日目の記事と結構かぶってしまいました( ))

266 | 267 |

環境変数

268 | 269 |

$+変数名が環境変数となります。また代入は、Pythonと同じように記述します。

270 | 271 |
username@hostname ~ $ $HOST
 272 | 'hostname'
 273 | username@hostname ~ $ $HOST = "hogefuga"
 274 | username@hostname ~ $ $HOST
 275 | 'hogefuga'
 276 | 
277 | 278 | 279 |

xonshスクリプトから参照したいときは、${}構文を使うことで文字列で参照できます。

280 | 281 |
username@hostname ~ $ ${"HOME"}
 282 | "/home/username"
 283 | 
284 | 285 | 286 |

Python-modeとSubprocess-mode

287 | 288 |

xonshを使っていると、まるでIPythonのような使い勝手でbash likeなPythonというように思えてきます。 289 | 両方の文法が使えるのですが、混在すると曖昧となるため、コマンドと同名の変数名やメソッド名は避けましょうというお話です。

290 | 291 |
username@hostname ~ $ ls = 44
 292 | username@hostname ~ $ ls
 293 | 44
 294 | username@hostname ~ $ del ls
 295 | username@hostname ~ $ ls
 296 | ...
 297 | 
298 | 299 | 300 |

Subprocessとして

301 | 302 |

$()でコマンドを実行すると、PythonでSubprocessを使ったときのように、コマンドの戻り値が文字列型で返ってきます。

303 | 304 |
username@hostname ~ $ $(ls)
 305 | hoge\nfuga\ndotfiles
 306 | 
307 | 308 | 309 |

$[]という書き方でもSubprocessとして動作させられますが、直に標準出力に出力されるようで、戻り値としてはNoneです。 310 | パイプなどで渡すとエラーとなりました。

311 | 312 |
username@hostname ~ $ $[ls]|less
 313 | hoge
 314 | fuga
 315 | ...
 316 | xonsh: For full traceback set: $XONSH_SHOW_TRACEBACK = True
 317 | AttributeError: 'NoneType' object has no attribute 'splitlines'
 318 | username@hostname ~ $ $[ls] == None
 319 | hoge
 320 | fuga
 321 | ...
 322 | True
 323 | 
324 | 325 | 326 |

プロセス置換ができない?

327 | 328 |

bashzshで一時ファイルを作成するかわりに、プロセス置換で作業することが多いです。 329 | プロセス置換はbashzshに搭載されている便利な機能の一つで、引数でファイルを指定するかわりにコマンドの実行結果をファイルとして扱える機能です。 330 | つまり中間ファイルが不要ということです。(どの中間ファイルを使ったか忘れがちですが、これだとコマンドの履歴として記録されます。)

331 | 332 |

入力をプロセス置換するのは、だいたい2つ以上のファイルを利用するコマンドを使うときです。(ひとつのファイルなら基本的にはパイプで) 333 | diffを取る時やjoin、pasteなどで、以下のように使います。

334 | 335 |
username@hostname ~ $ diff <(sort -nrk1 hoge.txt) <(sort -nrk1 fuga.txt)
 336 | 
337 | 338 | 339 |

Issueをprocess substitutionで検索してみると、現時点(2017/12/10)で一番新しいものはIssue 1307で、プロセス置換は使えないようです。 340 | $(echo Hi Mom > /tmp/mom)/tmp/momを返すので、プロセス置換と似たことができると読めたのですが、v0.6.0では空文字列が返ってきました。

341 | 342 |

詳しい人、教えてください。

343 | 344 |

使ってみて感想

345 | 346 |

シェルの設定ファイルを書かない人や、プロセス置換などを利用しない人にとっては、十分すぎる補完機能やシンタックスハイライトと感じました。

347 | 348 |

zshから乗り換えるとなると、勝手が違うため覚えることが多いように思います。

349 | 350 |

Python言語が使えるという利点を活かす利用法を考えたいです。 351 | たぶん、awkperlsedを使うかわりにうまく使えるのだと思います。

352 | 353 | 354 |
355 | 356 | 357 | 551 | 552 |
553 |
554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 578 | 579 | 580 | 581 | 582 | 583 | 584 |
585 |
586 | 587 | 591 | 592 |
593 | 594 | 595 | 971 | 972 | 973 |
974 |
975 | 976 | 977 | 978 | 979 | 980 | 981 | 982 | 983 | 984 | 985 | 986 | 987 |
988 |
989 | 990 | 1022 | 1023 | 1024 | 1025 | 1026 | 1027 | 1028 | 1029 | 1030 | 1031 |
1032 | 1039 | 1040 | 1041 | 1042 | 1043 | 1054 | 1055 | 1058 | 1059 |
1060 | 1065 | 1066 | 1071 | 1072 | 1076 | 1077 | 1080 |
1081 | 1082 | 1087 | 1088 | 1089 | 1090 | 1091 | 1112 | 1113 | 1114 | 1115 | 1116 | 1119 | 1120 | 1121 | 1122 | 1123 | 1124 | 1125 | 1126 | 1127 | 1128 | 1129 | 1130 | 1131 | 1132 | 1133 | 1134 | 1135 |
1136 | 1137 | 1138 | 1139 | 1140 | 1141 | 1142 | 1143 | 1144 | 1145 | 1146 | 1147 | 1148 | 1149 | 1150 | 1151 | 1152 | 1153 | 1154 | 1155 | 1156 |
1158 | 1159 | 1160 | 1161 |
1162 | 1163 | 1164 | 1165 |
1166 |
1167 | 1168 |
1169 | 1170 | 1171 | 1172 | 1173 |
-------------------------------------------------------------------------------- /tests/test_extractcontent3.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test 3 | """ 4 | 5 | import pytest 6 | from extractcontent3 import ExtractContent 7 | 8 | class TestExtractContentHatenaBlog(object): 9 | @pytest.fixture() 10 | def extractor(self): 11 | html = open("./tests/blog.html").read() 12 | extractor = ExtractContent() 13 | extractor.analyse(html) 14 | return extractor 15 | 16 | def test_text(self, extractor): 17 | text, title = extractor.as_text() 18 | assert text.strip().startswith("【Xonsh Advent Calendar 2017の13日目の記事です。】") 19 | 20 | def test_title(self, extractor): 21 | text, title = extractor.as_text() 22 | assert title == "Xonshを使ってみた - かんちゃんの備忘録" 23 | --------------------------------------------------------------------------------