Kategorier
blandet

Wallnot i version 2.0

En af Wallnots få (men trofaste) brugere, bad om arkiv- og søgefunktionalitet på Wallnot.

Det krævede en større omlægning af Wallnot fra:

  • En side, der viser links til et øjebliksbillede af gratisartikler fra forsiden af danske netaviser.

Til:

  • En side der løbende arkiverer links til gratisartikler fra danske netaviser

Det kræver:

  • En bagvedliggende database
  • Løbende vedligeholdelse så links, der ændrer status fra gratis- til betalingsartikler, fjernes fra siden

Den nye Wallnot har:

  • Søgefunktion på artikeloverskrifter
  • Arkiv, der hele tiden bliver større
  • Zetland- og delte Politiken-artikler fra de sidste par år. Zetlandarkivet er nærmest komplet.
  • En robot, der løbende tjekker links fra de sidste par dage for ændret betalingsmursstatus
  • Mulighed for at filtrere Ritzau-telegrammer og dubletartikler fra
  • Bevaret hurtig- og enkeltheden fra version 1.

Arkitekturen bag Wallnot version 2

Version 2 af Wallnot er udviklet i Django, mens robotterne der indsamler og vedligeholder links er skrevet i Python.

Selve omlægningen til Django er faktisk enkel.

I models.py beskrives datamodellen, altså felterne i den bagvedliggende database:

from django.db import models
from django.utils import timezone
from django.contrib import admin

# Create your models here.
class Article(models.Model):
	title = models.CharField('Overskrift', max_length=500)
	unique_id = models.CharField('Avisens artikel-id', max_length=20, unique=True, null=True, blank=True)
	date = models.DateTimeField('Publiceringstidspunkt')
	MEDIUM_CHOICES = (
		('politiken', 'Politiken'),
		('berlingske', 'Berlingske'),
		('jyllandsposten', 'Jyllandsposten'),
		('information', 'Information'),
		('kristeligtdagblad', 'Kristeligt Dagblad'),
		('weekendavisen', 'Weekendavisen'),
		('zetland', 'Zetland'),
		('finansdk', 'Finans.dk'),
		('borsen', 'Børsen'),
		('arbejderen', 'Arbejderen'),
	)
	medium = models.CharField('Medie', max_length=30, choices=MEDIUM_CHOICES)
	url = models.URLField('Adresse', max_length=400, unique=True)
	ritzau = models.BooleanField('Ritzautelegram', default=False, null=True, blank=True)
	excerpt = models.CharField('Første sætning', max_length=1000, null=True, blank=True)
	duplicate = models.BooleanField('Dublet', default=False, null=True, blank=True)
	user_reports_paywall = models.BooleanField('Brugerrapporteret paywall', default=False, null=True)
	created_at = models.DateTimeField('Tilføjet den', default=timezone.now, editable=False)

class ArticleAdmin(admin.ModelAdmin):
	list_display = ('title','unique_id','ritzau','duplicate','excerpt','date')
	list_filter = ('medium', 'user_reports_paywall', 'ritzau','duplicate')
	search_fields = ['title', 'unique_id', 'excerpt']

Derudover skal der bygges et view, der beskriver forespørgslen til databasen. Her i en forkortet udgave uden logikken bag brugerrapportering af links bag paywall:

from django.shortcuts import render
from django.core.paginator import Paginator
import requests
import json
from .models import Article

