Browse Source

Merge pull request #486 from a01200356/master

[enh] WolframAlpha no API engine (and tests for both)
Adam Tauber 9 years ago
parent
commit
b5a3dfca60

+ 26
- 9
searx/engines/wolframalpha_api.py View File

@@ -10,11 +10,18 @@
10 10
 
11 11
 from urllib import urlencode
12 12
 from lxml import etree
13
+from re import search
13 14
 
14 15
 # search-url
15 16
 base_url = 'http://api.wolframalpha.com/v2/query'
16 17
 search_url = base_url + '?appid={api_key}&{query}&format=plaintext'
17
-api_key = ''
18
+site_url = 'http://www.wolframalpha.com/input/?{query}'
19
+api_key = ''  # defined in settings.yml
20
+
21
+# xpath variables
22
+failure_xpath = '/queryresult[attribute::success="false"]'
23
+answer_xpath = '//pod[attribute::primary="true"]/subpod/plaintext'
24
+input_xpath = '//pod[starts-with(attribute::title, "Input")]/subpod/plaintext'
18 25
 
19 26
 
20 27
 # do search-request
@@ -45,16 +52,26 @@ def response(resp):
45 52
     search_results = etree.XML(resp.content)
46 53
 
47 54
     # return empty array if there are no results
48
-    if search_results.xpath('/queryresult[attribute::success="false"]'):
55
+    if search_results.xpath(failure_xpath):
49 56
         return []
50 57
 
51
-    # parse result
52
-    result = search_results.xpath('//pod[attribute::primary="true"]/subpod/plaintext')[0].text
53
-    result = replace_pua_chars(result)
58
+    # parse answers
59
+    answers = search_results.xpath(answer_xpath)
60
+    if answers:
61
+        for answer in answers:
62
+            answer = replace_pua_chars(answer.text)
63
+
64
+            results.append({'answer': answer})
65
+
66
+    # if there's no input section in search_results, check if answer has the input embedded (before their "=" sign)
67
+    try:
68
+        query_input = search_results.xpath(input_xpath)[0].text
69
+    except IndexError:
70
+        query_input = search(u'([^\uf7d9]+)', answers[0].text).group(1)
54 71
 
55
-    # append result
56
-    # TODO: shouldn't it bind the source too?
57
-    results.append({'answer': result})
72
+    # append link to site
73
+    result_url = site_url.format(query=urlencode({'i': query_input.encode('utf-8')}))
74
+    results.append({'url': result_url,
75
+                    'title': query_input + " - Wolfram|Alpha"})
58 76
 
59
-    # return results
60 77
     return results

+ 86
- 0
searx/engines/wolframalpha_noapi.py View File

@@ -0,0 +1,86 @@
1
+# WolframAlpha (Maths)
2
+#
3
+# @website     http://www.wolframalpha.com/
4
+# @provide-api yes (http://api.wolframalpha.com/v2/)
5
+#
6
+# @using-api   no
7
+# @results     HTML
8
+# @stable      no
9
+# @parse       answer
10
+
11
+from re import search, sub
12
+from json import loads
13
+from urllib import urlencode
14
+from lxml import html
15
+import HTMLParser
16
+
17
+# search-url
18
+url = 'http://www.wolframalpha.com/'
19
+search_url = url + 'input/?{query}'
20
+
21
+# xpath variables
22
+scripts_xpath = '//script'
23
+title_xpath = '//title'
24
+failure_xpath = '//p[attribute::class="pfail"]'
25
+
26
+
27
+# do search-request
28
+def request(query, params):
29
+    params['url'] = search_url.format(query=urlencode({'i': query}))
30
+
31
+    return params
32
+
33
+
34
+# get response from search-request
35
+def response(resp):
36
+    results = []
37
+    line = None
38
+
39
+    dom = html.fromstring(resp.text)
40
+    scripts = dom.xpath(scripts_xpath)
41
+
42
+    # the answer is inside a js function
43
+    # answer can be located in different 'pods', although by default it should be in pod_0200
44
+    possible_locations = ['pod_0200\.push\((.*)',
45
+                          'pod_0100\.push\((.*)']
46
+
47
+    # failed result
48
+    if dom.xpath(failure_xpath):
49
+        return results
50
+
51
+    # get line that matches the pattern
52
+    for pattern in possible_locations:
53
+        for script in scripts:
54
+            try:
55
+                line = search(pattern, script.text_content()).group(1)
56
+                break
57
+            except AttributeError:
58
+                continue
59
+        if line:
60
+            break
61
+
62
+    if line:
63
+        # extract answer from json
64
+        answer = line[line.find('{'):line.rfind('}') + 1]
65
+        try:
66
+            answer = loads(answer)
67
+        except Exception:
68
+            answer = loads(answer.encode('unicode-escape'))
69
+        answer = answer['stringified']
70
+
71
+        # clean plaintext answer
72
+        h = HTMLParser.HTMLParser()
73
+        answer = h.unescape(answer.decode('unicode-escape'))
74
+        answer = sub(r'\\', '', answer)
75
+
76
+        results.append({'answer': answer})
77
+
78
+    # user input is in first part of title
79
+    title = dom.xpath(title_xpath)[0].text.encode('utf-8')
80
+    result_url = request(title[:-16], {})['url']
81
+
82
+    # append result
83
+    results.append({'url': result_url,
84
+                    'title': title.decode('utf-8')})
85
+
86
+    return results

