やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

Pythonでニュースを重複なく取得するフローを考察した

 じつは重複する要因が複数あって面倒。

RSSリーダでさえニュースが重複して表示される例

 RSSにフィードを登録する。フィードは同一ソース源でありながら、異なるカテゴリのRSSとする。そして、ニュースが重複するものとする。このとき、RSSリーダ内にはニュースが重複して表示される。

 別ニュース源なら仕方ないと思うが、同一ソース源でURLも同じなら省いて欲しい。まさか同一ソース源でURLが違う同一ニュースなどないだろうから、これで重複を省けるはず。なのに表示されてしまう。

ニュースが重複する要因

  1. 同一ソース源における別カテゴリRSS
  2. RSSと既存DB
  3. 別ソース源で異なる最新日時

1. 同一ソース源における別カテゴリRSS

 あるニュースサイトでは、複数のカテゴリに分けてRSSを発信している。それらのニュースが重複する場合がしばしばある。

 やっかいなことに、公開日時が異なる。url,titleは同じなのに公開日時だけが違う。もちろん同一記事だ。これも削除対象。だが、これは今までのようにSQLite3の表制約UNIQUE(published,url)では防げない。UNIQUE(url,title)とすべきだろう。

 だがUNIQUE(url,title)だと、過去の記事と重複する可能性がある。ニュースサイトゆえにURLは一定期間で使い回すことが想定される。タイトルも完全一致することが無いとはいえない。人に見せようとするタイトルの付け方は偏るから。

2. RSSと既存DB

 RSSには取得済みの内容がありうる。RSSとDBの整合性はユーザが自力でとらねばならない。なぜならニュースの取得は以下のような仕組みだから。

  • RSS更新タイミングはプロバイダ依存である
    • それがいつになるかはプロバイダでさえ予想できない
      • ニュースとなる事件・事故がいつどこで起きるかわからない
  • ニュース取得はPull方式である(サーバPush方式ではない)
    • ユーザがリクエストしたタイミングで取得される

 もしこれがRSSなどのフィードではなくWebAPIなら少しは楽ができただろう。引数に日時を渡して、それ以降の記事だけを取得できるなら。だが、DBと自動的に整合性を保ってくれるわけではない以上、やはり日時チェックが必要になってしまうのは変わらない。

3. 別ソース源ごとに異なる更新日時

 ニュースは複数のソース源から得るだろう。その上でDBはひとつに統合したい。このときDB内には別ソース源の最新更新日時が含まれている。

 最新ニュースだけを取得したいときは、DB内にある最新日時より新しいニュースだけを取り込めばいい。だが、もしこのDBに異なるソース源の日時が混在していたら、ニュースの取りこぼしが発生しうる。

 たとえばソースAで2019-08-01 23:59:59のニュースを取り込んだが、ソースBで2019-08-01 19:00:00のニュースを取り込みたい場合がある。このとき、DB内で最新日時を取得し、それより新しいもののみを取得しようとすると、ソースBのニュースが取り込まれなくなってしまう。

select max(published) from news;

 これを解決するには、ソースごとに別々のDB内最新日時を取得する必要がある。SQLでいうと以下。

select s.source_name, max(n.published) 
from news n, source s 
group by news.source;

ソース源を管理するのは面倒なので、urlドメイン名でソース源を特定してもいいかもしれない。

select ドメイン名, max(published) 
from news 
group by ドメイン名;

 たとえばurlが以下のような場合。

url|published
http://A.co.jp/news1|1999-12-31
http://A.co.jp/news2|2000-01-01
https://B.co.jp/news1|2010-01-01
https://B.co.jp/news2|2010-12-31

 ソース源は以下のようになる。

domain|max(published)
A.co.jp|2000-01-01
B.co.jp|2010-12-31

 urlからどうやってドメイン名を取得するか、という課題が残る。また、同一ソース源が必ず同一ドメインのURLだけを持っているかわからない。ふつうはそのはずだが。

 おまけ。URLの部位名称は以下。

http://www.ytyaru.co.jp:80/dir/index.html?param1=111&param2=222
部位 名称
http スキーム
www ホスト名
ytyaru.co.jp ドメイン
80 ポート番号
dir/index.html パス
?以降 クエリ。Key=Valueの形式。&で複数列挙。

ソースごとに最新日時を取得すると、一気に難しくなる。対策を考えてみた。 * RSSのURL/記事URLパターン/ソース源名/DBファイル名の対応表

 できればURLから自動的にDBファイル名などを得たい。/,.,:などURLには記号が多い。これらをファイルシステムでも使える名前にすべく_に置換するなどの対応が必要か。www_ytyaru_co_jp.dbのような。

 また、同一ソース源の別カテゴリRSSなら、それらを同一ソース源としてまとめておきたい。RSSドメイン名が同一なら同一ソース源と判断していいと思う。

所感

 じつは整合性を保つだけならinsert or ignore ...で十分。重複したら挿入しない。だが、これだと応答速度がとてつもなく遅い。20〜24秒/1件。原因は、重複しているときでも常にURL先からHTML取得しているからだと思う。

 そこで、挿入データを取得する前に重複チェックして、重複したものは本文抽出など重い処理をしないようにしたかった。すると今回のように面倒なことになってしまう。

 まさか自力で整合性を保つことがこれほど面倒とは思わなかった。原因はソースごとの分離。もうソースは固定1つでいいと妥協しそうになる。だが、これはサービス終了される危険性がある以上、すべきでない。

 さて、どうするか。

対象環境

$ uname -a
Linux raspberrypi 4.19.42-v7+ #1218 SMP Tue May 14 00:48:17 BST 2019 armv7l GNU/Linux