def index(request):
	articles = Article.objects.order_by('-date')
	searchterm = request.GET.get('q')
	medium = request.GET.get('m')
	ritzau = request.GET.get('r')
	duplicates = request.GET.get('d')
	newwindow = request.GET.get('w')
	if searchterm:
		firstsearchcharacter = searchterm[:1]
		# Exclude queries by adding ! to searchterm
		if firstsearchcharacter == "!":
			searchterm = searchterm[1:]
			articles = articles.exclude(title__iregex=searchterm)
			searchterm = "!" + searchterm
		# Perform normal regex-enabled search
		else:
			articles = articles.filter(title__iregex=searchterm)
	if medium:
		articles = articles.filter(medium=medium)
	if ritzau:
		articles = articles.exclude(ritzau=True)
	if not duplicates and not medium:
		articles = articles.exclude(duplicate=True)
	paginator = Paginator(articles, 80)
	page_number = request.GET.get('page')
	page_obj = paginator.get_page(page_number)
	context = {'request': request, 'page_obj': page_obj, 'medium': medium, 'searchterm': searchterm, 'ritzau': ritzau, 'newwindow': newwindow, 'duplicates': duplicates}
	return render(request, 'wall/index.html', context)

Til sidst skrives en skabelon (template) der omsætter data til HTML. Her er fx den ganske korte bid kode, der spytter artikellinks ud på siden:

{% for article in page_obj %}
	{% ifchanged article.date|date %}<h3>{{ article.date|date }}</h3>{% endifchanged %}
	<p>{{ article.date|date:"H:i" }}: <a href="{{ article.url }}"{% if newwindow %} target="_blank"{% endif %}>{{ article.title }}</a> {% if article.ritzau %}<small><sup> ritzau </sup></small> {% endif %}{% if article.duplicate and not medium %}<small><sup> dublet </sup></small> {% endif %}<img title="Giv besked hvis artiklen er bag en paywall" id="{{ article.id }}" class="myBtnt" src="{% static "wall/alert.svg" %}"/></p>
{% endfor %}

God fornøjelse med den nye Wallnot!

Kategorier
blandet

Wallnots nye Twitter-robot

Opdatering: Jeg har lavet en ny, forbedret udgave af robottten. Læs om den her.

Både Zetland og Politiken har en feature, hvor abonnenter kan dele betalingsartikler med venner, bekendte og offentligheden. Artiklen får en unik URL, som låser op for betalingsmuren og lader alle og enhver læse artiklen.

Jeg tænkte at Wallnot – min hjemmeside med artikler, der ikke er bag betalingsmur – trængte til at være mere til stede på sociale medier.

Derfor har jeg lavet en robot, der gennemsøger Twitter for delte artikler fra Zetland og Politiken – og deler links’ne som tweets. Robotten opdater ca. på klokkeslettene 8.25, 12.25, 16.25 og 20.25. Det ville være skønt at kunne opdatere flere gange i døgnet, men så vil Twitter have penge.

Du finder Wallnots nye Twitter-robot her: https://twitter.com/wallnot_dk

Sådan ser det ud, når Wallnots robot tweeter.

Jeg brugte Python og modulet TwitterAPI. Hvis du selv vil køre programmet, skal du lave en udviklerkonto og en app hos Twitter. Se
https://developer.twitter.com/.

Her er det færdige program.

# -*- coding: utf-8 -*-
# Author: Morten Helmstedt. E-mail: helmstedt@gmail.com
# THIS PROGRAM POSTS NEW SHARED ARTICLES FROM ZETLAND.DK AND POLITIKEN.DK TO TWITTER

import requests
from bs4 import BeautifulSoup
from datetime import datetime
import json
import time
from nested_lookup import nested_lookup
from TwitterAPI import TwitterAPI

articlestopost = []

# API LOGIN - INSERT YOUR OWN VALUES HERE
client_key = ''
client_secret = ''
access_token = ''
access_secret = ''
api = TwitterAPI(client_key, client_secret, access_token, access_secret)


# POLITIKEN.DK SEARCH #
SEARCH_TERM = 'url:"politiken.dk/del/"'
PRODUCT = '30day'
LABEL = 'prod'

r = api.request('tweets/search/%s/:%s' % (PRODUCT, LABEL), 
                {'query':SEARCH_TERM})

tweet_data = json.loads(r.text)
prettyjson = json.dumps(tweet_data, ensure_ascii=False, indent=4) # Only needed for debugging to pretify json

# Looks for all instances of expanded_url in json	
linklist = list(set(nested_lookup('expanded_url', tweet_data)))