+ 9
- 7
searx/settings.yml View File

@@ -300,13 +300,15 @@ engines:
300 300
     engine : vimeo
301 301
     shortcut : vm
302 302
 
303
-# You can use the engine using the official stable API, but you need an API key
304
-# See : http://products.wolframalpha.com/api/
305
-#  - name : wolframalpha
306
-#    shortcut : wa
307
-#    engine : wolframalpha_api
308
-#    api_key: 'apikey' # required!
309
-#    timeout: 6.0
303
+  - name : wolframalpha
304
+    shortcut : wa
305
+    # You can use the engine using the official stable API, but you need an API key
306
+    # See : http://products.wolframalpha.com/api/
307
+    #    engine : wolframalpha_api
308
+    #    api_key: 'apikey' # required!
309
+    engine : wolframalpha_noapi
310
+    timeout: 6.0
311
+    disabled : True
310 312
 
311 313
 #The blekko technology and team have joined IBM Watson! -> https://blekko.com/
312 314
 #  - name : blekko images

+ 307
- 0
tests/unit/engines/test_wolframalpha_api.py View File

@@ -0,0 +1,307 @@
1
+# -*- coding: utf-8 -*-
2
+from collections import defaultdict
3
+import mock
4
+from searx.engines import wolframalpha_api
5
+from searx.testing import SearxTestCase
6
+
7
+
8
+class TestWolframAlphaAPIEngine(SearxTestCase):
9
+
10
+    def test_request(self):
11
+        query = 'test_query'
12
+        api_key = 'XXXXXX-XXXXXXXXXX'
13
+        dicto = defaultdict(dict)
14
+        dicto['api_key'] = api_key
15
+        params = wolframalpha_api.request(query, dicto)
16
+
17
+        self.assertIn('url', params)
18
+        self.assertIn(query, params['url'])
19
+        self.assertIn('wolframalpha.com', params['url'])
20
+
21
+        self.assertIn('api_key', params)
22
+        self.assertIn(api_key, params['api_key'])
23
+
24
+    def test_response(self):
25
+        self.assertRaises(AttributeError, wolframalpha_api.response, None)
26
+        self.assertRaises(AttributeError, wolframalpha_api.response, [])
27
+        self.assertRaises(AttributeError, wolframalpha_api.response, '')
28
+        self.assertRaises(AttributeError, wolframalpha_api.response, '[]')
29
+
30
+        xml = '''<?xml version='1.0' encoding='UTF-8'?>
31
+        <queryresult success='false' error='false' />
32
+        '''
33
+        # test failure
34
+        response = mock.Mock(content=xml)
35
+        self.assertEqual(wolframalpha_api.response(response), [])
36
+
37
+        xml = """<?xml version='1.0' encoding='UTF-8'?>
38
+        <queryresult success='true'
39
+            error='false'
40
+            numpods='6'
41
+            datatypes=''
42
+            timedout=''
43
+            timedoutpods=''
44
+            timing='0.684'
45
+            parsetiming='0.138'
46
+            parsetimedout='false'
47
+            recalculate=''
48
+            id='MSPa416020a7966dachc463600000f9c66cc21444cfg'
49
+            host='http://www3.wolframalpha.com'
50
+            server='6'
51
+            related='http://www3.wolframalpha.com/api/v2/relatedQueries.jsp?...'
52
+            version='2.6'>
53
+         <pod title='Input'
54
+             scanner='Identity'
55
+             id='Input'
56
+             position='100'
57
+             error='false'
58
+             numsubpods='1'>
59
+          <subpod title=''>
60
+           <plaintext>sqrt(-1)</plaintext>
61
+          </subpod>
62
+         </pod>
63
+         <pod title='Result'
64
+             scanner='Simplification'
65
+             id='Result'
66
+             position='200'
67
+             error='false'
68
+             numsubpods='1'
69
+             primary='true'>
70
+          <subpod title=''>
71
+           <plaintext></plaintext>
72
+          </subpod>
73
+          <states count='1'>
74
+           <state name='Step-by-step solution'
75
+               input='Result__Step-by-step solution' />
76
+          </states>
77
+         </pod>
78
+         <pod title='Polar coordinates'
79
+             scanner='Numeric'
80
+             id='PolarCoordinates'
81
+             position='300'
82
+             error='false'
83
+             numsubpods='1'>
84
+          <subpod title=''>
85
+           <plaintext>r1 (radius), θ90° (angle)</plaintext>
86
+          </subpod>
87
+         </pod>
88
+         <pod title='Position in the complex plane'
89
+             scanner='Numeric'
90
+             id='PositionInTheComplexPlane'
91
+             position='400'
92
+             error='false'
93
+             numsubpods='1'>
94
+          <subpod title=''>
95
+           <plaintext></plaintext>
96
+          </subpod>
97
+         </pod>
98
+         <pod title='All 2nd roots of -1'
99
+             scanner='RootsOfUnity'
100
+             id=''
101
+             position='500'
102
+             error='false'
103
+             numsubpods='2'>
104
+          <subpod title=''>
105
+           <plaintext>  (principal root)</plaintext>
106
+          </subpod>
107
+          <subpod title=''>
108
+           <plaintext>-</plaintext>
109
+          </subpod>
110
+         </pod>
111
+         <pod title='Plot of all roots in the complex plane'
112
+             scanner='RootsOfUnity'
113
+             id='PlotOfAllRootsInTheComplexPlane'
114
+             position='600'
115
+             error='false'
116
+             numsubpods='1'>
117
+          <subpod title=''>
118
+           <plaintext></plaintext>
119
+          </subpod>
120
+         </pod>
121
+        </queryresult>
122
+        """
123
+        # test private user area char in response
124
+        response = mock.Mock(content=xml)
125
+        results = wolframalpha_api.response(response)
126
+        self.assertEqual(type(results), list)
127
+        self.assertEqual(len(results), 2)
128
+        self.assertIn('i', results[0]['answer'])
129
+        self.assertIn('sqrt(-1) - Wolfram|Alpha', results[1]['title'])
130
+        self.assertEquals('http://www.wolframalpha.com/input/?i=sqrt%28-1%29', results[1]['url'])
131
+
132
+        xml = """<?xml version='1.0' encoding='UTF-8'?>
133
+            <queryresult success='true'
134
+                error='false'
135
+                numpods='2'
136
+                datatypes=''
137
+                timedout=''
138
+                timedoutpods=''
139
+                timing='1.286'
140
+                parsetiming='0.255'
141
+                parsetimedout='false'
142
+                recalculate=''
143
+                id='MSPa195222ad740ede5214h30000480ca61h003d3gd6'
144
+                host='http://www3.wolframalpha.com'
145
+                server='20'
146
+                related='http://www3.wolframalpha.com/api/v2/relatedQueries.jsp?id=...'
147
+                version='2.6'>
148
+             <pod title='Indefinite integral'
149
+                 scanner='Integral'
150
+                 id='IndefiniteIntegral'
151
+                 position='100'
152
+                 error='false'
153
+                 numsubpods='1'
154
+                 primary='true'>
155
+              <subpod title=''>
156
+               <plaintext>∫1/xxlog(x)+constant</plaintext>
157
+              </subpod>
158
+              <states count='1'>
159
+               <state name='Step-by-step solution'
160
+                   input='IndefiniteIntegral__Step-by-step solution' />
161
+              </states>
162
+              <infos count='1'>
163
+               <info text='log(x) is the natural logarithm'>
164
+                <link url='http://reference.wolfram.com/mathematica/ref/Log.html'
165
+                    text='Documentation'
166
+                    title='Mathematica' />
167
+                <link url='http://functions.wolfram.com/ElementaryFunctions/Log'
168
+                    text='Properties'
169
+                    title='Wolfram Functions Site' />
170
+                <link url='http://mathworld.wolfram.com/NaturalLogarithm.html'
171
+                    text='Definition'
172
+                    title='MathWorld' />
173
+               </info>
174
+              </infos>
175
+             </pod>
176
+             <pod title='Plots of the integral'
177
+                 scanner='Integral'
178
+                 id='Plot'
179
+                 position='200'
180
+                 error='false'
181
+                 numsubpods='2'>
182
+              <subpod title=''>
183
+               <plaintext></plaintext>
184
+               <states count='1'>
185
+                <statelist count='2'
186
+                    value='Complex-valued plot'
187
+                    delimiters=''>
188
+                 <state name='Complex-valued plot'
189
+                     input='Plot__1_Complex-valued plot' />
190
+                 <state name='Real-valued plot'
191
+                     input='Plot__1_Real-valued plot' />
192
+                </statelist>
193
+               </states>
194
+              </subpod>
195
+              <subpod title=''>
196
+               <plaintext></plaintext>
197
+               <states count='1'>
198
+                <statelist count='2'
199
+                    value='Complex-valued plot'
200
+                    delimiters=''>
201
+                 <state name='Complex-valued plot'
202
+                     input='Plot__2_Complex-valued plot' />
203
+                 <state name='Real-valued plot'
204
+                     input='Plot__2_Real-valued plot' />
205
+                </statelist>
206
+               </states>
207
+              </subpod>
208
+             </pod>
209
+             <assumptions count='1'>
210
+              <assumption type='Clash'
211
+                  word='integral'
212
+                  template='Assuming &quot;${word}&quot; is ${desc1}. Use as ${desc2} instead'
213
+                  count='2'>
214
+               <value name='IntegralsWord'
215
+                   desc='an integral'
216
+                   input='*C.integral-_*IntegralsWord-' />
217
+               <value name='MathematicalFunctionIdentityPropertyClass'
218
+                   desc='a function property'
219
+                   input='*C.integral-_*MathematicalFunctionIdentityPropertyClass-' />
220
+              </assumption>
221
+             </assumptions>
222
+            </queryresult>
223
+        """
224
+        # test integral
225
+        response = mock.Mock(content=xml)
226
+        results = wolframalpha_api.response(response)
227
+        self.assertEqual(type(results), list)
228
+        self.assertEqual(len(results), 2)
229
+        self.assertIn('log(x)+c', results[0]['answer'])
230
+        self.assertIn('∫1/xx - Wolfram|Alpha'.decode('utf-8'), results[1]['title'])
231
+        self.assertEquals('http://www.wolframalpha.com/input/?i=%E2%88%AB1%2Fx%EF%9D%8Cx', results[1]['url'])
232
+
233
+        xml = """<?xml version='1.0' encoding='UTF-8'?>
234
+        <queryresult success='true'
235
+            error='false'
236
+            numpods='4'
237
+            datatypes='Solve'
238
+            timedout=''
239
+            timedoutpods=''
240
+            timing='0.79'
241
+            parsetiming='0.338'
242
+            parsetimedout='false'
243
+            recalculate=''
244
+            id='MSPa7481f7i06d25h3deh2900004810i3a78d9b4fdc'
245
+            host='http://www5b.wolframalpha.com'
246
+            server='23'
247
+            related='http://www5b.wolframalpha.com/api/v2/relatedQueries.jsp?id=...'
248
+            version='2.6'>
249
+         <pod title='Input interpretation'
250
+             scanner='Identity'
251
+             id='Input'
252
+             position='100'
253
+             error='false'
254
+             numsubpods='1'>
255
+          <subpod title=''>
256
+           <plaintext>solve x^2+x0</plaintext>
257
+          </subpod>
258
+         </pod>
259
+         <pod title='Results'
260
+             scanner='Solve'
261
+             id='Result'
262
+             position='200'
263
+             error='false'
264
+             numsubpods='2'
265
+             primary='true'>
266
+          <subpod title=''>
267
+           <plaintext>x-1</plaintext>
268
+          </subpod>
269
+          <subpod title=''>
270
+           <plaintext>x0</plaintext>
271
+          </subpod>
272
+          <states count='1'>
273
+           <state name='Step-by-step solution'
274
+               input='Result__Step-by-step solution' />
275
+          </states>
276
+         </pod>
277
+         <pod title='Root plot'
278
+             scanner='Solve'
279
+             id='RootPlot'
280
+             position='300'
281
+             error='false'
282
+             numsubpods='1'>
283
+          <subpod title=''>
284
+           <plaintext></plaintext>
285
+          </subpod>
286
+         </pod>
287
+         <pod title='Number line'
288
+             scanner='Solve'
289
+             id='NumberLine'
290
+             position='400'
291
+             error='false'
292
+             numsubpods='1'>
293
+          <subpod title=''>
294
+           <plaintext></plaintext>
295
+          </subpod>
296
+         </pod>
297
+        </queryresult>
298
+        """
299
+        # test ecuation with multiple answers
300
+        response = mock.Mock(content=xml)
301
+        results = wolframalpha_api.response(response)
302
+        self.assertEqual(type(results), list)
303
+        self.assertEqual(len(results), 3)
304
+        self.assertIn('x=-1', results[0]['answer'])
305
+        self.assertIn('x=0', results[1]['answer'])
306
+        self.assertIn('solve x^2+x0 - Wolfram|Alpha'.decode('utf-8'), results[2]['title'])
307
+        self.assertEquals('http://www.wolframalpha.com/input/?i=solve+x%5E2%2Bx%EF%9F%990', results[2]['url'])