urllist = []
for link in linklist:
	if "politiken.dk/del" in link:
		urllist.append(link)

# Request articles and get titles and dates and sort by dates
articlelist = []
titlecheck = []

for url in urllist:
	try:
		data = requests.get(url)
		result = data.text
		if '"isAccessibleForFree": "True"' not in result:
			soup = BeautifulSoup(result, "lxml")
			# Finds titles and timestamps
			title = soup.find('meta', attrs={'property':'og:title'})
			title = title['content']
			timestamp = soup.find('meta', attrs={'property':'article:published_time'})
			timestamp = timestamp['content']
			dateofarticle = datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S%z')
			realurl = data.history[0].headers['Location']
			if title not in titlecheck:
				articlelist.append({"title": title, "url": realurl, "date": dateofarticle})
				titlecheck.append(title)			
	except:
		print(url)
			
articlelist_sorted = sorted(articlelist, key=lambda k: k['date'], reverse=True) 

# Check if article is already posted and update list of posted articles
with open("./pol_published.json", "r", encoding="utf8") as fin:
	alreadypublished = list(json.load(fin))
	for art in articlelist_sorted:
		url = art['url']
		token = url.index("?shareToken")
		url = url[:token]
		if url not in alreadypublished:
			alreadypublished.append(url)
			articlestopost.append(art)
	# Save updated already published links
	with open("./pol_published.json", "wt", encoding="utf8") as fout:
		alreadypublishedjson = json.dumps(alreadypublished)
		fout.write(alreadypublishedjson)

# ZETLAND.DK SEARCH #
SEARCH_TERM = 'url:"zetland.dk/historie"'
PRODUCT = '30day'
LABEL = 'prod'

r = api.request('tweets/search/%s/:%s' % (PRODUCT, LABEL), 
                {'query':SEARCH_TERM})

tweet_data = json.loads(r.text)
prettyjson = json.dumps(tweet_data, ensure_ascii=False, indent=4) # Only needed for debugging to pretify json

# Looks for all instances of expanded_url in json	
linklist = list(set(nested_lookup('expanded_url', tweet_data)))

urllist = []
for link in linklist:
	if "zetland.dk/historie" in link:
		urllist.append(link)

# Request articles and get titles and dates and sort by dates
articlelist = []
titlecheck = []

for url in urllist:
	try:
		data = requests.get(url)
		result = data.text

		# Soup site and create a dictionary of links and their titles and dates
		articledict = {}
		soup = BeautifulSoup(result, "lxml")

		title = soup.find('meta', attrs={'property':'og:title'})
		title = title['content']
		
		timestamp = soup.find('meta', attrs={'property':'article:published_time'})
		timestamp = timestamp['content']
		timestamp = timestamp[:timestamp.find("+")]
		dateofarticle = datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.%f')
		
		if title not in titlecheck:
			articlelist.append({"title": title, "url": url, "date": dateofarticle})
			titlecheck.append(title)
	except:
		print(url)
			
articlelist_sorted = sorted(articlelist, key=lambda k: k['date'], reverse=True) 

# Check if article is already posted and update list of posted articles
with open("./zet_published.json", "r", encoding="utf8") as fin:
	alreadypublished = list(json.load(fin))
	for art in articlelist_sorted:
		title = art['title']
		if title not in alreadypublished:
			alreadypublished.append(title)
			articlestopost.append(art)
	# Save updated already published links
	with open("./zet_published.json", "wt", encoding="utf8") as fout:
		alreadypublishedjson = json.dumps(alreadypublished, ensure_ascii=False)
		fout.write(alreadypublishedjson)


# POST TO TWITTER #
if articlestopost:
	for art in articlestopost:
		if "zetland" in art['url']:
			medium = "Zetland"
		else:
			medium = "Politiken"
		status = "En flink abonnent på " + medium + " har delt en betalingsartikel. God fornøjelse! " + art['url']
		r = api.request('statuses/update', {'status': status})
		time.sleep(5)