+ 193
- 0
tests/unit/engines/test_wolframalpha_noapi.py View File

@@ -0,0 +1,193 @@
1
+# -*- coding: utf-8 -*-
2
+from collections import defaultdict
3
+import mock
4
+from searx.engines import wolframalpha_noapi
5
+from searx.testing import SearxTestCase
6
+
7
+
8
+class TestWolframAlphaNoAPIEngine(SearxTestCase):
9
+
10
+    def test_request(self):
11
+        query = 'test_query'
12
+        dicto = defaultdict(dict)
13
+        dicto['pageno'] = 1
14
+        params = wolframalpha_noapi.request(query, dicto)
15
+        self.assertIn('url', params)
16
+        self.assertIn(query, params['url'])
17
+        self.assertIn('wolframalpha.com', params['url'])
18
+
19
+    def test_response(self):
20
+        self.assertRaises(AttributeError, wolframalpha_noapi.response, None)
21
+        self.assertRaises(AttributeError, wolframalpha_noapi.response, [])
22
+        self.assertRaises(AttributeError, wolframalpha_noapi.response, '')
23
+        self.assertRaises(AttributeError, wolframalpha_noapi.response, '[]')
24
+
25
+        html = """
26
+        <!DOCTYPE html>
27
+            <title> Parangaricutirimícuaro - Wolfram|Alpha</title>
28
+            <meta charset="utf-8" />
29
+            <body>
30
+                <div id="closest">
31
+                    <p class="pfail">Wolfram|Alpha doesn't know how to interpret your input.</p>
32
+                    <div id="dtips">
33
+                        <div class="tip">
34
+                            <span class="tip-title">Tip:&nbsp;</span>
35
+                                Check your spelling, and use English
36
+                            <span class="tip-extra"></span>
37
+                        </div>
38
+                    </div>
39
+                </div>
40
+            </body>
41
+        </html>
42
+        """
43
+        # test failed query
44
+        response = mock.Mock(text=html)
45
+        self.assertEqual(wolframalpha_noapi.response(response), [])
46
+
47
+        html = """
48
+        <!DOCTYPE html>
49
+            <title> sqrt(-1) - Wolfram|Alpha</title>
50
+            <meta charset="utf-8" />
51
+            <body>
52
+                <script type="text/javascript">
53
+                  try {
54
+                    if (typeof context.jsonArray.popups.pod_0100 == "undefined" ) {
55
+                      context.jsonArray.popups.pod_0100 = [];
56
+                    }
57
+                    context.jsonArray.popups.pod_0100.push( {"stringified": "sqrt(-1)","mInput": "","mOutput": ""});
58
+                  } catch(e) { }
59
+
60
+                  try {
61
+                    if (typeof context.jsonArray.popups.pod_0200 == "undefined" ) {
62
+                      context.jsonArray.popups.pod_0200 = [];
63
+                    }
64
+                    context.jsonArray.popups.pod_0200.push( {"stringified": "i","mInput": "","mOutput": ""});
65
+                  } catch(e) { }
66
+                </script>
67
+            </body>
68
+        </html>
69
+        """
70
+        # test plaintext
71
+        response = mock.Mock(text=html)
72
+        results = wolframalpha_noapi.response(response)
73
+        self.assertEqual(type(results), list)
74
+        self.assertEqual(len(results), 2)
75
+        self.assertEquals('i', results[0]['answer'])
76
+        self.assertIn('sqrt(-1) - Wolfram|Alpha', results[1]['title'])
77
+        self.assertEquals('http://www.wolframalpha.com/input/?i=+sqrt%28-1%29', results[1]['url'])
78
+
79
+        html = """
80
+        <!DOCTYPE html>
81
+            <title> integral 1/x - Wolfram|Alpha</title>
82
+            <meta charset="utf-8" />
83
+            <body>
84
+                <script type="text/javascript">
85
+                  try {
86
+                    if (typeof context.jsonArray.popups.pod_0100 == "undefined" ) {
87
+                      context.jsonArray.popups.pod_0100 = [];
88
+                    }
89
+                    context.jsonArray.popups.pod_0100.push( {"stringified": "integral 1\/x dx = log(x)+constant"});
90
+                  } catch(e) { }
91
+                </script>
92
+            </body>
93
+        </html>
94
+        """
95
+        # test integral
96
+        response = mock.Mock(text=html)
97
+        results = wolframalpha_noapi.response(response)
98
+        self.assertEqual(type(results), list)
99
+        self.assertEqual(len(results), 2)
100
+        self.assertIn('log(x)+c', results[0]['answer'])
101
+        self.assertIn('integral 1/x - Wolfram|Alpha', results[1]['title'])
102
+        self.assertEquals('http://www.wolframalpha.com/input/?i=+integral+1%2Fx', results[1]['url'])
103
+
104
+        html = """
105
+        <!DOCTYPE html>
106
+            <title> &int;1&#x2f;x &#xf74c;x - Wolfram|Alpha</title>
107
+            <meta charset="utf-8" />
108
+            <body>
109
+                <script type="text/javascript">
110
+                  try {
111
+                    if (typeof context.jsonArray.popups.pod_0100 == "undefined" ) {
112
+                      context.jsonArray.popups.pod_0100 = [];
113
+                    }
114
+                    context.jsonArray.popups.pod_0100.push( {"stringified": "integral 1\/x dx = log(x)+constant"});
115
+                  } catch(e) { }
116
+                </script>
117
+            </body>
118
+        </html>
119
+        """
120
+        # test input in mathematical notation
121
+        response = mock.Mock(text=html)
122
+        results = wolframalpha_noapi.response(response)
123
+        self.assertEqual(type(results), list)
124
+        self.assertEqual(len(results), 2)
125
+        self.assertIn('log(x)+c', results[0]['answer'])
126
+        self.assertIn('∫1/x x - Wolfram|Alpha'.decode('utf-8'), results[1]['title'])
127
+        self.assertEquals('http://www.wolframalpha.com/input/?i=+%E2%88%AB1%2Fx+%EF%9D%8Cx', results[1]['url'])
128
+
129
+        html = """
130
+        <!DOCTYPE html>
131
+            <title> 1 euro to yen - Wolfram|Alpha</title>
132
+            <meta charset="utf-8" />
133
+            <body>
134
+                <script type="text/javascript">
135
+                  try {
136
+                    if (typeof context.jsonArray.popups.pod_0100 == "undefined" ) {
137
+                      context.jsonArray.popups.pod_0100 = [];
138
+                    }
139
+                  context.jsonArray.popups.pod_0100.push( {"stringified": "convert euro1  (euro) to Japanese yen"});
140
+                  } catch(e) { }
141
+
142
+                  try {
143
+                    if (typeof context.jsonArray.popups.pod_0200 == "undefined" ) {
144
+                      context.jsonArray.popups.pod_0200 = [];
145
+                    }
146
+                    context.jsonArray.popups.pod_0200.push( {"stringified": "&yen;130.5  (Japanese yen)"});
147
+                  } catch(e) { }
148
+                </script>
149
+            </body>
150
+        </html>
151
+        """
152
+        # test output with htmlentity
153
+        response = mock.Mock(text=html)
154
+        results = wolframalpha_noapi.response(response)
155
+        self.assertEqual(type(results), list)
156
+        self.assertEqual(len(results), 2)
157
+        self.assertIn('¥'.decode('utf-8'), results[0]['answer'])
158
+        self.assertIn('1 euro to yen - Wolfram|Alpha', results[1]['title'])
159
+        self.assertEquals('http://www.wolframalpha.com/input/?i=+1+euro+to+yen', results[1]['url'])
160
+
161
+        html = """
162
+        <!DOCTYPE html>
163
+            <title> distance from nairobi to kyoto in inches - Wolfram|Alpha</title>
164
+            <meta charset="utf-8" />
165
+            <body>
166
+                <script type="text/javascript">
167
+                  try {
168
+                    if (typeof context.jsonArray.popups.pod_0100 == "undefined" ) {
169
+                      context.jsonArray.popups.pod_0100 = [];
170
+                    }
171
+[...].pod_0100.push( {"stringified": "convert distance | from | Nairobi, Kenya\nto | Kyoto, Japan to inches"});
172
+                  } catch(e) { }
173
+
174
+                  try {
175
+                    if (typeof context.jsonArray.popups.pod_0200 == "undefined" ) {
176
+                      context.jsonArray.popups.pod_0200 = [];
177
+                    }
178
+pod_0200.push({"stringified": "4.295&times;10^8 inches","mOutput": "Quantity[4.295×10^8,&amp;quot;Inches&amp;quot;]"});
179
+
180
+                  } catch(e) { }
181
+                </script>
182
+            </body>
183
+        </html>
184
+        """
185
+        # test output with utf-8 character
186
+        response = mock.Mock(text=html)
187
+        results = wolframalpha_noapi.response(response)
188
+        self.assertEqual(type(results), list)
189
+        self.assertEqual(len(results), 2)
190
+        self.assertIn('4.295×10^8 inches'.decode('utf-8'), results[0]['answer'])
191
+        self.assertIn('distance from nairobi to kyoto in inches - Wolfram|Alpha', results[1]['title'])
192
+        self.assertEquals('http://www.wolframalpha.com/input/?i=+distance+from+nairobi+to+kyoto+in+inches',
193
+                          results[1]['url'])